• 第3章 Spring Boot进阶,开发社区核心功能(上)


    1. 过滤敏感词

    image-20220714102936221

    1. 创建一个储存要过滤的敏感词的文本文件

    1. 首先创建一个文本文件储存要过滤的敏感词

    image-20220714163540442

    在下面的工具类中我们会读取这个文本文件,这里提前给出

    @PostConstruct   // 这个注解表示当容器实例化这个bean(服务启动的时候)之后在调用构造器之后这个方法会自动的调用
    public void init(){
        try(
                // 读取写有“敏感词”的文件,getClass表示从程序编译之后的target/classes读配置文件,读之后是字节流
                // java7语法,在这里的句子最后会自动执行close语句
                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());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    2. 开发过滤敏感词的工具类

    1. 开发过滤敏感词组件

    为了方便以后复用,我们把过滤敏感词写成一个工具类SensitiveFilter。

    @Component
    public class SensitiveFilter {
    
        private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
    
        // 当检测到敏感词后我们要把敏感词替换成什么符号
        private static final String REPLACEMENT = "***";
    
        // 根节点
        private TrieNode rootNode = new TrieNode();
    
        @PostConstruct   // 这个注解表示当容器实例化这个bean(服务启动的时候)之后在调用构造器之后这个方法会自动的调用
        public void init(){
            try(
                    // 读取写有“敏感词”的文件,getClass表示从程序编译之后的target/classes读配置文件,读之后是字节流
                    // java7语法,在这里的句子最后会自动执行close语句
                    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 tempNode = rootNode;
            for (int i = 0; i < keyword.length(); i++) {
                char c = keyword.charAt(i);
                TrieNode subNode = tempNode.getSubNode(c);
                if(subNode == null){
                    // subNode为空,初始化子节点;subNode不为空,直接用就可以了
                    subNode = new TrieNode();
                    tempNode.addSubNode(c, subNode);
                }
                // 指针指向子节点,进入下一轮循环
                tempNode = subNode;
            }
            // 最后要设置结束标识
            tempNode.setKeywordEnd(true);
        }
    
        /**
         * 过滤敏感词
         * @param text 待过滤的文本
         * @return  过滤后的文本
         */
        public String filter(String text){
            if(StringUtils.isBlank(text)){
                // 待过滤的文本为空,直接返回null
                return null;
            }
            // 指针1,指向树
            TrieNode tempNode = rootNode;
            // 指针2,指向正在检测的字符串段的首
            int begin = 0;
            // 指针3,指向正在检测的字符串段的尾
            int position = 0;
            // 储存过滤后的文本
            StringBuilder sb = new StringBuilder();
            while (begin < text.length()){
                char c = text.charAt(position);
    
                // 跳过符号,比如 “开票”是敏感词 #开#票# 这个字符串中间的 '#' 应该跳过
                if(isSymbol(c)){
                    // 是特殊字符
                    // 若指针1处于根节点,将此符号计入结果,指针2、3向右走一步
                    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字符串替换掉,存 REPLACEMENT (里面是***)
                    sb.append(REPLACEMENT);
                    // 进入下一个位置
                    begin = ++position;
                    // 重新指向根节点
                    tempNode = rootNode;
                } else {
                    // 检查下一个字符
                    position++;
                }
            }
            return sb.toString();
        }
    
        // 判断是否为特殊符号,是则返回true,不是则返回false
        private boolean isSymbol(Character c){
            // CharUtils.isAsciiAlphanumeric(c)方法:a、b、1、2···返回true,特殊字符返回false
            // 0x2E80  ~  0x9FFF 是东亚的文字范围,东亚文字范围我们不认为是符号
            return  !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
        }
    
        // 前缀树
        private class TrieNode{
    
            // 关键词结束标识
            private boolean isKeywordEnd = false;
    
            // 当前节点的子节点(key是下级字符、value是下级节点)
            private Map<Character, TrieNode> subNodes = new HashMap<>();
    
            public 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145

    image-20220714164533306

    上面就是过滤敏感词工具类的全部代码,接下来我们来解释一下开发步骤

    开发过滤敏感词组件分为三步:

    1. 定义前缀树(Tree)

    我们将定义前缀树写为SensitiveFilter工具类的内部类

    // 前缀树
    private class TrieNode{
    
        // 关键词结束标识
        private boolean isKeywordEnd = false;
    
        // 当前节点的子节点(key是下级字符、value是下级节点)
        private Map<Character, TrieNode> subNodes = new HashMap<>();
    
        public 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    image-20220714164751200

    1. 根据敏感词,初始化前缀树

    将敏感词添加到前缀树中

    // 将一个敏感词添加到前缀树中
    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为空,初始化子节点;subNode不为空,直接用就可以了
                subNode = new TrieNode();
                tempNode.addSubNode(c, subNode);
            }
            // 指针指向子节点,进入下一轮循环
            tempNode = subNode;
        }
        // 最后要设置结束标识
        tempNode.setKeywordEnd(true);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    image-20220714164834667

    1. 编写过滤敏感词的方法

    如何过滤文本中的敏感词:

    特殊符号怎么处理:

    image-20220714155813693

    敏感词前缀树初始化完毕之后,过滤文本中的敏感词的算法应该如下:

    定义三个指针:

    • 指针1指向Tree树
    • 指针2指向待过滤字符串段
    • 指针3指向待过滤字符串段
    /**
     * 过滤敏感词
     * @param text 待过滤的文本
     * @return  过滤后的文本
     */
    public String filter(String text){
        if(StringUtils.isBlank(text)){
            // 待过滤的文本为空,直接返回null
            return null;
        }
        // 指针1,指向树
        TrieNode tempNode = rootNode;
        // 指针2,指向正在检测的字符串段的首
        int begin = 0;
        // 指针3,指向正在检测的字符串段的尾
        int position = 0;
        // 储存过滤后的文本
        StringBuilder sb = new StringBuilder();
        while (begin < text.length()){
            char c = text.charAt(position);
    
            // 跳过符号,比如 “开票”是敏感词 #开#票# 这个字符串中间的 '#' 应该跳过
            if(isSymbol(c)){
                // 是特殊字符
                // 若指针1处于根节点,将此符号计入结果,指针2、3向右走一步
                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字符串替换掉,存 REPLACEMENT (里面是***)
                sb.append(REPLACEMENT);
                // 进入下一个位置
                begin = ++position;
                // 重新指向根节点
                tempNode = rootNode;
            } else {
                // 检查下一个字符
                position++;
            }
        }
        return sb.toString();
    }
    
    // 判断是否为特殊符号,是则返回true,不是则返回false
    private boolean isSymbol(Character c){
        // CharUtils.isAsciiAlphanumeric(c)方法:a、b、1、2···返回true,特殊字符返回false
        // 0x2E80  ~  0x9FFF 是东亚的文字范围,东亚文字范围我们不认为是符号
        return  !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    image-20220714164949096

    最后:建议在测试类中测试一下

    image-20220714165045793

    经测试,过滤敏感词的工具类开发完成,这个工具会在接下来的发布帖子的功能中用到。

    2. 发布帖子

    image-20220715090347434

    异步请求:当前网页不刷新,还要访问服务器,服务器会返回一些结果,通过这些结果提炼出来的数据对网页做一个局部的刷新(提示、样式等),实现异步请求的技术是AJAX。

    演示一下使用 JQuery 发送异步请求的示例

    引一个FastJson的包,用这个包里面的 api 处理json字符串的转换,效率更高一点,性能更好一点。

    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>fastjsonartifactId>
        <version>1.2.75version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    首先在 CommunityUtil 工具类中加几个处理json字符串的方法,因为服务器给浏览器返回json字符串,

    // 返回JSON字符串
    // 参数:编码、提示信息、Map(Map里面封装业务数据)
    public static String getJSONString(int code, String msg, Map<String, Object> map){
        /*
        封装成json对象,然后把json对象转换成字符串就得到了一个json格式的字符串
         */
        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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    image-20220715092035069

    image-20220715110308895

    写一个html页面 ajax-demo.html,在

    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>AJAXtitle>
    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);
                        // 将字符串转换成json对象
                        data = $.parseJSON(data);
                        console.log(typeof (data));
                        console.log(data.code);
                        console.log(data.msg);
    
                    }
                );
            }
        script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    引入 JQuery:在html页面body内加上下面这句话
    
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous">script>
    
    • 1
    • 2
    • 3

    image-20220715110615918

    测试:

    image-20220715110705634


    接下来我们来开发发布帖子的功能

    1. 数据访问层(dao)

    增加插入帖子的方法

    int insertDiscussPost(DiscussPost discussPost);
    /*
     声明插入帖子的功能
     */
    
    • 1
    • 2
    • 3
    • 4

    image-20220715163618969

    然后是对应的mapper配置文件

    <sql id="insertFields" >
        user_id, title, content, type, status, create_time, comment_count, score
    sql>
    
        <insert id="insertDiscussPost" parameterType="com.nowcoder.community.entity.DiscussPost">
            insert into discuss_post(<include refid="insertFields">include>)
            values(#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})
        insert>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    image-20220715163713940

    1. 业务层(service)
    
    @Service
    public class DiscussPostService {
    
        @Autowired
        private DiscussPostMapper discussPostMapper;
    
        @Autowired
        private SensitiveFilter sensitiveFilter;
    
        public List<DiscussPost> findDiscussPosts(int userId, int offest, int limit){
            return discussPostMapper.selectDiscussPosts(userId, offest, limit);
        }
    
        public int findDiscussPostRows(int userId){
            return discussPostMapper.selectDiscussPostRows(userId);
        }
    
        // 插入帖子
        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);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    image-20220715163935303

    1. 视图层(Controller和页面)

    DiscussPostController:

    @Component
    @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){
                // 如果没有登录,直接返回错误信息
                // 403 代表没有权限
                return CommunityUtil.getJSONString(403, "你还没有登录!");
            }
            // 可以执行到这里说明已经登录了
            DiscussPost post = new DiscussPost();
            post.setUserId(user.getId());
            post.setTitle(title);
            post.setContent(content);
            post.setCreateTime(new Date());
            // 帖子类型和状态、得分等默认就是0,不用设置
            discussPostService.addDiscussPost(post);
            // 报错的情况将来统一处理
            return CommunityUtil.getJSONString(0, "发布成功!");   // 0 表示是一个正确的状态
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    image-20220715164101990

    index.html:

    image-20220715162807617

    image-20220715164330898

    index.js:

    主页的发布帖子功能里面有js,我们修改一下js

    image-20220715164523573

    测试

    image-20220715164559219

    接着在数据库中看一下标签是否被转义以及敏感词是否被过滤

    image-20220715164722154

    3. 帖子详情

    image-20220715164957608

    在首页的帖子列表页面上,可以随便选一个帖子,然后点它的标题,我们就可以打开一个显示帖子详细信息的页面,然后把帖子的详细内容显示完整,这就是帖子详情的功能。

    1. 数据访问层(dao)

    dao接口中增加一个查询帖子的功能:

    DiscussPost selectDiscussPostById(int id);
    /*
    根据帖子id查询帖子的详细内容
     */
    
    • 1
    • 2
    • 3
    • 4

    image-20220715170009432

    然后编写它对应的mapper配置文件

    <sql id="selectFields" >
      id, user_id, title, content, type, status, create_time, comment_count, score
    sql>
    
    <select id="selectDiscussPostById" resultType="com.nowcoder.community.entity.DiscussPost" parameterType="Integer">
        select <include refid="selectFields">include>
        from discuss_post
        where id = #{id}
    select>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    image-20220715170328372

    1. 业务层(service)

    在DiscussPostService中增加一个查询帖子的方法

    public DiscussPost findDiscussPostById(int id){
        return discussPostMapper.selectDiscussPostById(id);
    }
    
    • 1
    • 2
    • 3

    image-20220715170551475

    1. 表现层(controller和html)

    DiscussPostController:

    @Autowired
    private UserService userService;
    
    @RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
    public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model){
        // 由id查询帖子
        DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
        model.addAttribute("post", post);
        // 由于discussPost显示的只有用户的id,我们显示在页面上的肯定是用户的username而不是id,所以我们还需要查一下username
        User user = userService.findUserById(post.getUserId());
        model.addAttribute("user", user);
    
        return "/site/discuss-detail";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    index.html:

    我们要在首页的每一个帖子上的标题上加一个链接,链接能够访问到上面DiscussPostController中的查询帖子的方法。

    image-20220715172853067

    discuss-detail.html:

    接着我们要处理详情页面数据的展现

    image-20220715174406096

    image-20220715174442425

    image-20220715174624907

    image-20220715174704274

    测试

    image-20220715174824972

    image-20220715174851109

    4. 事务管理

    事务管理常见知识

    image-20220716070756484

    image-20220716071047348

    第一类丢失更新

    image-20220716073106097

    第二类丢失更新

    image-20220716073356286

    脏读

    image-20220716073511481

    不可重复读

    image-20220716073549458

    幻读

    image-20220716073736147

    不同的隔离级别可以解决的问题

    image-20220716074350851

    image-20220716074710087

    Spring中的事务管理

    image-20220716074946222

    Spring中的事务管理有两种方式:

    • 声明式事务

      • 通过xml配置,声明某方法的事务特征

      • 通过注解,声明某方法的事务特征

    • 编程式事务

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

    两者选一种即可,建议选第一种,因为比较简单,当然如果业务比较复杂,仅仅只是想管理一小部分使用第二种

    写一个demo演示Spring中的事务管理

    1. 声明式事务管理事务

    这里我们通过加注解声明某方法的事务特征

    @Service
    public class AlphaService {
        @Autowired
        private AlphaDao alphaDao;
    
        @Autowired
        private UserMapper userMapper;
    
        @Autowired
        private DiscussPostMapper discussPostMapper;
    
        /*
        isolation 事务的隔离级别
        propagation 事务
    的传播机制:
            REQUIRED: 支持当前事务(外部事务),如果不存在则创建新事务.
            REQUIRES_NEW: 创建一个新事务,并且暂停当前事务(外部事务).
            NESTED: 如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),不存在外部事务就会和REQUIRED一样
         */
        @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
        public Object save1(){
            // 新增用户
            User user = new User();
            user.setUsername("alpha");
            user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
            user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
            user.setEmail("alpha@qq.com");
            user.setHeaderUrl("http://image.nowcoder.com/head/99t.png");
            user.setCreateTime(new Date());
            userMapper.insertUser(user);
    
            // 新增帖子
            DiscussPost post = new DiscussPost();
            post.setUserId(user.getId());         // 虽然上面我们没有给user设置id,但是执行过数据库操作之后,数据库给user的id
            post.setTitle("Hello");
            post.setContent("新人报道!");
            post.setCreateTime(new Date());
            discussPostMapper.insertDiscussPost(post);
    
            // 报错
            Integer.valueOf("abc");         // 将 "abc" 这个字符串转换为整数,肯定转不了,报错
    
            return "ok";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    image-20220716084142050

    测试:

    @RunWith(SpringRunner.class)
    @SpringBootTest
    @ContextConfiguration(classes = CommunityApplication.class)
    public class TransactionTests {
    
        @Autowired
        private AlphaService alphaService;
    
        @Test
        public void testSave1() {
            Object obj = alphaService.save1();
            System.out.println(obj);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    image-20220716084645109

    2. 编程式事务管理

    通过 TransactionTemplate 管理事务

    @Service
    public class AlphaService {
        @Autowired
        private AlphaDao alphaDao;
    
        @Autowired
        private UserMapper userMapper;
    
        @Autowired
        private DiscussPostMapper discussPostMapper;
    
        /*
        这个类是Spring自动创建的,我们无须配置,直接注入即可
        */
        @Autowired
        private TransactionTemplate 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";
                }
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    image-20220716090819431

    测试:

    @RunWith(SpringRunner.class)
    @SpringBootTest
    @ContextConfiguration(classes = CommunityApplication.class)
    public class TransactionTests {
    
        @Autowired
        private AlphaService alphaService;
    
        @Test
        public void testSave2() {
            Object obj = alphaService.save2();
            System.out.println(obj);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    image-20220716091005794

    5. 显示评论

    image-20220716102835471

    数据访问层(dao)

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

    数据库中的评论表

    CREATE TABLE `comment` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `user_id` int(11) DEFAULT NULL,	
      `entity_type` int(11) DEFAULT NULL,	# 回复的类型,1代表回复帖子,2代表回复评论
      `entity_id` int(11) DEFAULT NULL,		
      `target_id` int(11) DEFAULT NULL,
      `content` text,
      `status` int(11) DEFAULT NULL,
      `create_time` timestamp NULL DEFAULT NULL,
      PRIMARY KEY (`id`),
      KEY `index_user_id` (`user_id`),
      KEY `index_entity_id` (`entity_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=232 DEFAULT CHARSET=utf8;
    
    /*
    entity_type 代表评论的类型:比如 评论帖子、评论别人的评论等
    
    entity_id 代表评论的帖子的 id,比如评论的帖子的 id
    
    target_id 代表指向目标评论的人的 id,比如评论别人的帖子,target_id 表示评论的人的 id
    
    content 代表评论的内容
    
    status 代表评论的状态,0 代表正常,1 代表删除或者不可用
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    image-20220716133615324

    comment表对应的实体类

    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;
    
       	// 为了以免影响阅读体验,get、set、toString方法没有粘上来,但其实是有的
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    dao接口:

    @Mapper
    public interface CommentMapper {
    
        /*
         分页查询评论
         参数:1.评论的类型  2.评论的是哪个评论的id  3.起始页  4.每页限制条数
         */
        List<Comment> selectCommentsByEntity(int entityType, int entityId, int offset, int limit);
    
        /*
         查询评论的总条数
         参数:1.评论的类型  2.评论的是哪个评论的id
         */
        int selectCountByEntity(int entityType, int entityId);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    image-20220716141613738

    dao接口对应的mapper配置文件:

    
    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="com.nowcoder.community.entity.Comment" >
            select <include refid="selectFields">include>
            from comment
            where status = 0                 
            and entity_type = #{entityType}
            and entity_id = #{entityId}
            order by create_time
            limit #{offset}, #{limit}
        select>
    
        <select id="selectCountByEntity" resultType="Integer" >
            select count(id)
            from comment
            where status = 0
            and entity_type = #{entityType}
            and entity_id = #{entityId}
        select>
    
    mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    image-20220716141717466

    业务层(Service)

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

    CommentService:

    @Service
    public class CommentService {
    
        @Autowired
        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);
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    image-20220716142806853

    表现层(Controller和themeleaf模板)

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

    CommunityConstant 常量接口中设置两个常量分别表示评论的类型:

    /**
     * 回复的实体类型:帖子
     */
    int ENTITY_TYPE_POST = 1;
    
    /**
     * 回复的实体类型:评论
     */
    int ENTITY_TYPE_COMMENT = 2;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    image-20220716163557691

    DiscussPostController 中的getDiscussPost方法补充一些代码去在页面上展示具体的评论

    @Component
    @RequestMapping("/discuss")
    public class DiscussPostController implements CommunityConstant {
    
        @Autowired
        private DiscussPostService discussPostService;
    
        @Autowired
        private HostHolder hostHolder;
    
        @Autowired
        private UserService userService;
    
        @Autowired
        private CommentService commentService;
    
        @RequestMapping(path = "/add", method = RequestMethod.POST)
        @ResponseBody
        public String addDiscussPost(String title, String content){
            User user = hostHolder.getUser();
            if(user == null){
                // 如果没有登录,直接返回错误信息
                // 403 代表没有权限
                return CommunityUtil.getJSONString(403, "你还没有登录!");
            }
            // 可以执行到这里说明已经登录了
            DiscussPost post = new DiscussPost();
            post.setUserId(user.getId());
            post.setTitle(title);
            post.setContent(content);
            post.setCreateTime(new Date());
            // 帖子类型和状态、得分等默认就是0,不用设置
            discussPostService.addDiscussPost(post);
            // 报错的情况将来统一处理
            return CommunityUtil.getJSONString(0, "发布成功!");   // 0 表示是一个正确的状态
        }
    
        @RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
        public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page){
            // 由id查询帖子
            DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
            model.addAttribute("post", post);
            // 由于discussPost显示的只有用户的id,我们显示在页面上的肯定是用户的username而不是id,所以我们还需要查一下username
            User user = userService.findUserById(post.getUserId());
            model.addAttribute("user", user);
            /*
            评论:给帖子的评论
            回复:给评论的评论,因为不止帖子有评论,评论也可能有评论
             */
            page.setLimit(5);               // 设置每页显示的条数
            page.setPath("/discuss/detail/" + discussPostId);   // 设置查询的controller方法路径
            page.setRows(post.getCommentCount());               // 一共有多少条评论的数据
    
            // 评论列表,因为评论里面有user_id、target_id 我们还需要查user表查到username
            List<Comment> commentList = commentService.findCommentsByEntity(
                    ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit()
            );
    
            // 评论VO列表 (VO 意思是在页面上显示的对象 View Object )
            List<Map<String, Object>> commentVoList = new ArrayList<>();
            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) {
                            // 评论的每一个回复Vo
                            Map<String, Object> replyVo = new HashMap<>();
                            // 回复
                            replyVo.put("reply", reply);
                            // 作者
                            replyVo.put("user", userService.findUserById(reply.getUserId()));
                            // 回复目标,等于0表示只是普通的评论,
                            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";
        }
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106

    首先让 DiscussPostController 实现常量接口 CommunityConstant

    image-20220716163659056

    然后是方法的详细内容

    image-20220716165422659

    image-20220716165631778

    接下来我们就需要处理模板了:

    先处理一下首页 index.html 帖子的评论数量分页逻辑复用

    image-20220716170524784

    image-20220716182707387

    接下来是处理帖子的详细页面 discuss-detail.html

    image-20220716184810233

    image-20220716185206321

    image-20220716185341721

    cvoStat 状态的隐含对象:循环变量名后面加上 Stat

    cvoStat.count 循环到了第几次

    测试

    image-20220716185438265

    image-20220716185538821

    测试成功

  • 相关阅读:
    SQL语句常见分类
    运行软件后报错“未找到api-ms-win-core-com-l1-1-0.dll”
    一文看懂yolov7;yolov7详解
    每日小题打卡
    卷积神经网络(CNN)天气识别
    LeetCode:1402. 做菜顺序、2316. 统计无向图中无法互相到达点对数
    ClickHouse(01)什么是ClickHouse,ClickHouse适用于什么场景
    互联网应用主流框架整合之Spring Boot运维体系
    Vue快速入门二:Vue绑定事件、Vue中的this指向、增加class选择器、动态创建标签
    【Aseprite】2D像素人物制作
  • 原文地址:https://blog.csdn.net/qq_50313418/article/details/126288083