前言 PHP
是下划线命名变量, Java
是驼峰命名变量. 对于前端来说, 服务端有两种命名规则, 这是不合理的, 他们希望有一种统一的命名规则.
歪路1: 用ObjectMapper在Controller层做转换 为了赶进度, 我直接在Controller
层做了转换.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestController public class MyController { @PostMapping("/post1") public Object post1 () { MyObj obj = new MyObj (); ObjectMapper om = new ObjectMapper (); om.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); Object o = om.convertValue(result, new TypeReference <Map<String, Object>>() {}); return o; } }
问题迎刃而解, 简单粗暴. 但是这里有个待优化的地方
每个方法都要写一套转换代码, 就算转成AOP
的实现, 也不够优雅
每次都要new ObjectMapper()
和转换.
歪路2: Hack @ResponseBody 处理器 Jackson
提供了一个@JsonNaming
的注解, 用于修饰POJO
的序列化命名方式. 但是这个@JsonNaming
注解只能修饰在类上, 不能修饰在方法级别.
我们猜测, 可以创建一个@MyJsonNaming
的注解, 仿照@ResponseBody
处理器对返回体做处理.
1 2 3 4 5 6 7 8 9 10 @RestController public class MyController { @PostMapping("/post2") @MyJsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) public Object post2 () { MyObj obj = new MyObj (); return obj; } }
我们看看Spring MVC
是怎么拦截@ResponseBody
注解的?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware , InitializingBean { @Override public void afterPropertiesSet () { if (this .returnValueHandlers == null ) { List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers(); this .returnValueHandlers = new HandlerMethodReturnValueHandlerComposite ().addHandlers(handlers); } } private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers () { List<HandlerMethodReturnValueHandler> handlers = new ArrayList <>(); handlers.add(new RequestResponseBodyMethodProcessor (getMessageConverters(), this .contentNegotiationManager, this .requestResponseBodyAdvice)); if (getCustomReturnValueHandlers() != null ) { handlers.addAll(getCustomReturnValueHandlers()); } return handlers; } }
这里就有个问题了, 我通过实现WebMvcConfigurer
自定义的处理器, 它的优先级永远比RequestResponseBodyMethodProcessor
处理器的优先级要低. 也就是说, 我如果想使用@MyJsonNaming
注解覆盖@ResponseBody
的处理逻辑, 只能这样做.
所有代码里, 不允许使用@ResponseBody
和@RestController
, 只允许使用@MyJsonNaming
. 这是不可能也不允许的事情.
使用BeanPostProcessor
调整RequestMappingHandlerAdapter
的HandlerMethodReturnValueHandler
列表顺序. 看起来可行, 但总感觉走了弯路.
并且这样也有弊端, 如果同一个接口, 前端想用下划线的命名格式, 另一个Java
服务想用驼峰的命名格式. 使用这种方式也实现不了.
正解: 使用Accept请求头决定响应体是驼峰还是下划线 换个思路, Spring MVC
是支持xml
和json
的返回格式的.Spring MVC
里面是怎么做到同一套代码支持不同的返回格式的? 本质上我们这个需求也是不同的返回格式. 我们前面已经知道, @ResponseBody
的处理类是RequestResponseBodyMethodProcessor
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { @Override public void handleReturnValue (@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { super .writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage); } } public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler { protected <T> void writeWithMessageConverters (T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { MediaType selectedMediaType = null ; for (HttpMessageConverter<?> converter : this .messageConverters) { GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null ); if (genericConverter != null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) { if (genericConverter != null ) { genericConverter.write(body, targetType, selectedMediaType, outputMessage); } else { ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); } return ; } } } }
最终请求头Accept:application/json
会匹配到MappingJackson2HttpMessageConverter
. 它支持application/json
和application/*+json
, 注意这个通配符, 它会导致下面我的自定义请求头失效. 因为 HttpMessageConverter
匹配到第一个就直接 return
了.
请求头
例子
application/vnd.snake.case+json
my_name
application/vnd.upper.camel.case+json
MyName
application/vnd.lower.camel.case+json
myName
application/vnd.lower.case+json
myname
application/vnd.kebab+json
my-name
application/vnd.lower.dot+json
my.name
所以我们要做几个事情
修改原有的MappingJackson2HttpMessageConverter
, 让它只支持application/json
在原有的MappingJackson2HttpMessageConverter
后面追加我的自定义请求体的MessageConverter
实现类.
在最后添加一个处理application/*+json
的兜底MessageConverter
实现类.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 @Configuration public class JacksonHttpMessageConvertersWebMvcConfigurer implements WebMvcConfigurer { @Autowired private ObjectMapper objectMapper; @Bean public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter (ObjectMapper objectMapper) { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter (objectMapper); List<MediaType> mediaTypes = Arrays.asList( MediaType.APPLICATION_JSON ); converter.setSupportedMediaTypes(mediaTypes); return converter; } @Override public void configureMessageConverters (List<HttpMessageConverter<?>> converters) { List<MediaType> exceptMediaTypes = Collections.singletonList(MediaType.APPLICATION_JSON); int index = 0 ; for (int i = 0 ; i < converters.size(); i++) { HttpMessageConverter<?> converter = converters.get(i); if (converter instanceof MappingJackson2HttpMessageConverter) { index = i; List<MediaType> supportedMediaTypes = converter.getSupportedMediaTypes(); Assert.isTrue(Objects.equals(exceptMediaTypes, supportedMediaTypes), "第 1 个 MappingJackson2HttpMessageConverter 支持的 MediaType:" + supportedMediaTypes + "应为" + exceptMediaTypes); break ; } } index++; PropertyNamingStrategy strategy = PropertyNamingStrategy.SNAKE_CASE; ObjectMapper clone = objectMapper.copy(); clone.setPropertyNamingStrategy(strategy); MediaType mediaType = ExtMappingJackson2HttpMessageConverter.createMediaType(strategy); ExtMappingJackson2HttpMessageConverter converter = new ExtMappingJackson2HttpMessageConverter (clone, mediaType); converters.add(index, converter); } }
至此, 响应体已经能根据请求头Accept
来判断返回的数据格式是下划线命名还是驼峰命名了.
正解: 使用Content-Type请求头决定请求体是驼峰还是下划线 请求参数也一样可以根据请求头Content-Type
来判断. 还是RequestResponseBodyMethodProcessor
这个类来处理.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { @Override public void handleReturnValue (@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { Object arg = readWithMessageConverters(inputMessage, parameter, paramType); return arg; } } public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler { protected <T> Object readWithMessageConverters (HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { MediaType contentType = inputMessage.getHeaders().getContentType(); Object body = NO_VALUE; EmptyBodyCheckingHttpInputMessage message = new EmptyBodyCheckingHttpInputMessage (inputMessage); for (HttpMessageConverter<?> converter : this .messageConverters) { Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass(); GenericHttpMessageConverter<?> genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null ); if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) : (targetClass != null && converter.canRead(targetClass, contentType))) { if (message.hasBody()) { body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) : ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse)); } break ; } } return body; } }
可以看到和响应体的处理逻辑差不多, 也是筛选MessageConverter
. 那么我们之前的配置, 也可以派上用场了. 直接在请求头Content-Type
添加自定义的参数即可.
自定义 MessageConverter 不能处理的情况 现在的Web
开发, 基本都是前后端分离的架构, 所以基本每个请求都是用@ResponseBody
修饰的. 但是请求的话, 不一定都是application/json
的请求, 也不一定都是@RequestBody
修饰的请求参数.
对于客户端来说, 它可能会发送GET
请求过来, 也可能发请求头为application/x-www-form-urlencoded
的请求过来, 也可能发请求头为application/json
的请求过来.
GET 请求如何处理 对于GET
请求, 会走RequestParamMethodArgumentResolver
这个参数处理器.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver { private NamedValueInfo getNamedValueInfo (MethodParameter parameter) { NamedValueInfo namedValueInfo = createNamedValueInfo(parameter); namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo); return namedValueInfo; } private NamedValueInfo updateNamedValueInfo (MethodParameter parameter, NamedValueInfo info) { String name = info.name; if (info.name.isEmpty()) { name = parameter.getParameterName(); if (name == null ) { throw new IllegalArgumentException (); } } String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue); return new NamedValueInfo (name, info.required, defaultValue); } } public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver implements UriComponentsContributor { @Override protected NamedValueInfo createNamedValueInfo (MethodParameter parameter) { RequestParam ann = parameter.getParameterAnnotation(RequestParam.class); return (ann != null ? new RequestParamNamedValueInfo (ann) : new RequestParamNamedValueInfo ()); } }
GET
请求并不会根据请求头Content-Type
的不同而选择不同的命名规则. 代码里写的是什么就是什么. 如果想让GET
请求也支持根据请求头Content-Type
选择不同的命名规则, 只能写一个过滤器 来处理了. 可以参考这个实现: https://gist.github.com/azhawkes/3db84b194b3e47423df2
Content-Type: application/x-www-form-urlencoded 请求如何处理 form data
的请求方式会走ModelAttributeMethodProcessor
这个参数处理器. 同样的也不会受请求头Content-Type
影响, 代码里写的是什么参数就是什么参数. 同样也只能写一个过滤器 来处理.
总结 从RequestResponseBodyMethodProcessor
这个处理器的名字也可以看出, 它只支持@RequestBody
和@ResponseBody
. 其他格式的请求参数, 就只能用过滤器的形式Hack
了.
参考资料