1. 过滤敏感词(难点)
过滤敏感词可以直接使用API替换字符串,但是这样效率很低,因此采用前缀树过滤敏感词。算法与 LeetCode 208 类似。
- 为什么要使用 Trie 树? 过滤敏感词可以直接使用API替换字符串,但是这样效率很低,因此采用前缀树(Trie树)过滤敏感词。Trie 树的查找效率高,但是消耗内存大,应用于字符串检索、词频统计、字符串排序等。
- Trie 树的数据结构? Trie 树可以使用 HashMap 实现,因为一个节点的子节点个数未知,而 HashMap 可以动态扩展,而且可以在 O(1) 的时间复杂度内判断某个子节点是否存在。首先定义 Trie 树的节点,节点的结构为 HashMap,key 为字符串中的字符,value 为这个节点的子节点。在实现敏感词过滤前,首先需要初始化 Trie 树,将所有敏感词插入到 Trie 树中。
- 具体怎么初始化 Trie 树的呢? 将每个词语的每个字符一个个地添加到 Trie 树中,树中的每个节点代表一个字。
- 怎么实现敏感词过滤呢? 将待过滤文本与 Trie 树中的节点一个个地进行比较。使用三个指针,其中一个指针指向 Trie 树,另外两个指针指向待过滤文本的起始位置和结束位置。首先 p1 指针指向 root,指针 p2 和 p3 指向字符串中的第一个字符。算法从字符 a 开始,检测有没有以 a 作为前缀的敏感词,在这里就直接判断 root 中有没有 a 这个子节点即可。没有的话将 p2 和 p3 同时右移,而如果存在以 a 作为前缀的敏感词,那么就只右移 p3 继续判断 p2 和 p3 之间的这个字符串是否是敏感词。如果在字符串中找到敏感词,那么可以用其他字符串如
***
代替。接下来不断循环直到整个字符串遍历完成就可以了。 - https://www.jianshu.com/p/9919244dd7ad
2. 发布帖子
异步请求:当前网页不刷新,但是还会访问服务器,根据服务器返回的结果对网页做局部的刷新。实现异步请求的计数叫做 AJAX(Asynchronous JavaScript and XML) ,它能够将增量更新呈现在页面上。使用 JavaScript 的框架 jQuery 发送 AJAX 请求。
控制层:首先从 hostHolder 里取出当前用户,然后 new 出帖子模型,调用逻辑层添加帖子。
逻辑层:先过滤敏感词,然后调用 DAO 层新增帖子。
3. 帖子详情
控制层:调用逻辑层得到帖子详情内容,然后在 Model 中注入 post 和 user 传输给表现层。
4. 事务管理
4.1 事务管理基础
事务的隔离性比较重要,因为我们所开发的服务器程序是一个多线程的环境,每一个浏览器在访问服务器的时候,服务器就会创建一个线程来处理浏览器的请求。如果在这次请求中需要访问数据库,可能就需要事务管理。
第一类丢失更新是事务回滚导致的,第二类丢失更新是事务提交导致的。
- 事务 A 对某数据加了共享锁后,其他事务只能读取该数据。
- 事务 A 对某数据加了排他锁后,其他事务对该数据不能读也不能写。
4.2 Spring 事务管理
编程式事务更为复杂,但是细粒度更高。比如有一个业务逻辑有 10 步数据库的操作,而我们只需要使用事务管理控制中间的 5 步。这时候如果使用声明式事务,那么整个方法都会被事务管理,这时候编程式事务就派上用场了。
/**
* 声明式事务使用XML配置或注解声明某方法的事务特征
*
* 事务传播行为是为了解决业务层方法之间互相调用的事务问题,常用的有:
* REQUIRED:支持当前事务,如果不存在则创建新事务
* REQUIRED_NEW:创建一个新事务,并且暂停当前事务
* NESTED:如果当前存在事务,则嵌套在该事务中执行(独立的提交和回滚),否则就和REQUIRED一样
*/
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public Object save1() {
// 新增用户
User user = new User();
user.setUsername("alpha");
user.setSalt(CommunityUtil.generateUUID().substring(0,5));
user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
user.setEmail("alpha@qq.com");
user.setHeaderUrl("http://image.nowcoder.com/head/99t.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
// 新增帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle("Hello");
post.setContent("新人报到");
post.setCreateTime(new Date());
discussPostMapper.insertDiscussPost(post);
// 证明程序发生异常时数据会回滚
Integer abc = Integer.valueOf("abc");
return "ok";
}
/**
* 编程式事务通过TransactionTemplate管理事务,并通过它执行数据库的操作
* 如果业务逻辑比较复杂,而我们只想管理方法中一部分的逻辑,那么可以使用编程式事务
*/
public Object save2() {
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
return transactionTemplate.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus status) {
// 新增用户
User user = new User();
user.setUsername("beta");
user.setSalt(CommunityUtil.generateUUID().substring(0,5));
user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
user.setEmail("beta@qq.com");
user.setHeaderUrl("http://image.nowcoder.com/head/999t.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
// 新增帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle("你好");
post.setContent("我是新人");
post.setCreateTime(new Date());
discussPostMapper.insertDiscussPost(post);
// 证明程序发生异常时数据会回滚
Integer abc = Integer.valueOf("abc");
return "ok";
}
});
}
5. 显示评论
6. 添加评论
在增加评论、更新帖子评论数量的时候需要使用事务,使用的事务隔离级别是读已经提交,事务传播行为为 REQUIRED(即如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务)
7. 私信列表
8. 发送私信
9. 统一处理异常(针对表现层)
当数据层出现异常时,会向上抛给业务层,业务层会继续将异常抛给表现层,因此所有异常最终会汇集到表现层。因此只要抓住表现层,统一对表现层处理异常,就能处理系统中的所有异常。
目前没有用到 @ModelAttribute 和 @DataBinder 这两个注解。
基于 Springboot 的自动配置,直接在 /templates 目录下建立 error 文件夹,里面新增 404.html 和 500.html,Springboot 在捕获到异常后会根据错误信息为我们自动跳转到其中一个页面。但是 Springboot 处理只会跳转到 500.html,不会记录日志。为了记录服务器发生异常的信息,需要使用 @ControllerAdvice 声明一个 Controller 全局配置类,对所有 Controller 的异常做统一处理。
10. 统一记录日志
记录日志属于系统需求,不属于业务需求,为了不将系统需求和业务需求耦合在一起,统一使用Spring AOP记录业务日志。
如图所示,左边的一个个 Target 是我们需要处理的目标对象 Bean,我们采用 AOP 解决问题,不是在目标对象上直接写代码,而是需要把代码单独封装到一个组件里,这个组件就叫做 Aspect。我们是针对 Aspect 进行编程的,所以我们叫做面向切面编程(Aspect Oriented Programming)。那么需求目标怎么知道我们编写的逻辑呢?所以需要利用 AOP 框架进行织入,把切面组件的代码织入到目标对象里。
织入时机的早晚各有利弊。织入的时机比较早,可能很多运行时的特殊情况无法处理。织入的时机比较迟,效率可能比较低。Spring AOP 采用的是运行时织入。
pointcut 是execution (* com.nowcoder.community.service.*.*(..))
,即 service 包下的所有类的所有方法的所有参数的所有返回值都要处理。