【牛客讨论区】第三章:开发社区核心功能

2023-11-12

1. 过滤敏感词

在这里插入图片描述


resources目录下新建 sensitive-words.txt,随便写几个敏感词

新建敏感词过滤器

package com.nowcoder.community.util;

import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

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

@Component
public class SensitiveFilter { //适合单例模式,所以交给spring托管

    public static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);

    //替换符
    private static final String REPLACEMENT = "***";

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

    @PostConstruct //构造器之后就执行
    public void init() {
        //若是Ctrl+F9,sensitive-words.txt没被编译,可以执行maven命令 clean-compile
        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 (IOException e) {
            logger.error("加载敏感词文件失败:" + e.getMessage());
        }
    }

    //将一个敏感词添加到前缀树
    private void addKeyWord(String keyWord) {
        TrieNode tmp = rootNode;//指针
        for (int i = 0; i < keyWord.length(); i++) {
            char c = keyWord.charAt(i);
            TrieNode subNode = tmp.getSubNode(c);
            if (subNode == null) {
                //初始化子节点
                subNode = new TrieNode();
                tmp.addSubNode(c, subNode);
            }
            //移动指针,指向子节点
            tmp = subNode;
            //设置结束标志
            if (i == keyWord.length() - 1) {
                tmp.setKeyWordsEnd(true);
            }
        }
    }

    /**
     * 过滤敏感词
     * @param text 待过滤文本
     * @return 过滤后的文本
     */
    public String sensitiveWordFilter(String text) {
        //若是空字符串 返回空
        if (StringUtils.isBlank(text)) {
            return null;
        }
        // 根节点
        // 每次在目标字符串中找到一个敏感词,完成替换之后,都要再次从根节点遍历树开始一次新的过滤
        TrieNode tempNode = rootNode;
        // begin指针作用是目标字符串每次过滤的开头
        int begin = 0;
        // position指针的作用是指向待过滤的字符
        // 若position指向的字符是敏感词的结尾,那么text.subString(begin,position+1)就是一个敏感词
        int position = 0;
        //过滤后的结果
        StringBuilder result = new StringBuilder();

        //开始遍历 position移动到目标字符串尾部是 循环结束
        while (position < text.length()) {
            // 最开始时begin指向0 是第一次过滤的开始
            // position也是0
            char c = text.charAt(position);

            //忽略用户故意输入的符号 例如嫖※娼 忽略※后 前后字符其实也是敏感词
            if (isSymbol(c)) {
                //判断当前节点是否为根节点
                //若是根节点 则代表目标字符串第一次过滤或者目标字符串中已经被遍历了一部分
                //因为每次过滤掉一个敏感词时,都要将tempNode重新置为根节点,以重新去前缀树中继续过滤目标字符串剩下的部分
                //因此若是根节点,代表依次新的过滤刚开始,可以直接将该特殊符号字符放入到结果字符串中
                if (tempNode == rootNode) {
                    //将用户输入的符号添加到result中
                    result.append(c);
                    //此时将单词begin指针向后移动一位,以开始新的一个单词过滤
                    begin++;
                }
                //若当前节点不是根节点,那说明符号字符后的字符还需要继续过滤
                //所以单词开头位begin不变化,position向后移动一位继续过滤
                position++;
                continue;
            }
            //判断当前节点的子节点是否有目标字符c
            tempNode = tempNode.getSubNode(c);
            //如果没有 代表当前beigin-position之间的字符串不是敏感词
            // 但begin+1-position却不一定不是敏感词
            if (tempNode == null) {
                //所以只将begin指向的字符放入过滤结果
                result.append(text.charAt(begin));
                //position和begin都指向begin+1
                position = ++begin;
                //再次过滤
                tempNode = rootNode;
            } else if (tempNode.isKeyWordsEnd()) {
                //如果找到了子节点且子节点是敏感词的结尾
                //则当前begin-position间的字符串是敏感词 将敏感词替换掉
                result.append(REPLACEMENT);
                //begin移动到敏感词的下一位
                begin = ++position;
                //再次过滤
                tempNode = rootNode;
                //&& begin < position - 1
            } else if (position + 1 == text.length()) {
                //特殊情况
                //虽然position指向的字符在树中存在,但不是敏感词结尾,并且position到了目标字符串末尾(这个重要)
                //因此begin-position之间的字符串不是敏感词 但begin+1-position之间的不一定不是敏感词
                //所以只将begin指向的字符放入过滤结果
                result.append(text.charAt(begin));
                //position和begin都指向begin+1
                position = ++begin;
                //再次过滤
                tempNode = rootNode;
            } else {
                //position指向的字符在树中存在,但不是敏感词结尾,并且position没有到目标字符串末尾
                position++;
            }
        }
        return begin < text.length() ? result.append(text.substring(begin)).toString() : result.toString();
    }

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

    //前缀树
    private class TrieNode{
        //单词结尾
        private boolean isKeyWordsEnd = false;
        //当前节点的子节点,key是下级字符,value是下级节点
        private Map<Character, TrieNode> subNodes = new HashMap<>();

        public void setKeyWordsEnd(boolean keyWordsEnd) {
            isKeyWordsEnd = keyWordsEnd;
        }

        public boolean isKeyWordsEnd() {
            return isKeyWordsEnd;
        }

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

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

写个测试类

package com.nowcoder.community;

import com.nowcoder.community.util.SensitiveFilter;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest
public class SensitiveTests {

    @Resource
    private SensitiveFilter sensitiveFilter;

    @Test
    public void testSensitiveFilter() {
        String text = "这里可以赌博,可以吸毒,可以fuck!";
        String res = sensitiveFilter.sensitiveWordFilter(text);
        System.out.println(res);
    }
}

效果:

在这里插入图片描述

2. 发布帖子

pom.xml

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.47</version>
</dependency>

CommunityUtil 类增加方法

public static String getJSONString(int code, String msg, Map<String, Object> map) {
    JSONObject json = new JSONObject();
    json.put("code", code);
    json.put("msg", msg);
    if (map != null) {
        for (String key : map.keySet()) {
            json.put(key, map.get(key));
        }
    }
    return json.toJSONString();
}

public static String getJSONString(int code, String msg) {
    return getJSONString(code, msg, null);
}

public static String getJSONString(int code) {
    return getJSONString(code, null, null);
}

2.1 dao

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>

2.2 service

DiscussPostService 增加方法

@Resource
private SensitiveFilter sensitiveFilter;

public int addDiscussPost(DiscussPost post) {
    if (post == null) {
        throw new IllegalArgumentException("参数不能为空!");
    }
    //转义html标记 ,防止title中出现特殊标签
    post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));
    post.setContent(HtmlUtils.htmlEscape(post.getContent()));
    //过滤敏感词
    post.setTitle(sensitiveFilter.sensitiveWordFilter(post.getTitle()));
    post.setContent(sensitiveFilter.sensitiveWordFilter(post.getContent()));

    return discussPostMapper.insertDiscussPost(post);
}

2.3 controller

package com.nowcoder.community.controller;

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

import javax.annotation.Resource;
import java.util.Date;

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

    @Resource
    private HostHolder hostHolder;

    @PostMapping("/add")
    @ResponseBody
    public String addDiscussPost(String title, String content) {
        User user = hostHolder.getUser();
        if (user == null) {
            return CommunityUtil.getJSONString(403, "你还没有登录哦!");
        }
        DiscussPost discussPost = new DiscussPost();
        discussPost.setUserId(user.getId());
        discussPost.setTitle(title);
        discussPost.setContent(content);
        discussPost.setCreateTime(new Date());
        discussPostService.addDiscussPost(discussPost);
        //报错的情况,将来统一处理
        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
	$.post( //Ajax的三个参数
		CONTEXT_PATH + "/discuss/add", //请求路径
		{"title":title, "content":content}, //请求携带的数据
		function(data) { //回调函数,请求路径的方法返回值会封装到data中
			data = $.parseJSON(data);
			//在提示框中显示返回的消息
			$("#hintModalLabel").text(data.msg);
			//显示提示框
			$("#hintModal").modal("show");
			//两秒后自动隐藏
			setTimeout(function(){
				$("#hintModal").modal("hide");
				//刷新页面
				if (data.code == 0) {//如果状态码是0,表示成功发布帖子
					window.location.reload();
				}
			}, 2000);
		}
	)
}

index.html
未登陆时,不显示 “我要发布”
74行

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

启动,测试:登录账号之后点击“我要发布”

在这里插入图片描述

后台数据库:

在这里插入图片描述

3. 帖子详情

DiscussPostMapper 类新增查询方法

DiscussPost selectDiscussById(int id);

discusspost-mapper.xml

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

DiscussPostService 类新增方法

public DiscussPost findDiscussPostById(int id) {
    return discussPostMapper.selectDiscussById(id);
}

DiscussPostController 类新增方法

@Resource
private UserService userService;

@GetMapping("/detail/{discussPostId}")
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
127行

<a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}">备战春招,面试刷题跟他复习,一个月全搞定!</a>

site / discuss-detail.html
第2行

<html lang="en" xmlns:th="http://www.thymeleaf.org">

8-9行

<link rel="stylesheet" th:href="@{/css/global.css}" />
<link rel="stylesheet" th:href="@{/css/discuss-detail.css}" />

最后一个 srcipt 标签

<script th:src="@{/js/global.js}"></script>

15行

<!-- 头部 -->
<header class="bg-dark sticky-top" th:replace="index::header">

69行

<span th:utext="${post.title}">备战春招,面试刷题跟他复习,一个月全搞定!</span>

79行

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

82行

<div class="mt-0 text-warning" th:utext="${user.username}">寒江雪</div>

84行

发布于 <b th:text="${#dates.format(post.createTime, 'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</b>

94行

<!-- 正文 -->
<div class="mt-4 mb-3 content" th:utext="${post.content}">

启动程序,测试,点击之前发布的帖子:

在这里插入图片描述

4. 事务管理

在这里插入图片描述

在这里插入图片描述


第一类丢失更新
某一个事务的回滚, 导致另外一个事务已更新的数据丢失了

第二类丢失更新
某一个事务的提交, 导致另外一个事务已更新的数据丢失了。

脏读
某一个事务, 读取了另外一个事务未提交的数据。

不可重复读
某一个事务, 对同一个数据前后读取的结果不一致。

幻读
某一个事务, 对同一个表前后查询到的行数不一致。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


5. 显示评论

数据层

  • 根据实体查询一页评论数据。
  • 根据实体查询评论的数量。

业务层

  • 处理查询评论的业务。
  • 处理查询评论数量的业务。

表现层

  • 显示帖子详情数据时, 同时显示该帖子所有的评论数据。

5.1 实体类

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 +
                '}';
    }
}

5.2 dao

CommentMapper

package com.nowcoder.community.dao;

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

import java.util.List;

@Mapper
public interface CommentMapper {

    List<Comment> selectCommentsByEntity(@Param("entityType") int entityType,
                                         @Param("entityId") int entityId,
                                         @Param("offset") int offset,
                                         @Param("limit") int limit);

    int selectCountByEntity(@Param("entityType") int entityType, @Param("entityId") 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="selectCommentsByEntity" resultType="Comment">
        select <include refid="selectFields"/>
        from comment
        where status = 0
        and entity_type = #{entityType}
        order by create_time asc
        limit #{offset}, #{limit}
    </select>

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

</mapper>

5.3 service

package com.nowcoder.community.service;

import com.nowcoder.community.dao.CommentMapper;
import com.nowcoder.community.entity.Comment;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service
public class CommentService {

    @Resource
    private CommentMapper commentMapper;

    public List<Comment> findCommentsByEntity(int entityType, int entityId, int offset, int limit) {
        return commentMapper.selectCommentsByEntity(entityType, entityId, offset, limit);
    }

    public int findCommentCount(int entityType, int entityId) {
        return commentMapper.selectCountByEntity(entityType, entityId);
    }
}

5.4 controller

CommunityConstant 接口增加两个常量

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

DiscussPostController 类
实现该接口

public class DiscussPostController implements CommunityConstant {}

注入属性

@Resource
private CommentService commentService;

完善方法 getDiscussPost()

@GetMapping("/detail/{discussPostId}")
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());
    //评论VO列表
    List<Map<String, Object>> commentVoList = new ArrayList<>();
    if (commentList != null) {
        for (Comment comment : commentList) {
            //评论VO
            Map<String, Object> commentVo = new HashMap<>();
            //往VO中添加评论
            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);
                }
            }
            commentVo.put("replys", replyVoList);
            //回复的数量
            int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());
            commentVo.put("replyCount", replyCount);
            commentVoList.add(commentVo);
        }
    }
    model.addAttribute("comments", commentVoList);
    return "site/discuss-detail";
}

修改 index.html
136行

<li class="d-inline ml-2">回帖 <span th:text="${map.post.commentCount}">7</span></li>

site / discuss-detail.html
删掉 197-231 行的 “第2条回帖” 的 li 标签

108行

<h6><b class="square"></b> <i th:text="${post.commentCount}">30</i>条回帖</h6>

117行

<li class="media pb-3 pt-3 mb-3 border-bottom" th:each="cvo:${comments}">

119行

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

123行

<span class="font-size-12 text-success" th:utext="${cvo.user.username}">掉脑袋切切</span>

124行 span 标签

<span class="badge badge-secondary float-right floor">
	<i th:text="${page.offset + cvoStat.count}">1</i>>#
</span>

128行

<div class="mt-2" th:utext="${cvo.comment.content}">

132行

<span>发布于 <b th:text="${#dates.format(cvo.comment.createTime, 'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</b></span>

136行

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

删掉 164-186 “第2条回复” li 标签

142行

<!-- 第1条回复 -->
<li class="pb-3 pt-3 mb-3 border-bottom" th:each="rvo:${cvo.replys}">

143行的 div 标签

<div>
	<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>
</div>

154行

<span th:text="${#dates.format(rvo.reply.createTime, 'yyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</span>

158行

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

160行

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

index.html 的分页已经写完了,可以拿过来直接用,因此需要起个名字
index.html 143行

<!-- 分页 -->
<nav class="mt-5" th:if="${page.rows > 0}" th:fragment="pagination">

回到 discuss-detail.html
185行

<!-- 分页 -->
<nav class="mt-5" th:replace="index::pagination">

88行

<li class="d-inline ml-2"><a href="#replyform" class="text-primary">回帖 <i th:text="${post.commentCount}">7</i></a></li>

162行

<input type="text" class="input-size" th:placeholder="|回复${rvo.user.username}|"/>

启动,测试,随便点一条帖子查看。


在这里插入图片描述

【bug】


在这里插入图片描述


这俩值应该保持一致,都没有回帖,哪里来的 1楼?

从逻辑分析,到最后发现是 comment-mapper.xml 写错了:
看视频的时候一没留神就写漏了…

在这里插入图片描述

修正之后效果:

在这里插入图片描述

6. 添加评论

6.1 dao

CommentMapper 接口类增加方法

int insertComment(Comment comment);

comment-mapper.xml 实现该方法

<sql id="insertFields">
    user_id, entity_type, entity_id, target_id, content, status, create_time
</sql>

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

DiscussPostMapper 增加方法

int updateCommentCount(@Param("id") int id, @Param("commentCount") int commentCount);

discusspost-mapper.xml 实现该方法

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

6.2 service

DiscussPostService 类增加方法

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

CommentService 类

//实现常量接口
public class CommentService implements CommunityConstant {

@Resource
private SensitiveFilter sensitiveFilter;

@Resource
private DiscussPostService discussPostService;

@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.sensitiveWordFilter(comment.getContent()));
    int rows = commentMapper.insertComment(comment);
    //更新帖子的评论数量
    if (comment.getEntityType() == ENTITY_TYPE_POST) {
        int count = commentMapper.selectCountByEntity(ENTITY_TYPE_POST, comment.getEntityId());
        discussPostService.updateCommentCount(comment.getEntityId(), count);
    }
    return rows;
}

6.3 controller

新建一个 controller

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.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.Resource;
import java.util.Date;

@Controller
@RequestMapping("/comment")
public class CommentController {

    @Resource
    private CommentService commentService;

    @Resource
    private HostHolder hostHolder;

    @PostMapping("/add/{discussPostId}")
    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
200行

<!-- 回帖输入 -->
<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>

172行

<!-- 回复输入框 -->
<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>
</li>

160行

<div th:id="|huifu-${rvoStat.count}|" class="mt-4 collapse">
	<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>
</div>

7. 私信列表

私信列表

  • 查询当前用户的会话列表, 每个会话只显示一条最新的私信。
  • 支持分页显示。

私信详情

  • 查询某个会话所包含的私信。
  • 支持分页显示

对应message

7.1 entity

package com.nowcoder.community.entity;

import java.util.Date;

public class Message {

    private int id;
    private int fromId;
    private int toId;
    private String conversationId;
    private String content;
    private int status;
    private Date 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;
    }

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

7.2 dao

package com.nowcoder.community.dao;

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

import java.util.List;

@Mapper
public interface MessageMapper {

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

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

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

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

    //查询未读私信的数量
    int selectUnreadCount(@Param("userId") int userId,
                          @Param("conversationId") String conversationId);
}

message-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.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="selectUnreadCount" 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>

去测试类中测试一下,MapperTests 类

@Resource
private MessageMapper messageMapper;

@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.selectUnreadCount(131, "111_131");
    System.out.println(count);
}

7.3 service

package com.nowcoder.community.service;

import com.nowcoder.community.dao.MessageMapper;
import com.nowcoder.community.entity.Message;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service
public class MessageService {

    @Resource
    private 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.selectUnreadCount(userId, conversationId);
    }
}

7.4 controller

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.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Controller
public class MessageController {

    @Resource
    private MessageService messageService;

    @Resource
    private HostHolder hostHolder;

    @Resource
    private UserService userService;

    //私信列表
    @GetMapping("/letter/list")
    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
30行

<a class="nav-link position-relative" th:href="@{/letter/list}">消息<span class="badge badge-danger">12</span></a>

site / letter.html
第2行

<html lang="en" xmlns:th="http://www.thymeleaf.org">

8-9行

<link rel="stylesheet" th:href="@{/css/global.css}" />
<link rel="stylesheet" th:href="@{/css/letter.css}" />

386-387行

<script th:src="@{/js/global.js}"></script>
<script th:src="@{/js/letter.js}"></script>

删掉 140-301行,只留一个 li 标签即可

15行

<!-- 头部 -->
<header class="bg-dark sticky-top" th:replace="index::header">

69行

<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>

123行

<!-- 私信列表 -->
<ul class="list-unstyled">
	<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</span>
		<a href="profile.html">
			<img th:src="@{map.target.headerUrl}" class="mr-4 rounded-circle user-header" alt="用户头像" >
		</a>
		<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>
	</li>
</ul>

144页

<!-- 分页 -->
<nav class="mt-5" th:replace="index::pagination">

启动,测试,登录账号 aaa,,密码aaa,

在这里插入图片描述

可以看到,头像没显示成功,原因是 127 行的 @ 写错了,应该是 $

<img th:src="${map.target.headerUrl}">

7.5 私信详情

MessageController 类增加方法

@GetMapping("/letter/detail/{conversationId}")
public String getLetterDetail(@PathVariable("conversationId") String conversationId, Page page, Model model) {
    //分页信息
    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
135行

<a th:href="@{|/letter/detail/${map.conversation.conversationId}|}" th:utext="${map.conversation.content}">米粉车, 你来吧!</a>

letter-detail.html
2行

<html lang="en" xmlns:th="http://www.thymeleaf.org">

8-9行

<link rel="stylesheet" th:href="@{/css/global.css}" />
<link rel="stylesheet" th:href="@{/css/letter.css}" />

15行

<!-- 头部 -->
<header class="bg-dark sticky-top" th:replace="index::header">

288-289行

<script th:src="@{/js/global.js}"></script>
<script th:src="@{/js/letter.js}"></script>

67行

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

删掉 136-203行

119行

<li class="media pb-3 pt-3 mb-2" th:each="map:${letters}">

121行

<img th:src="${map.fromUser.headerUrl}" class="mr-4 rounded-circle user-header" alt="用户头像" >

125行

<strong class="mr-auto" th:utext="${map.fromUser.username}">落基山脉下的闲人</strong>

126行

<small th:text="${#dates.format(map.letter.createTime, 'yyyy-MM-dd HH:mm:ss')}">2019-04-25 15:49:32</small>

131行

<div class="toast-body" th:utext="${map.letter.content}">
	君不见, 黄河之水天上来, 奔流到海不复回!
</div>

138行

<!-- 分页 -->
<nav class="mt-5" th:replace="index::pagination">

70行

<button type="button" class="btn btn-secondary btn-sm" onclick="back();">返回</button>

在最后新建一个 script 标签,实现 back 函数

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

启动,测试,点击私信,跳转到私信详情页,点击返回,成功返回

在这里插入图片描述
在这里插入图片描述

7.6 发送私信

dao:

MessageMapper 增加方法

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

//修改消息的状态
int updateStatus(@Param("ids") List<Integer> ids, @Param("status") int status);

message-mapper.xml

<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>

service
MessageService 增加方法

@Resource
private SensitiveFilter sensitiveFilter;

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

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

UserService

public User findUserByName(String username) {
    return userMapper.selectByName(username);
}

controller
MessageController

@PostMapping("/letter/send")
@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(){
	$("#sendBtn").click(send_letter);
	$(".close").click(delete_msg);
});

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

	var toName = $("#recipient-name").val();
	var content = $("#message-text").val();
	$.post(
		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);
		}
	);
}

function delete_msg() {
	// TODO 删除数据
	$(this).parents(".media").remove();
}

letter-detail.html
88行

<input type="text" class="form-control" id="recipient-name" th:value="${target.username}">

MessageController 类新增方法

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

完善 getLetterDetail() 方法,在方法的最后添加逻辑

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

启动,测试,先登录 niuke,密码123,给 hahaha 发送一条私信

在这里插入图片描述

再切换到 hahaha,密码 123456,成功接收到私信!

在这里插入图片描述

点击该私信:

在这里插入图片描述

8. 统一处理异常

在这里插入图片描述


将 error 文件夹拖放到 templates 目录下

在这里插入图片描述

404.html
2行

<html lang="en" xmlns:th="http://www.thymeleaf.org">

8行

<link rel="stylesheet" th:href="@{/css/global.css}" />

14行

<!-- 头部 -->
<header class="bg-dark sticky-top" th:replace="index::header">

64行

<img th:src="@{/img/404.png}" >

133行

<script th:src="@{/js/global.js}"></script>

500.html 作同样处理

启动,测试,随便访问一个不存在的路径:http://localhost:8080/community/index/abc

在这里插入图片描述


在这里插入图片描述

HomeController 增加方法

@GetMapping("/error")
public String getErrorPage() {
    return "/error/500";
}

controller 包下新建一个包 advice

package com.nowcoder.community.controller.advice;

import com.nowcoder.community.util.CommunityUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;

@ControllerAdvice(annotations = Controller.class)  //只去扫描controller
public class ExceptionAdvice {

    public static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);

    @ExceptionHandler({Exception.class})
    public void handleException(Exception e, HttpServletRequest request,
                                HttpServletResponse response) throws IOException {
        logger.error("服务器发生异常:" + e.getMessage());
        for (StackTraceElement element : e.getStackTrace()) {
            logger.error(element.toString());
        }
        String xRequestedWith = request.getHeader("x-requested-with");
        if ("XMLHttpRequest".equals(xRequestedWith)) { //说明这是一个异步请求
            //需要响应字符串
            response.setContentType("application/plain;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write(CommunityUtil.getJSONString(1, "服务器异常!"));
        } else { //说明是普通请求,需要返回视图
            response.sendRedirect(request.getContextPath() + "/error");
        }
    }
}

启动测试,故意加个错

在这里插入图片描述
在这里插入图片描述


随便发个私信,结果如下:


在这里插入图片描述

9. 统一记录日志

AOP的概念

  • Aspect Oriented Programing, 即面向方面(切面)编程。
  • AOP是一种编程思想,是对OOP的补充, 可以进一步提高编程的效率。

AOP的实现

AspectJ

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

Spring AOP

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

Spring AOP

JDK动态代理

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

CGLib动态代理

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

示例:
新建包 aspect

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 {

    /**
     * 几个 * 的含义
     * 第1个:任意返回值
     * 2:service包下的所有类
     * 3:所有方法
     * .. 表示任意个参数
     */
    @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;
    }
}

启动,清空控制台,访问首页,查看控制台信息,然后这个 AlphaAspect 类就可以删掉了

【实战】记录项目日志

package com.nowcoder.community.aspect;

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.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;

@Component
@Aspect
public class ServiceLogAspect {

    public static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);

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

    }

    @Before("pointCut()")
    public void before(JoinPoint joinPoint) {
        //日志格式:用户[ip]在[时间点],访问了[com.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();
        logger.info(String.format("用户[%s],在[%s],访问了[%s].", ip, now, target));
    }
}

启动项目,清空控制台,访问首页,查看控制台信息:

在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【牛客讨论区】第三章:开发社区核心功能 的相关文章

随机推荐

  • python3--hashlib模块

    hashlib提供的摘要算法md5 sha1 sha224 sha256 sha384 sha512 blake2b blake2s sha3 224 sha3 256 sha3 384 sha3 512 shake 128 shake 2
  • 几个Linux实用快捷键分享

    1 ctrl c 强制停止 Linux某些程序的运行 如果想要强制停止它 可以使用快捷键ctrl c 命令输入错误 也可以通过快捷键ctrl c 退出当前输入 重新输入 2 ctrl d 退出或登出 可以通过快捷键 ctrl d 退出账户的
  • Vue 学习路线

    今天福州突然降温 然后跟一位学弟 我原来实验室带的师弟 问我vue学习路线怎么样 这里简单总结下 因为我也是突然被问到去年做的几款软件内容 从 0版本到现3 x版本 es6语法一定要去认真学习一遍 这是有js基础的 如果没有基础或者基础不牢
  • C语言课程设计猜数字流程图,(C语言课程设计猜数字游戏姚成.doc

    C语言课程设计猜数字游戏姚成 课 程 设 计 报 告 课程名称 C语言程序设计报告 课题名称 猜数字游戏 专 业 电气工程及其自动化 班 级 1104 学 号 23 姓 名 姚成 指导教师 黄晓宇 丁敬忠 谭小兰 2012年 6月 24 日
  • MIPI入门——D-PHY介绍(二)

    前面的文章中提到了 MIPI D PHY协议中规定了两种模式 LP模式和HS模式 其中HS模式只在高速数据传输中使用 而LP模式则同时包含控制模式 Control Mode 低功耗数据传输模式 LPDT 和极低功耗模式 ULPS 为了方便描
  • 使用python的aligo库实现阿里云盘多端同步

    借助aligo库 写了两个函数 第一个本地文件上传同步 并删除云端不同的文件 第二个下载同步函数 将云端文件下载 并删除 云端没有的文件 这里面的逻辑是 每台机器在工作前都需要执行一遍下载同步 保证本地最新文件和网盘内容相同 工作结束以后
  • 字符串(kmp匹配 ;马拉车回文串;多串字典树)

    目录 1 KMP 自匹配 且只与开头比较 2 kmp 两个字符串匹配 来找出最长公共字符串 3 马拉车算法 给定一个字符串 尽可能的少添加字符使其整体构成一个回文串 有多少个回文子串 输出以其为开头的最长即可 4 多个字符串匹配 字典树 1
  • Vue项目打包部署总结配合nginx部署

    你可能还想了解 https blog csdn net weixin 52901235 article details 129437990 spm 1001 2014 3001 5502 使用Vue做前后端分离项目时 通常前端是单独部署 用
  • NAT穿透解决方案介绍

    最近公司要实现在各种网络环境下面的多屏互动 机顶盒 android phone iphone及PC端 的需求 由于IP地址资源有限的原因 目前我们使用的各种终端设备都位于局域网后面也就是多台设备共享同一个公网IP 例如 如果位于局域网里面的
  • Html源代码加密?

    什么是Html源代码加密 使用JavaScript加密转化技术将Html变为密文 以此保护html源代码 这便是Html源码加密 同时 这种加密技术还可实现网页反调试 防复制 链接加密等功能 应用场景 什么情况下需要Html源代码加密 Ht
  • SpringBoot下swagger3.0的配置

    SpringBoot下swagger3 0的配置 1 swagger3 0依赖 2 swagger配置类 3 我的application yml配置 4 访问地址 5 Swagger注解说明 1 swagger3 0依赖
  • 初学者必看!我的第一个Invideo人工智能文字生成视频

    这是一个使用人工智能生成视频的在线平台 主要功能包括 视频脚本自动生成 可以通过输入主题 由AI自动生成视频故事剧本 人声合成 支持上传脚本 AI会合成自然的人声进行朗读 视频制作 有多种视频模板可选择 支持上传自己的素材 一键生成完整视频
  • 三十多岁的我,为了生活转行Java,开始我的小白之路!

    看你35岁要从体制里出来学java 而且看样子已经下定决心了 我真的替你感到悲哀 我也是java培训出来 转行到互联网的 所以我觉得我可以回答这个问题 跟我一起培训的同学大部分也还在做 我们这些人有的是24岁刚毕业出来的 有的是毕业两年三年
  • C#程序用Settings读取和保存参数

    C 程序用Settings读取和保存参数 通常比较大型的程序开发时 需要读取和保存许多用户设置的参数 比如数据文件夹路径 程序界面的颜色 字体名称 大小等 这些信息怎么能够方便的进行设置和保存呢 在C 开发程序时 可以用系统自带的Setti
  • 智能化工作流程,工作效率开始“狂飙”!|Parabola

    随着 AI 功能的日益强大 能够帮忙人们解决的工作问题越来越多 这也不可避免地引发了一场工作效率革命 尤其助力智能化工作流程的建立 Parabola Parabola是一款强大的自动化处理工具 能够帮助用户轻松地进行数据处理 转换和分析而无
  • Java有哪些自定义异常处理方式

    在Java中 异常是一种常见的处理机制 当程序运行出现错误时 Java会默认抛出一个异常 并通过栈回溯信息提供错误详情 从而让开发人员知道程序何时 为什么以及在哪里发生异常 然而 这仅仅是Java内置异常处理的一部分 Java也提供了许多自
  • Unity使用Xcode将项目打包成IPA

    Unity是个开放性的平台 打包时也可以选择多种打包类型 几乎包含了所有的平台 目前主流Android iOS平台 Android平台可以直接使用Unity自行打包 但iOS平台需要借助Mac电脑进行打包 本博客就iOS打包进行一个简单的说
  • C++知识点总结(三)

    1 什么是二叉搜索树 二叉搜索树又称二叉排序树 它或者是一棵空树 或者是具有以下性质的二叉树 若它的左子树不为空 则左子树上所有节点的值都小于根节点的值 若它的右子树不为空 则右子树上所有节点的值都大于根节点的值 它的左右子树也分别为二叉搜
  • [转] Hyper-V Cluster Shared Volume(叢集共用磁碟區)原理初探

    原文地址 http www dotblogs com tw daniel07793 archive 2012 05 19 72265 aspx 這篇是要來解說Hyepr V在運用Cluster Shared Volume的一些原理 可以先參
  • 【牛客讨论区】第三章:开发社区核心功能

    目录 1 过滤敏感词 2 发布帖子 2 1 dao 2 2 service 2 3 controller 3 帖子详情 4 事务管理 5 显示评论 5 1 实体类 5 2 dao 5 3 service 5 4 controller 6 添