带着问题看源码一

带着问题看源码一

问题

问题现象,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"
}

程序中能拿到的参数

image-20210827142746234

解决方法

手动添加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。

image-20210827105342540

​ 实体的属性名字没有问题,仅仅是因为set和get方法的命名不同,却导致最终解析出的BeanProperty出现问题,底层在获取BeanProperty到底是如何解析的?属性名和方法名分别占据了什么位置?未来如何避免这个问题?本着打破砂锅问到底的革命精神,我们继续追寻问题的根源。

​ 经过一段时间的苦苦追寻,终于找到了BeanProperty构建源头,下面梳理一下流程:

1、genericConverter.canRead方法内部会将待处理实体所对应的JsonDeserializer放入缓存中,JsonDeserializer中保存这我们需要的BeanProperty集合。

DeserializerCache._createAndCache2()  
    1DeserializerCache._createDeserializer() 创建某一个实体类的JsonDeserializer
    2DeserializerCache._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方法紧跟的单词要小写。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: .NET是一种跨平台的开发框架,它可以在Windows、Linux和macOS等操作系统上运行。串口工具是一个基于.NET框架开发的软件工具,可以通过串口与其他设备进行通信。 此外,该串口工具还附源码,这意味着用户可以查看和修改软件的源代码。由于源码是开放的,因此用户可以根据自己的需求对软件进行定制和改进。例如,用户可以添加新的功能或优化现有功能,以适应特定的应用场景。 从开发者的角度来看,查看源代码也是一种学习和提高技能的方式。通过分析和理解其他人编写的代码,开发者可以学习到新的开发技术和方法,并在自己的项目中应用这些技术。 在源码的帮助下,用户可以深入了解软件的内部实现和运行原理。这有助于用户更好地利用软件功能并避免潜在的问题。此外,用户还可以利用源码实现自己的想法和创新,推动软件的发展和进步。 因此,.NET串口工具附源码的做法是很有意义的,它为用户提供了更多的自由度和灵活性,同时也为开发者提供了更多的学习和交流机会。 ### 回答2: .NET 串口工具所附源码可以帮助开发者更好地了解串口通信的实现过程和原理,也能够方便地进行二次开发和定制化。通过阅读源码,可以深入了解串口通信的工作原理和协议规范,以及串口通信过程中可能遇到的常见问题和解决方法。此外,通过对源码的研究和修改,还可以实现个性化的串口收发控制方式、数据解析方式、错误处理方案等,从而更好地满足不同应用场景的需求。总之,.NET 串口工具附源码为开发者提供了一个很好的参考范例,帮助他们更快速、高效地开发出适应不同环境和需求的串口通信应用程序。 ### 回答3: .net 串口工具是一款开源的串口通信工具,它提供了一种简单、稳定并且易于使用的方式来进行串口通讯。该工具自源码,允许用户进行二次开发和定制,以满足他们的实际需求。 该串口工具在设计时充分考虑了用户的使用体验和易于开发的特性。它提供了多种不同的功能,例如串口的波特率、校验方式等参数可自由调整,同时还支持数据发送和接收,并且能够以十六进制或ASCII码的格式显示数据。 开发者可以通过阅读和理解该工具的源码,了解其核心设计思路和实现方法,进一步理解串口通讯的原理和应用场景。同时,也可以在该源码的基础上进行二次开发,以适合特定的应用场景。 总之,.net 串口工具附源码的特性使得其不仅仅是一款通讯工具,更是一种开源资源,允许用户在不断的实践和探索中不断完善并利用它。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值