最近在优化修改某个项目代码时碰到一个问题,某个接口采用json 方式进行前后端数据交互,原始代码时用一个字符串接受json,然后手动通过fastjson 转换成对应的javabean,其实这个参数解析工作完全可以交给spring框架去执行,无需手动解析,你只需定义对应的javaBean,@RequestBody 这个注解就可以轻松实现 json 数据的自动解析和绑定功能。
然而我改成注解形式的方式接受参数后,一个莫名的问题来了,有一个字段参数没有接受到前端传过来的值。此处为了演示该问题,我这里单独建了个demo工程演示还原问题场景,对应javaBean:
@Data public class User { private String userName; private Integer userAge; private String NICKName; }
对应参数接受代码如下:
@PostMapping("hello2") public String hello2(@RequestBody User user) throws InterruptedException { log.info("hello:{}",user); Thread.sleep(1000); return user.toString(); }
看到代码,可能有些老司机,已经知道问题出在哪里了,没错,
NICKName
这个属性无法接受到前端传过来的值,这里暂且不说结论,说下我当时的思路:
要解析json参数并且赋值给javaBean(User对象),那么势必会调用对象的get/set 方法,那么问题很可能就出在这里,因为我使用了@Data, lombok(一个自动实现get/set toString等方法的框架非常好用,可以大量缩减源代码,使你的代码简洁明了,便于维护) 框架,实现自动get/set 方法以及 toString 方法,于是我手动写了所有属性的get/方法,不使用lombok,结果是仍然无法接受到该参数(mmp).
虽然上面的思路没有通过验证,但是也使问题变得简单了,现在基本可以确定是springmvc 框架解析参数时导致的问题,而且对比其他参数差异,无非就是 “NICKName” 该属性的命名不符合java 驼峰命名规则,于是我修改了该参数命名“nickName”,结果是后台完美解析了该参数。
至此,基本已经可以确定是springmvc 解析参数导致的,但是还不清楚问题到底出在哪个环节。于是下面开始了源码分析debug模式,我们知道springmvc 默认的json 解析使用的是jackson。
下面是我的debug过程,当然中间饶了许多弯子,这里直接给出正确的跟踪过程:
如图,整个方法执行,spring采用了反射机制实现,这里
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
就是解析 controller 层接受的参数,继续跟:
继续跟踪:
方法走到了
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver
中,这个类是干嘛的呢,它是用来解析 reqeust body 的 参数的,并且是个抽象类,也就是说他会有多个实现。我们继续往下走:
注意到该类中持有一个 HttpMessageConverter 的结合引用List
我们直接跳过前面七个HttpMessageConverter,应为我们知道默认是由 jackson实现的,所以继续跟踪:
此处我们看到第一个这个方法:
JsonDeserializer
很明显spring 首先会从缓存中获取 对应的 javaBean 序列化解析器。没有找到才会走下面的方法:
deser = _createAndCacheValueDeserializer(ctxt, factory, type);
其实阅读spring 的源码,发现很多地方都使用了类似的方法实现,我们继续往下跟踪:
通过方法名和源码注释我们看到,这里是创建并且会缓存还序列化实现:
继续往下跟:
到这里,我们已经很接近事实的真相了(长舒一口气),通过类名:
POJOPropertiesCollector
其实我们就知道,这个类的功能就是pojo 也就是javabean 属性解析,
我们继续往下跟:
这里注意如何解析 NICKName属性的,
最后我们跟踪到 BeanUtil 类中,这里就是最终解析我们pojo 属性的地方,看源代码:
/** * Method called to figure out name of the property, given * corresponding suggested name based on a method or field name. * * @param basename Name of accessor/mutator method, not including prefix * ("get"/"is"/"set") */ protected static String legacyManglePropertyName(final String basename, final int offset) { final int end = basename.length(); if (end == offset) { // empty name, nope return null; } // next check: is the first character upper case? If not, return as is char c = basename.charAt(offset); char d = Character.toLowerCase(c); if (c == d) { return basename.substring(offset); } // otherwise, lower case initial chars. Common case first, just one char StringBuilder sb = new StringBuilder(end - offset); sb.append(d); int i = offset+1; for (; i < end; ++i) { c = basename.charAt(i); d = Character.toLowerCase(c); if (c == d) { sb.append(basename, i, end); break; } sb.append(d); } return sb.toString(); }
看注释以及源代码知道,这里是用get/set 方法名来解析pojo 的属性名称,并且是按照 驼峰表示类解析:
注意这段代码,如果去掉“get”之后首字符是大写,这里会转成小写,一直到遇到第一个小写字母位置。所以"NICKName" 属性最终被解析成了 nickname,
所以这就导致我们始终无法解析到前段传过来的值。至此我们终于搞清楚导致整个问题的原因,整个例子充分给我们证明了一个java 开发中的一个公理:“约定大于配置”。即我们参数命名一定要按照驼峰标识,按照大家约定好的规范编码可以减少很多不必要的麻烦以及bug,以上写的洋洋洒洒,有不对之处欢迎指正。
欢迎关注个人公众号: