• SpringBoot: Controller层的优雅实现


    1. 实现目标

    1. 优雅校验接口入参
    2. 响应体格式统一处理
    3. 异常统一处理

    2. 统一状态码

    • 创建状态码接口,所有状态码必须实现这个接口,统一标准
    package com.example.mavendemo.enums;
    
    public interface StatusCode {
    
        int getCode();
    
        String getMsg();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 定义状态码
    package com.example.mavendemo.enums;
    
    import lombok.AllArgsConstructor;
    import lombok.Getter;
    
    @Getter
    @AllArgsConstructor
    public enum ResponseCode implements StatusCode {
    
        SUCCESS(0, "请求成功"),
        FAILED(1, "请求失败"),
        VALIDATE_ERROR(2, "参数校验失败"),
        RESPONSE_ERROR(3, "返回失败"),
        APP_ERROR(40000,"服务内部异常"),
        BUSINESS_ERROR(40001,"业务异常");
    
        private int code;
    
        private String msg;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    3. 统一响应体

    • 响应结果统一使用ResultResponse进行包装,返回给前端
    package com.example.mavendemo.dto;
    
    import com.example.mavendemo.enums.ResponseCode;
    import com.example.mavendemo.enums.StatusCode;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    
    @Data
    @AllArgsConstructor
    public class ResultResponse {
    
        private int code;
    
        private String msg;
    
        private Object data;
    
        public static ResultResponse ok() {
            return new ResultResponse(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg(), null);
        }
    
        public static ResultResponse ok(Object data) {
            return new ResultResponse(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg(), data);
        }
    
        public static ResultResponse error(StatusCode statusCode) {
            return new ResultResponse(statusCode.getCode(), statusCode.getMsg(), null);
        }
    
        public static ResultResponse error(StatusCode statusCode, Object data) {
            return new ResultResponse(statusCode.getCode(), statusCode.getMsg(), data);
        }
    
    }
    
    
    • 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

    4. 统一异常

    • 自定义一个异常类,对于特定错误,传入响应的状态码
    package com.example.mavendemo.exception;
    
    import com.example.mavendemo.enums.ResponseCode;
    import com.example.mavendemo.enums.StatusCode;
    
    import lombok.Getter;
    
    @Getter
    public class ApiException extends RuntimeException {
    
        private StatusCode statusCode;
    
        public ApiException(String message) {
            super(message);
            this.statusCode = ResponseCode.APP_ERROR;
        }
    
        public ApiException(StatusCode statusCode, String message) {
            super(message);
            this.statusCode = statusCode;
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    5. 统一入参校验

    • 需要引入spring-boot-starter-validation
    
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0modelVersion>
        <parent>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-parentartifactId>
            <version>2.6.6version>
            <relativePath/> 
        parent>
        <groupId>com.examplegroupId>
        <artifactId>maven-demoartifactId>
        <version>0.0.1-SNAPSHOTversion>
        <name>maven-demoname>
        <description>maven-demodescription>
        <properties>
            <java.version>8java.version>
        properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
            dependency>
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-validationartifactId>
            dependency>
        dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-maven-pluginartifactId>
                    <executions>
                        <execution>
                            <goals>
                                <goal>repackagegoal>
                            goals>
                        execution>
                    executions>
                    <configuration>
                        <includeSystemScope>trueincludeSystemScope>
                        <mainClass>com.example.mavendemo.MavenDemoApplicationmainClass>
                    configuration>
                plugin>
            plugins>
        build>
    project>
    
    
    • 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
    • 定义实体类,对于需要校验的字段添加注解,例如@NotBlank,@Max
    package com.example.mavendemo.model;
    
    import javax.validation.constraints.Max;
    import javax.validation.constraints.NotBlank;
    
    import lombok.Data;
    
    @Data
    public class User {
    
        @NotBlank(message = "姓名不能为空")
        private String name;
    
        @Max(value = 18, message = "年龄不能大于18")
        private Integer age;
    
        private String address;
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 在传参时,使用@Valid,例如@RequestBody @Valid User user

    6. 统一返回结果

    • 如果不想统一响应体,可以定义一个注解,标记具体的接口进行排除
    package com.example.mavendemo.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface NotResponseAdvice {
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 实现ResponseBodyAdvice接口,重写supports和beforeBodyWrite方法,完成对response body的增强
    package com.example.mavendemo.advice;
    
    import com.example.mavendemo.annotation.NotResponseAdvice;
    import org.springframework.core.MethodParameter;
    import org.springframework.http.MediaType;
    import org.springframework.http.converter.HttpMessageConverter;
    import org.springframework.http.server.ServerHttpRequest;
    import org.springframework.http.server.ServerHttpResponse;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
    
    import com.example.mavendemo.dto.ResultResponse;
    
    /**
     * basePackages指定作用范围
     **/
    @RestControllerAdvice(basePackages = {"com.example.mavendemo.controller"})
    public class ControllerResponseAdvice implements ResponseBodyAdvice<Object> {
    
        /**
         * 校验是否执行beforeBodyWrite方法
         */
        @Override
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
            // 如果返回值已经是ResultResponse,或有NotResponseAdvice注解则不进行包装
            return !(returnType.getParameterType().isAssignableFrom(ResultResponse.class)
                || returnType.hasMethodAnnotation(NotResponseAdvice.class));
        }
    
        /**
         * 对返回值进行处理
         */
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
            Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
            ServerHttpResponse response) {
            return ResultResponse.ok(body);
        }
    }
    
    
    • 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
    • 这里需要注意String类型的返回值,使用上面的代码会报错java.lang.ClassCastException: cannot be cast to java.lang.String
    • 需要介绍一下HttpMessageConverter接口,负责将请求信息转换为一个java对象,将java对象输出为响应信息;所以在触发@ResponseBody注解时,Spring都会遍历这个HttpMessageConverter列表,然后选择第一个符合返回值类型的转换器然后进行转换。
    • HttpMessageConverter如下

    在这里插入图片描述

    • String类型会优先使用StringHttpMessageConverter转换器。实际上String类型既可以使用MappingJackson2HttpMessageConverter,也可以使用StringHttpMessageConverter来解析。所以可以将HttpMessageConverter列表反转,调换MappingJackson2HttpMessageConverter和StringHttpMessageConverter的顺序来解决。
    • 实现WebMvcConfigurer,重写configureMessageConverters方法
    package com.example.mavendemo.advice;
    
    import java.util.Collections;
    import java.util.List;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.converter.HttpMessageConverter;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class WebResponseConfig implements WebMvcConfigurer {
    
        /**
         * 反转HttpMessageConverter集合,将MappingJackson2HttpMessageConverter移到StringHttpMessageConverter前面
         */
        @Override
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            Collections.reverse(converters);
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    7. 统一异常处理

    • 使用@RestControllerAdvice注解对Controller进行增强,配合@ExceptionHandler使用,统一处理异常
    package com.example.mavendemo.advice;
    
    import com.example.mavendemo.exception.ApiException;
    import org.springframework.web.bind.MethodArgumentNotValidException;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    import com.example.mavendemo.dto.ResultResponse;
    import com.example.mavendemo.enums.ResponseCode;
    
    @RestControllerAdvice
    public class ControllerExceptionAdvice {
    
        /**
         * 参数错误异常
         */
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public ResultResponse methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
            return ResultResponse.error(ResponseCode.VALIDATE_ERROR,
                e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
        }
    
        /**
         * 自定义异常
         */
        @ExceptionHandler(ApiException.class)
        public ResultResponse apiExceptionHandler(ApiException e) {
            return ResultResponse.error(e.getStatusCode(), e.getMessage());
        }
    
        /**
         * 异常兜底
         */
        @ExceptionHandler(Exception.class)
        public ResultResponse exceptionHandler(Exception e) {
            return ResultResponse.error(ResponseCode.FAILED, e.getMessage());
        }
    
    }
    
    
    • 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

    8. 验证

    • 测试类
    package com.example.mavendemo.controller;
    
    import javax.validation.Valid;
    
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.example.mavendemo.annotation.NotResponseAdvice;
    import com.example.mavendemo.enums.ResponseCode;
    import com.example.mavendemo.exception.ApiException;
    import com.example.mavendemo.model.User;
    
    @RestController
    public class HelloController {
    
        /**
         * 测试入参校验和统一响应体
         */
        @PostMapping("/hello")
        public User hello(@RequestBody @Valid User user) {
            return user;
        }
    
        /**
         * 测试String类型返回值的响应体
         */
        @GetMapping("/name")
        public String getName(String name) {
            return name;
        }
    
        /**
         * 测试不统一响应体和统一异常处理情况
         */
        @NotResponseAdvice
        @GetMapping("/findUser")
        public User getAddress(String name, Integer age) {
            if (age > 18) {
                throw new ApiException(ResponseCode.BUSINESS_ERROR, "查无此人");
            }
            User user = new User();
            user.setName(name);
            user.setAge(age);
            user.setAddress("北京市王府井大街1号");
            return user;
        }
    
    }
    
    
    • 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

    在这里插入图片描述

    • 验证参数校验

    在这里插入图片描述

    • 验证统一响应体
    1. 返回值是java对象

    在这里插入图片描述

    1. 返回值是String类型

    在这里插入图片描述

    • 验证不需要统一的响应体

    在这里插入图片描述

    • 验证统一异常处理

    在这里插入图片描述

    9. 入参解密

    • 假如有些接口的请求参数需要加密传输,后端接收时需要进行统一解密
    • 新建解密注解,这里限制只能在参数上使用,不能在方法上使用
    package com.example.mavendemo.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.PARAMETER})
    public @interface DecryptRequestBody {
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 新建请求拦截,继承RequestBodyAdviceAdapter抽象类,重写supports和beforeBodyRead方法,这里加解密AesUtil工具类换成你自己的
    package com.example.mavendemo.advice;
    
    import java.io.ByteArrayInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.lang.reflect.Type;
    import java.nio.charset.StandardCharsets;
    
    import org.apache.commons.io.IOUtils;
    import org.springframework.core.MethodParameter;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpInputMessage;
    import org.springframework.http.converter.HttpMessageConverter;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;
    
    import com.example.mavendemo.annotation.DecryptRequestBody;
    import com.example.mavendemo.controller.HelloController;
    import com.example.mavendemo.util.AesUtil;
    
    @RestControllerAdvice(assignableTypes = {HelloController.class})
    public class ControllerRequestAdvice extends RequestBodyAdviceAdapter {
    
        @Override
        public boolean supports(MethodParameter methodParameter, Type targetType,
            Class<? extends HttpMessageConverter<?>> converterType) {
            return methodParameter.hasParameterAnnotation(DecryptRequestBody.class);
        }
    
        @Override
        public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
            Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
            HttpHeaders headers = inputMessage.getHeaders();
            String inputParam = IOUtils.toString(inputMessage.getBody(), StandardCharsets.UTF_8);
            String decrypt = AesUtil.decrypt(inputParam);
            ByteArrayInputStream decryptBody = new ByteArrayInputStream(decrypt.getBytes(StandardCharsets.UTF_8));
            return new HttpInputMessage() {
                @Override
                public InputStream getBody() {
                    return decryptBody;
                }
    
                @Override
                public HttpHeaders getHeaders() {
                    return headers;
                }
            };
        }
    }
    
    
    • 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
    • 使用方法,在接口入参处使用@DecryptRequestBody注解即可
        @PostMapping("/decrypt")
        public User decrypt(@DecryptRequestBody @RequestBody User user) {
            return user;
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 入参传入加密后的字符串即可

    在这里插入图片描述

  • 相关阅读:
    Vertica 向 GBase8a 迁移指南之CHAR(N)类型
    【cmake实战七】如何使用编译的库(动态库dll)2——windows系统
    docker版jxTMS使用指南:python服务之访问数据库
    【Qt】初识Qt&Qt Creator
    痞子衡嵌入式:i.MXRT中FlexSPI外设不常用的读选通采样时钟源 - loopbackFromSckPad
    蓝牙核心规范(V5.4)12.1-深入详解之PAwR
    Open set
    JavaWeb -- MySQL
    JVM基础_10_各种垃圾回收器
    【科技素养】蓝桥杯STEMA 科技素养组模拟练习试卷A
  • 原文地址:https://blog.csdn.net/qq_40977118/article/details/128017083