社区系统项目复盘-3

基于Springboot的核心功能实现

包括自定义前缀树过滤敏感词;使用异步请求的方式发布帖子;查看帖子详情;添加评论时需要同时增加评论的数据和修改帖子的评论数量,进行两步数据库操作,出于安全考虑采用事务管理;私信列表部分包括显示私信列表与私信详情两个子功能;采用异步方式发送私信;配置Controller的全局配置类进行统一异常处理;采用AOP实现统一记录日志。

过滤敏感词

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

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

    • 编写过滤敏感词的方法

      https://res.craft.do/user/full/fd148a50-4a5b-9a85-bec3-e1645571e2c7/doc/8DD0B865-8ACD-4D75-881A-7B9664DC763F/9E3219F6-85F7-480D-8265-7D08EB91804C_2/JD4Ydd5uwLuqv4zkrNls6vRbdYyP9bUQB36h1j4SsHAz/Image.png

发布帖子

该功能的实现需要异步请求,使用了AJAX

示例:怎么使用jQuery发送AJAX请求?

1.需要在Controller里添加处理异步请求的方法,一般请求方式为post,因为通常是浏览器通过异步的方式向服务器提交一些数据,然后服务端向浏览器响应一个提示,不需要返回网页而是返回一个字符串,所以需要添加@ResponseBody注解

// ajax示例
@RequestMapping(path = "/ajax", method = RequestMethod.POST)
@ResponseBody
public String testAjax(String name, int age) {
	// 请求处理逻辑
	System.out.println(name);
	System.out.println(age);
	// 返回JSON字符串,方法getJSONString()封装在工具类中
	return CommunityUtil.getJSONString(0, "操作成功!");
}

2.需要在网页中写一段jQuery代码,访问处理异步请求的方法。

分为两步:引入jQuery,使用jQuery发送异步请求

// ajax示例
<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) {
                    data = $.parseJSON(data);
                    console.log(typeof(data));
                    console.log(data.code);
                    console.log(data.msg);
                }
            );
        }
    </script>

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

https://res.craft.do/user/full/fd148a50-4a5b-9a85-bec3-e1645571e2c7/doc/8DD0B865-8ACD-4D75-881A-7B9664DC763F/33A61C32-9765-4A58-91B0-E3542A3DB34F_2/xBH8A0EQkg1ONoI6wV0aFGFylxZTvEyjydwp6SkCWXUz/Image.png

具体实现:

首先需要定义一个工具类方法处理Json相关的转换,因为服务端要向客户端返回一些提示信息和数据,使用了fastjson,需要提前导入fastjson的jar包

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

在DiscussPostService中添加增加帖子的相关业务层代码逻辑:

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

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

        return discussPostMapper.insertDiscussPost(post);
    }

新建DiscussPostController类,在里面写发布帖子的方法:

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

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

浏览器发送异步请求:

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

帖子详情

按照数据层,业务层,表现层逐级来写就行,注意 帖子详情中的内容很多,点赞功能暂未实现,点赞的相关内容后面补充(大概redis部分…)

https://res.craft.do/user/full/fd148a50-4a5b-9a85-bec3-e1645571e2c7/doc/8DD0B865-8ACD-4D75-881A-7B9664DC763F/F89C0D01-64DA-48CC-A241-345FFCE48BFA_2/T5oH6HVMrkxG24AnOURIpc6MlrNqxm2sTC7PmrbgRTYz/Image.png

https://res.craft.do/user/full/fd148a50-4a5b-9a85-bec3-e1645571e2c7/doc/8DD0B865-8ACD-4D75-881A-7B9664DC763F/E795B24E-D81F-4F3B-8A0C-B542D93C6D28_2/gRNQY3llCkBV0rvmObmRfdlhdgI9wt6HINs4qtGyKscz/Image.png

表现层代码相对复杂(详细如下),主要是套娃套娃套娃🪆🪆🪆…

帖子,评论(帖子的评论),回复(帖子的评论的回评论)…

// 帖子详情
    @RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
    public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page) {
        // 帖子
        DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
        model.addAttribute("post", post);
        // 作者
        User user = userService.findUserById(post.getUserId());
        model.addAttribute("user", user);
        // 点赞数量
        // 点赞状态
        // 评论分页信息
        page.setLimit(5);
        page.setPath("/discuss/detail/" + discussPostId);
        page.setRows(post.getCommentCount());
        // 评论: 给帖子的评论
        // 回复: 给评论的评论
        // 评论列表
        List<Comment> commentList = commentService.findCommentsByEntity(ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());
        // 评论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";
    }

添加评论

添加评论是在帖子详情页面,给帖子评论,或者是给帖子的评论进行评论(也叫回复)

因为既要增加评论的数据,又要修改帖子的评论数量,所以需要进行两步数据库操作,从安全性考虑,将这两步数据库操作放到一个事务里进行管理。(⚠️:事务管理)

使用声明式事务管理,用@Transactional进行注解,使用isolation属性声明事务的隔离级别,使用propagation属性声明事务的传播机制。

⚠️注意:只有对帖子进行评论的时候才需要更新帖子的评论数量,需要判断一下。业务层逻辑如下:

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

https://res.craft.do/user/full/fd148a50-4a5b-9a85-bec3-e1645571e2c7/doc/8DD0B865-8ACD-4D75-881A-7B9664DC763F/F9345099-7A71-4C54-83AC-B48F4E98CDFF_2/0SvwAtO5zMJcZUQdBcPxzWtxsBScaZKtIymj0CaTxWEz/Image.png

补充:为什么在添加评论的时候既需要触发评论事件,又需要触发发帖事件?

  • 触发评论事件是因为,系统向被评论用户发送系统通知时是采用消息队列的方式实现的。
  • 仅在对帖子进行评论的时候会触发发帖事件,因为当对帖子进行评论时,帖子评论数量发生改变,也就是post.commentCount会有变化,所以要将Elasticsearch中的数据刷新一下。

私信列表

私信列表包括两个子功能,显示会话列表和私信详情信息。

▶️ 对于显示会话列表,需要查询当前用户的会话列表,每个会话只显示一条最新的私信,分页显示该列表,详细步骤主要包括:首先获得当前登录用户,并且设置分页信息,然后查询当前用户的会话列表,遍历会话列表,查询每一条会话所需要显示的其他信息(该会话私信的总数,该会话未读私信的数量,该会话对面用户的用户信息),除此之外,会话列表中还需要显示当前用户所有未读消息的数量。

▶️ 对于私信详情功能,需要查询某个会话所包含的私信列表,分页显示该列表,并且将显示的私信设置为已读状态,详细步骤主要包括:设置分页信息,根据会话Id查询私信列表,遍历私信列表,查询该条私信发送者有关的用户信息,最后将所有的未读私信设置为已读。

https://res.craft.do/user/full/fd148a50-4a5b-9a85-bec3-e1645571e2c7/doc/8DD0B865-8ACD-4D75-881A-7B9664DC763F/51D39B57-A1FB-422D-8A0E-7B85E6348494_2/xlILUHrnIJWaUn4maZkvm3muyyqk2WxniemKR6livwUz/Image.png

发送私信

采用异步方式发送私信,发送成功后刷新私信列表

🔎详细步骤:首先根据私信接收者的用户名获得接收者的用户信息,如果接收者用户不存在,直接返回错误提示;如果接收者用户存在,那么构造message对象,将message存到数据库中。

https://res.craft.do/user/full/fd148a50-4a5b-9a85-bec3-e1645571e2c7/doc/8DD0B865-8ACD-4D75-881A-7B9664DC763F/05983D89-9BB3-495B-90D0-2303AF9CB665_2/lcFdk1n7GWMam7HoPKBmTDHZn5upTg8IisWKegwRz2wz/Image.png

统一处理异常

服务端的三层架构:表现层 → 业务层 → 数据层。

浏览器发送的请求一律发给表现层,表现层调用业务层,业务层调用数据层。数据层出现异常后会抛出给它的调用者业务层,业务层会把异常抛出给表现层,所以无论是哪个层的异常,最终都会汇集到表现层,所以对表现层的异常进行捕获和处理就可以处理所有的异常。

https://res.craft.do/user/full/fd148a50-4a5b-9a85-bec3-e1645571e2c7/doc/8DD0B865-8ACD-4D75-881A-7B9664DC763F/93DB8CA5-66D0-47A1-8D94-53A45D1EB35D_2/RdT4bGqyNsp2TvHMRWRZqpwbnU6Fdu65wATALDmFXX0z/Image.png

🔎 Springboot提供的方案:只需要在特定路径src/main/resources/templates/error下,添加对应错误状态的页面,那么在发现相应错误的时候,就会自动的跳转到对应页面。错误状态页面的名字必须是错误状态,比如404。

跳出错误页面是表面上的处理,内在记录日志部分还没有处理。spring提供了@ControllerAdvice注解进行相关处理。

  • @ControllerAdvice:用于修饰类,表示该类是Controller的全局配置类,在此类中,可以对Controller进行如下三种全局配置:异常处理方案、绑定数据方案、绑定参数方案
  • 异常处理方案:@ExceptionHandler:用于修饰方法,该方法会在Controller出现异常后被调用,用于处理捕获到的异常
  • 绑定数据方案:@ModelAttribute:用于修饰方法,该方法会在Controller方法执行前被调用,用于为Model对象绑定参数
  • 绑定参数方案:@DataBinder:用于修饰方法,该方法会在Controller方法执行前被调用,用于绑定参数的转换器

具体实现:

  • 在Controller层写好处理error请求的方法getErrorPage()。
  • 利用@ControllerAdvice注解声明一个Controller全局配置类,对所有Controller的异常做统一的处理。在controller/advice路径下,新建一个类ExceptionAdvice,并用@ControllerAdvice进行注解。
  • 定义处理异常的方法handleException()并用@ExceptionHandler进行注解。
  • 记录日志,包括详细的日志信息。
  • 给浏览器一个响应,重定向到错误页面。注意:这里可能是普通请求也有可能是异步请求,需要区分处理,通过request对象来获取请求方式request.getHeader("x-requested-with");如果请求方式为 XMLHttpRequest,说明是异步请求,响应一个字符串。否则重定向到错误页面。

https://res.craft.do/user/full/fd148a50-4a5b-9a85-bec3-e1645571e2c7/doc/8DD0B865-8ACD-4D75-881A-7B9664DC763F/F3F39B20-BCB2-470A-B5CF-6593E72E8E0B_2/wCOszwfxzz984aA7j4Gv5BYvZWuEHYmdU7tFxmjXSZcz/Image.png

小结:不需要对Controller做任何处理,只需要使用@ControllerAdvice注解声明一个Controller的全局配置类,对添加了@Controller注解的所有类进行一个统一的异常处理。在全局配置类中使用@ExceptionHandler注解定义一个异常处理方法,对于所有类型的异常进行处理,处理过程是:首先输出异常信息(包括异常的详细信息),然后根据请求方式的不同,进行不同的响应,如果是异步请求,则响应给浏览器一个字符串;如果是普通请求,则重定向到错误页面。请求方式是通过request对象来获取的。

// 代码实现
@ControllerAdvice(annotations = Controller.class)
public class ExceptionAdvice {

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

统一记录日志

1.AOP的基本实现:

首先定义一个方面组件,该方面组件用@Component @Aspect处理,在方面组件里定义切点和通知。

切点:通过@Pointcut注解实现

// 示例
@Pointcut("execution(* com.community.service.*.*(..))")
public void pointcut() {

}

通知:(共有五种通知方式)

  1. 在连接点前织入 @Before
// 示例
@Before("pointcut()")
public void before() {
	System.out.println("before");
}
  1. 在连接点后织入@After
// 示例
@After("pointcut()")
public void after() {
	System.out.println("after");
}
  1. 在有了返回值以后再处理逻辑 @AfterReturning
// 示例
@AfterReturning("pointcut()")
public void afterRetuning() {
	System.out.println("afterRetuning");
}
  1. 在抛异常的时候织入代码 @AfterThrowing
// 示例
@AfterThrowing("pointcut()")
public void afterThrowing() {
	System.out.println("afterThrowing");
}
  1. 环绕通知,既想在前面织入逻辑,又想在后面织入逻辑 @Around

要有返回值(Object)和参数(ProceedingJoinPoint joinPoint),抛出异常throws Throwable。除了环绕通知以外的其他通知也可以添加连接点JointPoint的参数。

利用连接点,jointPoint.proceed();就是调用目标对象的方法逻辑,将目标组件的返回值return(return obj)。

// 示例
@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;
}

2.统一记录日志需求:对所有的业务组件记录日志,在业务组件调用的一开始记录日志,采用@Before的方式。日志记录格式:用户xxx,在xxx,访问了xxx。

https://res.craft.do/user/full/fd148a50-4a5b-9a85-bec3-e1645571e2c7/doc/8DD0B865-8ACD-4D75-881A-7B9664DC763F/BE7C609C-8CB4-4BC4-9D35-2413BBA348D0_2/XpQPCZI4Iuh8GF7a5inl9AaHz1Evntj18q1epT9U960z/Image.png

具体实现:

  • 定义一个方面组件:ServiceLogAspect,使用@Component@Aspect进行注解
  • 声明切点pointcut(),使用了@Pointcut注解, 切点为所有的业务层方法
@Pointcut("execution (* com.nowcoder.community.service.*.*(..))")
public void pointcut(){

}
  • 定义通知,日志格式:用户[1.2.3.4],在[xxx],访问了[service.xxx()].因为是在业务组件调用的一开始就记录日志,也就是在切点前织入代码逻辑,所以使用@Before注解。
    问题1:用户IP怎么获取?可以通过request对象,先利用RequestContextHolder工具类中的getRequestAttributes()方法获取attributes;然后attributes.getRequest()获得request对象;最后使用request.getRemoteHost获得用户IP。
    问题2:如何获得当前时间?new Date()获取。
    问题3:如果获得调用的是哪个类哪个方法?jointPoint是程序织入的目标,也就是目标组件要调用的方法,通过jointPoint就可以得到调用的是哪个类的哪个方法。

    • joinPoint.getSignature().getDeclaringTypeName():获得类名;
    • joinPoint.getSignature().getName():获得方法名。

    补充:JoinPoint类,用来获取代理类和被代理类的信息。JoinPoint.getSignature()

@Before("pointcut()")
public void before(JoinPoint joinPoint){
	// 用户[1.2.3.4],在[xxx],访问了[com.nowcoder.community.service.xxx()].
	ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
	if(attributes == null){
		return;
	}
	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));
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
IT项目复盘会议模板用于总结和评估已完成的IT项目,以确定项目的成功与短板,并为未来的项目提供改进和学习的机会。以下是一个常见的IT项目复盘会议模板的简要内容: 1. 会议目的: - 确定项目成功的关键因素和挑战 - 分享项目经验和教训 - 提供改进建议和措施 2. 项目概述: - 提供项目背景和目标 - 明确项目范围和时间表 3. 成功因素: - 总结项目的成功因素,例如团队合作、领导支持、资源管理等 - 强调成功因素的积极影响和实施方法 4. 挑战和教训: - 讨论项目中的挑战和困难 - 反思潜在的教训和应对方法,包括项目管理、沟通、风险管理等方面 5. 项目绩效评估: - 评估项目的时间表、预算和质量目标的达成情况 - 分析任何超出或不足的情况,并确定其中的原因 6. 改进建议: - 提供项目改进的建议和措施 - 确定未来项目中需要避免的问题,并提出解决方案 7. 团队反馈: - 鼓励团队成员分享对项目的观点和经验 - 倾听并考虑团队成员提出的意见和建议 8. 行动计划: - 制定行动计划,明确改进措施和责任人 - 确定实施时间表和监控方法 9. 会议总结: - 概括会议讨论的要点和重点 - 强调重要的改进措施和下一步行动 IT项目复盘会议模板的内容可以根据实际项目的需要进行调整和添加,以确保全面而有针对性的评估和总结。该模板有助于促进项目范围、进度和质量的改进,提高项目的成功率和效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值