使用自定义参数解析器同一个参数支持多种Content-Type
事出有因, 原先上线的接口现在被要求用Java重写,按照原暴露出去的文档然后毫无疑问的,按照Java的惯例,
一定是@RequestBody然后去接收application/json;charset=utf-8,然后一通参数接收处理逻辑。
结果测试都通过了,上线的时候,刚把原接口切到新接口上,日志就狂飙
application/x-www-form-urlencoded;charset=utf-8 NOT SUPPORT
What?然后就一通问号脸。赶紧把接口切回到老接口,然后跑去问PHP的同事,什么情况,原对接参数不是json吗?
然后才明白,草,原来要同时支持application/json;charset=utf-8和application/x-www-form-urlencoded;charset=utf-8
PHP咱是不懂的,但是Java对这个需求的原生支持却不是很好,印象中没有现成的。
因为一般我们定义对象接收参数,如果使用了@RequestBody接收,那么传参一定要使用post+一个对应的参数解析器一个可读的流,按照现在的情况即application/json;charset=utf-8。
要么是直接一个对象接收,不要加任何注解,这个时候对应的Content-Type是application/x-www-form-urlencoded;charset=utf-8则参数可正常解析。
但是这两种情况是矛盾的,如果一个加了@RequestBody的参数对应的Content-Type是application/x-www-form-urlencoded;charset=utf-8, 则最终无法解析。反过来如果一个未加@RequestBody的参数对应的Content-Type是application/json;charset=utf-8则也无法解析。
那么现在就只能来看一下如何定义一个自定义的参数解析器来完成这个需求了。但是这个自定义的参数解析器还有点不太一样,因为数据格式本身不是我们自定义的,本身就存在对应标准的解析器。只是SpringMVC在根据参数去找对应解析器的时候没有对应起来。我们现在只要让自己的解析器能够让这个参数转发到对应可以解析的参数解析器上就可以了。
探究Springmvc参数解析器工作流程
现在就要不怕麻烦的还看一下原来的参数解析器是如何工作的,毕竟不知道它怎么写的我也不知道怎么抄。
SpringMVC项目,二话不说,直接找到org.springframework.web.servlet.DispatcherServlet#doDispatch这个方法,看整个处理器的流程,这里直接简化找到最终映射到方法后的执行
// Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
方法跳转流程如下
public abstract class AbstractHandlerMethodAdapter extends WebContentGenerator implements HandlerAdapter, Ordered { @Override @Nullable public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return handleInternal(request, response, (HandlerMethod) handler); } }
我再跳
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#handleInternal
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { /** * 参数解析器列表 */ @Nullable private HandlerMethodArgumentResolverComposite argumentResolvers; @Override protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ModelAndView mav; checkRequest(request); // 这里删除了大量相关判断方法,只关注实际跳转执行方法 // No synchronization on session demanded at all... mav = invokeHandlerMethod(request, response, handlerMethod); } /** * 上面跳到了这里 */ @Nullable protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { // 这里又删除了大量的代码 ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); // 将本类自己的参数解析器列表赋值给ServletInvocableHandlerMethod if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) { invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } invocableMethod.setDataBinderFactory(binderFactory); invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); // 带着使命继续往下执行 invocableMethod.invokeAndHandle(webRequest, mavContainer); } }
接着跳到了
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); // 再删除无关代码 } }
这里总算看到了一些关键信息了
org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest
public class InvocableHandlerMethod extends HandlerMethod { @Nullable public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { // 这里就是解析参数 Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); if (logger.isTraceEnabled()) { logger.trace("Arguments: " + Arrays.toString(args)); } return doInvoke(args); } /** * 上面方法跳到了这里 */ protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { // 这里就是获取入参的参数类型,如单个字符串的每个参数,或者是某个对象参数,甚至是HttpServletRequest MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } Object[] args = new Object[parameters.length]; // 下面就是遍历每个参数类型,然后挨个解析 for (int i = 0; i < parameters.length; i++) { MethodParameter parameter = parameters[i]; parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); args[i] = findProvidedArgument(parameter, providedArgs); if (args[i] != null) { continue; } // org.springframework.web.method.support.HandlerMethodArgumentResolverComposite#supportsParameter // 后面就贴出了这个方法的内部,就是遍历所有的参数解析器判断是否能够解析当前参数 if (!this.resolvers.supportsParameter(parameter)) { throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver")); } try { // 获取参数解析器,解析当前参数 args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); } catch (Exception ex) { // Leave stack trace for later, exception may actually be resolved and handled... if (logger.isDebugEnabled()) { String exMsg = ex.getMessage(); if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) { logger.debug(formatArgumentError(parameter, exMsg)); } } throw ex; } } return args; } }
上面关于找到参数解析器的关键代码
org.springframework.web.method.support.HandlerMethodArgumentResolverComposite
public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { private final List<HandlerMethodArgumentResolver> argumentResolvers = new LinkedList<>(); private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache = new ConcurrentHashMap<>(256); /** * 判断参数是否能够找到对应的参数解析器 */ @Override public boolean supportsParameter(MethodParameter parameter) { return getArgumentResolver(parameter) != null; } @Nullable private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { // 这里指向一个本地缓存,如果一个参数类型可以被某个参数解析器解析,则缓存下次无须遍历 HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); if (result == null) { // 遍历所有的参数解析器,判断是否支持当前参数类型 for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { if (resolver.supportsParameter(parameter)) { result = resolver; // 如果支持则放入本地缓存,下次直接从缓存中取 this.argumentResolverCache.put(parameter, result); break; } } } return result; } @Override @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { // 获取该参数类型对应的参数解析器 HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); if (resolver == null) { throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first."); } // 解析参数 return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); } }
通过对
org.springframework.web.method.support.HandlerMethodArgumentResolverComposite#getArgumentResolver
的调试, 我们贴出几张图来说明系统中目前所有的参数解析器,以及我们目前需要的用来解析@RequestBody和application/x-www-form-urlencoded对应的参数解析器
application/x-www-form-urlencoded对应的解析器为ServletModelAttributeMethodProcessor @RequestBody application/json对应的解析器为RequestResponseBodyMethodProcessor public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { @Override public boolean supportsParameter(MethodParameter parameter) { // 可以看到必须要有RequestBody这个注解,该参数解析器才会工作 return parameter.hasParameterAnnotation(RequestBody.class); } }
顺便说下为什么我们在controller方法入参的时候写HttpServletRequest和HttpServletResponse也能够入参进来,就是因为有对应的参数解析器,这里也给找出来了
org.springframework.web.servlet.mvc.method.annotation.ServletRequestMethodArgumentResolver org.springframework.web.servlet.mvc.method.annotation.ServletResponseMethodArgumentResolver
不想看废话的可以直接进结果
定义一个注解用于标注在参数上,用以标识这个参数希望用我们的参数解析器进行解析
import java.lang.annotation.*; /** * <p>标识参数可以被多个参数解析器尝试进行参数解析</p > * * 同一个参数支持application/json和application/x-www-form-urlencoded * * @see com.company.content.risk.order.common.handle.MultiArgumentResolverMethodProcessor * @author Snowball * @version 1.0 * @date 2020/08/31 18:57 */ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MultiArgumentResolver { }
实现一个自定义参数解析器,要实现接口org.springframework.web.method.support.HandlerMethodArgumentResolver。在解析方法里我们去判断当前客户端传入的Content-Type, 如果是application/json则将参数解析交给RequestResponseBodyMethodProcessor, 如果是application/x-www-form-urlencoded, 则将参数解析交给ServletModelAttributeMethodProcessor。
HandlerMethodArgumentResolver接口要实现两个方法
- supportsParameter 判断当前解析器是否支持入参对象
- resolveArgument 解析逻辑
import com.company.content.risk.order.common.annotation.MultiArgumentResolver; import com.google.common.collect.ImmutableList; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite; import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor; import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * 自定义参数解析器用以支持同一个参数支持application/json和application/x-www-form-urlencoded解析 * * @see MultiArgumentResolver * @author Snowball * @version 1.0 * @date 2020/08/31 19:00 */ public class MultiArgumentResolverMethodProcessor implements HandlerMethodArgumentResolver { @Autowired private RequestMappingHandlerAdapter requestMappingHandlerAdapter; private static final String CONTENT_TYPE_JSON = "application/json"; private static final String CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded"; /** * 支持的content_type */ private static final ImmutableList<String> SUPPORT_CONTENT_TYPE_LIST = ImmutableList.of(CONTENT_TYPE_JSON, CONTENT_TYPE_FORM_URLENCODED); /** * 参考这个写法, 同一个类型的参数解析后缓存对应的参数解析器,不过这里的key改为了Content-Type * @see HandlerMethodArgumentResolverComposite#argumentResolverCache */ private final Map<String, HandlerMethodArgumentResolver> argumentResolverCache = new ConcurrentHashMap<>(8); @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.getParameter().isAnnotationPresent(MultiArgumentResolver.class); } /** * 解析参数 */ @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { String contentType = webRequest.getHeader("Content-Type"); isSupport(contentType); List<HandlerMethodArgumentResolver> argumentResolvers = requestMappingHandlerAdapter.getArgumentResolvers(); HandlerMethodArgumentResolver handlerMethodArgumentResolver = argumentResolverCache.get(contentType); if (handlerMethodArgumentResolver != null) { return handlerMethodArgumentResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); } for (HandlerMethodArgumentResolver argumentResolver : argumentResolvers) { if (isJson(contentType) && argumentResolver instanceof RequestResponseBodyMethodProcessor) { argumentResolverCache.put(contentType, argumentResolver); return argumentResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); } else if (isFormUrlEncoded(contentType) && argumentResolver instanceof ServletModelAttributeMethodProcessor) { argumentResolverCache.put(contentType, argumentResolver); return argumentResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); } } return null; } private boolean isJson(String contentType) { return contentType.contains(CONTENT_TYPE_JSON); } private boolean isFormUrlEncoded(String contentType) { return contentType.contains(CONTENT_TYPE_FORM_URLENCODED); } /** * 判断当前参数解析器是否支持解析当前的Content-Type * @param contentType * @return * @throws HttpMediaTypeNotSupportedException */ private boolean isSupport(String contentType) throws HttpMediaTypeNotSupportedException { if (contentType == null) { throw new HttpMediaTypeNotSupportedException("contentType不能为空"); } boolean isMatch = false; for (String item : SUPPORT_CONTENT_TYPE_LIST) { if (contentType.contains(item)) { isMatch = true; break; } } if (!isMatch) { throw new HttpMediaTypeNotSupportedException("支持Content-Type" + SUPPORT_CONTENT_TYPE_LIST.toString()); } return true; }
将参数解析器注册成bean,添加到系统参数解析器列表即可
import com.company.content.risk.order.common.handle.MultiArgumentResolverMethodProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.List; /** * <p>web核心配置</p > * * @author Snowball * @version 1.0 * @date 2020/08/31 18:57 */ @Configuration @Order public class CoreWebConfig implements WebMvcConfigurer { /** * 注册自定义参数解析器 * @return */ @Bean public MultiArgumentResolverMethodProcessor multiArgumentResolverMethodProcessor() { return new MultiArgumentResolverMethodProcessor(); } /** * 添加自定义参数解析器 * @param resolvers */ @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(0, multiArgumentResolverMethodProcessor()); } }
使用,将@MultiArgumentResolver标识在controller方法的某个入参对象即可
@PostMapping(value = "text/submit") public OutApiResponse<OutTextResponseBody> submitText(@MultiArgumentResolver OutTextRequest outTextRequest) { }
补充
在上面自定义参数解析器的类中,注入了一个bean,类为RequestMappingHandlerAdapter, 目的是为了从这个类中获取到目前系统中已有的参数解析器列表。那么如何知道这个类里面包含了哪些参数解析器呢?摘录相关代码如下。这个类实现了接口InitializingBean,在bean初始化完成后调用afterPropertiesSet,然后在里面判断加入了默认的参数解析器列表
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { @Nullable private HandlerMethodArgumentResolverComposite argumentResolvers; @Override public void afterPropertiesSet() { // Do this first, it may add ResponseBody advice beans initControllerAdviceCache(); if (this.argumentResolvers == null) { List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers(); this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } } private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() { List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(); // Annotation-based argument resolution resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false)); resolvers.add(new RequestParamMapMethodArgumentResolver()); resolvers.add(new PathVariableMethodArgumentResolver()); resolvers.add(new PathVariableMapMethodArgumentResolver()); resolvers.add(new MatrixVariableMethodArgumentResolver()); resolvers.add(new MatrixVariableMapMethodArgumentResolver()); resolvers.add(new ServletModelAttributeMethodProcessor(false)); resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); resolvers.add(new RequestHeaderMapMethodArgumentResolver()); resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new SessionAttributeMethodArgumentResolver()); resolvers.add(new RequestAttributeMethodArgumentResolver()); // Type-based argument resolution resolvers.add(new ServletRequestMethodArgumentResolver()); resolvers.add(new ServletResponseMethodArgumentResolver()); resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RedirectAttributesMethodArgumentResolver()); resolvers.add(new ModelMethodProcessor()); resolvers.add(new MapMethodProcessor()); resolvers.add(new ErrorsMethodArgumentResolver()); resolvers.add(new SessionStatusMethodArgumentResolver()); resolvers.add(new UriComponentsBuilderMethodArgumentResolver()); // Custom arguments if (getCustomArgumentResolvers() != null) { resolvers.addAll(getCustomArgumentResolvers()); } // Catch-all resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true)); resolvers.add(new ServletModelAttributeMethodProcessor(true)); return resolvers; } }
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程宝库。
核心点1、实现接口org.springframework.web.method.support.HandlerMethodArgumentResolversupportsParameter ...