SpringBoot 封装统一响应体(二)

在 SpringBoot 封装统一响应体(一) 中介绍了使用统一结果类 ServerResponse 来封装统一响应体对象,需要用类似 ServerResponse.ok(data) 的形式进行响应。

在 Spring 3.2 中,新增了 @ControllerAdvice ,是一个 Controller 增强器,可对 Controller 中被 @RequestMapping 注解的方法加一些逻辑处理,最常用的就是异常处理。这篇文章介绍一种通过 @ControllerAdvice 和基于 AOP 实现的统一响应体方案。

1. 创建统一结果类

为了方便返回结果,这里还是创建一个统一结果类,其中 lombok 的 @AllArgsConstructor 注解用来向类中添加全参构造方法:

@Data
@AllArgsConstructor
public class ServerResponse {
     
    private Boolean success;
    private Integer code;
    private String message;
    private Object data;
}

2. 创建统一响应注解

统一响应注解是一个标记是否开启统一响应增强的注解:

@Retention(RetentionPolicy.RUNTIME)
@Target({
     ElementType.METHOD, ElementType.TYPE})
public @interface BaseResponse {
     

}

3. 创建结果枚举类

结果枚举类还是复用之前的 ResultEnum

@Getter
@AllArgsConstructor
public enum ResultEnum {
     
    OK(true, 200, "成功"),
    BAD_REQUEST(false, 400, "参数错误"),
    UNAUTHORIZED(false, 401, "未授权,请登录"),
    NOT_FOUND(false, 404, "找不到请求的资源");

	private Boolean success;
    private Integer code;
    private String message;
}

这边使用了 @AllArgsConstructor 创建全参构造方法

4. 创建业务异常类

业务异常类是用于识别业务相关的异常,使用 ResponseCode 作为入参可以通过捕获异常获得返回的状态码信息:

@Data
@EqualsAndHashCode(callSuper = false)
public class BaseException extends RuntimeException{
     

    private ResponseCode code;

    public BaseException(ResponseCode code) {
     
        this.code = code;
    }

    public BaseException(Throwable cause, ResponseCode code) {
     
        super(cause);
        this.code = code;
    }
}

这里有几个注意点:

  • 自定义的异常可以继承 RuntimeException ,也可以继承 Exception 。Spring 对于 RuntimeException 类的异常才会进行事务回滚,所以我们一般自定义异常都继承该异常类;
  • @EqualsAndHashCode 注解会生成 equals(Object other)hashCode() 方法。有时候我们需要重写一个类的 @equals@hashcode 方法,就可以使用 @EqualsAndHashCode 注解;
  • @Data 相当于@Getter@Setter@RequiredArgsConstructor@ToString@EqualsAndHashCode 这5个注解的合集;
  • @EqualsAndHashCode(callSuper = false) 不调用父类的属性,如果子类属性相同,那么两个对象的 hashCode 值就相等;

在实际开发的时候,不同的异常除了通过状态码区分,还可以自定义不同的异常类,方便后面进行异常处理:

public class IllegalArgumentsException extends RuntimeException {
     
	// ...
}

public class UnauthorizedException extends RuntimeException{
     
	// ...
}

5. 创建异常处理类

在 Controller 中抛出异常,这里进行异常处理:

@ControllerAdvice(annotations = BaseResponse.class)
@Slf4j
public class GlobalExceptionHandler {
     
	/**
     * 处理 400 错误
     * @return
     */
    @ResponseBody // 不加 @ResponseBody 返回的是框架默认的响应体
    @ExceptionHandler(value = IllegalArgumentsException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ServerResponse handleIllegalArgumentsException() {
     
        log.info("参数错误");
        return new ServerResponse(
                ResultEnum.BAD_REQUEST.getSuccess(),
                ResultEnum.BAD_REQUEST.getCode(),
                ResultEnum.BAD_REQUEST.getMessage(),
                null
        );
    }

    /**
     * 处理 401 错误
     * @return
     */
    @ResponseBody
    @ExceptionHandler(value = UnauthorizedException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ServerResponse handleUnauthorizedException() {
     
        log.info("未授权,请登录");
        return new ServerResponse(
                ResultEnum.UNAUTHORIZED.getSuccess(),
                ResultEnum.UNAUTHORIZED.getCode(),
                ResultEnum.UNAUTHORIZED.getMessage(),
                null
        );
    }
}

这里需要注意 @ExceptionHandler 注解传入的参数可以是一个数组,但是传入的参数不能相同,也就是不能使用两个@ExceptionHandler去处理同一个异常。

还有一个问题,在自定义类上面使用 @ControllerAdvice 注解,为了能返回自己封装的响应体,需要给成员方法都加上 @ResponseBody 。如果不加 @ResponseBody 注解,例如下面的代码:

@ControllerAdvice(annotations = BaseResponse.class)
public class GlobalExceptionHandler {
     
	@ExceptionHandler(value = IllegalArgumentsException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ServerResponse handleIllegalArgumentsException() {
     
        return new ServerResponse(
                ResultEnum.BAD_REQUEST.getSuccess(),
                ResultEnum.BAD_REQUEST.getCode(),
                ResultEnum.BAD_REQUEST.getMessage(),
                null
        );
    }
}

响应的结果如下,可以看到是框架默认的响应体:
SpringBoot 封装统一响应体(二)_第1张图片
加上 @ResponseBody 之后就可以正常响应了:
SpringBoot 封装统一响应体(二)_第2张图片
顺便再解释一下 @ControllerAdvice@RestControllerAdvice 的区别。简单地说,@RestControllerAdvice 注解包含了 @ControllerAdvice 注解和 @ResponseBody 注解:

  • @RestController = @Controller + @ResponseBody
  • @RestControllerAdvice = @ControllerAdvice + @ResponseBody

如果自定义类上面添加了 @RestControllerAdvice 注解,返回自己封装的响应体就无需再用 @ResponseBody

@RestControllerAdvice(annotations = BaseResponse.class)
public class GlobalExceptionHandler {
     
	@ExceptionHandler(value = IllegalArgumentsException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ServerResponse handleIllegalArgumentsException() {
     
        return new ServerResponse(
                ResultEnum.BAD_REQUEST.getSuccess(),
                ResultEnum.BAD_REQUEST.getCode(),
                ResultEnum.BAD_REQUEST.getMessage(),
                null
        );
    }
}

6. 创建响应增强类

创建一个响应增强类 ServerResponseAdvice 实现 ResponseBodyAdvice 接口:

@ControllerAdvice(annotations = BaseResponse.class)
@Slf4j
public class ServerResponseAdvice implements ResponseBodyAdvice<Object> {
     

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
     
        log.info("returnType:" + returnType);
        log.info("converterType:" + converterType);
        return true;
    }

    @Override
    public ServerResponse beforeBodyWrite(
            Object body,
            MethodParameter returnType,
            MediaType selectedContentType,
            Class selectedConverterType,
            ServerHttpRequest request,
            ServerHttpResponse response
    ) {
     
        return new ServerResponse(
                ResultEnum.OK.getSuccess(),
                ResultEnum.OK.getCode(),
                ResultEnum.OK.getMessage(), // 成功展示默认提示信息
                body // 传给前端的参数
        );
    }
}

这里需要实现 supportsbeforeBodyWrite 两个方法:

  • supports 返回布尔值,用来判断哪些方法需要拦截(例如需要鉴权的请求),returnType 参数可以拿到调用的方法名;
  • beforeBodyWrite 就是在响应前的拦截操作,可以对响应头和响应体的内容进行修改,body 参数就是 Controller 方法中返回的内容;

如果不想在每一个接口都写一遍 ServerResponse.ok(data) 这样的代码,就可以使用 beforeBodyWrite 方法进行统一封装:

@RestControllerAdvice
public class ServerResponseAdvice implements ResponseBodyAdvice<Object> {
     

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
     
        return true;
    }

    @Override
    public ServerResponse beforeBodyWrite(
            Object body,
            MethodParameter returnType,
            MediaType selectedContentType,
            Class selectedConverterType,
            ServerHttpRequest request,
            ServerHttpResponse response
    ) {
     
        // 如果已经是 Result 类型,就直接返回
        if (body instanceof Result) {
     
            return body;
        }

        // 不是 Result 类型,就用 Result 包装后返回
        return Result.success(body);
    }
}

7. 在 Controller 中使用

这边写了一个简单的 Controller 方法,在类上面加上了封装的统一响应体注解:

@BaseResponse // 使用统一响应体注解
@RestController
@RequestMapping("ann-response")
public class AnnResponseController {
     
    @PostMapping("{userId}")
    public Integer pathValidate(@PathVariable Integer userId) {
     
        if(userId.equals(0)) {
     
            throw new IllegalArgumentsException();
        }
        if(userId.equals(1)) {
     
            throw new UnauthorizedException();
        }
        return userId;
    }
}

下面对接口进行测试:

http://localhost:8080/ann-response/0

SpringBoot 封装统一响应体(二)_第3张图片

http://localhost:8080/ann-response/1

SpringBoot 封装统一响应体(二)_第4张图片

http://localhost:8080/ann-response/2

SpringBoot 封装统一响应体(二)_第5张图片

8. ControllerAdvice 注解说明

上面的代码中,我们看到在异常处理类和响应增强类中都用到了如下代码:

@ControllerAdvice(annotations = BaseResponse.class)

或者

@RestControllerAdvice(annotations = BaseResponse.class)

我们看到注解后面带了一个 annotations 的参数,这边解释一下注解的用法。

在一个类上面加 @ControllerAdvice 或者 @RestControllerAdvice 注解,就是定义该类为异常处理类。如果不带任何参数,那么这个类就是全局异常处理类,例如在 SpringBoot 参数校验 中我们创建的 GlobalExceptionHandler 。有时候如果不希望捕获全局异常,可以通过给注解传递参数,指定处理的 Controller 范围:

1. basePackages :指定一个或多个包,这些包及其子包下的所有 Controller 都被该 @ControllerAdvice 管理

@ControllerAdvice(basePackages = {
     "com.tfjybj.intern.provider.controller"})
public class FaultBarrier{
     }

2. basePackageClasses :是 basePackages 的一种变形,指定一个或多个 Controller 类,这些类所属的包及其子包下的所有 Controller 都被该 @ControllerAdvice 管理

@ControllerAdvice(basePackageClasses = {
     Test.class})
public class FaultBarrier{
     }

3. assignableTypes :指定一个或多个 Controller 类,这些类被该 @ControllerAdvice 管理

@ControllerAdvice(assignableTypes= {
     Test.class})
public class FaultBarrier{
     }

4. annotations :指定一个或多个注解,被这些注解所标记的 Controller 会被该 @ControllerAdvice 管理

@ControllerAdvice(annotations = {
     RestController.class})
public class FaultBarrier{
     }

可以看到,我们使用的是第四种方式。另外还要注意下,{} 在 Java 中表示数组,例如我们想指定多个注解,可以这样用:

@ControllerAdvice(annotations = {
     RestController1.class, RestController2.class})
public class FaultBarrier{
     }

在异常处理类中,我们可以使用 @ExceptionHandler 注解定义异常处理方法,其中 value 为需要处理的异常类的 class 。

当然,@ControllerAdvice 的缺点也很明显,只能处理 Controller 层抛出的异常,对例如 Interceptor(拦截器)层的异常、定时任务中的异常、异步方法中的异常,不会进行处理。

9. 总结

通过上面的代码可以看出,这种封装方法是通过抛出特定的异常或者传特定的状态码,然后对异常进行处理,实际上跟 ServerResponse.okServerResponse.badRequest 用法异曲同工,但是可以对异常处理更精细。

然后在 beforeBodyWrite 中对响应内容进行了封装,因此无需在 Controller 方法写类似 ServerResponse.ok 之类的代码,但是成功的响应通常不止 200 请求,常见的还有 201204206 等等,因此上面封装的方法很难对成功的请求进行细分。

参考

SpringBoot统一响应体解决方案
spring-web-unified-response-demo
SpringBoot - @ControllerAdvice 处理异常
Lombok 的 @EqualsAndHashCode(callSuper = false) 的使用
SpringBoot ResponseBodyAdvice 接口实现自定义返回数据类型(响应头)
SpringBoot 使用 beforeBodyWrite 实现统一的接口返回类型

你可能感兴趣的:(Java,Spring)