使用自定義參數(shù)解析器同一個參數(shù)支持多種Content-Type
一堆廢話
事出有因, 原先上線的接口現(xiàn)在被要求用Java重寫,按照原暴露出去的文檔然后毫無疑問的,按照Java的慣例,
一定是@RequestBody然后去接收application/json;charset=utf-8,然后一通參數(shù)接收處理邏輯。
結(jié)果測試都通過了,上線的時候,剛把原接口切到新接口上,日志就狂飆
application/x-www-form-urlencoded;charset=utf-8 NOT SUPPORT
What?然后就一通問號臉。趕緊把接口切回到老接口,然后跑去問PHP的同事,什么情況,原對接參數(shù)不是json嗎?
然后才明白,草,原來要同時支持application/json;charset=utf-8和application/x-www-form-urlencoded;charset=utf-8
PHP咱是不懂的,但是Java對這個需求的原生支持卻不是很好,印象中沒有現(xiàn)成的。
因為一般我們定義對象接收參數(shù),如果使用了@RequestBody接收,那么傳參一定要使用post+一個對應(yīng)的參數(shù)解析器一個可讀的流,按照現(xiàn)在的情況即application/json;charset=utf-8。
要么是直接一個對象接收,不要加任何注解,這個時候?qū)?yīng)的Content-Type是application/x-www-form-urlencoded;charset=utf-8則參數(shù)可正常解析。
但是這兩種情況是矛盾的,如果一個加了@RequestBody的參數(shù)對應(yīng)的Content-Type是application/x-www-form-urlencoded;charset=utf-8, 則最終無法解析。反過來如果一個未加@RequestBody的參數(shù)對應(yīng)的Content-Type是application/json;charset=utf-8則也無法解析。
那么現(xiàn)在就只能來看一下如何定義一個自定義的參數(shù)解析器來完成這個需求了。但是這個自定義的參數(shù)解析器還有點不太一樣,因為數(shù)據(jù)格式本身不是我們自定義的,本身就存在對應(yīng)標準的解析器。只是SpringMVC在根據(jù)參數(shù)去找對應(yīng)解析器的時候沒有對應(yīng)起來。我們現(xiàn)在只要讓自己的解析器能夠讓這個參數(shù)轉(zhuǎn)發(fā)到對應(yīng)可以解析的參數(shù)解析器上就可以了。
探究Springmvc參數(shù)解析器工作流程
現(xiàn)在就要不怕麻煩的還看一下原來的參數(shù)解析器是如何工作的,畢竟不知道它怎么寫的我也不知道怎么抄。
SpringMVC項目,二話不說,直接找到org.springframework.web.servlet.DispatcherServlet#doDispatch這個方法,看整個處理器的流程,這里直接簡化找到最終映射到方法后的執(zhí)行
// Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
方法跳轉(zhuǎn)流程如下
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 { /** * 參數(shù)解析器列表 */ @Nullable private HandlerMethodArgumentResolverComposite argumentResolvers; @Override protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ModelAndView mav; checkRequest(request); // 這里刪除了大量相關(guān)判斷方法,只關(guān)注實際跳轉(zhuǎn)執(zhí)行方法 // 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); // 將本類自己的參數(shù)解析器列表賦值給ServletInvocableHandlerMethod if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) { invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } invocableMethod.setDataBinderFactory(binderFactory); invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); // 帶著使命繼續(xù)往下執(zhí)行 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); // 再刪除無關(guān)代碼 } }
這里總算看到了一些關(guān)鍵信息了
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 { // 這里就是解析參數(shù) 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 { // 這里就是獲取入?yún)⒌膮?shù)類型,如單個字符串的每個參數(shù),或者是某個對象參數(shù),甚至是HttpServletRequest MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } Object[] args = new Object[parameters.length]; // 下面就是遍歷每個參數(shù)類型,然后挨個解析 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 // 后面就貼出了這個方法的內(nèi)部,就是遍歷所有的參數(shù)解析器判斷是否能夠解析當前參數(shù) if (!this.resolvers.supportsParameter(parameter)) { throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver")); } try { // 獲取參數(shù)解析器,解析當前參數(shù) 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; } }
上面關(guān)于找到參數(shù)解析器的關(guān)鍵代碼
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); /** * 判斷參數(shù)是否能夠找到對應(yīng)的參數(shù)解析器 */ @Override public boolean supportsParameter(MethodParameter parameter) { return getArgumentResolver(parameter) != null; } @Nullable private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { // 這里指向一個本地緩存,如果一個參數(shù)類型可以被某個參數(shù)解析器解析,則緩存下次無須遍歷 HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); if (result == null) { // 遍歷所有的參數(shù)解析器,判斷是否支持當前參數(shù)類型 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 { // 獲取該參數(shù)類型對應(yīng)的參數(shù)解析器 HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); if (resolver == null) { throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first."); } // 解析參數(shù) return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); } }
通過對
org.springframework.web.method.support.HandlerMethodArgumentResolverComposite#getArgumentResolver
的調(diào)試, 我們貼出幾張圖來說明系統(tǒng)中目前所有的參數(shù)解析器,以及我們目前需要的用來解析@RequestBody和application/x-www-form-urlencoded對應(yīng)的參數(shù)解析器
application/x-www-form-urlencoded對應(yīng)的解析器為ServletModelAttributeMethodProcessor @RequestBody application/json對應(yīng)的解析器為RequestResponseBodyMethodProcessor public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { @Override public boolean supportsParameter(MethodParameter parameter) { // 可以看到必須要有RequestBody這個注解,該參數(shù)解析器才會工作 return parameter.hasParameterAnnotation(RequestBody.class); } }
順便說下為什么我們在controller方法入?yún)⒌臅r候?qū)慔ttpServletRequest和HttpServletResponse也能夠入?yún)⑦M來,就是因為有對應(yīng)的參數(shù)解析器,這里也給找出來了
org.springframework.web.servlet.mvc.method.annotation.ServletRequestMethodArgumentResolver org.springframework.web.servlet.mvc.method.annotation.ServletResponseMethodArgumentResolver
不想看廢話的可以直接進結(jié)果
定義一個注解用于標注在參數(shù)上,用以標識這個參數(shù)希望用我們的參數(shù)解析器進行解析
import java.lang.annotation.*; /** * <p>標識參數(shù)可以被多個參數(shù)解析器嘗試進行參數(shù)解析</p > * * 同一個參數(shù)支持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 { }
實現(xiàn)一個自定義參數(shù)解析器,要實現(xiàn)接口org.springframework.web.method.support.HandlerMethodArgumentResolver。在解析方法里我們?nèi)ヅ袛喈斍翱蛻舳藗魅氲腃ontent-Type, 如果是application/json則將參數(shù)解析交給RequestResponseBodyMethodProcessor, 如果是application/x-www-form-urlencoded, 則將參數(shù)解析交給ServletModelAttributeMethodProcessor。
HandlerMethodArgumentResolver接口要實現(xiàn)兩個方法
- supportsParameter 判斷當前解析器是否支持入?yún)ο?/li>
- 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; /** * 自定義參數(shù)解析器用以支持同一個參數(shù)支持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); /** * 參考這個寫法, 同一個類型的參數(shù)解析后緩存對應(yīng)的參數(shù)解析器,不過這里的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); } /** * 解析參數(shù) */ @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); } /** * 判斷當前參數(shù)解析器是否支持解析當前的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; }
將參數(shù)解析器注冊成bean,添加到系統(tǒng)參數(shù)解析器列表即可
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 { /** * 注冊自定義參數(shù)解析器 * @return */ @Bean public MultiArgumentResolverMethodProcessor multiArgumentResolverMethodProcessor() { return new MultiArgumentResolverMethodProcessor(); } /** * 添加自定義參數(shù)解析器 * @param resolvers */ @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(0, multiArgumentResolverMethodProcessor()); } }
使用,將@MultiArgumentResolver標識在controller方法的某個入?yún)ο蠹纯?/p>
@PostMapping(value = "text/submit") public OutApiResponse<OutTextResponseBody> submitText(@MultiArgumentResolver OutTextRequest outTextRequest) { }
補充
在上面自定義參數(shù)解析器的類中,注入了一個bean,類為RequestMappingHandlerAdapter, 目的是為了從這個類中獲取到目前系統(tǒng)中已有的參數(shù)解析器列表。那么如何知道這個類里面包含了哪些參數(shù)解析器呢?摘錄相關(guān)代碼如下。這個類實現(xiàn)了接口InitializingBean,在bean初始化完成后調(diào)用afterPropertiesSet,然后在里面判斷加入了默認的參數(shù)解析器列表
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; } }
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java網(wǎng)絡(luò)IO模型詳解(BIO、NIO、AIO)
Java支持BIO、NIO和AIO三種網(wǎng)絡(luò)IO模型,BIO是同步阻塞模型,適用于連接數(shù)較少的場景,NIO是同步非阻塞模型,適用于處理多個連接,支持自JDK1.4起,AIO是異步非阻塞模型,適用于異步操作多的場景,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2024-10-10