Skip to content

Spring Boot Starter 设计:实现统一返回与异常处理

1. 需求背景

在基于 Spring Boot 开发企业级应用时,为了减少基础工程搭建的步骤,确保 API 的一致性和健壮性至关重要。如果没有统一的返回和异常规范,开发人员往往需要编写大量重复的代码,即便通过复制粘贴,也会耗费时间和精力。API 的一致性和健壮性不仅能减少前后端的工作量,还能保证项目的规范性,提升系统的可维护性。 为此,我们设计了一个自定义的 Spring Boot Starter,专门用于 Web 工程的基础配置。本文将介绍该 Starter 的一部分功能,后续文章将继续介绍 Web 工程的其他部分功能。 主要功能 统一的返回格式:实现 ResponseBodyAdvice 接口,确保所有 API 响应都遵循一致的格式,便于前端解析和处理。 统一异常处理:通过 @RestControllerAdvice 集中处理各种异常,避免因未捕获的异常导致系统崩溃或返回不友好的错误信息。

2. 方案设计

2.1 实现 ResponseBodyAdvice 接口

ResponseBodyAdvice 实现类 + @RestControllerAdvice 注解, 相当于一个返回结果集的切面,supports 方法表示是否需要增强,beforeBodyWrite 表示增强方法。

2.2 @RestControllerAdvice + ExceptionHandler 实现统一返回

编写一个类,然后标注 @RestControllerAdvice 注解,当系统需要处理对应类型的错误,编写处理方法,方法上标注两个注解 @ExceptionHandler(value =异常类型),当出现对应类型的错误时,系统会调用该方法并执行相关逻辑,最终通过 @ResponseBody 注解返回 JSON 数据出去。

3. 实现步骤

3.1. 定义 RestfulResponse 返回实体类

统一格式包含以下字段:

  • code:返回码
  • message:消息
  • data :数据载体
java
@Data
public class RestfulResponse implements Serializable {

    private Integer code;

    private String message;

    private Object data;

    private RestfulResponse(){}

    public static RestfulResponse of(Object data){
        RestfulResponse response = new RestfulResponse();
        response.code = HttpConstants.SUCCESS_CODE;
        response.message = HttpConstants.SUCCESSFUL_MESSAGE;
        response.data = data;
        return response;
    }

    public static RestfulResponse fail(Integer code,String message){
        RestfulResponse response = new RestfulResponse();
        response.code = code;
        response.message = message;
        return response;
    }

    public static RestfulResponse fail(ResponseBaseEnums responseBaseEnums) {
        RestfulResponse response = new RestfulResponse();
        response.code = responseBaseEnums.getCode();
        response.message = responseBaseEnums.getMessage();
        return response;
    }

    public static RestfulResponse fail(String message){
        RestfulResponse response = new RestfulResponse();
        response.code = HttpConstants.ERROR_CODE;
        response.message = message;
        return response;
    }

    public static RestfulResponse fail(){
        RestfulResponse response = new RestfulResponse();
        response.code = HttpConstants.ERROR_CODE;
        response.message = HttpConstants.FAILED_MESSAGE;
        return response;
    }

}

3.2 编写需要处理结果的注解

@JsonResponse 注解继承 RestController, 所以到时候需要统一格式返回在类上加@JsonResponse 即可, 同时不需要的话,在类上或者方法上 ignore 设置为 true 即可

java
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@RestController
public @interface JsonResponse {

    /**
     * 是否忽略
     * @return
     */
    boolean ignore() default false;
}

3.3. 编写返回结果处理器

java
@ControllerAdvice
public class JsonResponseHandler implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> converterType) {
        Method method = methodParameter.getMethod();
        Class<?> clazz = Objects.requireNonNull(method, "method is null").getDeclaringClass();
        // 看看类上有没有JsonResponse
        JsonResponse annotation = clazz.getAnnotation(JsonResponse.class);
        // 看看方法上是否有注解
        JsonResponse methodAnnotation = method.getAnnotation(JsonResponse.class);
        if (methodAnnotation != null) {
            annotation = methodAnnotation;
        }
        if (method.getAnnotatedReturnType().getType().getTypeName().equalsIgnoreCase(FileSystemResource.class.getName())) {
            return false;
        }
        return annotation != null && !annotation.ignore();
    }

    @Override
    @SneakyThrows
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aclazz, ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof String && !MediaType.APPLICATION_XML_VALUE.equals(mediaType.toString())) {
            ObjectMapper om = new ObjectMapper();
            response.getHeaders().set("Content-Type", "application/json");
            RestfulResponse restfulResponse = RestfulResponse.of(body);
            return om.writeValueAsString(restfulResponse);
        }
        if (Objects.isNull(body) && MediaType.TEXT_HTML_VALUE.equals(mediaType.toString())) {
            ObjectMapper om = new ObjectMapper();
            response.getHeaders().set("Content-Type", "application/json");
            RestfulResponse restfulResponse = RestfulResponse.of(null);
            return om.writeValueAsString(restfulResponse);
        }
        return RestfulResponse.of(body);
    }
}

3.4. 定义全局异常处理器

使用 @RestControllerAdvice 来集中处理全局异常,确保所有未被捕获的异常都能被优雅地处理。

java

@RestControllerAdvice
public class ExceptionAdvanceHandler {

    @ExceptionHandler(value = NullPointerException.class)
    public RestfulResponse nullPointerException(NullPointerException e) {
        return RestfulResponse.fail(OpenCode.NULL_POINT_ERROR.getCode(),e.getMessage());
    }


    @ExceptionHandler(value = IllegalArgumentException.class)
    public RestfulResponse handleIllegalArgumentException(IllegalArgumentException e) {
        return RestfulResponse.fail(OpenCode.PARAMS_ERROR.getCode(),e.getMessage());
    }

    @ExceptionHandler(value = NoHandlerFoundException.class)
    public RestfulResponse noHandlerFoundException(NoHandlerFoundException e) {
        return RestfulResponse.fail(OpenCode.NO_HANDLER_EXCEPTION.getCode(),e.getMessage());
    }

    @ExceptionHandler(value = BizException.class)
    public RestfulResponse baseException(BizException e) {
        return RestfulResponse.fail(e.getCode(),e.getMessage());
    }


    @ExceptionHandler(MethodArgumentNotValidException.class)
    public RestfulResponse bindException(MethodArgumentNotValidException e) {
        BindingResult bindingResult = e.getBindingResult();
        Set<String> errSet = bindingResult.getAllErrors().stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.toSet());
        String errorMessage = String.join(", ", errSet);
        return RestfulResponse.fail(OpenCode.PARAMS_VALID_ERROR.getCode(),errorMessage);
    }

    /**
     * 参数绑定错误
     *
     * @param ex
     * @return
     */
    @ExceptionHandler(value = BindException.class)
    public RestfulResponse exception(BindException ex) {
        String defaultMessage = Objects.requireNonNull(ex.getBindingResult().getFieldError()).getDefaultMessage();
        return RestfulResponse.fail(defaultMessage);
    }

    /**
     * HttpRequestMethodNotSupportedException
     * @param e
     * @return
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public RestfulResponse httpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        return RestfulResponse.fail(OpenCode.METHOD_NOT_SUPPORT.getCode(),e.getMessage());
    }

    @ExceptionHandler(value = Exception.class)
    public RestfulResponse exception(Exception e) {
        return  RestfulResponse.fail(OpenCode.SERVER_ERROR.getCode(),e.getMessage());
    }

}

3.5. 配置自动装配

3.5.1. 自动装配类

java
```java
@Configuration
@Import(value = {ExceptionAdvanceHandler.class, JsonResponseHandler.class})
public class WebConfig {

}

3.5.2. resources 下 META-INF 创建 spring.factories,写以下内容

text
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.ssn.kit.interceptor.WebConfig

4. 组件测试

4.1 引入 starter

xml
        <dependency>
            <groupId>com.ssn.kit</groupId>
            <artifactId>corp-kit-web</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

4.2 编写测试 controller

java
@JsonResponse
@RequestMapping("test")
public class TestController {

    @GetMapping("string")
    public String test() {
        return "test";
    }

    @GetMapping("obj")
    public UserInfo userInfo() {
        UserInfo info = new UserInfo();
        info.setId("1");
        info.setName("test");
        return info;
    }

    @GetMapping("obj2")
    @JsonResponse(ignore = true)
    public UserInfo userInfo2() {
        UserInfo info = new UserInfo();
        info.setId("1");
        info.setName("test");
        return info;
    }

    @GetMapping("obj3")
    @JsonResponse(ignore = true)
    public UserInfo userInfo3() {
        UserInfo info = null;
        if (info == null) {
            throw new BizException(BizCode.USER_NOT_FOUND);
        }
        return info;
    }

}

api 调试

  1. 测试接口返回 string
http
http://localhost:8090/test/string
json
{ "code": 0, "message": "SUCCESS", "data": "test" }
  1. 测试返回对象
http
http://localhost:8090/test/obj
json
{ "code": 0, "message": "SUCCESS", "data": { "id": "1", "name": "test" } }
  1. 不包装返回
http
http://localhost:8090/test/obj2
json
{ "id": "1", "name": "test" }
  1. 异常捕获
http
http://localhost:8090/test/obj3
json
{ "code": 40000, "message": "找不到用户信息", "data": null }

5. 结论

通过上述实现,我们可以在 Spring Boot 应用中轻松实现统一返回格式和统一异常处理。这不仅提高了 API 的一致性和健壮性,还简化了开发和维护工作.

Released under the MIT License.