一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第15天,点击查看活动详情。
前言
相信你已经看过铺天盖的统一风格返回的文章了,什么切面得都给你用上,在自定义一个Result类,类中有code、msg、data字段,是不是很熟悉,但是加切面确实有点大体小做了,其实SpringBoot中还可以用@RestControllerAdvice来做统一返回。
一般都知道他是处理异常的,做全局异常统一返回,但是当标有@RestControllerAdvice的类实现ResponseBodyAdvice接口,那他会被赋予一个额外的功能,这个功能就是在向客户端真正输出信息之前,在尝试做最后一次返回值的修改。
@RestControllerAdvice原理
这些都是SpringMVC中的功能,理解@RestControllerAdvice还要追随到DispatcherServlet下,当DispatcherServlet调用到我们的Controller下,如果发生异常,SpringMVC会进行捕获,最后会进入下面这个方法进行结果处理,最后一个参数就是我们Controller下发生的异常信息。
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
在processDispatchResult中就会先判断有没有异常发生,有的话会交给HandlerExceptionResolver去处理。
/**
* 处理异常,如果异常不为null
*/
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
} else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
//交给异常转换器,把这个异常处理成为ModelAndView
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
而HandlerExceptionResolver下由SpringMVC提供的实现类会收集所有带有ControllerAdvice注解的类,这里的收集就是从容器中查找,如果有异常,会尝试交给这些带有ControllerAdvice的类去处理,同时会判断这个类是不是ResponseBodyAdvice的实现类,如果是,则保存起来,为最后向客户端输出之前做准备,也就是上面我们说的调用所有ResponseBodyAdvice实现类去尝试做最后的数据更改。
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
}
中间就不看了,我们在看最后,也就是向客户端输出内容时,需要进入下面这个类中的一个方法。
AbstractMessageConverterMethodProcessor.writeWithMessageConverters
调用getAdvice()获取一个调用链,因为ResponseBodyAdvice可能不止一个,所以要把他们形成一个链,依次调用,上一个ResponseBodyAdvice的返回值会传递给下一个ResponseBodyAdvice作为参数。
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
示例
到这里基本就结束了,下面我们编写一个ResponseBodyAdvice作为示例
下面定义一个接口,多了一个自定义注解@ResultResponse,用来表示会将这个返回值封装为另一个对象。
class User {
var name: String = ""
}
@RestController
class TestController {
@ResultResponse
@GetMapping("test")
fun test(): User {
return User().apply { this.name="zhangsan" }
}
}
在看ResponseBodyAdvice的实现类,泛型表示输入的类型,也表示输出的类型,输入类型是Controller的返回值,输出类型是我们最后尝试封装成其他对象的类型,所以没办法用具体类型,需要用Any表示。
data class Result( val code: Int, val msg: String, val data: Any) {}
fun create(code: Int, msg: String, data: Any?): Result {
data?.run { return Result(code, msg, data) }
return Result(code, msg, "null")
}
fun createWithSuccess(data: Any?): Result {
return create(0, "OK", data)
}
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class ResultResponse()
@RestControllerAdvice
class TestAdvice : ResponseBodyAdvice<Any> {
override fun supports(returnType: MethodParameter, converterType: Class<out HttpMessageConverter<*>>): Boolean {
var resultResponse: ResultResponse? = returnType.getMethodAnnotation(ResultResponse::class.java)
?: AnnotationUtils.findAnnotation(returnType.containingClass, ResultResponse::class.java)
return resultResponse != null
}
override fun beforeBodyWrite(
body: Any?,
returnType: MethodParameter,
selectedContentType: MediaType,
selectedConverterType: Class<out HttpMessageConverter<*>>,
request: ServerHttpRequest,
response: ServerHttpResponse
): Result {
return createWithSuccess(body)
}
}
supports方法用来判断需不需要转换,参数说明如下:
returnType:返回值类型封装,也就是我们Controller的返回值
converterType:内部会选择一个HttpMessageConverter用来HTTP消息转换
beforeBodyWrite参数说明如下
body: 返回值
returnType: 返回值类型
selectedContentType: Media类型
selectedConverterType: HttpMessageConverter
request和response就不用说了吧。
这样当我们方法上或者类上标明了ResultResponse注解后,实际就会被转换为下面这种格式,
{"code":0,"msg":"OK","data":{"name":"zhangsan"}}