项目理解(三)核心功能(前缀树、事务、AOP、统一处理异常、AJAX)

目录

  1. 前缀树:Trie、字典树、查找树
  2. 发布帖子
  3. 帖子详情
  4. 显示评论、添加评论(事务)
  5. 私信列表
  6. 发送私信
  7. 统一处理异常
  8. 统一记录日志

 

前缀树:Trie、字典树、查找树

查找效率高;消耗内存大;用于字符串检索、词频统计、字符串排序等;

实现敏感词过滤器:

1、定义前缀树;

2、根据敏感词,初始化前缀树;

3、编写过滤敏感词的方法;

    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);
        }
        
    }

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

    @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 (IOException e){
            logger.error("加载敏感词失败:"+e.getMessage());
        }
    }

//在sensitive-words.txt中定义了敏感词


    public void addKeyword(String keyword){
        TrieNode tempNode = rootNode;//当前节点
        for(int i= 0; i<= keyword.length()-1; 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){//最后一个字符了,设置最后的标志位为true
                tempNode.setKeywordEnd(true);
            }

        }
    }

编写过滤敏感词的方法:

    //过滤敏感词,输入的是待过滤的字符串;输出的是过滤后(替换)的字符串
    public String filter(String txt){
        if(StringUtils.isBlank(txt)){
            return null;
        }
        //
        int left = 0;
        int right = 0;

        TrieNode tempNode = rootNode;
        StringBuilder sb = new StringBuilder();

        while (right <= txt.length()-1){
            char c  = txt.charAt(right);
            if(isSymbol(c)){//跳过符号
                if(tempNode == rootNode){
                    sb.append(c);
                    left++;
                }
                right++;
                continue;
            }
            tempNode =tempNode.getSubNode(c);
            if(tempNode == null){
                sb.append(txt.charAt(left));
                left++;
                right = left;
                tempNode = rootNode;
            }else if(tempNode.isKeywordEnd()){
                sb.append(REPLACEMENT);//替换
                left = ++right;
                tempNode = rootNode;
            }else {//检查下一个字符
                right++;
            }
        }
        sb.append(txt.substring(left));
        return sb.toString();
    }

以上定义在一个工具类中;在需要过滤的地方调用filter方法即可:

调用:

 

发布帖子

使用jQuery发送AJAX请求

    //添加帖子
    @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, "你还没有登录哦!");
            //403代表没有权限的意思
            //异步的Json格式的数据
            //使用jQuery发送AJAX请求,网页能够将增量更新呈现在页面上,而不需要刷新整个页面
        }

        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, "发布成功!");
    }

server层:

    //加入一条帖子
    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(HtmlUtils.htmlEscape(post.getTitle())));
        post.setContent(sensitiveFilter.filter(HtmlUtils.htmlEscape(post.getContent())));
//        post.setTitle(sensitiveFilter.filter(post.getTitle()));
//        post.setContent(sensitiveFilter.filter(post.getContent()));

        return discussPostMapper.insertDiscussPost(post);
    }

mapper(dao层)

    int insertDiscussPost(DiscussPost discussPost);

xml文件:

    <insert id="insertDiscussPost" parameterType="DiscussPost">
        insert into discuss_post(<include refid="insertFields"></include>)
        values(#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})
    </insert>

发布帖子使用jQuery发送AJAX请求:index.js页面:

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

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

	// 获取标题和内容
	var title = $("#recipient-name").val();
	var content = $("#message-text").val();
	// 发送异步请求(POST)
	$.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);
	    }
	);

}

帖子详情

DiscussPostMapper、DiscussPostService、DiscussPostController

Index.html:在帖子标题上增加访问详情页面的链接

Discuss_detail.html:处理静态资源的访问路径;复用index.html的header区域;显示标题、作者、发布时间、帖子正文等内容;复用了分页功能;

DiscussPostMapper:(一个帖子discussPostId的全部内容)

    //帖子详情
    @RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
    public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page) {
        //我们希望显示用户头像,用户名;而在帖子表中得到的是用户id;有两种方法
        //1、mapper中实现查询的时候,写一个关联查询,关联到user表中;查到后结果需要做处理;mybatis是支持的
        //这种方式效率较高:一次查到了所有信息;缺点:数据有冗余,不需要的时候也查询到了
        //2、可以先查到userid,用userService来查到用户,通过model将用户发送给模板,这样也可以

        //还要支持评论区的分页功能:page用来接收、整理分页的条件,只要是实体类型bean在条件中作为一个参数的
        // 话,那么最终springMvc都会把这个bean存到model中;所以在页面上就可以通过model获得这个配置

        // 帖子
        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<>();
                // 评论
                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";
        //对于显示帖子详情(先不管评论),模板中需要处理:1首页添加链接,需要能够访问到这个模板 2帖子详情页面:用户信息及帖子详情
        //加上评论的处理:首页的处理:1、index评论数量:直接从帖子处获取,在首页帖子列表discussPosts
        // 2、帖子详情页面:根据传入进去的数据参数都修改一遍,
        // 如:先取得comments,对其中每一个元素commentVo:取user,replys,comment等等,将这些数据传到前端模板页面进行显示
        //在模板中评论区域可以复用首页的分页逻辑
    }
    public DiscussPost findDiscussPostById(int id) {
        return discussPostMapper.selectDiscussPostById(id);
    }
    <select id="selectDiscussPostById" resultType="DiscussPost">
        select <include refid="selectFields"></include>
        from discuss_post
        where id = #{id}
    </select>

 

显示评论、添加评论(事务)

需要用到事务的知识:添加评论与修改帖子评论数据要么都成功,要么都失败;

事务管理:声明式事务,做配置就可以;编程式事务;当前事务在整个范围内,并不是局部,加个注解就可以了。

对于discuss_post表,它冗余的存储了comment_count字段,加快了查询速度(由于经常查询)

显示评论
数据层- 根据实体查询一页评论数据。- 根据实体查询评论的数量。
业务层- 处理查询评论的业务。-处理查询评论数量的业务。
表现层- 显示帖子详情数据时-同时显示该帖子所有的评论数据。

添加评论: 
数据层- 增加评论数据。- 修改帖子的评论数量
业务层- 处理添加评论的业务:先增加评论、再更新帖子的评论数量。
表现层- 处理添加评论数据的请求。- 设置添加评论的表单

1、显示评论,在显示详情的时候写了表现层,显示帖子详情数据时,同时显示该帖子所有的评论数据。

2、显示评论,业务层:

    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);
    }

2、显示评论,数据层:

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

    int selectCountByEntity(int entityType, int entityId);

3、xml:

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

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

    <select id="selectCommentsByEntity" 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>

4、添加评论,表现层:处理添加评论数据的请; 设置添加评论的表单。

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

    }

5、添加评论,业务层:先增加评论、再更新帖子的评论数量:(事务)

    @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(HtmlUtils.htmlEscape(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;
    }

6、添加评论,数据层 :增加评论数据; 修改帖子的评论数量;

    int insertComment(Comment comment);
    <insert id="insertComment" parameterType="Comment">
        insert into comment(<include refid="insertFields"></include>)
        values(#{userId},#{entityType},#{entityId},#{targetId},#{content},#{status},#{createTime})
    </insert>
业务层中引入了discussPostService中的:对应的dicussMapper及xml
    public int updateCommentCount(int id, int commentCount) {
        return discussPostMapper.updateCommentCount(id, commentCount);
    }
    int updateCommentCount(int id, int commentCount);
    <update id="updateCommentCount">
        update discuss_post set comment_count = #{commentCount} where id = #{id}
    </update>

私信列表

私信列表: 1、查询当前用户的会话列表;2、每个会话只显示一条最新的私信。 支持分页显示(复用)。
私信详情: 查询某个会话所包含的私信。 支持分页显示

私信列表:MessageController:

    // 私信列表
    @RequestMapping(path = "/letter/list", method = RequestMethod.GET)
    public String getLetterList(Model model, Page page) {
//        Integer.valueOf("abc");//故意写一个错误,用来测试异常处理
        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());
        //得到会话列表后,取出每一个会话的相关的信息,加入到map中:conversations
        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()));
                //取会话列表用户相关的信息:得到target
                int targetId = user.getId() == message.getFromId() ? message.getToId() : message.getFromId();
                map.put("target", userService.findUserById(targetId));

                conversations.add(map);
            }
        }
        model.addAttribute("conversations", conversations);//将conversations加入模型中

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

        return "/site/letter";
    }
私信列表:MessageService
    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);
    }
私信列表:MessageMapper
    // 查询当前用户的会话列表,针对每个会话只返回一条最新的私信.
    List<Message> selectConversations(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);

massage—mapper:

   <sql id="selectFields">
        id, from_id, to_id, conversation_id, content, status, create_time
    </sql>

    <sql id="insertFields">
        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>

SQL:

CREATE TABLE `message` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `from_id` int(11) DEFAULT NULL,
  `to_id` int(11) DEFAULT NULL,
  `conversation_id` varchar(45) NOT NULL,
  `content` text,
  `status` int(11) DEFAULT NULL COMMENT '0-未读;1-已读;2-删除;',
  `create_time` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_from_id` (`from_id`),
  KEY `index_to_id` (`to_id`),
  KEY `index_conversation_id` (`conversation_id`)
) ENGINE=InnoDB AUTO_INCREMENT=355 DEFAULT CHARSET=utf8

辅助:

查询会话数量:(重复的会话算一个)

通过查询最大的消息id来查询每个会话中最新的一个会话:

 

私信详情页面:

MessageController:(显示一个conversationId中的所有内容)

    //私信页面详情
    @RequestMapping(path = "/letter/detail/{conversationId}", method = RequestMethod.GET)
    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));

        //设置为已读
        List<Integer> ids = getletterIds(letterList);
        if(!ids.isEmpty()){
            messageService.readMessage(ids);
        }

        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);
        }
    }
    //得到所有未读的私信的id
    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){
                     //0为未读
                    ids.add(message.getId());
                }
            }
        }
        return ids;
    }
MessageService:
    public List<Message> findLetters(String conversationId, int offset, int limit) {
        return messageMapper.selectLetters(conversationId, offset, limit);
    }
    public int readMessage(List<Integer> ids){//对id集合:更新读的状态
        return messageMapper.updateStatus(ids, 1);//0为未读,1为已读,2为无权限
    }
MessageMapper:
    //修改消息的状态
    int updateStatus(List<Integer> ids, int status);

    //一个用户,多个会话;一个会话是两个人之间的会话,一个会话,多条消息;
    // 查询本用户的会话列表:包含对方的user信息,与用户会话的最后一条消息;

xml:

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

发送私信

发送私信:采用异步的方式发送私信。发送成功后刷新私信列表。
设置已读:访问私信详情时,将显示的私信设置为已读状态。(上面已实现)
 

    //发送私信
    @RequestMapping(path = "/letter/send",method = RequestMethod.POST)
    @ResponseBody
    public String sendLetter(String toName, String content){
//        Integer.valueOf("abc");//故意写一个错误,用来测试异常处理

        //toName接收用户名,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);//正常执行,返回状态0
    }
    public int addMessage(Message message){
        message.setContent(sensitiveFilter.filter(HtmlUtils.htmlEscape(message.getContent())));
        return messageMapper.insertMessage(message);
    }
    //新增消息
    int insertMessage(Message message);
    <insert id="insertMessage" parameterType="Message" keyProperty="id">
        insert into message(<include refid="insertFields"></include>)
        values(#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime})
    </insert>

采用异步的方式发送私信。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();
}

项目中需要着重关注的技术点:

安全,性能,分布式

统一处理异常

放式一:需要将error文件夹放到templates目录下,error文件夹下有404,500两个文件;Spring自动集成好了,当遇到服务器出错时,自动返回500页面;当找不到网页时自动返回到404页面;可处理较简单的问题

方式二:使用spring的注解:@ControllerAdvice,可以记录一些日志信息;

这里实现所有的带有Controller注解的bean的异常处理:

@ControllerAdvice(annotations = Controller.class)//只需要扫描带有Controller注解的bean,即Collector组件
public class ExceptionAdvice {

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

    @ExceptionHandler({Exception.class})//Exception是所以方法的父类;此处所有异常都用Exception来处理
    public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
        logger.error("服务器发生异常: " + e.getMessage());
        for (StackTraceElement element : e.getStackTrace()) {
            logger.error(element.toString());
        }

        //记完日志后给浏览器一个响应;重定向到错误页面;注意:
        // 浏览器访问服务器可能是普通的请求,返回网页;也可能是异步的请求,希望返回一个json
        //先判断是普通请求,还是异步请求
        String xRequestedWith = request.getHeader("x-requested-with");
        if ("XMLHttpRequest".equals(xRequestedWith)) {
            //XMLHttpRequest表示一个异步请求
            //XMLHttpRequest 用于在后台与服务器交换数据。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。
            response.setContentType("application/plain;charset=utf-8");//返回一个普通的字符串,先确保传入是json格式
            PrintWriter writer = response.getWriter();
            writer.write(CommunityUtil.getJSONString(1, "服务器异常!"));
        } else {
            response.sendRedirect(request.getContextPath() + "/error");
        }
    }

}

统一记录日志

传统:将记录日志的方法封装到一个组件里,在不同的service中去调用;可以在每个业务中实现日志的记录;有很大的弊端:如有的时候需求在方法前记日志,有些在方法后记日志;有些是在报异常的时候才记日志。
记录日志实际上属于系统需求,不是业务需求;可以将记录日志的需求单独拿出来实现,而不是硬编码到业务方法中。

AOP:面向切面编程,用于处理系统中分布于各个模块的横切关注点,比如事务管理、日志、缓存等。
AOP实现原理:AOP实现的关键在于AOP框架自动创建AOP代理,AOP代理主要分为静态代理动态代理,静态代理的代表为AspectJ;而动态代理则以Spring AOP为代表;
AspectJ:1) AspectJ是语言级的实现,它扩展了Java语言,定义了AOP语法。2) AspectJ在编译期织入代码,它有一个专门的编译器,用来生成遵守Java字节码规范的class文件。
Spring AOP:1) Spring AOP使用纯Java实现,它不需要专门的编译过程,也不需要特殊的类装载器。2) Spring AOP在运行时通过代理的方式织入代码,只支持方法类型的连接点。3) Spring支持对AspectJ的集成。(实际应用中也只用到方法类型的连接点)

Spring AOP使用的动态代理,在每次运行时生成AOP代理对象。所谓的动态代理就是说AOP框架不会去修改字节码,而是在内存中临时为方法生成一个代理对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

Spring AOP的动态代理主要有两种方式:JDK动态代理和CGLIB动态代理JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。Spring AOP默认采用此种方式。如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library)是一个代码生成的类库,可以在运行时动态生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

这里实现所有的service层的日志记录:

@Component
@Aspect
public class ServiceLogAspect {

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

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

    }

    @Before("pointcut()")
    public void before(JoinPoint joinPoint) {
        // 用户[1.2.3.4],在[xxx],访问了[com.liu.community.service.xxx()].
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();//得到HttpServletRequest
        String ip = request.getLocalAddr();//ip
        String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());//时间
        //得到切入处的:getDeclaringTypeName类名,getName方法名;
        String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
        logger.info(String.format("用户[%s],在[%s],访问了[%s].", ip, now, target));
    }

}

filter、interceptor、aspect、controllerAdvice

1、filter,这是java的过滤器,和框架无关的,是所有过滤组件中最外层的,从粒度来说是最大的。配置方式,有直接实现Filter+@component,@Bean+@configuration(第三方的filter);可以控制最初的http请求,但是更细一点的类和方法控制不了。

2、interceptor,spring框架的拦截器配置方式,@configuration+继承WebMvcConfigurationSupport类添加过滤器。可以控制请求的控制器和方法,但控制不了请求方法里的参数。

3、aspect,可以自定义切入的点,有方法的参数,但是拿不到http请求,可以通过其他方式如RequestContextHolder获得,粒度最小。加个注解用效果更佳。

4、controllerAdvice,是controller的增强,和ExceptionHandler一起用来做全局异常。

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值