牛客后端项目(4)-帖子模块开发
核心功能开发完成~
过滤敏感词
Trie树
树的节点包括:子节点(用Map<String, TrieNode>表示),是否是单词结尾的标志(boolean isEnd)
两个关键方法:
- 向Trie树添加字符串
- 对字符串进行敏感词过滤(要注意start指针要一位一位移动,否则会漏判,除了start-end是一个敏感词,start直接跳到end+1)
- 注意要跳过敏感词中的符号,比如❤福❤彩❤:
用CharUtils.isAsciiAlphanumeric
判断字符是否是0-9a-zA-Z范围内(参考文档),以及是否是0x2e80~0x9FF之间(这是东亚文字的区间)
发布帖子
AJAX
- 在页面中引入jQuery:
1
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
- 页面中写异步请求发送的方法,举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14function send(){
$.post(
"/community/ajax",
{"name": "liz", "age": 24},
function (data){
console.log(typeof data);
console.log(data);
data = $.parseJSON(data);
console.log(typeof data);
console.log(data);
}
);
}
发布帖子
dao层:insert方法
service层:处理标题和内容,insert
controller层:从前端获取title和content,创建DiscussPost对象,之后调用service,返回是否成功的结果
视图:处理返回结果,发布成功时刷新,失败时留在发布界面
注意:
- 帖子的标题和内容需要合法性处理
- 帖子内容如果有html标签,要转义一下,避免内容对页面的损害。用
HtmlUtils.htmlEscape
方法即可
帖子详情
dao:selectById方法
service:调用dao的selectById方法
controller:postId在路径中传,调用service的select方法,找到的DiscussPost和相关的User对象,送入Model,传到前端视图中显示。
事务
事务:一组操作,要么全部执行,要么全部不执行
四个性质:ACID,原子性、一致性、隔离性、持久性
隔离性:事务与事务之间的操作是相互隔离的。主要解决并发异常问题。
常见的并发异常:1. 第一类丢失更新、第二类丢失更新;2. 脏读、不可重复度、幻读
常见的隔离级别:
Read Uncommitted
Read Committed
并发异常问题与事务隔离级别
第一类丢失更新:事务A和事务B同时对同一个数据操作,事务A更新完了,但是事务B发现进行了回滚,那么对于事务A的更新就丢失了。(B回滚导致A丢失)
图片来源:牛客网课程
第二类丢失更新:
B提交导致A丢失
图片来源:牛客网课程
脏读:事务A读了事务B还没有提交但修改了的数据
不可重复读:事务B在修改数据,事务A两次读同一个数据但读到了不一样的数据。
幻读:事务B在插入数据,事务A两次读但是读到了不一样多的数据
事务隔离级别:
Y表示问题会出现,N表示问题不会出现
图片来源:牛客网课程
实现机制-锁🔒
数据库自带:悲观锁(即出现并发就一定会有问题,所以一定要加锁)
- 共享锁(S锁):事务A对数据加了共享锁后,其他事务只能加共享锁,不能加排它锁
- 排它锁(X锁):事务A对数据加了排他锁后,其他事务什么锁都不能加
自定义:乐观锁(即认为出现并发不一定会有问题)
用时间戳或版本号等来实现。当要更新数据的时候,发现版本号发生变化了,就取消更新,不然就更新数据(当然也要更新一下版本号)。
扩展:MVCC到底是什么?这一篇博客就够啦_flying$的博客-CSDN
MySQL中MVCC的正确打开方式(源码佐证)_Waves___的博客-CSDN博客
Spring的事务管理
对任意数据库,有一套统一的API来使用🙆♀️
两种实现方法:1. 声明式,通过xml或注解配置;2.编程式,通过TransactionTemplate,再用接口回调。
@Transactinoal
isolation选择事务隔离等级
propagation选择事务传播机制,一个方法同时调用serviceA和serivceB,如果涉及事务就需要考虑A和B之间事务的关系是什么
REQUIRED:保持当前事务,如果没有则创建新事务
REQUIRES_NEW:创建新事务,并暂停当前事务
NESTED:如果存在当前事务,则嵌套在其中执行(对B会有单独的提交和回滚);没有就和REQUIRED一样
更多Propagation的解读:spring事务-说说Propagation及其实现原理_那个天真的人的博客-CSDN
显示评论
数据结构:设计一张表来保存所有评论,由于帖子、帖子的评论都可以有评论,为了减少冗余,把它们都存放在同一张表里,并用entity type
来指明是帖子或评论的评论,entity id
指明(帖子或评论的)id
dao:(1)用entity type
和entity id
查询评论列表(分页);(2)用entity type
和entity id
查询评论数量
service:读取某个帖子对应的所有评论,注意评论中还有对某个评论的回复。
controller:通过访问/discuss/{discussPostId}来访问所有评论,用service读,注意要分页,replyList就不用了。设计一个ViewObject返回要显示在网页上的内容,用List保存评论commentList
,其中每个元素是一个map,包括一条评论所有相关的内容,即评论内容comment
、发表评论的用户user
(需要用户名、id和headerUrl)、该评论的回复replyList
,其中replyList
是和commentList
相同的结构,用for循环保存起来,需要注意的是回复有target id
,即回复的是哪一条评论。另外还要保存replyList的评论数量,commentList的数量可以用discussPost.commentCount来读取,省得在数据库中搜索浪费时间了。
注意:
- 视图处理上要仔细,别的没啥。难点在于返回网页的内容处理上
问题解决
- controller层和service层的内容,该交给哪个层去完成感觉总是弄混。比如说这里要封装一个给视图显示的viewObject。为什么不交给Service层去处理?我的理解:Service层应具有高可复用性,如果交给service去处理的话,返回一个视图显示对象,这是不太合理的。
回复评论
dao层:插入一条评论;更新帖子的回复数量
service层:DiscussPostService更新帖子的回复数量。CommentService插入一条评论(注意要html转义、敏感词处理)、调用DiscussPostService方法更新帖子的回复数量。
视图层:controller得到comment,前端传回content、targetId、entityType、entityId,剩下的补充一下。调用service方法完成添加评论后要重定向到当前页面,从而进行刷新。
注意:
- CommentService的插入评论方法,同时对数据库有两次修改,需要放在一个事务中处理。
私信
数据结构:message表,from_id
和to_id
分别表示发送用户和接受用户;为了便于查询,将两者拼接为一个字符串表示会话id即conversation_id
;其他还有创建时间、内容、状态。注意用户之间的会话,两者不分谁发送谁接收,这个会话是他们共有的。
私信列表
私信列表包括朋友私信和系统通知,两者逻辑可以复用。
对于朋友私信而言,列表的每一个元素是一个用户,显示该用户头像、用户名(User),以及最近一次会话的内容和时间(Message),以及所有会话数量和未读会话数量。整个列表是当前用户拥有的所有会话(无论当前用户是发送方还是接收方)
推出dao层需要的方法:
- UserMapper:根据id查询用户
- MessageMapper:(1)分页查询会话中的最近一次消息(用会话id和limit);(2)查询和某个用户之间的会话数量(用会话id和status);(3)为了分页,查询该用户的所有未删除的会话数量(用userId和status)
service:直接把dao层封装一下
controller:封装一个传给视图层的列表
注意:
- 在统计未读消息的时候,只有对于接收方是未读的消息才要统计,对于发送方而言这条消息不是未读的!
- 在写sql语句的时候,因为只需要会话中的最近一次消息,用max(id)并按照conversationId分组即可
私信详情
即一个会话的多个消息。用分页展示。其他和私信列表类似。私信的信息注意用户保存的是发送方,可以提前保存起来。
发送私信
dao:insert一条message
service:内容的合法性要处理,
统一的异常处理
- 用户登录验证。有些方法只有在用户登录之后才可以使用(如回帖、发消息等)
- 不存在的网页(404)和服务器出错的网页(500)
用户登录验证
使用拦截器Interceptor,对需要登录才可以继续的方法进行拦截。为了方便,可以对这些方法统一使用相同的注释来标记(eg. LoginRequired),在拦截器中判断方法是否包含该注释,再判断用户是否已登录,如果没有登录就重定向到登录页。
404和500页面
直接把404页面放到template/error文件夹下面,spring会自动处理。如果没有使用模板引擎,就可以把404页面放到error文件夹中,error文件夹再放到static文件夹下即可。
而对于500页面,是服务器异常,我们需要把这些异常记录到日志中去,不能直接交给spring boot自动处理。
我们使用@ControllerAdvice注解定义一个新的类,从而统一扩增控制器的功能。@ControllerAdvice注解和@ExceptionHandler搭配使用,可以统一处理异常。而@ControllerAdvice中的参数可以指定需要对哪些控制器进行功能增强,如@ControllerAdvice(annotations = Controller.class)
对标有Controller注解的所有类进行增强。
统一的日志记录
AOP
面向切面编程。是一种OOP编程的扩展。将项目中多个模块共有的业务看做一个切面,如下图所示(图片来源:牛客网课程)
Joinpoint:连接点,指明aspect是在哪个位置织入。可以是属性或方法等。
Target:目标对象,Spring中就是一个Bean
Weaving(织入):有三种织入方式,分别是(1)编译时织入;(2)装载时织入;(3)运行时织入
Aspect:方面组件,我理解是在要实现的统一方法是什么(如我们要实现的就是日志这个功能)。其中由包括Pointcut和Advice,前者说明这个Aspect可以在哪里加入,后者是具体逻辑。
AOP的实现
- AspectJ:是一种扩展了Java的新语言;在编译时进行织入
- Spring AOP:用Java实现;在运行时通过动态代理进行织入;但只支持方法类型的连接点
日志功能
用Spring AOP来实现。
Spring AOP的代理有两种方式:
- JDK动态代理。是默认方法;如果target是接口,就用JDK动态代理来进行织入
- CGLib动态代理。在没有接口的情况下,就用这个方式来代理
具体实现:
- 类用@Component、@Aspect标注。
- 定义切入点:
@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
- 定义通知逻辑。根据在织入点前后处理,可以分为Before、AfterReturning,以及Around、AfterThrowing等