• 三、问题分析和猜想验证

    1. 问题分析

    前台确实传递了ids[],后台接收不到ids[],代码逻辑在上个版本是可行的,未对用户模块更新。思来想去得出的结论,此次的全局性的改动引发出来的问题。去其他页面功能点击批量删除,确实都不可用了。

    想到全局性的改动,记得自己当时为了全局配置日期格式转换还有Long传值到前台精度丢失的问题重写了WebMvcConfigurationSupport,代码如下:

    import com.alibaba.fastjson.serializer.SerializeConfig;
    import com.alibaba.fastjson.serializer.SerializerFeature;
    import com.alibaba.fastjson.serializer.ToStringSerializer;
    import com.alibaba.fastjson.support.config.FastJsonConfig;
    import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.MediaType;
    import org.springframework.http.converter.HttpMessageConverter;
    import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

    import java.math.BigInteger;
    import java.util.ArrayList;
    import java.util.List;

    @Configuration
    public class WebMvcConfig extends WebMvcConfigurationSupport {

        @Override
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            //1、定义一个convert转换消息的对象
            FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
            //2、添加FastJson的配置信息
            FastJsonConfig fastJsonConfig = new FastJsonConfig();

            //Long类型转String类型
            SerializeConfig serializeConfig = SerializeConfig.globalInstance;
            serializeConfig.put(BigInteger.class, ToStringSerializer.instance);
            serializeConfig.put(Long.class, ToStringSerializer.instance);
            // serializeConfig.put(Long.TYPE, ToStringSerializer.instance); //不转long值
            fastJsonConfig.setSerializeConfig(serializeConfig);

            fastJsonConfig.setSerializerFeatures(
                    SerializerFeature.WriteMapNullValue, // 保留map空的字段
                    SerializerFeature.WriteNullStringAsEmpty, // 将String类型的null转成""
                    SerializerFeature.WriteNullNumberAsZero, // 将Number类型的null转成0
                    SerializerFeature.WriteNullListAsEmpty, // 将List类型的null转成[]
                    SerializerFeature.WriteNullBooleanAsFalse, // 将Boolean类型的null转成false
                    SerializerFeature.WriteDateUseDateFormat,  //日期格式转换
                    SerializerFeature.DisableCircularReferenceDetect // 避免循环引用
            );
            //3、在convert中添加配置信息
            fastConverter.setFastJsonConfig(fastJsonConfig);
            //4、解决响应数据非json和中文响应乱码
            List<MediaType> jsonMediaTypes = new ArrayList<>();
            jsonMediaTypes.add(MediaType.APPLICATION_JSON);
            fastConverter.setSupportedMediaTypes(jsonMediaTypes);
            //5、将convert添加到converters中
            converters.add(fastConverter);
            //6、追加默认转换器
            super.addDefaultHttpMessageConverters(converters);
        }

        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/**").addResourceLocations(
                    "classpath:/static/");
            registry.addResourceHandler("swagger-ui.html").addResourceLocations(
                    "classpath:/META-INF/resources/");
            registry.addResourceHandler("/webjars/**").addResourceLocations(
                    "classpath:/META-INF/resources/webjars/");
            super.addResourceHandlers(registry);
        }
    }

    想到这,二话不说把@Configuration注释掉,让Spring启动不加载这个配置类,结果如猜想,可以传值了。

    那么问题就出现在这个配置类中,毫无头绪的我只想找个背锅的。我第一篇有关项目问题的总结就是FastJSON,嘿嘿,然后就把消息转换器的代码configureMessageConverters注释了,然而并没有啥用。

    其实主要问题在于对SpringMVC读取请求参数的流程不清楚,如果把流程梳理清楚了,应该就知道参数在哪丢了?!

    附:SpringMVC请求处理流程(可略过)

    声明这里单纯的因为自己对Spring请求处理的流程不熟悉,可能和下文引出的问题产生原因并无直接关联,东西有点多不感兴趣的童鞋可以直接跳过。后面我会单独整理篇有关SpringMVC请求处理流程,这里就问题案例来进行的流程分析。

    接下来在源码的角度层面来认识SpringMVC处理请求的过程。

    SpringMVC处理请求流程从DispatcherServlet#doService方法作为入口,请求处理核心部分委托给doDispatch方法。

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    ...

    try {
        try {
            ModelAndView mv = null;
            Object dispatchException = null;

            try {
                processedRequest = this.checkMultipart(request);
                multipartRequestParsed = processedRequest != request;
                //  获取 HandlerExecutionChain处理器执行链,由handler处理器和interceptor拦截器组成 
                mappedHandler = this.getHandler(processedRequest);
                ...

                // 根据handler获取对应的handlerAdapter去执行这个handler(Controller的方法)
                HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                ...
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }

                this.applyDefaultViewName(processedRequest, mv);
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            } 
            ...
            this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
        }
        ...

    }

    根据请求信息的请求路径和方法类型(get/post/put/delete)从HandlerMapping映射集合获取HandlerExecutionChain处理器执行链(包含handler和interceptor)。

    通过获得的handler类型去适配handlerAdapter执行对应的逻辑。那怎么去找适配器呢?首先你至少知道你的handler是什么类型吧。在此之前,引入一个概念HandlerMethod,简单点说就是你控制器Controller里用来处理请求的方法的信息、还有方法参数信息等。

    调试时发现这里使用的是AbstractHandlerMethodAdapter,看下内部用来做适配的supports方法。handler instanceof HandlerMethod这个判断点明了一切。

    public abstract class AbstractHandlerMethodAdapter extends WebContentGenerator implements HandlerAdapterOrdered {
        

        public final boolean supports(Object handler) {
            return handler instanceof HandlerMethod && this.supportsInternal((HandlerMethod)handler);
        }

        protected abstract boolean supportsInternal(HandlerMethod var1);

        @Nullable
        public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            return this.handleInternal(request, response, (HandlerMethod)handler);
        }
        
        @Nullable
        protected abstract ModelAndView handleInternal(HttpServletRequest var1, HttpServletResponse var2, HandlerMethod var3) throws Exception;
    }

    找到适配器后,执行其handle方法,调用内部方法handleInternal,交由其子类RequestMappingHandlerAdapter实现,我们平时开发最常用的也就是这个适配器了。来看下RequestMappingHandlerAdapter#handleInternal方法。

    protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
        ModelAndView mav;
        ...
        // 调用RequestMappingHandlerAdapter#invokeHandlerMethod方法
        mav = this.invokeHandlerMethod(request, response, handlerMethod);
        ...
        return mav;
    }

    调用内部方法RequestMappingHandlerAdapter#invokeHandlerMethod,继续走。

    protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
        ...
        Object result;
        try {
            ...
            // 生成一个可调用的方法invocableMethod
            ServletInvocableHandlerMethod invocableMethod = this.createInvocableHandlerMethod(handlerMethod);
            if (this.argumentResolvers != null) {
                // 绑定参数解析器
                invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
            }

            if (this.returnValueHandlers != null) {
                invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
            }

            invocableMethod.setDataBinderFactory(binderFactory);
            invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
            ...
            // 核心 通过调用ServletInvocableHandlerMethod的invokeAndHandle方法执行Controller里处理请求的方法
            invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);
            ...
        }
        return (ModelAndView)result;
    }

    将HandlerMethod转化成ServletInvocableHandlerMethod,可以说这个ServletInvocableHandlerMethod是SpringMVC最最核心的部分了。至于为什么这么说?

     public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
        // 调用请求
        Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
        ...
        try {
            // 处理返回值
            this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);
        }
        ...
    }

    本篇的BUG也就在于处理请求阶段的问题,所以我们来看下ServletInvocableHandlerMethod#invokeForRequest方法。

    public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
        Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
        ...
        return this.doInvoke(args);
    }

    方法内部通过调用父类InvocableHandlerMethod#getMethodArgumentValues方法获取请求参数。

    protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
        MethodParameter[] parameters = this.getMethodParameters();
        if (ObjectUtils.isEmpty(parameters)) {
            return EMPTY_ARGS;
        } else {
            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) {
                    ...
                    try {
                        // 调用HandlerMethodArgumentResolverComposite的resolveArgument解析参数获取返回值
                        args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
                    }
                    ...
                }
            }

            return args;
        }
    }

    this.resolvers是HandlerMethodArgumentResolverComposite(相当于组合模式的变种),同时实现了HandlerMethodArgumentResolver接口,内部又包含所有参数解析器的列表。

    public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
        // SpringMVC参数解析器的集合列表
        private final List<HandlerMethodArgumentResolver> argumentResolvers = new LinkedList();
        ...
        public boolean supportsParameter(MethodParameter parameter) {
            return this.getArgumentResolver(parameter) != null;
        }

        @Nullable
        public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
            HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);
            if (resolver == null) {
                throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
            } else {
                return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
            }
        }
        
        @Nullable
        private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
            HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter);
            if (result == null) {
                Iterator var3 = this.argumentResolvers.iterator();

                // 遍历寻找适配的参数解析器
                while(var3.hasNext()) {
                    HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();
                    if (resolver.supportsParameter(parameter)) {
                        result = resolver;
                        this.argumentResolverCache.put(parameter, resolver);
                        break;
                    }
                }
            }

            return result;
        }
        
    }

    MethodParameter是处理器方法(HandlerMethod)的一个HandlerMethodParameter处理器方法参数信息,这里面其中就包含了描述方法参数的注解信息(eg:@RequestParam)。然后需要根据参数信息从参数解析器列表查找适配的参数解析器。

    终于,在27个参数解析器中找到了RequestParamMapMethodArgumentResolver解析器,那我们去看下这个解析器做的适配方法supportsParameter。

    public boolean supportsParameter(MethodParameter parameter) {
        if (parameter.hasParameterAnnotation(RequestParam.class)) {
            if (!Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
                return true;
            } else {
                RequestParam requestParam = (RequestParam)parameter.getParameterAnnotation(RequestParam.class);
                return requestParam != null && StringUtils.hasText(requestParam.name());
            }
        } else if (parameter.hasParameterAnnotation(RequestPart.class)) {
            return false;
        } else {
            parameter = parameter.nestedIfOptional();
            if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
                return true;
            } else {
                return this.useDefaultResolution ? BeanUtils.isSimpleProperty(parameter.getNestedParameterType()) : false;
            }
        }
    }

    可以看到RequestParamMapMethodArgumentResolver支持被注解@RequestParam、@RequestPart修饰的方法参数。

    在确定了参数解析器后,使用解析器的resolveArgument方法解析参数。RequestParamMapMethodArgumentResolver自身没有resolveArgument方法,而是使用父类AbstractNamedValueMethodArgumentResolver的resolveArgument的方法。

    public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        // 提取注解的属性键值(eg: 注解RequestParam的name、required、defaultValue)
        AbstractNamedValueMethodArgumentResolver.NamedValueInfo namedValueInfo = this.getNamedValueInfo(parameter);
        MethodParameter nestedParameter = parameter.nestedIfOptional();
        // 获取处理器方法参数名
        Object resolvedName = this.resolveStringValue(namedValueInfo.name);
        if (resolvedName == null) {
            throw new IllegalArgumentException("Specified name must not resolve to null: [" + namedValueInfo.name + "]");
        } else {
            //  根据参数名从request请求对象获取值
            Object arg = this.resolveName(resolvedName.toString(), nestedParameter, webRequest);
            if (arg == null) {
                if (namedValueInfo.defaultValue != null) {
                    arg = this.resolveStringValue(namedValueInfo.defaultValue);
                } else if (namedValueInfo.required && !nestedParameter.isOptional()) {
                    this.handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
                }

                arg = this.handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
            } else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
                arg = this.resolveStringValue(namedValueInfo.defaultValue);
            }

            if (binderFactory != null) {
                WebDataBinder binder = binderFactory.createBinder(webRequest, (Object)null, namedValueInfo.name);

                try {
                    arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
                } catch (ConversionNotSupportedException var11) {
                    throw new MethodArgumentConversionNotSupportedException(arg, var11.getRequiredType(), namedValueInfo.name, parameter, var11.getCause());
                } catch (TypeMismatchException var12) {
                    throw new MethodArgumentTypeMismatchException(arg, var12.getRequiredType(), namedValueInfo.name, parameter, var12.getCause());
                }
            }

            this.handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
            return arg;
        }
    }

    这样一个一个的解析处理器方法参数,直到把方法所有的参数都从request拿到对应的值之后,返回args。对应逻辑在ServletInvocableHandlerMethod#invokeForRequest,最后返回参数数组args。这样整个参数解析完成之后执行后面的逻辑this.doInvoke(args)。

    至此,SpringMVC请求处理流程就结束了。

    总结下整个SpringMVC请求处理的流程:

    2. 猜想验证

    其实上面扯了这么多,还没说到关键点为什么重写了WebMvcConfigurationSupport会导致后台接收不了FormData?按照之前的分析我们需要的FormData数据可能在哪个阶段丢了。前台传过来的数据肯定会存在request对象中,既然这样,笨办法是不是可以想比较下没有重写和重写的情景,看看两次的request对象是否有差异不就行了。

    我们把断点打在InvocableHandlerMethod#getMethodArgumentValues方法中,因为这里是从request对象中提取出参数的方法。我们要做的只需观察两次的request对象的差异即可。

    果不其然,重写过WebMvcConfigurationSupport后,少了formParams这个属性,而formParams包含了我们想要的参数ids[]。

    至于为什么重写WebMvcConfigurationSupport会丢失formParams?是不是毫无头绪?别急,我们先看下这个formParams是什么。

    从上图可以看得到formParams是FormContentFilter中静态内部类FomContentRequestWrapper的一个属性。猜想formParams应该是使用FormContentFilter过滤器从request对象提取出来的,那现在少了formParams应该是过滤器FormContentFilter没有加载。

    重写配置类之前没有配置过FormContentFilter过滤器,所以这个过滤器应该是SpringBoot自动配置并加载的。来看下SpringBoot的WebMvc自动配置类WebMvcAutoConfiguration。这个类配置在spring.factories里,SpringBoot启动时自动加载配置在里面的类,是SpringBoot的扩展机制,类似java的SPI。

    FormContentFilter如我们所料在SpringBoot的WebMvc自动配置类中,随着SpringBoot启动自动装配。那至于为什么重写了WebMvcConfigurationSupport就会导致自动配置失效了呢?再看下WebMvcAutoConfiguration的头部注解描述。

    @ConditionalOnMissingBean({WebMvcConfigurationSupport.class}),意思就是Spring容器中没有WebMvcConfigurationSupport类相关bean自动配置才会生效。而我这里重写WebMvcConfigurationSupport并加载到Spring容器中,显然导致SpringBoot自动配置不能生效,最终表现出来的现象是后台接收不到前台FromData传值。

    四、解决方案

        @Bean
        public FormContentFilter formContentFilter(){
            FormContentFilter formContentFilter=new OrderedFormContentFilter();
            return  formContentFilter;
        }
    

    这种方案问题在于还是重写了WebMvcConfigurationSupport,除了本篇说到的问题还有其他自动配置同样会失效,感觉终究还是不是太好的方案。

    五、总结

    其实就是项目中批量删除失败看似一个很小的BUG,引出SpringMVC有关请求参数处理原理和SpringBoot自动装配原理。因为时间和篇幅的原因,SpringMVC和SpringBoot原理后面会开专栏细说。最后说一下老生常谈的问题,最好的学习方式是结合工作项目中学,个人完全赞同这种说法。而我们大多数人终究逃不了那循环CRUD的命,但千万别浪费时间去抱怨,有时候所处的环境无法改变时,要相信自己可以创造环境。哪怕重复的CRUD、哪怕再小的BUG,我们也可以做到独具慧眼看到深藏在它们背后的东西,而我们要的就是这些藏在这背后的东西。废话了很多,所有都得看自己,有帮助的话希望点个关注❤

    05-25 16:39