小BUG大原理:重写WebMvcConfigurationSupport后SpringBoot自动配置失效

一、背景

公司的项目前段时间发版上线后,测试反馈用户的批量删除功能报错。正常情况下看起来应该是个小 BUG,可怪就怪在上个版本正常,且此次发版未涉及用户功能的改动。因为这个看似小 BUG 我了解到不少未知的东西,在这里和你们分享下。

先声明下具体原因为了避免耽误找解决问题方法的小伙伴们的宝贵时间,因为项目重写了 WebMvcConfigurationSupport,如果你的项目没有重写这个配置类,赶紧到别处找找,祝你很快找到解决 BUG 获取经验值升级。

二、问题描述

用户批量删除功能:前台传递用户 ID 数组,后台使用@RequestParam 解析参数为 list

错误提示:

Required List parameter 'ids[]' is not present

前台代码:

$.ajax({
    url"/users",
    type"delete",
    data: {ids: ids},
    successfunction (response{
        if (response.code === 0) {
             layer.msg("删除成功");
        }
    }
})

后台代码:

@DeleteMapping("/users")
@ResponseBody
public Result delete(@RequestParam(value = "ids[]") List<Long> ids) {
    boolean status = sysUserService.deleteByIds(ids);
    return Result.status(status);
}

知识点:

  1. 后台为什么使用@RequestParam 解析?

    ajax 如果不指定上传数据类型 Content-Type,默认的是 application/x-www-form-urlencoded,这种编码格式后台需要通过 RequestParam 来处理。

  2. 后台为什么参数名称是 ids[]?

image
image

三、问题分析和猜想验证

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 最最核心的部分了。至于为什么这么说?

  1. 绑定了 HandlerMethodArgumentResolver 参数解析器
  2. 绑定了 HandlerMethodReturnValueHandler 返回值处理器
  3. 核心方法 invokeAndHandle 囊括了从请求到响应几乎整个 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 请求处理的流程:

  1. 请求由 DispatcherServlet 拦截处理。
  2. 根据 request 信息从 HandlerMapping 映射关系获取对应的 HandlerExecutionChain 执行链(包含了处理器 handler 和 interceptor)。
  3. 从 HandlerExecutionChain 获取处理器 handler,根据 handler 类型去匹配对应的适配器。我们平时最常写的 Controller 用来处理请求的方法对应的 handler 类型是 HandlerMethod,匹配的适配器是 RequestMappingHandlerAdapter。
  4. 请求处理委托给 RequestMappingHandlerAdapter 处理,适配器将拿到的 handler 转换成 ServletInvocableHandlerMethod,其内部绑定了参数解析器 HandlerMethodArgumentResolver 和返回值处理器 HandlerMethodReturnValueHandler。
  5. 执行 ServletInvocableHandlerMethod 的 invokeAndHandle 方法。整个方法包含了请求调用和响应处理,请求中包含了参数的解析过程。

2.猜想验证

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

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

image
image

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

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

image
image

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

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

image
image

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

image
image

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

四、解决方案

  1. 既然自动配置失效,手动配置吧
    @Bean
    public FormContentFilter formContentFilter(){
        FormContentFilter formContentFilter=new OrderedFormContentFilter();
        return  formContentFilter;
    }