1. 过滤敏感词(难点)

03-开发社区核心功能 - 图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. 发布帖子

    03-开发社区核心功能 - 图2
    异步请求:当前网页不刷新,但是还会访问服务器,根据服务器返回的结果对网页做局部的刷新。实现异步请求的计数叫做 AJAX(Asynchronous JavaScript and XML) ,它能够将增量更新呈现在页面上。使用 JavaScript 的框架 jQuery 发送 AJAX 请求。

控制层:首先从 hostHolder 里取出当前用户,然后 new 出帖子模型,调用逻辑层添加帖子。
逻辑层:先过滤敏感词,然后调用 DAO 层新增帖子。

3. 帖子详情

03-开发社区核心功能 - 图3
控制层:调用逻辑层得到帖子详情内容,然后在 Model 中注入 post 和 user 传输给表现层。

4. 事务管理

4.1 事务管理基础

03-开发社区核心功能 - 图4
事务的隔离性比较重要,因为我们所开发的服务器程序是一个多线程的环境,每一个浏览器在访问服务器的时候,服务器就会创建一个线程来处理浏览器的请求。如果在这次请求中需要访问数据库,可能就需要事务管理。

03-开发社区核心功能 - 图5
03-开发社区核心功能 - 图6
03-开发社区核心功能 - 图7

第一类丢失更新是事务回滚导致的,第二类丢失更新是事务提交导致的。

03-开发社区核心功能 - 图8
03-开发社区核心功能 - 图9
03-开发社区核心功能 - 图10
03-开发社区核心功能 - 图11
03-开发社区核心功能 - 图12

  • 事务 A 对某数据加了共享锁后,其他事务只能读取该数据。
  • 事务 A 对某数据加了排他锁后,其他事务对该数据不能读也不能写。

其中悲观锁是数据库自带的,而乐观锁需要我们自己实现的。

4.2 Spring 事务管理

03-开发社区核心功能 - 图13
编程式事务更为复杂,但是细粒度更高。比如有一个业务逻辑有 10 步数据库的操作,而我们只需要使用事务管理控制中间的 5 步。这时候如果使用声明式事务,那么整个方法都会被事务管理,这时候编程式事务就派上用场了。

  1. /**
  2. * 声明式事务使用XML配置或注解声明某方法的事务特征
  3. *
  4. * 事务传播行为是为了解决业务层方法之间互相调用的事务问题,常用的有:
  5. * REQUIRED:支持当前事务,如果不存在则创建新事务
  6. * REQUIRED_NEW:创建一个新事务,并且暂停当前事务
  7. * NESTED:如果当前存在事务,则嵌套在该事务中执行(独立的提交和回滚),否则就和REQUIRED一样
  8. */
  9. @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
  10. public Object save1() {
  11. // 新增用户
  12. User user = new User();
  13. user.setUsername("alpha");
  14. user.setSalt(CommunityUtil.generateUUID().substring(0,5));
  15. user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
  16. user.setEmail("alpha@qq.com");
  17. user.setHeaderUrl("http://image.nowcoder.com/head/99t.png");
  18. user.setCreateTime(new Date());
  19. userMapper.insertUser(user);
  20. // 新增帖子
  21. DiscussPost post = new DiscussPost();
  22. post.setUserId(user.getId());
  23. post.setTitle("Hello");
  24. post.setContent("新人报到");
  25. post.setCreateTime(new Date());
  26. discussPostMapper.insertDiscussPost(post);
  27. // 证明程序发生异常时数据会回滚
  28. Integer abc = Integer.valueOf("abc");
  29. return "ok";
  30. }
/**
 * 编程式事务通过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. 显示评论

03-开发社区核心功能 - 图14

6. 添加评论

03-开发社区核心功能 - 图15
增加评论、更新帖子评论数量的时候需要使用事务,使用的事务隔离级别是读已经提交,事务传播行为为 REQUIRED(即如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务)

添加评论时同样需要进行敏感词过滤。

7. 私信列表

03-开发社区核心功能 - 图16

8. 发送私信

03-开发社区核心功能 - 图17

9. 统一处理异常(针对表现层)

03-开发社区核心功能 - 图18

当数据层出现异常时,会向上抛给业务层,业务层会继续将异常抛给表现层,因此所有异常最终会汇集到表现层。因此只要抓住表现层,统一对表现层处理异常,就能处理系统中的所有异常。

目前没有用到 @ModelAttribute @DataBinder 这两个注解。

基于 Springboot 的自动配置,直接在 /templates 目录下建立 error 文件夹,里面新增 404.html 和 500.html,Springboot 在捕获到异常后会根据错误信息为我们自动跳转到其中一个页面。但是 Springboot 处理只会跳转到 500.html,不会记录日志。为了记录服务器发生异常的信息,需要使用 @ControllerAdvice 声明一个 Controller 全局配置类,对所有 Controller 的异常做统一处理。

10. 统一记录日志

记录日志属于系统需求,不属于业务需求,为了不将系统需求和业务需求耦合在一起,统一使用Spring AOP记录业务日志。

03-开发社区核心功能 - 图19
03-开发社区核心功能 - 图20
03-开发社区核心功能 - 图21

如图所示,左边的一个个 Target 是我们需要处理的目标对象 Bean,我们采用 AOP 解决问题,不是在目标对象上直接写代码,而是需要把代码单独封装到一个组件里,这个组件就叫做 Aspect。我们是针对 Aspect 进行编程的,所以我们叫做面向切面编程(Aspect Oriented Programming)。那么需求目标怎么知道我们编写的逻辑呢?所以需要利用 AOP 框架进行织入,把切面组件的代码织入到目标对象里。

织入时机的早晚各有利弊。织入的时机比较早,可能很多运行时的特殊情况无法处理。织入的时机比较迟,效率可能比较低。Spring AOP 采用的是运行时织入。

pointcut 是execution (* com.nowcoder.community.service.*.*(..)),即 service 包下的所有类的所有方法的所有参数的所有返回值都要处理。

03-开发社区核心功能 - 图22
03-开发社区核心功能 - 图23