@RequestParam接收多文件参数名不对不抛异常解决办法

  • 问题:@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());
            }
        }
    }
    
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值