03 开发社区核心功能

本贴最后更新于 470 天前,其中的信息可能已经时移世改

过滤敏感词

  • 前缀树

    • 名称:Trie、字典树、查找树
    • 特点:查找效率高,消耗内存大
    • 应用:字符串检索、词频统计、字符串排序等
  • 敏感词过滤器

    • 定义前缀树
    • 根据敏感词,初始化前缀树
    • 编写过滤敏感词的方法

敏感词算法:

流程

一个 trie 树存放敏感词,一对快慢指针,快指针指向给给定的字符串的起始位置,如果检测到了当前字符符合某个过滤词的第一个字母,就让慢指针开始往后匹配这个过滤词的其他字符,如果没有完全匹配(这里指不是过滤词或者只是过滤词的部分,并没有完全精确匹配),则让快慢指针都跳到快指针所指向的后一个字母的位置,继续按照此逻辑执行。匹配到了的话快慢指针都往前移一位,直到完全匹配就修改敏感词词汇。

image

image

遇到特殊符号的情况,快指针不动,慢指针动。

算法实现

Util​包下创建 SensitiveFilter​类:

package com.nowcoder.community.util;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.CharUtils;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Component
public class SensitiveFilter {
    //替换符
    public static final String REPLACEMENT="***";

    //根节点
    private TrieNode rootNode=new TrieNode();

    //在实例化bean后,服务器初始化之前调用
    @PostConstruct
    public void init(){
        try(
            InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
            BufferedReader reader=new BufferedReader(new InputStreamReader(is));
        ){
            String keyword;
            while((keyword=reader.readLine())!=null){
                //将敏感词添加到前缀树
                this.addKeyword(keyword);
            }
        } catch (Exception e) {
            log.error("加载敏感词文件失败:"+e.getMessage());
        }
    }

    //将一个敏感词添加到前缀树中
    private void addKeyword(String keyword){
        TrieNode tempNode=rootNode;
        for(int i=0;i<keyword.length();i++){
            char c=keyword.charAt(i);
            TrieNode subNode=tempNode.getSubNode(c);

            if(subNode==null){
                //初始化子节点
                subNode=new TrieNode();
                tempNode.addSubNode(c,subNode);
            }

            //存在的话,指向子节点,进入下一轮循环
            tempNode=subNode;

            //设置结束标记
            if(i==keyword.length()-1){
                tempNode.setKeywordEnd(true);
            }

        }
    }

    /**
     * 过滤敏感词
     * @param text 待过滤的文本
     * @return
     */
    public String filter(String text){
        if(text.isBlank()){
            return null;
        }

        //指针1
        TrieNode tempNode=rootNode;
        //指针2
        int begin=0;
        //指针3
        int position=0;
        //结果
        StringBuilder sb=new StringBuilder();

        while(begin<text.length()){
            if(position<text.length()){
                char c=text.charAt(position);

                //跳过符号
                if(isSymbol(c)){
                    //若指针1处于根节点,将此符号计入结果,让指针2走向下一步
                    if(tempNode == rootNode){
                        sb.append(c);
                        begin++;
                    }
                    //无论符号在开头或者中间,指针3都向下走一步
                    position++;
                    continue;
                }

                //检查下级节点
                tempNode=tempNode.getSubNode(c);
                if(tempNode==null){//没有下个节点,说明是最后节点,而且之前的词汇都不是敏感词
                    //以begin开头的字符串不是敏感词
                    sb.append(text.charAt(begin));
                    //进入下一个位置的过滤词的判断,这一波的过滤词已经结束了,开始新的一轮
                    position = ++begin;
                    //重新指向根节点
                    tempNode=rootNode;
                }else if(tempNode.isKeywordEnd()){
                    //发现了并完全匹配敏感词,将begin-position字符串替换掉
                    sb.append(REPLACEMENT);
                    //进入下一个位置的过滤词的判断,这一波的过滤词已经结束了,开始新的一轮,从这个过滤词的结束位置+1开始算
                    begin = ++position;
                    //重新指向根节点
                    tempNode=rootNode;
                }else{
                    //检查下一个字符
                    position++;
                }
            }else{
                //position遍历越界仍未匹配到敏感词,就将当前字符加入结果,开始下一个字符的过滤词筛查
                sb.append(text.charAt(begin));
                position=++begin;
                tempNode=rootNode;
            }
        }


        return sb.toString();
    }

    //判断是否为符号
    private boolean isSymbol(Character c){
        //0x2E80~0x9FFF是东亚文字范围,isAsciiAlphanumeric判断是不是普通字符
        return !CharUtils.isAsciiAlphanumeric(c) && (c<0x2E80||c>0x9FFF);
    }



    //前缀树
    private class TrieNode{
        //关键词标识
        private boolean isKeywordEnd = false;

        //子节点(key是下级字符,value是下级节点)
        private Map<Character,TrieNode> subNodes=new HashMap<>();

        private boolean isKeywordEnd(){
            return isKeywordEnd;
        }

        public void setKeywordEnd(boolean keywordEnd){
            isKeywordEnd=keywordEnd;
        }

        //添加子节点
        public void addSubNode(Character c,TrieNode node){
            subNodes.put(c,node);
        }

        //获取子节点
        public TrieNode getSubNode(Character c){
            return subNodes.get(c);
        }
    }


}

  1. 首先判断待过滤的文本是否为空或空格,如果是,则返回 null。

  2. 创建三个指针:tempNode 指向根节点,begin 指向待过滤文本的开头位置,position 指向当前遍历的位置。

  3. 创建一个 StringBuilder 对象用于存储过滤后的文本结果。

  4. 进入循环,遍历文本的每个字符。

  5. 如果字符是符号,则判断 tempNode 是否为根节点,如果是,则将符号加入结果,并将 begin 指针向后移动一位(如 ♥ 赌 ♥♥♥ 博,这种情况我们只让第一个 ♥ 加入结果,赌字之后的 ♥ 就直接 pass,不加入结果);无论是否为根节点,都将 position 指针向后移动一位。

  6. 如果字符不是符号,则检查 tempNode 的下一个节点是否存在。

    • 如果不存在,说明之前的词汇都不是敏感词,将以 begin 开头的字符串加入结果,并将 begin 和 position 指针都移动到下一个位置,重新指向根节点。

    • 如果存在,继续判断是否为敏感词的结束节点。

      • 如果是结束节点,说明发现了一个完全匹配的敏感词,将敏感词替换成指定的替换字符,并将 begin 指针移动到 position 的下一个位置,重新指向根节点。
      • 如果不是结束节点,继续检查下一个字符。
  7. 循环结束后,将最后一批字符加入结果。

  8. 返回过滤后的文本结果。

ps:记得从 3.1​下面复制 sensitive-words.txt​到项目下。

写个测试类测试结果

package com.nowcoder.community;

import com.nowcoder.community.util.SensitiveFilter;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes=CommunityApplication.class)
public class SensitiveTests {
    @Autowired
    private SensitiveFilter sensitiveFilter;

    @Test
    public void testSensitiveFilter(){
        String text="这里可以赌博,♥赌♥♥♥博可以嫖娼,可以吸毒,可以开票,赌博哈哈哈赌博吸毒嫖娼";
        text=sensitiveFilter.filter(text);
        System.out.println(text);
    }
}

image

发布帖子

  • AJAX

    • Asynchronous JavaScript and XML
    • 异步的 JavaScript 与 XML,不是一门新技术,只是一个新的术语。
    • 使用 AJAX,网页能够将增量更新呈现在页面上,而不需要刷新整个页面。
    • 虽然 X 代表 XML,但目前 JSON 的使用比 XML 更加普遍。
    • https://developer.mozilla.org/zh-CN/docs/Web/Guide/AJAX
  • 示例

    • 使用 jQuery 发送 AJAX 请求。
  • 实践

    • 采用 AJAX 请求,实现发布帖子的功能。

示例——使用 jQuery 发送 AJAX 请求

AlphaController​:

    //ajax实例
    @RequestMapping(path = "/ajax",method = RequestMethod.POST)
    @ResponseBody
    public String testAjax(String name,int age){
        System.out.println(name);
        System.out.println(age);
        return CommunityUtil.getJSONString(0,"操作成功");
    }

static/html​下创建 ajax-demo.html​:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Ajax</title>
</head>
<body>
    <p>
        <input type="button" value="发送" onclick="send();">
    </p>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
    <script>
        function send(){
            $.post(
                "/community/alpha/ajax",
                 {"name":"张三","age":23},
                 function (data){
                    console.log(typeof(data));
                    console.log(data);
                    data=$.parseJSON(data);
                    console.log(typeof(data));
                    console.log(data.code);
                    console.log(data.msg);
                 }
            );
        }
    </script>
</body>
</html>

在网页测试:http://localhost:8080/community/html/ajax-demo.html

image

发布帖子

DiscussPostMapper​下新增一个方法 int insertDiscussPost(DiscussPost discussPost);

discusspost_mapper.xml​下:

    <sql id="insertFields">
        user_id, title, content, type, status, create_time, comment_count, score
    </sql>
    <insert id="insertDiscussPost" parameterType="DiscussPost">
        insert into discuss_Post(<include refid="insertFields"></include>)
        values(#(userId),#(title),#(content),#(type),#(status),#(createTime),#(commentCount),#(score))
    </insert>

在​DiscussPostService​:

    public int addDiscussPost(DiscussPost post){
        if(post==null){
            throw new IllegalArgumentException("参数不能为空");
        }

        //转义HTML标记
        post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));
        post.setContent(HtmlUtils.htmlEscape(post.getContent()));
        //过滤敏感词
        post.setTitle(sensitiveFilter.filter(post.getTitle()));
        post.setContent(sensitiveFilter.filter(post.getContent()));
        return discussPostMapper.insertDiscussPost(post);
    }

Controller​下新建 DiscussPostController​:

package com.nowcoder.community.controller;

import com.nowcoder.community.entity.DiscussPost;
import com.nowcoder.community.service.DiscussPostService;
import com.nowcoder.community.util.CommunityUtil;
import com.nowcoder.community.util.HostHolder;
import com.nowcoder.community.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Date;

@Controller
@RequestMapping("/discuss")
public class DiscussPostController {
    @Autowired
    private DiscussPostService discussPostService;

    @Autowired
    private HostHolder hostHolder;

    @RequestMapping(path = "/add",method = RequestMethod.POST)
    @ResponseBody
    public String addDiscussPost(String title,String content){
        User user=hostHolder.getUser();
        if(user==null){
            return CommunityUtil.getJSONString(403,"你还没有登录哦!");
        }
        DiscussPost post=new DiscussPost();
        post.setUserId(user.getId());
        post.setTitle(title);
        post.setContent(content);
        post.setCreateTime(new Date());
        discussPostService.addDiscussPost(post);

        //报错的情况,将来统一处理
        return CommunityUtil.getJSONString(0,"发布成功!");

    }
}

修改 index.js​:

$(function(){
	$("#publishBtn").click(publish);
});

function publish() {
	$("#publishModal").modal("hide");

	//获取标题和内容
	var title=$("#recipient-name").val();
	var content=$("#message-text").val();
	//发送异步请求
	$.post(
		CONTEXT_PATH+"/discuss/add",
		{"title":title,"content":content},
		function (data){
			data=$.parseJSON(data);
			//在提示框中显示返回信息
			$("#hintBody").text(data.msg);
			//显示提示框
			$("#hintModal").modal("show");
			//2秒后,自动隐藏提示框
			setTimeout(function (){
				$("#hintModal").modal("hide");
				//刷新页面
				if(data.code==0){
					window.location.reload();
				}
			},2000)
		}
	);
}

修改 index.html​的内容,只在登录情况下,显示 我要发布​按钮:

<button type="button" class="btn btn-primary btn-sm position-absolute rt-0" data-toggle="modal" data-target="#publishModal" th:if="${loginUser!=null}">我要发布</button>

image

帖子详情

DiscussPostMapper​下新增一个方法 DiscussPost selectDiscussPostById(int id);​。

在​discusspost_mapper.xml​:

    <select id="selectDiscussPostById" resultType="DiscussPost">
        select <include refid="selectFields"></include>
        from discuss_post
        where id = #{id}
    </select>

DiscussPostController​:

    @RequestMapping(path = "/detail/{discussPostId}",method = RequestMethod.GET)
    public String getDiscussPost(@PathVariable("discussPostId")int discussPostId, Model model){
        //帖子
        DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
        model.addAttribute("post",post);
        //作者
        User user = userService.findUserById(post.getUserId());
        model.addAttribute("user",user);

        return "/site/discuss-detail";
    }

修改 index.html​,帖子详情链接跳转:<a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}" >备战春招,面试刷题跟他复习,一个月全搞定!

处理 discuss-detail.html​:

  • 使用模板引擎,修改相对路径,复用头部。
  • 修改帖子标题:<span th:utext="${post.title}">备战春招,面试刷题跟他复习,一个月全搞定!
  • 修改头像:​<img th:src="${user.headerUrl}" class="align-self-start mr-4 rounded-circle user-header" alt="用户头像" >
  • 修改用户名:<div class="mt-0 text-warning" th:utext="${user.username}">寒江雪
  • 修改发布时间:​发布于 <b th:text="${#dates.format(post.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18
  • 修改帖子正文:​<div class="mt-4 mb-3 content" th:utext="${post.content}">

image

事务管理

回顾

  • 什么是事务

    事务是由 N 步数据库操作序列组成的逻辑执行单元,这系列操作要么全执行,要么全放弃执行。

  • 事务的特性(ACID)

    • 原子性(Atomicity):事务是应用中不可再分的最小执行体。
    • 一致性(Consistency):事务执行的结果,须使数据从一个一致性状态,变为另一个一致性状态。
    • 隔离性(Isolation):各个事务的执行互不干扰,任何事务的内部操作对其他的事务都是隔离的。
    • 持久性(Durability):事务一旦提交,对数据所做的任何改变都要记录到永久存储器中。
  • 事务的隔离性

    • 常见的并发异常

      • 第一类丢失更新、第二类丢失更新。
      • 脏读、不可重复读、幻读。
    • 常见的隔离级别

      • Read Uncommitted:读取未提交的数据。
      • Read Committed:读取已提交的数据。
      • Repeatable Read:可重复读。
      • Serializable:串行化。
  • 丢失更新(重要):

    • image
    • image
    • image
    • image
    • image
  • 事务隔离级别:​

    • image(基本选第 2、3 种级别)

实现机制

  • 悲观锁(数据库)

    • 共享锁(S 锁) 事务 A 对某数据加了共享锁后,其他事务只能对该数据加共享锁,但不能加排他锁。
    • 排他锁(X 锁) 事务 A 对某数据加了排他锁后,其他事务对该数据既不能加共享锁,也不能加排他锁。
  • 乐观锁(自定义)

    • 版本号、时间戳等 在更新数据前,检查版本号是否发生变化。若变化则取消本次更新,否则就更新数据(版本号 +1)。

Spring 事务管理

  • 声明式事务

    • 通过 XML 配置,声明某方法的事务特征。
    • 通过注解,声明某方法的事务特征。
  • 编程式事务

    • 通过 TransactionTemplate 管理事务, 并通过它执行数据库的操作。

示例

声明式事务

AlphaService​:

    //REQUIRED:支持当前事务(外部事务,如果不存在则创建新事务)
    //REQUIRES_NEW:创建一个新事务,并且暂停当前事务(外部事务)
    //NESTED:如果当前存在事务(外部事务),则嵌套在该事务中执行)
    @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.valueOf("abc");

        return "ok";
    }

创建 TransactionTests​:

package com.nowcoder.community;

import com.nowcoder.community.service.AlphaService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes=CommunityApplication.class)
public class TransacationTests {

    @Autowired
    private AlphaService alphaService;

    @Test
    public void testSave1(){
        Object obj=alphaService.save1();
        System.out.println(obj);
    }
}

编程式事务

AlphaService​,注入 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.valueOf("abc");

                return "ok";
            }
        });
    }

测试:

    public void testSave2(){
        Object obj=alphaService.save2();
        System.out.println(obj);
    }

显示评论

创建相关结构

创建 Comment​类:

package com.nowcoder.community.entity;

import java.util.Date;

public class Comment {
    private int id;
    private int userId;
    private int entityType;
    private int entityId;
    private int targetId;
    private String content;
    private int status;
    private Date createTime;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public int getEntityType() {
        return entityType;
    }

    public void setEntityType(int entityType) {
        this.entityType = entityType;
    }

    public int getEntityId() {
        return entityId;
    }

    public void setEntityId(int entityId) {
        this.entityId = entityId;
    }

    public int getTargetId() {
        return targetId;
    }

    public void setTargetId(int targetId) {
        this.targetId = targetId;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    @Override
    public String toString() {
        return "Comment{" +
                "id=" + id +
                ", userId=" + userId +
                ", entityType=" + entityType +
                ", entityId=" + entityId +
                ", targetId=" + targetId +
                ", content='" + content + '\'' +
                ", status=" + status +
                ", createTime=" + createTime +
                '}';
    }
}

创建 CommentMapper​:

package com.nowcoder.community.dao;

import com.nowcoder.community.entity.Comment;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface CommentMapper {
    List<Comment> selectCommentByEntity(int entityType,int entityId,int offset,int limit);

    int selectCountByEntity(int entityType,int entityId);
}

创建 comment-mapper.xml​:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nowcoder.community.dao.CommentMapper">
    <sql id="selectFields">
        id,user_id,entity_type,entity_id,target_id,content,status,create_time
    </sql>

    <select id="selectCommentByEntity" resultType="Comment">
        select <include refid="selectFields"></include>
        from comment
        where status=0
        and entity_type=${entityType}
        and entity_id=#{entityId}
        order by create_time asc
        limit #{offset},#{limit}
    </select>

    <select id="selectCountByEntity" resultType="int">
        select count(id)
        from comment
        where status=0
        and entity_type=${entityType}
        and entity_id=#{entityId}
    </select>
</mapper>

业务实现

创建 CommentService​:

package com.nowcoder.community.service;

import com.nowcoder.community.dao.CommentMapper;
import com.nowcoder.community.entity.Comment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class CommentService {
    @Autowired
    private CommentMapper commentMapper;

    //根据帖子类型查询帖子
    public List<Comment> findCommentsByEntity(int entityType, int entityId, int offset, int limit){
        return commentMapper.selectCommentByEntity(entityType,entityId,offset,limit);
    }

    //查询帖子总数
    public int findCommentCount(int entityType,int entityId){
        return commentMapper.selectCountByEntity(entityType,entityId);
    }

}

定义一个常量:

    //实体类型:帖子
    int ENTITY_TYPE_POST=1;
    //实体类型:评论
    int ENTITY_TYPE_COMMENT=2;

DiscussPostController​:

    /**
     * 帖子详情+评论分页
     * @param discussPostId
     * @param model
     * @return
     */
    @RequestMapping(path = "/detail/{discussPostId}",method = RequestMethod.GET)
    public String getDiscussPost(@PathVariable("discussPostId")int discussPostId, Model model, Page page){
        //帖子
        DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
        model.addAttribute("post",post);
        //作者
        User user = userService.findUserById(post.getUserId());
        model.addAttribute("user",user);

        //评论分页信息
        page.setLimit(5);
        page.setPath("/discuss/detail/"+discussPostId);
        page.setRows(post.getCommentCount());

        //评论:给帖子的评论
        //回复:给评论的评论

        //评论列表
        List<Comment> commentList = commentService.findCommentsByEntity(ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());
        List<Map<String,Object>> commentVoList=new ArrayList<>();

        //评论Vo列表
        if(commentList!=null){
            for(Comment comment:commentList){
                //评论VO
                Map<String,Object> commentVo=new HashMap<>();
                //评论
                commentVo.put("comment",comment);
                //作者
                commentVo.put("user",userService.findUserById(comment.getUserId()));

                //回复列表,不分页显示,有多少显示多少
                List<Comment> replyList = commentService.findCommentsByEntity(ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE);

                //回复VO列表
                List<Map<String,Object>> replyVoList=new ArrayList<>();
                if(replyList!=null){
                    for(Comment reply:replyList){
                        Map<String,Object> replyVo=new HashMap<>();
                        //回复
                        replyVo.put("reply",reply);
                        //作者
                        replyVo.put("user",userService.findUserById(reply.getUserId()));

                        //回复目标
                        User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());
                        replyVo.put("target",target);

                        replyVoList.add(replyVo);

                    }
                }

                //将回复放入评论VO
                commentVo.put("replys",replyVoList);

                //回复数量
                int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());
                commentVo.put("replyCount",replyCount);

                //将每一条评论VO装到评论区里
                commentVoList.add(commentVo);
            }
        }

        model.addAttribute("comments",commentVoList);
        return "/site/discuss-detail";
    }

修改网页

index.html​:

  • 修改回帖数量:​<li class="d-inline ml-2">回帖 <span data-type="text">7</span>
  • 给分页取名:​<nav class="mt-5" th:if="${page.rows>0}" th:fragment="pagination">

discuss-detail.html​:

  • 删除第 2 条回帖。

  • 修改回帖数据:​<h6><b class="square"></b> <i th:text="${post.commentCount}">30</i>条回帖

  • 修改回复数据的遍历:​<li class="media pb-3 pt-3 mb-3 border-bottom" th:each="cvo:${comments}">

  • 修改评论用户头像:<img th:src="${cvo.user.headerUrl}" class="align-self-start mr-4 rounded-circle user-header" alt="用户头像" >

  • 修改评论用户名:​<span class="font-size-12 text-success" th:utext="${cvo.user.username}">掉脑袋切切

  • 显示层数:cvoStat​是 th 的一个隐含对象,可以获取到循环次数。

    <span class="badge badge-secondary float-right floor">
    									<i th:text="${page.offset+cvoStat.count}">1</i>#</span>
    
  • 修改评论内容:​<div class="mt-2" th:utext="${cvo.comment.content}">这开课时间是不是有点晚啊。。。

  • 修改时间:​<span>发布于 <b th:text="${#dates.format(cvo.comment.createTime,&#39;yyyy-MM-dd HH:mm:ss&#39;)}">2019-04-15 15:32:18</b>

  • 修改回复数量:​<li class="d-inline ml-2"><a href="#" class="text-primary">回复(<i th:text="${cvo.replyCount}"></i>)</a>

  • 删除第二条回复。

  • 修改回复的遍历:​<li class="pb-3 pt-3 mb-3 border-bottom" th:each="rvo:${cvo.replys}">

  • 修改回复列表显示:如果是直接评论楼主,则不显示回复谁,如果评论的其他人则显示回复谁:

    <span th:if="${rvo.target==null}">
    	<b class="text-info" th:text="${rvo.user.username}">寒江雪</b>:&nbsp;&nbsp;
    </span>
    <span th:if="${rvo.target!=null}">
    	<i class="text-info" th:text="${rvo.user.username}">Sissi</i>回复
    	<b class="text-info" th:text="${rvo.target.username}">寒江雪</b>:&nbsp;&nbsp;
    </span>
    
  • 修改回复内容:​<span th:utext="${rvo.reply.content}">这个是直播时间哈,觉得晚的话可以直接看之前的完整录播的~

  • 修改回复时间:​<span th:text="${#dates.format(rvo.reply.createTime,'yyyy-MM-dd HH:mm:ss')}" >2019-04-15 15:32:18

  • 修改回复框评论的绑定:image

    • <li class="d-inline ml-2"><a th:href="|#huifu-${rvoStat.count}|" data-toggle="collapse" class="text-primary">回复</a>

    • <div th:id="|huifu-${rvoStat.count}|" class="mt-4 collapse">

  • 复用首页的分页逻辑:​<nav class="mt-5" th:replace="index::pagination">​​

  • 修改作者下的回帖数量:​<li class="d-inline ml-2"><a href="#replyform" class="text-primary">回帖 <i th:text="${map.post.commentCount}">7</i></a>

​​image​​

添加评论

image

创建相关结构

CommentMapper​新增一个方法:​int insertComment(Comment comment);

在 mapper 下写:

    <insert id="insertComment" parameterType="comment">
        insert into comment (<include refid="insertFields"></include>)
        values(#{userId},#{entityType},#{entityId},#{targetId},#{content},#{status},#{createTime})
    </insert>

DiscussPostMapper​下新增一个方法:​int updateCommentCount(int id,int commentCount);

配置 mapper:

    <update id="updateCommentCount">
        update discuss_post
        set comment_count = #{commentCount}
        where id=#{id};
    </update>

业务实现

DiscussPostService​:

    public int updateCommentCount(int id,int commentCount){
        return discussPostMapper.updateCommentCount(id, commentCount);
    }

CommentService​:

    /**
     * 添加评论
     * @param comment
     * @return
     */
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
    public int addComment(Comment comment){
        if(comment==null){
            throw new IllegalArgumentException("参数不能为空");
        }
        //添加评论
        comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));
        comment.setContent(sensitiveFilter.filter(comment.getContent()));
        int rows=commentMapper.insertComment(comment);
        //更新帖子评论数量
        if(comment.getEntityType()==ENTITY_TYPE_POST){
            int count=commentMapper.selectCountByEntity(comment.getEntityType(),comment.getEntityId());
            discussPostService.updateCommentCount(comment.getEntityId(),count);
        }
        return rows;
    }

新建 CommentController​:

package com.nowcoder.community.controller;

import com.nowcoder.community.entity.Comment;
import com.nowcoder.community.service.CommentService;
import com.nowcoder.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.util.Date;

@Controller
@RequestMapping("/comment")
public class CommentController {
    @Autowired
    private CommentService commentService;

    @Autowired
    private HostHolder hostHolder;

    /**
     * 添加评论
     * @param discussPostId
     * @param comment
     * @return
     */
    @RequestMapping(path = "/add/{discussPostId}",method = RequestMethod.POST)
    public String addComment(@PathVariable("discussPostId")int discussPostId, Comment comment){
        comment.setUserId(hostHolder.getUser().getId());
        comment.setStatus(0);//有效
        comment.setCreateTime(new Date());
        commentService.addComment(comment);
        return "redirect:/discuss/detail/"+discussPostId;
    }
}

修改网页

discuss-detail.html​:

  • 修改提交路径:<form class="replyform" method="post" th:action="@{|/comment/add/${post.id}|">

  • 加别名:<textarea placeholder="在这里畅所欲言你的看法吧!" name=""content>

  • 帖子的评论:

    <!-- 回帖输入 -->
    <div class="container mt-3">
    	<form class="replyform" method="post" th:action="@{|/comment/add/${post.id}|}">
    		<p class="mt-3">
    			<a name="replyform"></a>
    			<textarea placeholder="在这里畅所欲言你的看法吧!" name="content"></textarea>
    			<input type="hidden" name="entityType" value="1">
    			<input type="hidden" name="entityId" th:value="${post.id}">
    		</p>
    		<p class="text-right">
    			<button type="submit" class="btn btn-primary btn-sm">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</button>
    		</p>
    	</form>
    </div>
    
  • 评论的直接评论:

    <!-- 回复输入框 -->
    <li class="pb-3 pt-3">
    	<form method="post" th:action="@{|/comment/add/${post.id}|}">
    		<div>
    			<input type="text" class="input-size" name="content" placeholder="请输入你的观点"/>
    			<input type="hidden" name="entityType" value="2">
    			<input type="hidden" name="entityId" th:value="${cvo.comment.id}">
    		</div>
    		<div class="text-right mt-2">
    			<button type="submit" class="btn btn-primary btn-sm" onclick="#">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</button>
    		</div>
    	</form>
    
  • 评论中的评论:

    <form method="post" th:action="@{|/comment/add/${post.id}|}">
    	<div>
    		<input type="text" class="input-size" name="content" th:placeholder="|回复${rvo.user.username}|"/>
    		<input type="hidden" name="entityType" value="2"/>
    		<input type="hidden" name="entityId" th:value="${cvo.comment.id}"/>
    		<input type="hidden" name="targetId" th:value="${rvo.user.id}"/>
    	</div>
    	<div class="text-right mt-2">
    		<button type="submit" class="btn btn-primary btn-sm" onclick="#">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</button>
    	</div>
    </form>
    

image

image

image

私信列表

image

image

如图所示,两条数据我们只视作一条,无论谁发给谁,都是一个对话。小的放前面,大的放后面,查询效率会高一点。

创建相关结构

entity​新建 Message​类:

package com.nowcoder.community.entity;

import java.util.Date;

public class Message {
    private int id;
    private int fromId;
    private int toId;

    @Override
    public String toString() {
        return "Message{" +
                "id=" + id +
                ", fromId=" + fromId +
                ", toId=" + toId +
                ", conversationId='" + conversationId + '\'' +
                ", content='" + content + '\'' +
                ", status=" + status +
                ", createTime=" + createTime +
                '}';
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getFromId() {
        return fromId;
    }

    public void setFromId(int fromId) {
        this.fromId = fromId;
    }

    public int getToId() {
        return toId;
    }

    public void setToId(int toId) {
        this.toId = toId;
    }

    public String getConversationId() {
        return conversationId;
    }

    public void setConversationId(String conversationId) {
        this.conversationId = conversationId;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    private String conversationId;
    private String content;
    private int status;
    private Date createTime;
}

创建 MessageMapper​:

package com.nowcoder.community.dao;

import com.nowcoder.community.entity.Message;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface MessageMapper {
    //查询当前用户的会话列表,针对每个会话只返回一条最新的私信
    List<Message> selectConversation(int userId, int offset, int limit);

    //查询当前用户的会话数量
    int selectConversationCount(int userId);

    //查询某个会话所包含的私信列表
    List<Message> selectLetters(String conversationId,int offset,int limit);

    //查询某个会话所包含的私信数量
    int selectLetterCount(String conversationId);

    //查询未读私信的数量
    int selectLetterUnreadCount(int userId,String conversationId);
}

配置 mapper:

from_id=1 是系统用户。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nowcoder.community.dao.MessageMapper">
    <sql id="selectFields">
        id,from_id,to_id,conversation_id,content,status,create_time
    </sql>

    <select id="selectConversations" resultType="Message">
        select <include refid="selectFields"></include>
        from message
        where id in(
            select max(id)
            from message
            where status != 2 and from_id != 1 and (from_id=#{userId} or to_id=#{userId})
            group by conversation_id
        )
        order by id desc
        limit #{offset},#{limit}
    </select>

    <select id="selectConversationCount" resultType="int">
        select count(m.maxid) from(
            select max(id) as maxid from message
            where status != 2 and from_id != 1 and (from_id=#{userId} or to_id=#{userId})
            group by conversation_id
        ) as m
    </select>

    <select id="selectLetters" resultType="Message">
        select <include refid="selectFields"></include>
        from message
        where status!=2 and from_id!=1 and conversation_id=#{conversationId}
        order by id desc
        limit #{offset},#{limit}
    </select>

    <select id="selectLetterCount" resultType="int">
        select count(id)
        from message
        where status!=2 and from_id!=1 and conversation_id=#{conversationId}
    </select>

    <select id="selectLetterUnreadCount" resultType="int">
        select count(id)
        from message
        where status=0 and from_id!=1 and to_id=#{userId}
        <if test="conversationId!=null">
            and conversation_id=#{conversationId}
        </if>
    </select>
</mapper>

写个测试:

    @Test
    public void testSelectLetters(){
        List<Message> list = messageMapper.selectConversations(111, 0, 20);
        for(Message message:list){
            System.out.println(message);
        }

        int count=messageMapper.selectConversationCount(111);
        System.out.println(count);

        list = messageMapper.selectLetters("111_112", 0, 10);
        for(Message message:list){
            System.out.println(message);
        }

        count=messageMapper.selectLetterCount("111_112");
        System.out.println(count);

        count=messageMapper.selectLetterUnreadCount(131,"111_131");
        System.out.println(count);
    }

业务实现

创建 MessageService​:

package com.nowcoder.community.service;

import com.nowcoder.community.dao.MessageMapper;
import com.nowcoder.community.entity.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class MessageService {
    @Autowired
    public MessageMapper messageMapper;

    public List<Message> findConversations(int userId,int offset,int limit){
        return messageMapper.selectConversations(userId,offset,limit);
    }

    public int findConversationCount(int userId){
        return messageMapper.selectConversationCount(userId);
    }

    public List<Message> findLetters(String conversationId,int offset,int limit){
        return messageMapper.selectLetters(conversationId,offset,limit);
    }

    public int findLetterCount(String conversationId){
        return messageMapper.selectLetterCount(conversationId);
    }

    public int findLetterUnreadCount(int userId,String conversationId){
        return messageMapper.selectLetterUnreadCount(userId,conversationId);
    }
}

创建 MessageController​:

package com.nowcoder.community.controller;

import com.nowcoder.community.entity.Message;
import com.nowcoder.community.entity.Page;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.MessageService;
import com.nowcoder.community.service.UserService;
import com.nowcoder.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Controller
public class MessageController {
    @Autowired
    private MessageService messageService;

    @Autowired
    private HostHolder hostHolder;

    @Autowired
    private UserService userService;

    //私信列表
    @RequestMapping(path = "/letter/list",method = RequestMethod.GET)
    public String getLetterList(Model model, Page page){
        User user = hostHolder.getUser();
        //分页信息
        page.setLimit(5);
        page.setPath("/letter/list");
        page.setRows(messageService.findConversationCount(user.getId()));//会话总数

        //会话列表
        List<Message> conversationList = messageService.findConversations(user.getId(), page.getOffset(), page.getLimit());
        List<Map<String,Object>> conversations=new ArrayList<>();
        if(conversationList!=null){
            for(Message message:conversationList){
                Map<String,Object> map=new HashMap<>();
                map.put("conversation",message);
                map.put("letterCount",messageService.findLetterCount(message.getConversationId()));
                map.put("unreadCount",messageService.findLetterUnreadCount(user.getId(),message.getConversationId()));
                int targetId= user.getId()==message.getFromId()? message.getToId() : message.getFromId();
                map.put("target",userService.findUserById(targetId));
                conversations.add(map);
            }
        }
        model.addAttribute("conversations",conversations);

        //查询未读消息数量
        int letterUnreadCount= messageService.findLetterUnreadCount(user.getId(), null);
        model.addAttribute("letterUnreadCount",letterUnreadCount);

        return "/site/letter";
    }
}

修改网页

index.html​:

  • 修改信息的链接:​<a class="nav-link position-relative" th:href="@{/letter/list}">消息<span data-type="text">12</span>

letter.html​:

  • 使用模板引擎,修改相对路径,,复用头部。

  • 删除多余的私信列表,只保留一个 li。

  • 修改朋友私信链接,显示私信数量:<a class="nav-link position-relative active" th:href="@{/letter/list}">朋友私信<span class="badge badge-danger" th:text="${letterUnreadCount}" th:if="${letterUnreadCount!=0}">3</span></a>

  • 更改私信会话的遍历:<li class="media pb-3 pt-3 mb-3 border-bottom position-relative" th:each="map:${conversations}">

  • 更改某个会话的未读数量:​<span class="badge badge-danger" th:text="${map.unreadCount}" th:if="${map.unreadCount!=0}">3

  • 更改会话用户头像:<img th:src="${map.target.headerUrl}" class="mr-4 rounded-circle user-header" alt="用户头像" >

  • 更改某个会话相关信息:

    <div class="media-body">
    	<h6 class="mt-0 mb-3">
    		<span class="text-success" th:utext="${map.target.username}">落基山脉下的闲人</span>
    		<span class="float-right text-muted font-size-12" th:text="${#dates.format(map.conversation.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-28 14:13:25</span>
    	</h6>
    	<div>
    		<a href="letter-detail.html" th:utext="${map.conversation.content}">米粉车, 你来吧!</a>
    		<ul class="d-inline font-size-12 float-right">
    			<li class="d-inline ml-2"><a href="#" class="text-primary"><i th:text="${map.letterCount}">5</i>条会话</a></li>
    		</ul>
    	</div>
    </div>
    
  • 复用 index 的分页。

image

私信会话查看

    //私信详情
    @RequestMapping(path = "/letter/detail/{conversationId}",method = RequestMethod.GET)
    public String getLetterDetail(@PathVariable("conversationId") String conversationId,Model model, Page page){
        //分页信息
        page.setLimit(5);
        page.setPath("/letter/detail/"+conversationId);
        page.setRows(messageService.findLetterCount(conversationId));//会话总数

        //私信列表
        List<Message> letterList = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());
        List<Map<String,Object>> letters=new ArrayList<>();
        if(letterList!=null){
            for(Message message:letterList){
                Map<String,Object> map=new HashMap<>();
                map.put("letter",message);
                map.put("fromUser",userService.findUserById(message.getFromId()));
                letters.add(map);
            }
        }
        model.addAttribute("letters",letters);

        //查询私信目标
        model.addAttribute("target",getLetterTarget(conversationId));
        return "/site/letter-detail";
    }

    private User getLetterTarget(String conversationId){
        String[] ids=conversationId.split("_");
        int id0=Integer.parseInt(ids[0]);
        int id1=Integer.parseInt(ids[1]);
        if(hostHolder.getUser().getId()==id0){
            return userService.findUserById(id1);
        }else{
            return userService.findUserById(id0);
        }
    }

修改网页

letter.html​:

  • 修改私信详情链接:<a th:href="@{|/letter/detail/${map.conversation.conversationId}|}" th:utext="${map.conversation.content}">米粉车, 你来吧!

letter-detail.html​:

  • 使用模板引擎,修改相对路径,,复用头部。

  • 更改来自谁的用户私信:<h6><b class="square"></b> 来自 <i class="text-success" th:utext="${target.username}">落基山脉下的闲人</i> 的私信

  • 私信列表只保留一条 li。

  • 更改私信列表:

    <!-- 私信列表 -->
    <ul class="list-unstyled mt-4">
    	<li class="media pb-3 pt-3 mb-2" th:each="map:${letters}">
    		<a href="profile.html">
    			<img th:src="${map.fromUser.headerUrl}" class="mr-4 rounded-circle user-header" alt="用户头像" >
    		</a>
    		<div class="toast show d-lg-block" role="alert" aria-live="assertive" aria-atomic="true">
    			<div class="toast-header">
    				<strong class="mr-auto" th:utext="${map.fromUser.username}">落基山脉下的闲人</strong>
    				<small th:text="${#dates.format(map.letter.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-25 15:49:32</small>
    				<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
    					<span aria-hidden="true">&times;</span>
    				</button>
    			</div>
    			<div class="toast-body" th:utext="${map.letter.content}">
    				君不见, 黄河之水天上来, 奔流到海不复回!
    			</div>
    		</div>
    	</li>
    </ul>
    
  • 复用分页。

  • 更改返回链接:<button type="button" class="btn btn-secondary btn-sm" onclick="back();">返回</button>

  • 加个 js:

    <script>
    	function back(){
    		location.href=CONTEXT_PATH+"/letter/list";
    	}
    </script>
    

image

发送私信

image

创建相关结构

MessageMapper​:

    //新增信息
    int insertMessage(Message message);

    //修改信息的状态
    int updateStatus(List<Integer> ids,int status);

配置 mapper:

    <sql id="insertFields">
        from_id,to_id,conversation_id,content,status,create_time
    </sql>
    <insert id="insertMessage" parameterType="Message" keyProperty="id">
        insert into message(<include refid="insertFields"></include>)
        values (#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime})
    </insert>

    <update id="updateStatus">
        update message set status = #{status}
        where id in
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
    </update>

在​UserService​:

    /**
     * 根据用户名查找用户
     * @return
     */
    public User findUserByName(String username){
        return userMapper.selectByName(username);
    }

业务实现

MessageService​:

    public int addMessage(Message message){
        message.setContent(HtmlUtils.htmlEscape(message.getContent()));
        message.setContent(sensitiveFilter.filter(message.getContent()));
        return messageMapper.insertMessage(message);
    }

    public int readMessage(List<Integer> ids){
        return messageMapper.updateStatus(ids,1);
    }

MessageController​:

    /**
     * 发送私信
     * @param toName
     * @param content
     * @return
     */
    @RequestMapping(path = "/letter/send",method = RequestMethod.POST)
    @ResponseBody
    public String sendLetter(String toName,String content){
        User target = userService.findUserByName(toName);
        if(target==null){
            return CommunityUtil.getJSONString(1,"目标用户不存在!");
        }
        Message message=new Message();
        message.setFromId(hostHolder.getUser().getId());
        message.setToId(target.getId());
        if(message.getFromId()<message.getToId()){
            message.setConversationId(message.getFromId()+"_"+message.getToId());
        }else {
            message.setConversationId(message.getToId()+"_"+message.getFromId());
        }
        message.setContent(content);
        message.setCreateTime(new Date());
        messageService.addMessage(message);

        return CommunityUtil.getJSONString(0);
    }

修改网页

letter.js​:

function send_letter() {
	$("#sendModal").modal("hide");

	var toName=$("#recipient-name").val();
	var content=$("#message-text").val();
	$.pos(
		CONTEXT_PATH+"/letter/send",
		{"toName":toName,"content":content},
		function (data){
			data=$.parseJSON(data);
			if (data.code==0){
				$("#hintBody").text("发送成功!");
			}else {
				$("#hintBody").text(data.msg);
			}

			//刷新
			$("#hintModal").modal("show");
			setTimeout(function (){
				$("#hintModal").modal("hide");
				location.reload();
			},2000);
		}
	);
}

letter-detail.html​:

  • 更改发私信弹窗的私信人用户名:​<input type="text" class="form-control" id="recipient-name" th:value="${target.username}">

image

image

设置已读私信

    /**
     * 获得未读消息id
     * @param letterList
     * @return
     */
    private List<Integer> getLetterIds(List<Message> letterList){
        List<Integer> ids=new ArrayList<>();
        if(letterList!=null){
            for(Message message:letterList){
                if(hostHolder.getUser().getId()==message.getToId()&&message.getStatus()==0){
                    ids.add(message.getId());
                }
            }
        }
        return ids;
    }

    /**
     * 私信详情
     * @param conversationId
     * @param model
     * @param page
     * @return
     */
    @RequestMapping(path = "/letter/detail/{conversationId}",method = RequestMethod.GET)
    public String getLetterDetail(@PathVariable("conversationId") String conversationId,Model model, Page page){
        //分页信息
        page.setLimit(5);
        page.setPath("/letter/detail/"+conversationId);
        page.setRows(messageService.findLetterCount(conversationId));//会话总数

        //私信列表
        List<Message> letterList = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());
        List<Map<String,Object>> letters=new ArrayList<>();
        if(letterList!=null){
            for(Message message:letterList){
                Map<String,Object> map=new HashMap<>();
                map.put("letter",message);
                map.put("fromUser",userService.findUserById(message.getFromId()));
                letters.add(map);
            }
        }
        model.addAttribute("letters",letters);

        //查询私信目标
        model.addAttribute("target",getLetterTarget(conversationId));

        //设置已读
        List<Integer> ids=getLetterIds(letterList);
        if(!ids.isEmpty()){
            messageService.readMessage(ids);
        }
        return "/site/letter-detail";
    }

image

统一处理异常

image

  • @ControllerAdvice

    • 用于修饰类,表示该类是 Controller 的全局配置类。
    • 在此类中,可以对 Controller 进行如下三种全局配置:
      异常处理方案、绑定数据方案、绑定参数方案。
  • @ExceptionHandler

    • 用于修饰方法,该方法会在 Controller 出现异常后被调用,用于处理捕获到的异常。
  • @ModelAttribute

    • 用于修饰方法,该方法会在 Controller 方法执行前被调用,用于为 Model 对象绑定参数。
  • @DataBinder

    • 用于修饰方法,该方法会在 Controller 方法执行前被调用,用于绑定参数的转换器。

预准备

error 文件夹必须放到 templates 下,文件下的 html 名称都是错误码。

image

修改 404、500 网页,使用 th 引擎,修改相对路径。

image​​

代码实现

在​HomeController​:

    @RequestMapping(path = "/error",method = RequestMethod.GET)
    public String getErrorPage(){
        return "/error/500";
    }

Controller​下创建一个 advice​包,再创建 ExceptionAdvice​,

package com.nowcoder.community.controller.advice;

import com.nowcoder.community.util.CommunityUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@Slf4j
@ControllerAdvice(annotations = Controller.class)
public class ExceptionAdvice {

    @ExceptionHandler(Exception.class)
    public void handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
        log.error("服务器发送异常:"+e.getMessage());
        for(StackTraceElement element:e.getStackTrace()){
            log.error(element.toString());
        }

        //判断是主动请求还是异步请求
        String xRequestedWith = request.getHeader("x-requested-with");
        //异步请求
        if("XMLHttpRequest".equals(xRequestedWith)){
            response.setContentType("application/plain;charset=utf-8");
            //确保响应的内容以纯文本形式传输,并且使用UTF-8编码。

            PrintWriter writer=response.getWriter();

            writer.write(CommunityUtil.getJSONString(1,"服务器异常!"));
            //获取一个JSON格式的字符串,并将该字符串写入响应流中。这样可以将一个包含错误信息的JSON字符串作为响应返回给客户端。
        }else{
            //主动请求
            response.sendRedirect(request.getContextPath()+"/error");
        }

    }

}

image

统一记录日志

image

概念

AOP 的概念 ​

  • Aspect Oriented Programing, 即面向方面(切面)编程。

  • AOP 是一种编程思想,是对 OOP 的补充, 可以进一步提高编程的效率。

image

AOP 的术语

image

AOP(面向切面编程)是一种编程范式,它通过在程序中定义切面(Aspect)来实现横切关注点(Cross-cutting Concerns)的模块化。以下是一些 AOP 的术语:

  1. 切面(Aspect):切面是一个模块,它包含了与横切关注点相关的一组通用行为。它定义了在哪些地方(连接点)以及如何(通知)执行这些行为。例如,日志记录、异常处理等可以作为一个切面。
  2. 连接点(Join Point):连接点是在程序执行过程中可以插入切面的点。它可以表示方法调用、方法执行、异常抛出等。例如,在方法执行前或执行后的连接点都可以作为切面的插入点。
  3. 通知(Advice):通知定义了在连接点处执行的切面代码。它表示了切面在连接点的何时和如何执行。常见的通知类型有前置通知(Before)、后置通知(After)、环绕通知(Around)、异常通知(AfterThrowing)和返回通知(AfterReturning)。
  4. 切入点(Pointcut):切入点定义了在哪些连接点上应用切面。它通过表达式或者指定的规则来匹配连接点。例如,匹配所有以"get"开头的方法调用的切入点可以用表达式 execution(* get*(..))​来表示。
  5. 引入(Introduction):引入允许在不修改现有代码的情况下,向现有类添加新的方法或属性。它可以使现有的类实现新的接口或继承新的父类。引入可以用于为现有类添加新的功能。
  6. 织入(Weaving):织入是将切面应用到目标对象上,使切面的行为与目标对象的代码进行合并的过程。它可以在编译时、加载时或运行时进行。织入可以通过编译器、类加载器或使用代理对象实现。

例如,假设我们有一个 Web 应用程序,我们想要记录每个请求的执行时间。我们可以创建一个切面来实现这个功能。以下是一个使用 AOP 术语描述的例子:

  1. 切面:日志记录切面。
  2. 连接点:所有 Controller 类的公共方法。
  3. 通知:前置通知,在连接点处记录请求的开始时间。
  4. 切入点:匹配所有 Controller 类的公共方法。

AOP 的实现

  • AspectJ

    • AspectJ 是语言级的实现,它扩展了 Java 语言,定义了 AOP 语法。
    • AspectJ 在编译期织入代码,它有一个专门的编译器,用来生成遵守 Java 字节码规范的 class 文件。
  • Spring AOP

    • Spring AOP 使用纯 Java 实现,它不需要专门的编译过程,也不需要特殊的类装载器。
    • Spring AOP 在运行时通过代理的方式织入代码,只支持方法类型的连接点。
    • Spring 支持对 AspectJ 的集成。

Spring AOP

  • JDK 动态代理

    • Java 提供的动态代理技术,可以在运行时创建接口的代理实例。
    • Spring AOP 默认采用此种方式,在接口的代理实例中织入代码。
  • CGLib 动态代理(写的 service 没有接口)

    • 采用底层的字节码技术,在运行时创建子类代理实例。
    • 当目标对象不存在接口时,Spring AOP 会采用此种方式,在子类实例中织入代码

示例

项目下创建 aspect​包,创建 AlphaAspect​类:

package com.nowcoder.community.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class AlphaAspect {

    @Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
    public void pointcut(){

    }
    @Before("pointcut()")
    public void before(){
        System.out.println("before");
    }

    @After("pointcut()")
    public void after(){
        System.out.println("after");
    }

    @AfterReturning("pointcut()")
    public void afterReturning(){
        System.out.println("afterReturning");
    }

    @AfterThrowing("pointcut()")
    public void afterThrowing(){
        System.out.println("afterThrowing");
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
        System.out.println("around before");
        Object obj=joinPoint.proceed();//调用原始对象的方法,代理它允许
        System.out.println("around after");
        return obj;
    }
}

image

记录日志

创建 ServiceLogAspect​类:

package com.nowcoder.community.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.Date;

@Slf4j
@Component
@Aspect
public class ServiceLogAspect {
    @Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
    public void pointcut(){

    }

    @Before("pointcut()")
    public void before(JoinPoint joinPoint){
        //用户[1.2.3.4] 在[xxx],访问了[com.nowcoder.community.service.xxx()].
        ServletRequestAttributes attributes= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request=attributes.getRequest();
        String ip=request.getRemoteHost();
        String now=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        //获取方法
        String target=joinPoint.getSignature().getDeclaringTypeName()+"."+joinPoint.getSignature().getName();
        log.info(String.format("用户[%s],在[%s],访问了[%s]",ip,now,target));
    }
}

image

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3186 引用 • 8212 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...