一、背景
公司的项目前段时间发版上线后,测试反馈用户的批量删除功能报错。正常情况下看起来应该是个小 BUG,可怪就怪在上个版本正常,且此次发版未涉及用户功能的改动。因为这个看似小 BUG 我了解到不少未知的东西,在这里和你们分享下。
先声明下具体原因为了避免耽误找解决问题方法的小伙伴们的宝贵时间,因为项目重写了 WebMvcConfigurationSupport,如果你的项目没有重写这个配置类,赶紧到别处找找,祝你很快找到解决 BUG 获取经验值升级。
二、问题描述用户批量删除功能:前台传递用户 ID 数组,后台使用@RequestParam 解析参数为 list
错误提示:
Required List parameter 'ids[]' is not present前台代码:
$.ajax({url: "/users",
type: "delete",
data: {ids: ids},
success: function (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);
}
知识点:
后台为什么使用@RequestParam 解析?
ajax 如果不指定上传数据类型 Content-Type,默认的是 application/x-www-form-urlencoded,这种编码格式后台需要通过 RequestParam 来处理。
后台为什么参数名称是 ids[]?
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 HandlerAdapter, Ordered {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 最最核心的部分了。至于为什么这么说?
绑定了 HandlerMethodArgumentResolver 参数解析器
绑定了 HandlerMethodReturnValueHandler 返回值处理器