带着问题看源码一
问题
问题现象,sDate采用lombok自动生成set和get方法,导致接口参数无法被正常解析
实体
@Data
public class TuMoClientTestQueryDto {
private String eDate;
private String sDate;
public String geteDate() {
return eDate;
}
public void seteDate(String eDate) {
this.eDate = eDate;
}
}
消息体
{
"eDate": "2021-07-20",
"sDate": "2021-08-25"
}
程序中能拿到的参数
解决方法
手动添加TuMoClientTestQueryDto中eDate和sDate属性的get和set方法
追踪源码
一、基础知识
1、HandlerMethodArgumentResolver 接口参数解析器
每次调用接口都会自动调用符合条件的接口参数解析器,Spring Web使用这个解析器将请求体中的Json对象转为Java实体,
我们也可以写自己的参数解析器,给接口做统一的参数处理。
2、JsonParser
Json包装类,一条’key’:'value’就是一个Parser,本次用到的实现类是UTF8StreamJsonParser
3、BeanProperty
Java实体一个属性对应一个BeanProperty
二、流程
1、Spring Web通过RequestResponseBodyMethodProcessor实现了HandlerMethodArgumentResolver接口做接口参数处理
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
......
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
......
return adaptArgumentIfNecessary(arg, parameter);
}
2、readWithMessageConverters方法主要工作是在HttpMessageConverter集合中找到符合条件的转换器进行参数转换
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
......
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
if (message.hasBody()) {
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
......
}
3、根据JsonParser寻找符合条件的BeanProperty
public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException
{
......
if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
String propName = p.getCurrentName(); //获取请求体中Json的key值
do {
p.nextToken();
SettableBeanProperty prop = _beanProperties.find(propName); //通过key值在集合中寻找BeanProperty
if (prop != null) { // normal case
try {
prop.deserializeAndSet(p, ctxt, bean);
} catch (Exception e) {
wrapAndThrow(e, bean, propName, ctxt);
}
continue;
}
handleUnknownVanilla(p, ctxt, bean, propName);
} while ((propName = p.nextFieldName()) != null);
}
return bean;
}
4、使用BeanProperty 中的set方法把Json中对应的数据注入到实体中
public void deserializeAndSet(JsonParser p, DeserializationContext ctxt,
Object instance) throws IOException
{
Object value;
if (p.hasToken(JsonToken.VALUE_NULL)) {
if (_skipNulls) {
return;
}
value = _nullProvider.getNullValue(ctxt);
} else if (_valueTypeDeserializer == null) {
value = _valueDeserializer.deserialize(p, ctxt); //从JsonParser拿到json中的value
// 04-May-2018, tatu: [databind#2023] Coercion from String (mostly) can give null
if (value == null) {
if (_skipNulls) {
return;
}
value = _nullProvider.getNullValue(ctxt);
}
} else {
value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer);
}
try {
_setter.invoke(instance, value); //调用属性的set方法注入到实体中
} catch (Exception e) {
_throwAsIOE(p, e, value);
}
}
如果一切正常,由此遍历完Json中的全部key后,json串成功转换为Java实体,不幸的是,中间出问题了。。。。。
三、问题出处
经过上述对整个流程的分析,我们发现最有可能出现问题的是3,3处关键就是根据json的key找到BeanProperty,如果之前解析出来的BeanProperty的name和json的key不一致,无法进行下面的注入流程。
通过debug查看内存信息发现BeanProperty解析出来的name确实出问题了,本来应该是sDate被解析成了sdate。
实体的属性名字没有问题,仅仅是因为set和get方法的命名不同,却导致最终解析出的BeanProperty出现问题,底层在获取BeanProperty到底是如何解析的?属性名和方法名分别占据了什么位置?未来如何避免这个问题?本着打破砂锅问到底的革命精神,我们继续追寻问题的根源。
经过一段时间的苦苦追寻,终于找到了BeanProperty构建源头,下面梳理一下流程:
1、genericConverter.canRead方法内部会将待处理实体所对应的JsonDeserializer放入缓存中,JsonDeserializer中保存这我们需要的BeanProperty集合。
DeserializerCache._createAndCache2()
1、DeserializerCache._createDeserializer() 创建某一个实体类的JsonDeserializer
2、DeserializerCache._cachedDeserializers 加入到缓存中
2、收集实体的BeanProperty放到集合中
BeanDeserializerFactory._findCreatorsFromProperties() 填充BeanDescription beanDesc 中的_properties
BasicBeanDescription findProperties()
POJOPropertiesCollector getProperties()
POJOPropertiesCollector collectAll() 构造POJOPropertyBuilder
protected void collectAll()
{
LinkedHashMap<String, POJOPropertyBuilder> props = new LinkedHashMap<String, POJOPropertyBuilder>();
// First: gather basic data
_addFields(props); //遍历所有属性,添加到集合
_addMethods(props); //遍历所有set和get方法,添加到集合
// Remove ignored properties, first; this MUST precede annotation merging
// since logic relies on knowing exactly which accessor has which annotation
_removeUnwantedProperties(props); //移除掉不符合条件的Property
// and then remove unneeded accessors (wrt read-only, read-write)
_removeUnwantedAccessor(props);
// Rename remaining properties
_renameProperties(props);
_sortProperties(props);
_properties = props;
_collected = true;
}
3、遍历get和set方法,值得注意的是,这里确定是get还是set是通过参数数量判断的。
protected void _addMethods(Map<String, POJOPropertyBuilder> props)
{
final AnnotationIntrospector ai = _annotationIntrospector;
for (AnnotatedMethod m : _classDef.memberMethods()) {
/* For methods, handling differs between getters and setters; and
* we will also only consider entries that either follow the bean
* naming convention or are explicitly marked: just being visible
* is not enough (unlike with fields)
*/
int argCount = m.getParameterCount();
if (argCount == 0) { // getters (including 'any getter')
_addGetterMethod(props, m, ai);
} else if (argCount == 1) { // setters
_addSetterMethod(props, m, ai);
} else if (argCount == 2) { // any getter?
if (ai != null) {
if (Boolean.TRUE.equals(ai.hasAnySetter(m))) {
if (_anySetters == null) {
_anySetters = new LinkedList<AnnotatedMethod>();
}
_anySetters.add(m);
}
}
}
}
}
4、通过方法名推算属性名,basename是方法名,offset是去除set和get两个单词后的偏移量
这里basename=‘setSDate’, offset = 3
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();
}
带着参数走一遍代码,我们就能知道,默认情况下程序把开头连续大写的字符序列,都当做一个单词处理,并将其转为小写,因此SetSDate得到的属性名为sdate和SeteDate得到的属性名eDate。罪魁祸首就在这里了。
总结
以后避免这些问题,使用驼峰命名法时,尽量不要使用一个字母代指一个单词,如果非做不可,其set和get方法紧跟的单词要小写。