• SpringBoot后端系统的基础架构


    前言

    前段时间完成了毕业设计课题——《基于Spring Boot + Vue的直播后台管理系统》,项目名为LBMS,主要完成了对直播平台数据的可视化展示和分级的权限管理。虽然相当顺利地通过了答辩,但是由于时间以及本人水平的不足,其实后端系统的代码还仅仅停留在“能跑就行”。因此这篇文章主要也是为了反思一下项目中亟待完善的地方,我后续也会考虑在此基础上编写一个后端管理系统的通用架构模板。

    2020/6/10 这个模板项目已经在做了:common-MS
    2020/6/12 完成了日志处理、异常处理、结果封装、参数校验模块

    日志处理

    日志框架

    Java中可用的日志框架有很多,并且通常都有着抽象层+实现层的结构,在实际应用中,只需要考虑抽象层提供的功能接口而不用了解实现层的具体结构。Spring Boot默认的日志框架为Slf4j + logback。在我的毕设项目中,虽然引入了日志框架,但是却很少使用。

    Slf4j的输出级别有5种:trace、debug、info、warn、error,可以通过在properties或yml文件中通过logging.level.root参数指定日志输出的级别,其中root代表配置对整个项目生效,可以修改为其他路径进行自定义配置

    日志代码的简化

    使用lombok可以简化代码的编写:

    Logger logger = LoggerFactory.getLogger(MyLog.class);
    logger.info("logger info test");
    
    @Slf4j
    // ...
    log.info("lombok info test")
    

    对于日志信息中的变量,建议使用占位符形式而非字符串拼接

    log.info(time + " " + methodName + "is invoked");
    
    log.info("{} {} is invoked", time, methodName)
    

    将日志输出到文件

    这里用了某位大牛写的logback-spring.xml进行配置(可以访问我的Github获取具体文件),配置完成后可以将日志按级别的不同输出到指定目录下的不同文件,并且对每天的日志分开保存,日志文件大小超过100MB时,还可以自动分块。

    基于AOP的日志处理

    之前用DRF做一个项目时,发现它很贴心地在控制台展示了每个请求的参数、返回状态码等信息,SpringBoot当然也可以实现类似的功能。

    想要实现上述需求,毫无疑问要在Controller层使用AOP了。对每个请求,我想要输出对应的URL、请求方法、参数、返回状态码等信息。

    AOP的切点切面:

    @Pointcut("execution(* priv.zzz.controller..*.*(..))")
    public void controllerAspect() {}
    
    @Before("controllerAspect()")
    public void before(JoinPoint joinPoint){
        log.info(getRequestMessage(joinPoint));
    }
    
    @AfterReturning(pointcut = "controllerAspect()", returning = "returnValue")
    public void after(JoinPoint joinPoint, Object returnValue){
        if (returnValue instanceof Result){
                log.info(getResponseMessage(joinPoint, ((Result) returnValue).getStatus()));
        }
        if (returnValue instanceof ResultSet){
            log.info(getResponseMessage(joinPoint, ((ResultSet) returnValue).getStatus()));
        }
    }
    

    URL、rquestMethod:

    private String getBaseMessage(JoinPoint joinPoint) {
    
        HttpServletRequest request = ((ServletRequestAttributes)(Objects.requireNonNull(RequestContextHolder.getRequestAttributes()))).getRequest();
        String url = request.getRequestURI();
        String requestMethod = request.getMethod();
        String datetime = DateFormatter.format(new Date());
    
        return datetime + " " + url + " " + requestMethod;
    }
    

    请求参数:

    private String getRequestMessage(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
        Object[] args = joinPoint.getArgs();
        String[] parameters = methodSignature.getParameterNames();
    
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < Math.min(args.length, parameters.length); i++){
            stringBuilder.append(parameters[i]).append(":").append(args[i]).append(" ");
        }
        String params = "{ "+stringBuilder.toString()+"}";
    
        return this.getBaseMessage(joinPoint) + " " + params;
    }
    
    private String getResponseMessage(JoinPoint joinPoint, int status) {
        return this.getBaseMessage(joinPoint) + " " + status;
    }
    

    最终效果:

    2020-06-11 13:10:32 /log GET { name:test number:1 }
    2020-06-11 13:10:32 /log GET 200
    

    结果封装

    前后端分离的情况下前后端一般都是通过Json数据进行交互,使用@RestController注解可以将返回的对象转为Json格式,在那之前,我们需要对返回的结果封装为Result对象。Result中主要要包含的字段有status、message和data,对于status和message,我使用枚举类型ResultCode进行封装,其中包含SUCCESS、NOT_FOUND、UNAUTHORIZED等常见状态码。data要考虑返回的数据是否是一个列表,如果是列表,还需要实现分页功能。

    在LBMS中,我将这两种结果集(单个对象和列表对象)封装为同一个结果集,在新的模板项目中,我尝试使用Result和ResultSet两种结果集进行封装。这样做的好处是返回结果更加清晰,缺点是有些地方可能需要一些额外的处理,比如在日志模块获取controller返回的状态码时,具体的优劣有待更加深入的使用。

    Result示例:

    {
      "timestamp": "2020-06-12T15:44:02.106+08:00",
      "status": 200,
      "message": "success",
      "data": 123,
      "path": "/result"
    }
    

    ResultSet示例:

    {
      "timestamp": "2020-06-12T15:38:01.130+08:00",
      "total": 2,
      "status": 200,
      "message": "success",
      "list": [
        {
          "username": "Alice",
          "age": 20,
          "sex": 0,
          "email": "12345@qq.com"
        },
        {
          "username": "Eric",
          "age": 21,
          "sex": 1,
          "email": "12345@163.com"
        }
      ],
      "path": "/result/set"
    }
    

    结果封装还要考虑的一个问题是对异常的处理,这个我在异常处理章节会谈到。

    参数校验

    上一个项目中的参数校验做的相当有限,目前Spring Boot主流的参数校验方式有hibernate-validator、Assert等。使用validator参数校验的位置可以在实体类字段处,也可以在Controller传参处。

    网上大部分文章说spring-boot-starter-web已经包含了hibernate-validator,但我不知道为什么无法直接使用@NotNull等注解,因此手动引入validator:

    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>6.1.5.Final</version>
    </dependency>
    

    一个简单的例子:

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class TestUser {
    
        @NotNull(message = "用户名不能为空")
        @NotBlank(message = "用户名不能为空")
        @Length(max = 20, message = "用户名过长")
        private String username;
    
        @Min(0)
        private Integer age;
    
        @Range(min = 0, max = 1)
        private Integer sex;
    
        @Email(message = "邮箱格式错误")
        private String email;
    
    }
    

    使用Assert进行校验:

    Assert.notNull(user.getUsername(), "用户名不能为空");
    

    validator校验失败时,会抛出MethodArgumentNotValidException异常。

    Assert校验失败时会抛出IllegalArgumentException

    实际应用中我们可以灵活使用这两种校验方式,并且可以通过ExceptionHandler对这些异常进行捕获和统一处理。

    异常处理

    LBMS中,我的异常处理采用的是自定义异常+@ResponseStatus注解的方式,在特定的地方抛出异常,交给ResponseStatusExceptionResolver去处理。

    @ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "无法识别的操作")
    public class BadOperationException extends Exception {
    
        public BadOperationException(){
            super();
        }
    
        public BadOperationException(String msg){
            super(msg);
        }
    }
    

    在common-MS中,异常处理采用@ControllerAdvice+@ExceptionHandler实现,@ControllerAdvice将一个类标注为全局的异常处理类,@ExceptionHandler用于捕获不同的异常进行对应处理。同理,对于异常的返回结果也与正常返回结果格式保持一致,使用Result封装。

    例如,捕获上述validator抛出的MethodArgumentNotValidException异常并进行处理的代码为:

    @ExceptionHandler(value = { MethodArgumentNotValidException.class })
    public Result<String> validatorException(HttpServletResponse response, MethodArgumentNotValidException e) {
        // validator设置了message时返回message,未设置则返回“非法参数”
        FieldError error = e.getBindingResult().getFieldError();
        String message = "非法参数";
        if(error != null){
            message = error.getField() + error.getDefaultMessage();
        }
        response.setStatus(400);
        return Result.failure(400, message);
    }
    

    当提交的邮箱格式错误时返回:

    {
      "timestamp": "2020-06-12T15:45:07.874+08:00",
      "status": 400,
      "message": "email邮箱格式错误",
      "data": null,
      "path": "/user"
    }
    

    同理,还可以对自定义的异常进行处理:

    public class ExampleException extends Exception{
    
        public ExampleException() {super();}
    
        public ExampleException(String message) {
            super(message);
        }
    }
    

    使用时直接抛出异常即可:

    @RequestMapping(value = "exception", method = RequestMethod.GET)
    public Result exampleException() throws ExampleException {
        throw new ExampleException("这是一个测试异常");
    }
    

    如果需要修改Response的状态码而不仅仅是使用自定义的status,可以在@ExceptionHandler注解的方法内引入并使用

    response.setStatus(400);
    

    待续~

    todo:Shiro、分页功能、Redis等。

    完整代码移步Github:common-MS

  • 相关阅读:
    单链表
    队列
    产品经理们,遇到Bug请别十万火急
    2008年7月3日
    JS URL 参数
    NET 产品版权保护方案 (.NET源码加密保护)
    常用正则表达式
    网线接法
    C#创建多文档的界面
    TGE学习笔记04 billboard
  • 原文地址:https://www.cnblogs.com/2511zzZ/p/13109151.html
Copyright © 2020-2023  润新知