通过自定义starter,实现接口层统一返回和异常统一处理

185 阅读5分钟

上一期我们实现了接口的统一返回和异常封装,但是接口层却显得异常的臃肿,存在大量冗余的代码,本期通过开发一个starter将统一返回、接口异常统一处理、接口校验统一进行封装,是的接口层变得优雅。

  • 参数校验过多地耦合了业务代码,违背单一职责原则
  • 可能在多个业务中都抛出同一个异常,导致代码重复
  • 各种异常反馈和成功响应格式不统一,接口对接不友好

一、 创建一个Spring boot工程

创建spring boot工程可以看之前的文章(此处选择之前的文章),本文要创建一个spring boot starter,与之前创建spring boot 工程不同之处,starter不需要启动类和配置文件,可以从工程中删除掉。

1创建starter.png

2删掉文件.png

二、统一请求返回封装

2.1 首选,了解一个知识点:

Spring 中提供了一个类 ResponseBodyAdvice ,ResponseBodyAdvice 是对 Controller 返回的内容在 HttpMessageConverter 进行类型转换之前拦截,进行相应的处理操作后,再将结果返回给客户端。

统一包装的工作放到这个类里面:

    public interface ResponseBodyAdvice<T> {
        boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
    
        @Nullable
        T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);
    }
  • supports: 判断是否要交给 beforeBodyWrite 方法执行,ture:需要;false:不需要
  • beforeBodyWrite: 对 response 进行具体的处理

2.2 创建统一返回对象封装类

    //常用结果返回码枚举类
    package com.holmium.framwork.core.enums;
    
    import lombok.AllArgsConstructor;
    import lombok.ToString;
    
    /**
     * 接口返回code枚举类
     * @author holmium
     * @date 2023/4/15 23:38
     */
    @ToString
    @AllArgsConstructor
    public enum CodeEnum {
        SUCCESS("001","成功"),
        FAIL("000","失败"),
        EXCEPTION("999","接口异常");
    
        public final   String code;
        public final   String message;
    
    }
    
    //封装返回结果
    package com.holmium.framwork.core.response;
    
    import com.holmium.framwork.core.enums.CodeEnum;
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.EqualsAndHashCode;
    import org.springframework.http.HttpStatus;
    
    /**
     * @author holmium
     * @description:  统一的返回结果
     * @date 2023年04月15日15:11
     */
    @Data
    @AllArgsConstructor
    @EqualsAndHashCode
    @Builder
    public class Result<T> {
        /**
         * 请求状态
         */
        private Integer status;
        /**
         * 请求响应代码,如:001-成功,000-失败
         */
        private String code;
    
        /**
         * 请求返回信息描述或异常信息
         */
        private String message;
        /**
         * 接口请求返回业务对象数据
         */
        private T data;
    
        public Result() {
        }
    
        /**
         * 请求成功,对返回结果进行封装
         */
        public static Result<?> success(Object data) {
            return build(CodeEnum.SUCCESS.code,CodeEnum.SUCCESS.message,data);
        }
    
        /**
         * 请求失败,对返回结果进行封装
         */
        public static Result<?> failed(String message) {
            return build(CodeEnum.FAIL.code,message,null);
        }
        /**
         * 返回结果统一封装
         */
        private static Result <?> build(String code,String message,Object data){
            return Result.builder()
                    .status(HttpStatus.OK.value())
                    .code(code)
                    .message(message)
                    .data(data)
                    .build();
        }
    }

2.3 对接口返回结果进行统一处理

    package com.holmium.framwork.core.api;
    
    import com.holmium.framwork.core.response.Result;
    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.servlet.mvc.method.annotation.ResponseBodyAdvice;
    
    /**
     * @author holmium
     * @date 2023年04月16日 18:01
     */
    public class ResponseBodyHandler implements ResponseBodyAdvice<Object> {
    
        @Override
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
            // 如果不需要进行封装的,可以添加一些校验手段,比如添加标记排除的注解
            return true;
        }
    
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
            // 提供一定的灵活度,如果body已经被包装了,就不进行包装
            if (body instanceof Result) {
                return body;
            }
            return Result.success(body);
        }
    }

注:如果引入了swagger或knife4j的文档生成组件,这里需要仅扫描自己项目的包,否则文档无法正常生成 @RestControllerAdvice(basePackages = "com.holmium.*")

2.4 编写自动配置类

    package com.holmium.framwork.core.config;
    
    import com.holmium.framwork.core.api.ResponseBodyHandler;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * @author holmium
     * @date 2023年04月16日 21:58
     */
    @Configuration //注解是:定义一个配置类
    public class ApiConfiguration {
        @Bean //将ResponseBodyHandler注入spring容器中
        public ResponseBodyHandler responseBodyHandler(){
            return  new ResponseBodyHandler();
        }
    
    }
    

src/main/resources/META-INF/spring/目录下创建

org.springframework.boot.autoconfigure.AutoConfiguration.imports,文件中配置ApiConfiguration路径,如下:
com.holmium.framwork.core.config.ApiConfiguration

注:可以在src/main/resources/META-INF/目录下创建spring.factories,然后通过下述配置也能实现相同的作用:

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
      com.holmium.framwork.core.config.ApiConfiguration

然后对工程进行build,这样我们第一个自定义starter就完成了。

2.5改造之前服务

引入我们构建好的jar到我们之前的工程

    <dependency>
     <groupId>com.holmium.framwork</groupId>
     <artifactId>holmium-spring-boot-core-starter</artifactId>
     <version>0.0.1-SNAPSHOT</version>
    </dependency>

删除重复的文件 Result.java 和 CodeEnum.java

3删除多余文件.png

2.6 改造UserApi.java类

    package com.holmium.springboot.app.api;
    
    import com.holmium.springboot.app.convert.UserConvert;
    import com.holmium.springboot.app.vo.UserVo;
    import com.holmium.springboot.domain.user.User;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.annotation.Resource;
    
    /**
     * @author holmium
     * @description:
     * @date 2023年04月15日23:50
     */
    @RestController
    public class UserApi {
    
       @Autowired
       User user;
       @Autowired
       UserConvert userConvert;
    
        @GetMapping(value = "/userinfo")
        public UserVo gerUserInfo(@RequestParam("id") Long id) {
            //接口返回数据对象
            UserVo userVo = new UserVo();
            if(id==1) {
                userVo = userConvert.convertUserVo();
            }else{
                try {
                    user.getUser();
                } catch (Exception e) {
    
                }
            }
            return userVo;
        }
    }

2.6 启动服务进行测试

4封装后的测试.png

三、统一异常封装

原来的API层抛出异常有一下问题:

  • 抛出的异常不够具体,只是简单地把错误信息放到了 Exception 中
  • 抛出异常后,Controller 不能具体地根据异常做出反馈
  • 虽然做了参数自动校验,但是异常返回结构和正常返回结构不一致

自定义异常是为了后面统一拦截异常时,对业务中的异常有更加细颗粒度的区分,拦截时针对不同的异常作出不同的响应

统一拦截异常的目的一个是为了可以与前面定义下来的统一包装返回结构能对应上,另一个是我们希望无论系统发生什么异常,Http 的状态码都要是 200 ,尽可能由业务来区分系统的异常

自定义异常


    /**
     * @author holmium
     * @date 2023/04/17
     * @apiNote 自定义业务异常
     */
    @ResponseStatus(value = HttpStatus.OK, reason = "业务异常!")
    @Data
    @EqualsAndHashCode(callSuper = true)
    public class BusinessException extends RuntimeException {
        /**
         * 业务错误码
         */
        private String code;
        /**
         * 错误提示
         */
        private String message;
    
        /**
         * 空构造方法,避免反序列化问题
         */
        public BusinessException() {
        }
    
        public BusinessException(String message) {
            super(message);
        }
    
        public BusinessException(CodeEnum codeEnum) {
            this.code = codeEnum.code;
            this.message = codeEnum.message;
        }
    
        public BusinessException(String code, String message) {
            this.code = code;
            this.message = message;
        }
    
    }
    
    /**
     * @author holmium
     * @description: 自定义异常
     */
    public class CustomizeException extends Exception {
    
        public CustomizeException(String message, Throwable cause) {
            super(message, cause);
        }
    
        public CustomizeException(String message) {
            super(message);
        }
    
    }
    
    /**
     * @author holmium
     * @description: 自定义参数异常
     */
    @Getter
    public class ParamException extends CustomizeException {
    
        private List<String> fieldList;
        private List<String> msgList;
    
        public ParamException(String message) {
            super(message);
        }
    
        public ParamException(String message, Throwable cause) {
            super(message, cause);
        }
    
        public ParamException(List<String> fieldList, List<String> msgList) throws CustomizeException {
            super(generatorMessage(fieldList, msgList));
            this.fieldList = fieldList;
            this.msgList = msgList;
        }
    
        public ParamException(List<String> fieldList, List<String> msgList, Exception ex) throws CustomizeException {
            super(generatorMessage(fieldList, msgList), ex);
            this.fieldList = fieldList;
            this.msgList = msgList;
        }
    
        private static String generatorMessage(List<String> fieldList, List<String> msgList) throws CustomizeException {
            if (CollectionUtils.isEmpty(fieldList) || CollectionUtils.isEmpty(msgList) || fieldList.size() != msgList.size()) {
                return "参数错误";
            }
    
            StringBuilder message = new StringBuilder();
            for (int i = 0; i < fieldList.size(); i++) {
                String field = fieldList.get(i);
                String msg = msgList.get(i);
                if (i == fieldList.size() - 1) {
                    message.append(field).append(":").append(msg);
                } else {
                    message.append(field).append(":").append(msg).append(",");
                }
            }
            return message.toString();
        }
    
    
    }
    
    /**
     * @author holmium
     * @description: 自定义异常
     */
    public class CustomizeException extends Exception {
    
        public CustomizeException(String message, Throwable cause) {
            super(message, cause);
        }
    
        public CustomizeException(String message) {
            super(message);
        }
    
    }
    
    
    /**
     * @author holmium
     * 服务器异常 Exception
     */
    @Data
    @EqualsAndHashCode(callSuper = true)
    public final class ServerException extends RuntimeException {
    
        /**
         * 全局错误码
         *
         */
        private String code;
        /**
         * 错误提示
         */
        private String message;
    
        /**
         * 空构造方法,避免反序列化问题
         */
        public ServerException() {
        }
    
        public ServerException(String message) {
            super(message);
        }
    
        public ServerException(CodeEnum codeEnum) {
            this.code = codeEnum.code;
            this.message = codeEnum.message;
        }
    
        public ServerException(String code, String message) {
            this.code = code;
            this.message = message;
        }
    
    }
    

统一拦截异常

    /**
     * @author holmium
     * @description:  统一异常处理类
     */
    @Slf4j
    @RestControllerAdvice
    public class GlobExceptionHandler {
    
        @ExceptionHandler(value = BusinessException.class)
        public Result<?> handleBusinessException(BusinessException bx) {
            log.info("接口调用业务异常信息{}", bx.getMessage());
            // 返回统一处理类
            return Result.failed( bx.getMessage());
        }
        /**
         * 捕获 {@code ParamException} 异常
         */
        @ExceptionHandler(value = ParamException.class)
        public Result<?> paramExceptionHandler(ParamException ex) {
            log.info("接口调用异常:{},异常信息{}", ex.getMessage(), ex.getMessage());
            // 返回统一处理类
            return Result.failed(ex.getMessage());
        }
    
        /**
         * 顶级异常捕获并统一处理,当其他异常无法处理时候选择使用
         */
        @ExceptionHandler(value = Exception.class)
        public Result<?> handle(Exception ex) {
            log.info("接口调用异常:{},异常信息{}", ex.getMessage(), ex.getMessage());
            // 返回统一处理类
            return Result.failed(ex.getMessage());
        }
    
    }

统一拦截异常测试

5接口异常测试.png

6修改后自定义异常.png