前言
某项目里已经存在对外的固定报文格式,最近需求增加,需要兼容另一个系统的接口请求和返回格式。虽然对请求的解析也不一致,但这里要讲的是对返回值的处理,所以忽略请求。先来看看原先的和目前需要兼容的返回值上面有什么区别:
- 旧:JSON 明文、RSA 签名
- 新:Base64 + URLEncode 格式化(后面统一称为加密)、RSA 签名
Spring(SpringMVC) 提供了非常多切入点来实现这个需求。下面我会结合着自己的 实践经历和源码 来简单地谈谈各种实现方式。看完这篇你可能会了解到以下几点:
- 一个 HTTP 请求在 SpringMVC 中会经过哪些步骤?
- SpringMVC 对消息的转换是在哪个类做的?
- 如何自定义实现
HandlerMethodReturnValueHandler、HttpMessageConverter?有哪些注意点? - 如何自定义实现
@ResponseBody注解对应的功能 ControllerAdvice功能是什么?以及是在源码中哪一步实现的?- 如何使用 Filter、AOP 等等实现自定义输出
大概能对 Java Web 实现对响应内容、形式等改变的需求的几种实现方式有所了解,再遇到类似场景就可以挑选合适的方式来满足实际业务。
一、固定调用实现
我们能想到的 最简单朴实的方法是在代码里面用逻辑调用来实现,使用统一的父类接受业务层的返回,在每个 Controller 方法中固定调用签名和加密方法。如下:
public class NewOpenApiController {
@Autowired
private NewService newService;
@PostMapping(value = {"/var/{action}"})
@ResponseBody
public NewBaseResponseVo webapi(@PathVariable("action") String action) {
return doSignAndCodec(service.progress());
}
private NewBaseResponseVo doSignAndCodec(NewBaseResponseVo responseVo) {
// do sign
// do codec
return responseVo;
}
}
假如我们这个 Controller 对外提供 100 个方法,那我们这个调用至少需要写 100 次,虽然可以用 @PathVariable 来尽可能地减少 Controller 层的接口方法数量。但调用这个动作依旧是重复的,这个方式确实不够优雅,我们先把它作为最后的妥协方案。
二、AOP 拦截器
我们设想是能够将签名、加密方法与业务逻辑分离开,业务层只管自己返回,剩下的签名加密在另外的类实现。到这里我们自然而然就能想到 Spring 提供的 AOP 功能(日志切面、权限控制等)。只需要使用简单的注解就能对指定方法的增强,从而 将一些通用的切面逻辑与业务隔离开。如下:
@Aspect
@Component
public class NewOpenApiAspect {
@Around("execution(* com.xxx.xxx.Controller..*.*(..))")
public String handleResponse(ProceedingJoinPoint pjp) {
// 自定义签名、加密逻辑
return "处理后的响应报文";
}
}
这样子还不够,在 Spring 中 Aspect 拦截比 SpringMVC 对结果值的 convert 操作优先,结果输出和格式化最终还是在 SpringMVC 内置的转换器中进行。
而对结果值的 convert 操作在 Spring 容器启动时已经绑定好,假如我们 Controller 方法此时的返回值声明的还是 NewBaseResponseVo 类型,利用 AOP 我们在 运行时动态地修改成了 String 类型,那执行时就会 报错。为了避免这个问题,这里还需要将 Controller 方法的返回值也同样改为 String,如下:
public class NewOpenApiController {
@Autowired
private NewService newService;
@PostMapping(value = {"/var/{action}"})
@ResponseBody
public String webapi(@PathVariable("action") String action) {
return doformat(service.progress());
}
private String doformat(NewBaseResponseVo responseVo) {
// 将实体类 format 成 String 类型
return responseVo;
}
}
到这里我们已经可以实现需求了。虽然隔离了签名加密逻辑,但在 Controller 层除了调用业务层逻辑外还有 format 操作,与第一种方法相比也没优雅多少。再来看看其他的方法。
三、基于 @ControllerAdvice
假如是该需求只是实现对返回报文进行签名,那 @ControllerAdvice 注解已经能满足需求
@ControllerAdvice(assignableTypes = {NewOpenApiController.class})
public class NewOpenApiResponseAdvice implements ResponseBodyAdvice<NewBaseResponseVo> {
@Override
public boolean supports(MethodParameter t, Class<? extends HttpMessageConverter<?>> c) {
return true;
}
@Override
public NewBaseResponseVo beforeBodyWrite(NewBaseResponseVo body, MethodParameter mp, MediaType mt, Class<? extends HttpMessageConverter<?>> t, ServerHttpRequest r, ServerHttpResponse s) {
// 自定义签名、加密逻辑
return body;
}
}
自定义类,实现 ResponseBodyAdvice 接口,并将返回值抽象为统一的一个父类 NewBaseResponseVo。为了不影响其他的 Controller 类。注解上的 assignableTypes 属性指定为需要拦截 Controller 类。
我们可以从 beforeBodyWrite 方法名字上面看出,这是 SpringMVC 在将响应实体写入响应流前的动作,只能改变响应的内容,并不能改变响应报文的格式,且从类设计上面也能看出该方法的返回值是在用了 ResponseBodyAdvice 类级别泛型,当创建类时,已不可改变。
我们需要做的不仅仅是对响应内容进行改变,在响应格式上也需要进行 Base64 等操作,所以在这里单单使用该注解是并不能满足我们的需求的。
那我们如果 结合 AOP 再对定义好的 Advice 进行拦截从而改变响应格式呢?理论上讲这么做是没问题,但是这么实现过于 “野蛮”,相当于是在 AOP 的基础上再进行了一次 AOP,在这里完全不推荐这么做。
那还有其他的方式实现么?继续往下看。
四、使用 Filter 拦截
在一开始学 Severlet 时,我们就学过 Filter 技术,这个 不属于 SpringMVC 范畴,但是确实是能达到我们的目的。如下:
public class NewOpenApiFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 将 response 进行包装
ServletResponseWrapper custResponse = new ServletResponseWrapper((HttpServletResponse) response);
chain.doFilter(request, custResponse);
// 拿到 Controller 层往实际
String result = custResponse.getResult();
// 自定义签名、加密逻辑
// 对外输出
response.getWriter().write(result);
}
}
public class ServletResponseWrapper extends HttpServletResponseWrapper {
private PrintWriter cachedWriter;
private CharArrayWriter bufferedWriter;
public ServletResponseWrapper(HttpServletResponse response) {
super(response);
bufferedWriter = new CharArrayWriter();
cachedWriter = new PrintWriter(bufferedWriter);
}
@Override
public PrintWriter getWriter() {
return cachedWriter;
}
public String getResult() {
return new String(bufferedWriter.toCharArray());
}
}
这里需要注意几个点:
ServletResponseWrapper是自定义的包装类,为的是能在 不清空对外输出的 buffer 情况下 获取到 Controller 的输出内容filter属于 Severlet 容器的范畴,而不是 SpringMVC,所以那些 Spring 注解(@Autowired)在这里无效- 示例省略了将自定义 filter 注册进 Web 容器的动作,要用自己补上
如果采用这个方案,那 Controller 响应也要改
public class NewOpenApiController {
@Autowired
private NewService newService;
@PostMapping(value = {"/var/{action}"})
@ResponseBody
public void webapi(@PathVariable("action") String action, HttpServletResponse response) {
return doWrite(service.progress(), response);
}
private void doWrite(NewBaseResponseVo responseVo, HttpServletResponse response) {
// 写入自定义 buffer
response.getWriter().write(JSON.toJSONString(responseVo));
}
}
这时候,Controller 方法就没有返回值了,统一将业务层响应格式化成 JSON 后,输出到自定义 buffer 中。接着 filter 就能拿到 buffer 进一步处理。
这个方式还是没有达到我们的要求,那还有吗?当然是有的,接着我们再往深一些,来看一下 SpringMVC 的源码。
五、使用 HandlerMethodReturnValueHandler 实现 + 部分源码剖析
我们在 Controller 方法上打个断点,能得到如下调用栈
从 DispatcherServlet#doDispatch 开始最终会到 RequestMappingHandlerAdapter#invokeHandlerMethod 开始进行实际方法调用。仔细来看下 invokeHandlerMethod 方法。
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); // 1
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); // 2
}
// 省略...
invocableMethod.invokeAndHandle(webRequest, mavContainer); // 3
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}
无关的部分已经省略。动作 1 设置了参数解析器,动作 2 设置了返回值处理器,动作 3 进行实际 Controller 层方法调用。我们再点进 invokeAndHandle 方法看下:
进入 handleReturnValue 方法
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
代码很清晰,首先选出合适的 HandlerMethodReturnValueHandler 实例,然后调用实例的 handleReturnValue 方法处理 Controller 层的返回值。
到这里我们是不是可以知道,实现 HandlerMethodReturnValueHandler 接口创建自定义返回值处理器,并把这个处理器注册给 SpringMVC 容器。就可以实现自定义返回。开始尝试,首先我们创建自己的处理类
@Component
public class NewOpenApiReturnValueHandler implements HandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return false;
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
}
}
要实现两个方法。先看 supportsReturnType 方法,回想上文的 selectHandler 方法。是的!这个方法就是用于挑选合适的 HandlerMethodReturnValueHandler。你需要根据传入参数来决定你当前所支持的情况。
对应到现在的情况,我们和其他的 Controller 能区别的点是返回值,我们统一了 NewBaseResponseVo 返回值,所以当方法返回值是 NewBaseResponseVo 类时是我们能支持的情况,那这个方法里面怎么才能拿到呢?暂时不知道。放一边,看看另一个 handleReturnValue 方法。
returnValue 一看就是我们 Controller 的返回值,强转一下就能拿到响应实例,这个简单。接着进行签名、加密等操作,也简单。接着下一步就是输出了,这里怎么 没有我们熟悉的 ServletResponse,拿不到输出流,怎么办?
为了解决这两个问题,只能调试了。先注册!注册方法也非常方便,SpringMVC 提供了对外的扩展点,很容易把自定义处理实例加入处理器列表
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private NewOpenApiReturnValueHandler returnValueHandler;
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
handlers.add(returnValueHandler);
}
}
我们兴奋地在自定义的返回值处理器中打上断点,却发现断点怎么都进不来,怎么回事呢?问题就出在 HandlerMethodReturnValueHandlerComposite#selectHandler 方法中,我们看一下这个源码
@Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
原来所有的处理器都放在一个列表里,使用 for 循环,匹配到能支持的第一个就立即返回。而我们在 handlers.add 时,由于是列表肯定是 往后追加,排在默认的处理器最后,由于先匹配到了前边的,后边的便不会再执行。所以我们需要 改写注册方法,调整处理器位置,将自定义的处理器放在第一位:
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
if (handlers instanceof ArrayList) {
handlers.add(0, returnValueHandler);
}
else {
throw new RuntimeException("类型不是 ArrayList 无法初始化返回值处理器");
}
}
再次调试发现 还没有进入断点。怀疑我们自定义的实例是不是没有加入到处理器列表,调试 selectHandler 方法
自定义处理器存在列表里,但不是排在最前面。为什么会这样子?注册时明明是放在了第一位。继续在注册方法上面打断点
发现 Spring 对外的提供的注册口,只是将处理器加入到 customReturnValueHandlers 列表里,而实际上用的是一个叫
returnValueHandlers 的列表。且在 RequestMappingHandlerAdapter#getDefaultReturnValueHandlers 方法里,**优先往 returnValueHandlers 里面放默认的处理器,再放 customReturnValueHandlers,**所以有了明明调整成了第一位实际却不是的效果。代码如下:
private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>();
// 优先放默认处理器
handlers.add(new ModelAndViewMethodReturnValueHandler());
// 省略
// 再放自定义处理器
if (getCustomReturnValueHandlers() != null) {
handlers.addAll(getCustomReturnValueHandlers());
}
// 省略
return handlers;
}
查看了 WebMvcConfigurer 接口发现 Spring 并没有提供操作 returnValueHandlers 的扩展点。既然没办法调整顺序,那到底怎样才能进入到我们自己的处理器中呢?
其实很简单,把 Controller 方法上面的 @ResponseBody 注解去掉即可。这个注解 一部分作用就是在声明需要当前方法需要用 RequestResponseBodyMethodProcessor 来处理,观察其 supportsReturnType 便可知。
收!终于能回到我们需要解决的问题上了。先查看 supportsReturnType 方法的参数 MethodParameter 到底是个什么东西,调试
根据 getParameterType() 能拿到响应实例类型,那我们只需要判断 NewBaseResponseVo 是不是相同类型,或是否是其超类或超接口即可,如下
NewBaseResponseVo.class.isAssignableFrom(returnType.getParameterType());
接着怎么解决没有 ServletResponse,拿不到输出流问题。还记得上文提到过的 RequestResponseBodyMethodProcessor 类吗?其实我们实现的功能应该跟它是大致类似的,所以我们只要拷贝其代码即可
protected ServletServerHttpResponse createOutputMessage(NativeWebRequest webRequest) {
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
Assert.state(response != null, "No HttpServletResponse");
return new ServletServerHttpResponse(response);
}
拿到了输出流,第二个问题也随之解决了。
那么你可能会想我们自定义的处理器只是在
RequestResponseBodyMethodProcessor上增加了一些功能 为什么不继承这个类?这个问题你尝试过就会发现,此类 构造函数必须存在 converters 参数,且不存在无参构造函数,我们也拿不到它所需要的参数converters,所以无法实现,只能退而求其次实现接口HandlerMethodReturnValueHandler
两个问题都解决了,来看下 自定义处理器最终的实现效果
@Component
public class NewOpenApiReturnValueHandler implements HandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return NewBaseResponseVo.class.isAssignableFrom(returnType.getParameterType());
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
// 强转
NewBaseResponseVo responseVo = (NewBaseResponseVo) returnValue;
// 进行签名、加密
ServletServerHttpResponse serverHttpResponse = this.createOutputMessage(webRequest);
serverHttpResponse.getServletResponse().getWriter().write("签名、加密后的内容");
}
private ServletServerHttpResponse createOutputMessage(NativeWebRequest webRequest) {
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
Assert.state(response != null, "No HttpServletResponse");
return new ServletServerHttpResponse(response);
}
}
而且,我们的 Controller 只需要把 @ResponseBody 去掉,并不需要再 format 或者调用其他的方法。至此,相对起来说,这种算是比较优雅的一种方式了。不过还有更好的。最后一种,接着往下看。
思考!既然实现了自定义的返回值处理器,那我们是不是可以自定义实现一个类似
@ResponseBody的注解,当打上这个注解时,使用我们自己的处理器?
六、使用 HttpMessageConverter 实现
在 HandlerMethodReturnValueHandler 里边更深入一点,Spring 还提供了一种实现自定义输出的扩展点,就是 HttpMessageConverter,我们来看下源码。
还是来看看 RequestResponseBodyMethodProcessor#handleReturnValue 方法,点进去发现调用了父类的 AbstractMessageConverterMethodProcessor#writeWithMessageConverters 方法。跳过不关心的方法,往下可以看到:
动作 1、2 是不是似曾相识,与上文在挑选 HandlerMethodReturnValueHandler 并判断是否支持当前输出的动作简直一模一样。只不过对象不一样,这里是挑选一个合适的 HttpMessageConverter。(PS:是不是有中举一反三的感觉,明白一点,另一点竟然也会了)
有了经验后,所以我们只需 把处理思路照搬:是不是只要定义自己的 HttpMessageConverter,并注册进这个 this.messageConverters 就可以达到自定义输出的效果。接着来
@Component
public class NewOpenApiMessageConverter extends AbstractHttpMessageConverter<NewBaseResponseVo> {
@Override
public List<MediaType> getSupportedMediaTypes() {
List<MediaType> temp = new ArrayList<>();
temp.add(MediaType.TEXT_PLAIN);
return temp;
}
@Override
protected boolean supports(Class<?> clazz) {
return NewBaseResponseVo.class.isAssignableFrom(clazz);
}
/**
* 不会有将 WebapiBaseResponseVo 类型作为参数的情况
* 所以暂时先直接返回为 null
*/
@Override
protected NewBaseResponseVo readInternal(Class<? extends NewBaseResponseVo> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
protected void writeInternal( responseVo, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
Writer writer = new OutputStreamWriter(outputMessage.getBody(), StandardCharsets.UTF_8);
try {
writer.write(this.signAndSerialize(responseVo));
}
catch (Exception ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
}
writer.flush();
}
private String signAndSerialize(NewBaseResponseVo responseVo) {
// 签名、加密逻辑
return "";
}
}
这里我们继承了 AbstractHttpMessageConverter 类,复写了几个方法。需要注意的是 supports 方法是对输入、输出都有效。
getSupportedMediaTypes 方法返回当前 Converter 类支持的 Content-Type 类型,这里我们选择支持文本明文。接着来看其他的方法:
假设你在 Controller 中有个方法,使用 NewBaseResponseVo 类型来接收参数,会走到 readInternal 逻辑中,同理 Controller 方法返回 NewBaseResponseVo 类型则会走到 writeInternal 方法中。因为此时我们不会将该类型作为输入参数使用所以直接将 read 方法返回为 null。再来看看注册
// 建议用
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
if (converters instanceof ArrayList) {
// 还是老样子,需要放到第一位,不然会被默认转换器优先返回
converters.add(0, newOpenApiMessageConverter);
}
else {
throw new RuntimeException("类型不是 ArrayList 无法初始化消息转换器");
}
}
// 不建议用
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}
注意,WebMvcConfigurer 提供了两个扩展方法。就如命名字面意思,extendMessageConverters 用于扩展,configureMessageConverters 用于配置。区别在于 extendMessageConverters 操作不会影响默认转换器的注册,而 configureMessageConverters 一旦 add 了新的 Converter 默认转换器注册就会被关闭(PS:可以试下删除一个 Converter 会怎样)!慎重使用!
Note that adding converters to the list, turns off default converter registration. To simply add a converter without impacting default registration, consider using the method
再看看我们的 Controller,由于我们没有改 HandlerMethodReturnValueHandler 所以并不需要把 @ResponseBody 注解去掉,还是按照正常开发即可。
public class NewOpenApiController {
@Autowired
private NewService newService;
@PostMapping(value = {"/var/{action}"}, produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody
public NewBaseResponseVo webapi(@PathVariable("action") String action, HttpServletResponse response) {
return newService.progress();
}
}
非常的干净和优雅。当新增方法或者新增 Controller 实例时,只要返回值是 NewBaseResponseVo 就一定能走到我们自定义的 MessageConverter 中。此场景下 非常建议用这种方式!
至此!6 种方式全部介绍完毕!