-
问题:@RequestParam注解在接收多文件时,如果参数名称不对,并不会抛异常,影响业务。
-
原因:
-
查看@RequestParam注解的实现,跟进源码查看,大致的调用流程如下(为节省篇幅,忽略了一些代码)
// org\springframework\web\method\support\InvocableHandlerMethod.invokeForRequest,解析参数值并调用方法获取返回值。 public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { // 解析参数值 Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); return doInvoke(args); }
下一步 ↓
// 在同一个类中,getMethodArgumentValues方法负责解析参数 protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { 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); // HandlerMethodArgumentResolver接口的实现方法,判断是否有已注册的解析器可以解析 if (!this.resolvers.supportsParameter(parameter)) { throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver")); } try { // HandlerMethodArgumentResolver接口的实现方法,调用解析器的解析方法处理 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... throw ex; } } return args; }
下一步 ↓
// 这里使用了策略模式,org\springframework\web\method\support\HandlerMethodArgumentResolverComposite.java 为策略管理器类,客户端调用此类来间接调用具体的策略类。 // 接下来是调用的具体策略类:org\springframework\web\method\annotation\AbstractNamedValueMethodArgumentResolver.java public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); MethodParameter nestedParameter = parameter.nestedIfOptional(); Object resolvedName = resolveStringValue(namedValueInfo.name); // 解析参数名称的关键方法 Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); // 由下面代码可知,当上面解析参数名称返回空且没有默认值的时候,会走到handleMissingValue方法抛异常。 if (arg == null) { if (namedValueInfo.defaultValue != null) { arg = resolveStringValue(namedValueInfo.defaultValue); } else if (namedValueInfo.required && !nestedParameter.isOptional()) { handleMissingValue(namedValueInfo.name, nestedParameter, webRequest); } arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); } else if ("".equals(arg) && namedValueInfo.defaultValue != null) { arg = resolveStringValue(namedValueInfo.defaultValue); } return arg; }
下一步 ↓
// 接下来就是具体解析参数名称的方法了,我想要的效果是,当传入多个文件的时候,如果参数名称不对,它可以检测出来并抛异常,也就等同于让这个方法返回null。 // 解析参数名称方法:org\springframework\web\method\annotation\RequestParamMethodArgumentResolver.resolveName protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); // 这里会进行解析参数 if (servletRequest != null) { Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { return mpArg; } } }
下一步 ↓
// 看下最终的解析参数实现方法:org\springframework\web\multipart\support\MultipartResolutionDelegate.resolveMultipartArgument public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request) throws Exception { MultipartHttpServletRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class); boolean isMultipart = (multipartRequest != null || isMultipartContent(request)); // 这里判断是单文件的情况 if (MultipartFile.class == parameter.getNestedParameterType()) { if (multipartRequest == null && isMultipart) { multipartRequest = new StandardMultipartHttpServletRequest(request); } // 单文件时,按文件名获取文件,如果获取不到,则证明参数名称不对,返回值为null,刚好能触发resolveArgument方法的异常处理。 return (multipartRequest != null ? multipartRequest.getFile(name) : null); } // 多文件,list情况,set并未处理 else if (isMultipartFileCollection(parameter)) { if (multipartRequest == null && isMultipart) { multipartRequest = new StandardMultipartHttpServletRequest(request); } // 当参数类型为 List<MultipartFile> 时,按名称取值,但是这个方法在取不到时,返回的是个空list,而不是null,所以无法触发resolveArgument方法的异常处理,也就会导致没有校验到错误的参数名。 return (multipartRequest != null ? multipartRequest.getFiles(name) : null); } // 多文件,数组情况 else if (isMultipartFileArray(parameter)) { if (multipartRequest == null && isMultipart) { multipartRequest = new StandardMultipartHttpServletRequest(request); } if (multipartRequest != null) { List<MultipartFile> multipartFiles = multipartRequest.getFiles(name); // 与上面的实现同理。 return multipartFiles.toArray(new MultipartFile[0]); } else { return null; } } else if (Part.class == parameter.getNestedParameterType()) { return (isMultipart ? request.getPart(name): null); } else if (isPartCollection(parameter)) { return (isMultipart ? resolvePartList(request, name) : null); } else if (isPartArray(parameter)) { return (isMultipart ? resolvePartList(request, name).toArray(new Part[0]) : null); } else { return UNRESOLVABLE; } }
从上面的代码分析中可以得知,@RequestParam注解不能满足我的需求,它的处理逻辑在单文件时没问题,多文件满足不了需求。
-
-
解决办法:
-
自定义注解替代@RequestParam注解:
定义注解:MyFile@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MyFile { String value() default ""; String name() default ""; boolean required() default true; String defaultValue() default ValueConstants.DEFAULT_NONE; }
创建MyRequestParamResolver类实现HandlerMethodArgumentResolver接口
@Slf4j @Component public class MyRequestParamResolver implements HandlerMethodArgumentResolver { @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { log.info("进入自定义注解@MyFile实现方法"); return null; } @Override public boolean supportsParameter(MethodParameter parameter) { // 如果有自定义@MyFile注解就返回true return parameter.hasParameterAnnotation(MyFile.class); } }
创建ResolverBeanPostProcessor类实现BeanPostProcessor接口(实现在bean实例化前后操作,参数解析器是在bean实例化后添加)
@Slf4j @Component public class ResolverBeanPostProcessor implements BeanPostProcessor { @Autowired MyRequestParamResolver myRequestParamResolver; @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { log.info("初始化后-------------------------------" + beanName); if(beanName.equals("requestMappingHandlerAdapter")){ // requestMappingHandlerAdapter进行修改 RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter)bean; List<HandlerMethodArgumentResolver> argumentResolvers = adapter.getArgumentResolvers(); // 添加自定义参数解析器 ArrayList<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(argumentResolvers); resolvers.add(myRequestParamResolver); adapter.setArgumentResolvers(resolvers); } return bean; } }
先解释下为什么要修改requestMappingHandlerAdapter,参数解析器的注册是在requestMappingHandlerAdapter的bean实例化后进行的注册。
// org\springframework\web\servlet\mvc\method\annotation\RequestMappingHandlerAdapter.java 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); } if (this.initBinderArgumentResolvers == null) { List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers(); this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } if (this.returnValueHandlers == null) { List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers(); this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); } } // 获取所有的参数解析器 private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() { List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(); // Annotation-based argument resolution resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false)); // 这就是解析@RequestParam注解的解析器 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; }
-
测试:添加完自定义注解后,再次测试,发现解析器还是使用的@RequestParam注解的解析器
-
再次修改:
// 出现上述问题的原因其实也很简单,和上面的策略类有关org\springframework\web\method\support\HandlerMethodArgumentResolverComposite.java。看下这个类的两个方法就基本清楚了。 // 解析参数 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); } // 获取解析类,这里是按顺序循环已注册的解析器,进行匹配,匹配到则跳出 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; } // 再看下@RequestParam解析器类中的方法 public boolean supportsParameter(MethodParameter parameter) { if (parameter.hasParameterAnnotation(RequestParam.class)) { if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class); return (requestParam != null && StringUtils.hasText(requestParam.name())); } else { return true; } } else { if (parameter.hasParameterAnnotation(RequestPart.class)) { return false; } parameter = parameter.nestedIfOptional(); // 这里会匹配成功,因为参数结构确实是mulitpart if (MultipartResolutionDelegate.isMultipartArgument(parameter)) { return true; } else if (this.useDefaultResolution) { return BeanUtils.isSimpleProperty(parameter.getNestedParameterType()); } else { return false; } } }
// 其实可以认为是误拦截了,根据上面注册解析器的顺序可以知道,我的自定义注解的解析器在@RequestParam解析器后面,所以一旦@RequestParam解析器匹配成功,不会询问我的解析器。所以我需要将我的解析器放置到@RequestParam解析器的前面即可。 // 修改如下: @Slf4j @Component public class ResolverBeanPostProcessor implements BeanPostProcessor { @Autowired MyRequestParamResolver myRequestParamResolver; @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { // 初始化后的操作 log.info("初始化后-------------------------------" + beanName); if(beanName.equals("requestMappingHandlerAdapter")){ //requestMappingHandlerAdapter进行修改 RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter)bean; List<HandlerMethodArgumentResolver> argumentResolvers = adapter.getArgumentResolvers(); ArrayList<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(); resolvers.add(myRequestParamResolver); resolvers.addAll(argumentResolvers); adapter.setArgumentResolvers(resolvers); } return bean; } }
-
再次测试:可以成功的使用到自定义注解的解析器,剩下的只是在解析器内做一些逻辑判断了。
-
-
最终的自定义注解解析器代码:
// 只需要去继承AbstractNamedValueMethodArgumentResolver这个抽象类做少量的重写就可以达到目的了。 @Component public class MyRequestParamResolver extends AbstractNamedValueMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { // 如果有自定义@MyFile注解并且是文件请求就返回true return parameter.hasParameterAnnotation(MyFile.class) && MultipartResolutionDelegate.isMultipartArgument(parameter); } @Override protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); if (servletRequest != null) { Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { // 处理下多文件的情况,数组文件或者list文件,返回不为空,判断下直接置空,与单文件处理保持一致 if (mpArg instanceof List && ((List) mpArg).isEmpty()) { mpArg = null; } else if (mpArg instanceof MultipartFile[] && ((MultipartFile[]) mpArg).length == 0) { mpArg = null; } return mpArg; } } Object arg = null; MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class); if (multipartRequest != null) { List<MultipartFile> files = multipartRequest.getFiles(name); if (!files.isEmpty()) { arg = (files.size() == 1 ? files.get(0) : files); } } if (arg == null) { String[] paramValues = request.getParameterValues(name); if (paramValues != null) { arg = (paramValues.length == 1 ? paramValues[0] : paramValues); } } return arg; } @Override protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { MyFile myFile = parameter.getParameterAnnotation(MyFile.class); return (myFile != null ? new MyFileNamedValueInfo(myFile) : new MyFileNamedValueInfo()); } private static class MyFileNamedValueInfo extends NamedValueInfo { public MyFileNamedValueInfo() { super("", false, ValueConstants.DEFAULT_NONE); } public MyFileNamedValueInfo(MyFile annotation) { super(annotation.name(), annotation.required(), annotation.defaultValue()); } } }