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

3.6 添加评论

image-20220717081307260

数据访问层(dao)

首先是在 CommentMapper 接口中增加两个方法添加评论

int insertComment(Comment comment);
/**
 * 插入一条评论
 */

image-20220717103805158

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

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

image-20220717103854104

然后我们需要在 DiscussPostMapper 接口中增加一个更新帖子数量的方法:

int updateCommentCount(int id, int commentCount);
/**
 * 根据帖子id更新评论数量
 */

image-20220717103920650

然后是对应的mapper配置文件

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

image-20220717103947994

业务层(Service)

我们先在 DiscussPostService 中加一个更新评论数量的方法,然后在 CommentService 中开发时可以依赖于 DiscussPostService 组件

DiscussPostService :

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

image-20220717104139627

CommentService:

为了方便使用常量,CommentService要实现常量接口 CommunityConstant

@Autowired
private SensitiveFilter sensitiveFilter;

@Autowired
private DiscussPostService discussPostService;


    // 增加评论数量,另外,因为是两个DML操作:增加评论、更新评论数量,所以交给事务管理
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)  // 事务级别、传播机制
    public int addComment(Comment comment){
        if(comment == null){
            throw new IllegalArgumentException("参数不能为空!");
        }
        // 添加评论        转义html标记     过滤敏感词
        comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));
        comment.setContent(sensitiveFilter.filter(comment.getContent()));
        int rows = commentMapper.insertComment(comment);

        // 更新帖子评论数量,只有评论给帖子的时候才更新帖子的评论数量
        if (comment.getEntityType() == ENTITY_TYPE_POST) {
            int count = commentMapper.selectCountByEntity(comment.getEntityType(), comment.getEntityId());
            discussPostService.updateCommentCount(comment.getEntityId(), count);
        }
        return rows;
    }

image-20220717104502482

image-20220717104527639

表现层(controller 和 thymeleaf 模板)

新建一个 CommentController:

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

    @Autowired
    private HostHolder hostHolder;

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

}

image-20220717104740012

然后就是处理 discuss-detail.html 模板了

回复有三种类型:

  • 一种是直接在帖子最下面回复

image-20220717104935438

  • 一种是回复别人对于帖子的评论的评论

image-20220717105117324

  • 最后一种是回复别人评论别人的评论

image-20220717105405129

测试

image-20220717105433421

经过自己测试,三种方式的评论都测试成功!

3.7 私信列表 和 私信详情

image-20220717145914509

这次我们开发的是私信列表私信详情两个功能,这里我们一起开发。

首先是私信对应的实体类 Message

public class Message {

    private int id;											// 主键
    private int fromId;									// 发信方id
    private int toId;										// 首先方id
    private String conversationId;      // 会话id,有fromId和toId组成
    private String content;							// 绘画内容
    private int status;									// 状态 0-未读   1-已读  2-删除
    private Date createTime;						// 日期
		// 以免影响阅读体验,get。set。toString方法 没有粘,但开发时这些方法是需要的
}
  1. 数据访问层(dao)

因为是一个新业务,所以我们创建一个新的dao接口

@Mapper
public interface 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);

    // 查询未读私信的数量, 这里conversationId作为动态的条件去拼,不是一定会有,传了就拼上去,这样这个方法能够实现两种业务
    int selectLetterUnreadCount(int userId, String conversationId);

}

image-20220718104614902

然后是对应的mapper配置文件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>

    <!--查询所有会话,每个会话查询和某个人的最后一个对话,from_id=1是系统用户是通知不是私信-->
    <select id="selectConversations" resultType="com.nowcoder.community.entity.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="Integer">
        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="Integer">
        select count(id)
        from message
        where status != 2
        and from_id != 1
        and conversation_id = #{conversationId}
    </select>
    <!--查询未读私信的数量-->
    <select id="selectLetterUnreadCount" resultType="Integer">
        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>

image-20220718104705094

  1. 业务层

新建一个Service:MessageService

@Service
public class MessageService {


    @Autowired
    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.selectLetterUnreadCount(userId, conversationId);
    }
}

image-20220718104913944

  1. 表现层

表现层我们需要两个功能,一个是私信列表,一个是私信详情。这两个功能写在一个controller里就可以。

先处理私信列表

新建一个Controller叫 MessageController

@Controller
public class MessageController {

    @Autowired
    private MessageService messageService;

    @Autowired
    private HostHolder hostHolder;

    @Autowired
    private UserService userService;

    // 私信列表
    @RequestMapping(path = "/letter/list", method = RequestMethod.GET)
    public String getLetterList(Model model, Page page){
        User user = hostHolder.getUser();
        // 分页信息
        page.setLimit(5);               //设置一页显示5条
        page.setPath("/letter/llist");  //设置路径
        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<>();
                // 将 message 封装进去
                map.put("conversation", message);
                // 私信数量
                map.put("letterCount", messageService.findLetterCount(message.getConversationId()));
                // 将 未读消息数量 封装进去
                map.put("unreadCount", messageService.findLetterUnreadCount(user.getId(), message.getConversationId()));
                // 找到谁给“自己”传了消息的id,然后传入,因为我们在页面上还要显示这个人的用户名和头像
                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";
    }
  
}

image-20220718105055527

首先要将首页 index.html“消息” 链到 controller 上

image-20220718091742873

然后是处理私信列表的模板 letter.html

image-20220718091948246

image-20220718092101195

image-20220718092655941

到这里私信列表功能就已经实现了,到这里可以启动一下项目测试一下


接下来开发 私信详情 功能

MessageController 中添加下面方法

// 私信详情
@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));


    return "/site/letter-detail";
}

// 根据conversationId查询目标用户
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);
    }
}

image-20220718105144995

然后我们就可以处理模板了

首先我们在会话列表里 letter.html 做一个处理,把那个链接(可以链到详情页面)做好

image-20220718101751168

然后处理私信详情页面 letter-detail.html

image-20220718104108032

image-20220718104207393

image-20220718104343612

image-20220718104424136

image-20220718104452073

到这里私信详情就开发完了

测试

image-20220718105307160

image-20220718105238305

经测试,功能开发成功!

3.8 发送私信

image-20220718145742909

这次开发发送私信设置已读功能

首先开发发送私信的功能

  1. 数据访问层(dao)

在之前创建好的dao组件MessageMapper中增加 发送私信设置已读 方法

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

// 修改私信的状态(未读-->已读或添加私信后首先直接设置为未读货值设置为删除)
int updateStatus(List<Integer> ids, int status);

image-20220718153914495

然后是对应的mapper配置文件

image-20220718154113387

  1. 业务层(Service)

在之前创建的MessageService中添加 发送私信设置已读 方法

@Autowired
private SensitiveFilter sensitiveFilter;
// 新增私信
public int addMessage(Message message){
  // 过滤一下消息(过滤标签和敏感词)
  message.setContent(HtmlUtils.htmlEscape(message.getContent()));
  message.setContent(sensitiveFilter.filter(message.getContent()));
  return messageMapper.insertMessage(message);
}
// 更新私信的状态
public int readMessage(List<Integer> ids) {
  return messageMapper.updateStatus(ids, 1);  // 1 表示已读
}

image-20220718154743154

  1. 表现层

UserService中增加一个根据用户名查询用户的方法(dao接口之前已经写过这个方法),方便MessageController使用这个方法

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

image-20220718160351905

然后在MessageController中增加添加私信方法

// 发送私信
@RequestMapping(path = "/letter/send", method = RequestMethod.POST)
@ResponseBody      // 因为是异步方式,所以要加上这个注解
public String sendLetter(String toName, String content) {
    User target = userService.findUserByName(toName);
    if (target == null) {
        return CommunityUtil.getJSONString(1, "目标用户不存在!"); // 返回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);
    // 异步方式返回JSON字符串
    return CommunityUtil.getJSONString(0);  // 新增私信默认状态为0,表示未读
}

然后我们需要修改 私信列表 letter.html 处的发私信按钮,这个功能是在 letter.js 中实现的,所以我们需要修改一下letter.js

image-20220718163015040

image-20220718163032329

image-20220718163148096

然后我们需要处理一下私信详情页面 letter-detail.html的私信时会默认带上对方的用户名,我们要处理一下

image-20220718163538245

到这里发送私信的功能就开发完了。

然后我们接下来需要开发当用户读私信时设置已读功能

我们只需要在 MessageController 的 getLetterDetail 访问私信详情方法里,把私信列表里面的未读的消息提取出来,然后自动的设置为已读就可以了

image-20220718165013138

这样,设置已读功能就开发完成了,开发完成之后可以测试一下。

3.9 统一异常处理

SpringBoot中异常的自动处理

浏览器发送请求的时候一律发送给表现层,然后 表现层 调用 业务层,业务层 调用 数据层,假如 数据层 出异常了,它会抛给 业务层,然后 业务层 会抛给 表现层。所以整个三层架构,无论 数据层还是业务层哪个层次的异常,最终都会汇集到表现层,因此只要我们统一的对表现层捕获异常就能处理系统的所有异常

SpringBoot中异常处理

SpringBoot会自动处理异常,跳转到相应的 404.html 、500.html 对应的错误状态页面。

我们先将 404.html、500.html页面处理一下:

文件夹名字一定要叫 error,且在 templates 目录下,错误文件的名字一定要叫错误状态。

image-20220719085500452

image-20220719085644094

image-20220719090041068

运行项目测试:

image-20220719090059262

image-20220719090137416

SpringBoot会自动给异常进行一个处理,但是这个处理还不是一定符合我们的预期,

比如 404 这个就ok,找不到资源没有更好的办法,500 就不一样,500 意味着服务端报错了,报错的时候我们最好记个日志,好以后分析,所以直接跳转页面这只是表面上的一个处理,我们内在需要记个日志这个还没有处理。

SpringBoot中异常的详细处理

接下来我们来看一下如何统一记录日志:

image-20220719081530683

  • @ModelAttribute

绑定数据的意思是:

比如 controller 有很多请求,这些请求当中要用到同一个数据给模板,我们需要往 model

里装同一个数据,我们可以利用 @ModelAttribute 加一个方法给 model 统一绑定参数,给所有的 controller 用。

  • @DataBinder

    这个注解的作用是,页面向服务器传参,会被自动的做转换,是因为内部调了很多的参数转换器,万一程序当中默认的参数转换器不够用,有一个特殊的类型需要处理,可以自定义一个转换器,然后用 @DataBinder 把它注册上。

上面的 @ModelAttribute 和 @DataBinder 目前没有需求要用,所以就不演示了。接下来我们演示利用 @ExceptionHandler 统一处理所有 controller 可能发生的异常。

演示

首先在 HomeController 里加上一个请求,为什么呢?因为controller发生异常之后,我们统一处理,记了日志,处理完以后我们得去到 500那个页面,这个时候因为是我们人为处理的,我们需要手动的重定向过去,所以我们需要提前把 500 页面这个请求的访问给它配一下,增加一下这个请求的处理。

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

image-20220719101404514

接下来我们需要用@ControllerAdvice 声明一个 controller 全局配置类,然后对所有 controller 的异常做统一的处理

@ControllerAdvice 不加参数的话会扫描所有的bean,范围太大了,我们通常需要一个限制,让它范围小一点,

@ControllerAdvice(annotations = Controller.class) 的意思是 这个注解只扫描带有 @Controller 注解的那些组件,

我们需要在这个类里面加一个方法处理所有的错误的情况,方法之前加一个注解@ExceptionHandler 表示这个方法是处理所有异常的方法

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

    // 为了记日志
    private static Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);

    @ExceptionHandler({Exception.class})   // 里面的参数表示处理所有异常
    public void handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {        // 方法名无所谓
        // 记录一下异常的概括
        logger.error("服务器发生异常: " + e.getMessage());
        // 详细记录异常的栈的信息
        for (StackTraceElement element : e.getStackTrace()) {
            logger.error(element.toString());
        }
        /*
            普通的请求重定向到刚刚500的controller
            异步请求的话返回json,json字符串里面写错误信息
         */
        // 判断请求是普通请求还是异步请求
        String xRequestedWith = request.getHeader("x-requested-with");
        if ("XMLHttpRequest".equals(xRequestedWith)) {
            // 异步请求
            response.setContentType("application/plain;charset=utf-8"); // plain表示向浏览器返回的是一个普通的字符串
            PrintWriter writer = response.getWriter();
            writer.write(CommunityUtil.getJSONString(1, "服务器异常!"));
        } else {
            // 普通请求重定向到错误页面
            response.sendRedirect(request.getContextPath() + "/error");
        }
    }
}

image-20220719093924630

image-20220719101522812

上面的处理的话如果是 500 错误,表示服务器错误,会记个日志,然后跳到 500 那个controller,然后这个 controller会跳转到 500.html 界面

如果是 404 错误,也就是找不到资源,会直接跳转到 404.html 页面

测试

image-20220719101736530

普通请求的 500 异常

image-20220719101914295

异步发私信的 500 异常

image-20220719101759548

到这里,统一异常处理就完成了。

3.10 统一记录日志

image-20220719154250916

这里日志的意思是平时不发生异常的话也记录日志。

image-20220719154720536

image-20220719155449555

image-20220719162102990

image-20220719162535840

切入点表达式:

# 切入点表达式
    切入点表达式
    execution(返回值 包.类名.方法名(参数类型))
    execution(返回值 包.类名.*(..))
        *代表类中所有方法  ..代表参数任意
        aop.*.*(..) 代表aop包下的所有类的所有方法在执行时都要加前置通知
    execution:
        第一个代表方法的返回值,*代表不关心方法的返回值
        之后空格写包名(包.类.方法名),切类中所有方法且不关心方法的参数写*(..)

以前写的关于AOP的博客:

SpringBoot中AOP切面编程和文件上传

Spring中AOP编程本质(动态代理机制)以及前置通知的开发

SpringAOP使用示例

Spring AOP 的示例

下面写一个示例体会一下 Spring Aop 的语法

写完这个示例之后将类的两个注解注释掉,防止影响记录日志时的数据观看


@Component
@Aspect
public class AlphaAspect {

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

    }
    // 前置通知
    @Before("pointcut()")
    public void before() {
        System.out.println("before");
    }
    // 后置通知
    @After("pointcut()")
    public void after() {
        System.out.println("after");
    }
    // 如果想在有了返回值以后处理一下逻辑可以使用这个注解(相当于后置通知的增强版)
    @AfterReturning("pointcut()")
    public void afterRetuning() {
        System.out.println("afterRetuning");
    }
    // 在抛异常之后置入代码
    @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;                         // 目标组件方法的返回值
    }
}

/*
around before
before
2022-07-19 17:00:43,555 DEBUG [http-nio-8080-exec-1] c.n.c.d.L.selectByTicket [BaseJdbcLogger.java:143] ==>  Preparing: select id, user_id, ticket, status, expired from login_ticket where ticket = ? 
2022-07-19 17:00:43,555 DEBUG [http-nio-8080-exec-1] c.n.c.d.L.selectByTicket [BaseJdbcLogger.java:143] ==> Parameters: e626e90074e84641993380de204f177e(String)
2022-07-19 17:00:43,557 DEBUG [http-nio-8080-exec-1] c.n.c.d.L.selectByTicket [BaseJdbcLogger.java:143] <==      Total: 1
afterRetuning
after
around after
*/

image-20220719170818303

对所有业务组件记录日志
@Component
@Aspect
public class ServiceLogAspect {

    // 日志
    private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);
    // 切点
    @Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
    public void pointcut() {
    }
    // 使用前置通知记录日志
    @Before("pointcut()")
    public void before(JoinPoint joinPoint) {    // 这个参数是为了知道访问了哪个类的哪个方法
        // 用户[1.2.3.4,ip地址],在[xx时间],访问了[com.nowcoder.community.service.xxx()].
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        // 得到request对象
        HttpServletRequest request = attributes.getRequest();
        String ip = request.getRemoteHost();        // 利用request得到ip地址
        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));
    }

}

访问首页:

image-20220719173449969

经测试,统一记录日志功能完成

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值