在spring容器初始化的时候,BeanDefinition中配置的bean的属性值一般都为String类型,如何将String类型转换为Bean中属性对应的类型呢,在这个过程中就需要用到类型转换器了。spring实例化bean过程中对属性值的转换主要是使用BeanWrapperImpl实现的。
首先来看下BeanWrapperImpl的使用
定义一个处理日期的转换器
创建一个BeanWrapperImpl用于包装目标bean(这里来模拟spring的内部实现)。然后注册Date类型的转换器,将值使用DatePropertyEditor转换为Date类型。调用setPropertyValue的时候,给testBean的birthday字段设置一个字符串类型的时间,在实际赋值的过程中会调用到类型转换器将字符串转换为日期类型。
同样可以实现一个String trim的转换器
BeanWrapper支持嵌套属性的赋值,当存在嵌套属性的时候需要设置 setAutoGrowNestedPaths=true。
接下来我们就来研究一下BeanWrapperImpl的实现过程
java.beans包中声明了一个PropertyEditor接口,用于提供对属性的访问和赋值。
PropertyEditor有个默认实现类PropertyEditorSupport,如果要自定义属性转换器,直接继承该类,然后覆写setAsText、getAsText、setValue、getValue方法。在类型转换的时候如果值是字符串会调用setAsText来赋值value,其他情况下会调用setValue来赋值value,然后再调用getValue获取改变后的value值完成类型转换。相当于将PropertyEditor当做一个加工作坊,传进去一个值,返回想要类型的值
注意 :PropertyEditor是线程不安全的,有状态的,因此每次使用时都需要创建一个,不可重用;
首先先看下BeanWrapperImpl的继承图。还有一个跟BeanWrapperImpl平级的实现类DirectFieldAccessor,用于处理getter和setter的字段访问
从图可以看出,BeanWrapperImpl实现了3个顶级接口,提供了对象属性访问、类型转换器的注册和查找、类型转换功能
从继承图可以看到PropertyEditorRegistrySupport实现了PropertyEditorRegistry接口,该类默认实现了类型转换器的注册和查找功能
在PropertyEditorRegistrySupport声明了多个存储结构用于存储不同的类型转换器来源。这里需要注意的是PropertyEditor是存在在Map中的,目标类型作为key,所以对于一个类型只能注册一个PropertyEditor,后面注册的会覆盖前面注册的
同时还看到一个conversionService变量,spring提供了另一种类型转换接口Converter,通过conversionService调用对应的Converter进行类型转换,在PropertyEditorRegistrySupport同样可以注册进来conversionService,用于使用Converter进行类型转换。conversionService的详细使用会在下篇文章中讲到。
接下来来看PropertyEditor的注册过程。
在上面提到过PropertyEditor存在在一个Map中,key是目标类型,那么这个参数propertyPath是干嘛的呢?这是为了给属性名指定专属的类型的转换器。因为一个目标类型只能有一个PropertyEditor的限制。但是有时候确实某个属性的类型转换比较特殊,这个时候就可以给这个属性名单独注册一个类型转换器,不会覆盖其他的哦。在类型转换的时候,会先根据属性名去customEditorsForPath中找可以用的PropertyEditor。
来看PropertyEditor的查找流程
首先根据属性名从customEditorsForPath查找特定的类型转换器
根据类型查找定义的类型转换器,如果没有对应的,则查找父类对应的类型转换器
PropertyEditorRegistrySupport 中默认添加了一些转换器,当调用 getDefaultEditor(requiredType)的时候会进行注册
TypeConverter接口定义了将一个值转换为目标类型的值的功能。在继承图中可以看出TypeConverterSupport对类型转换提供了默认实现。
TypeConverterSupport将类型转换功能委托给typeConverterDelegate实现
TypeConverterDelegate实现了类型转换的功能,创建TypeConverterDelegate的时候需要一个propertyEditorRegistry对象,用于查找匹配的类型转换器
首先通过propertyEditorRegistry查找自定义的类型转换器PropertyEditor和ConversionService
当该类型没有注册自定义的PropertyEditor,并且存在conversionService的时候,使用conversionService进行类型转换。如果conversionService中没有配置对应的converter,那么继续尝试使用默认注册的PropertyEditor。
如果目标类型存在自定义PropertyEditor 或者 目标类型和值类型不一样则需要进行类型转换(当没有找到ProperEditor的时候会尝试查找默认注册的PropertyEditor)
类型转换,主要分三种情况处理,值类型不是字符串,值类型是字符串数组,值类型是字符串
最后,需要对转换后的值和目标类型进行判断,是否符合要求,如果不符合继续处理。这里主要处理了集合类型还有Number类型、基本类型转String、枚举类型。
在attemptToConvertStringToEnum方法中自动根据枚举的名称转换为枚举的对象
PropertyAccessor接口定义了属性访问的功能。通过实现setPropertyValue 和 getPropertyValue方法实现对象属性的赋值和访问。
AbstractPropertyAccessor实现了PropertyAccessor接口,不过没有对setPropertyValue和getPropertyValue进行实现,而是单独提供了一个 setPropertyValues的方法, 用于批量设置属性值,同时可以通过 ignoreUnknown和ignoreInvalid参数忽略未知的属性
在AbstractNestablePropertyAccessor类中实现了setPropertyValue和getPropertyValue功能。
由于存在嵌套属性赋值的情况,对于嵌套的处理,其实只需要对嵌套的最底层进行类型转换,上层每一层就创建默认的值然后set到再上层对象的属性中。在这里spring使用了递归解决这个问题,创建每一层属性的对象值,使用BeanWrapper包装该对象,那么又是一个BeanWrapperImpl的赋值流程。
来看这个递归的内部实现,在getNestedPropertyAccessor完成对外层属性的初始化和将该值赋值到所依赖的对象中。然后使用BeanWrapper封装属性对象,后续走属性对象的赋值流程
创建属性对象,并将该对象set到宿主对象。因为对象是指针引用的,所以在这步已经完成对宿主对象的属性赋值,接下来的流程只要对属性对象中的依赖属性进行赋值。
使用递归解决了嵌套赋值的问题,那么接下来就是针对最底层BeanWrapperImpl的属性赋值流程
在processLocalProperty方法中,首先通过子类获取属性处理器,通过PropertyHandler对属性赋值。在赋值之前再次判断属性是否已经进行了类型转换,如果没有再次调用类型转换器进行转换,如果已经完成类型转换,使用ConvertedValue
对属性的访问和设置spring进行了更小粒度的封装。提供了 PropertyHandler抽象类。为什么在这里进行抽象,看PropertyHandler的两个实现,可以看到一个是BeanPropertyHandler,一个是FieldPropertyHandler,不难想象,属性一种是由getter和setter方法进行访问,一种是没有getter和setter直接反射字段进行的。
如果要对map进行控制,我们可以再提供一个专门处理map的实现了类handler
BeanWrapperImpl提供了BeanPropertyHandler,将setter和getter传入
BeanPropertyHandler提供对setter和getter的访问
AbstractNestablePropertyAccessor的另一个实现类DirectFieldAccessor,专门用于给字段赋值,不依赖setter和getter,那么这个是怎么实现的,看源码发现是DirectFieldAccessor中提供了一个PropertyHandler的实现类,通过Field的反射实现了setValue和getValue
1、application.properties需设置spring.jpa.hibernate.ddl-auto=create;
2、springboot会通过实体类创建表;
3、再执行import.sql。
所以,只要有实体类user存在,则可以在import.sql中直接使用insert语句插入数据到user表即可,不需要在import.sql中定义创建user表的ddl语句。(import.sql中是可以定义create等ddl语句的,不单只能insert *** 作。)
1、在application.properties修改以下内容:
2、在src/main/resources目录下新建import.sql(名字一定要是import.sql),内容如下
3、启动应用,进行初始化数据
4、启动完成后,停止应用,修改spring.jpa.hibernate.ddl-auto=create成spring.jpa.hibernate.ddl-auto=update后,再次启动应用。即spring.jpa.hibernate.ddl-auto设置成update时,import.sql不会执行。
缺点:第一次执行,导入数据后。需要将spring.jpa.hibernate.ddl-auto=create设置成spring.jpa.hibernate.ddl-auto=update。不然第二次执行,会因spring.jpa.hibernate.ddl-auto设置是create值,先drop表,再创建表,导致第一次启动应用后的数据丢失。(比如,新增了另一个用户,就这样在第二次启动应用丢失了)
1、在application.properties新增以下内容:
2、在src/main/resources目录下新建schema.sql(名字可以自定义)、data.sql(名字可以自定义)。
3、启动应用,进行初始化数据
4、启动完成后,停止应用,步骤一设置的 spring.datasource.initialization-mode 等注释掉。
再次启动应用。使用jdbc方式,不跟spring.jpa.hibernate.ddl-auto值有关系,无论是create和update都不影响jdbc这种初始化方式。
第一种方式启动的时候jpa会根据实体类创建表,import.sql负责初始化数据(insert等 *** 作)。
第二种方式启动的时候,不会创建表,需要在初始化脚本schema.sql,data.sql中判断是否存在表,再初始化脚本中的步骤。
如果使用jpa的话,那就选择第一种方式;如果使用的mybatis,则可选择第二种方式。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)