笼统的说一个系统主要是由3个部分组成的:
- 执行程序:主要负责处理业务逻辑,对接用户操作。
- 内部数据:嵌套在源码中的数据,用于指导程序运行。
- 外部数据:业务数据,外部配置数据。
内部数据本身就是程序的一部分,在Java中这些数据通常停留在类的静态成员变量中。而外部数据往往与代码无关,所以对于程序而言要“读懂”它们需要进行一些前置处理。例如用户在前端页面提交的数据我们从RequestContext中获取的数据类型都是字符串,而我们的业务需要将字符串转换成数字、列表、对象等等,这就引入了我们接下来要介绍的内容——数据类型转换。
JavaBean对于J2SE或者J2EE而言有着非常重要的意义,ORACLE为了统一各个组织对JavaBean的使用方式制定了详尽的JavaBean规范,包括BeanInfo、PropertyEditor、
PropertyEditorSupport
等方面的内容。本文会涉及到JavaBean的一些规范,但是重点是介绍Spring的数据管理。
(可执行代码请到本人gitee库下载,本文的代码在chkui.springcore.example.hybrid.beanmanipulation包)
Properties结构转换为实体
标准资源文件*.properties是Java程序常用的数据存储文件,Spring提供了BeanWrapper接口将*.properties文件中的数据转换成一个标准的JavaBean对象。看下面的例子:
有一个实体类Person:
class Person {
private String name;
private int age;
private boolean license;
private Date birtday;
private Address address;
private Map<String, String> otherInfo;
// Getter & Setter ......
}
然后可以通过BeanWrapper将Properties对象中的数据设置到对象中:
private void simpleDataBind() {
BeanWrapper wrapper = new BeanWrapperImpl(new Person());
//使用 BeanWrapper::setPropertyValue 接口设置数据
wrapper.setPropertyValue("name", "niubility");
wrapper.setPropertyValue("age", 18);
wrapper.setPropertyValue("license", true);
print(wrapper.getWrappedInstance());
//使用 Properties对象设置数据,Properties实例可以来源于*.properties文件
Properties p = new Properties();
p.setProperty("name", "From Properties");
p.setProperty("age", "25");
p.setProperty("license", "false");
p.setProperty("otherInfo[birthday]", "2000-01-01");
wrapper.setPropertyValues(p);
print(wrapper.getWrappedInstance());
}
这样,使用Spring的BeanWrapper接口,可以快速的将Properties数据结构转换为一个JavaBean实体。
除了配置单个实体的数据,BeanWrapper还可以为嵌套结构的实体设置数据。现在增加一个实体Vehicle:
public class Vehicle {
private String name;
private String manufacturer;
private Person person; //Person对象
// Getter & Setter ......
}
在Vehicle中有一个Person类型的成员变量(person域),我们可以利用下面具备嵌套结构的语法来设置数据:
private BeanManipulationApp nestedDataBind() {
// 数据嵌套转换
BeanWrapper wrapper = new BeanWrapperImpl(new Vehicle(new Person()));
Properties p = new Properties();
p.setProperty("name", "Envision");
p.setProperty("manufacturer", "Buick");
//person.name表示设置person域的name变量数值
p.setProperty("person.name", "Alice");
p.setProperty("person.age", "25");
p.setProperty("person.license", "true");
p.setProperty("person.otherInfo[license code]", "123456789");
wrapper.setPropertyValues(p);
print(wrapper.getWrappedInstance());
return this;
}
在*.properties文件中,经常使用path.name=param的的语法来指定一个嵌套结构(比如LOG4J的配置文件),这里也使用类似的方式来指定嵌套结构。person.name在程序执行时会调用Vehicle::getPerson::setName方法来设定数据。
除了设定单个数据BeanWrapper还提供了更丰富的方法来设置数据,以上面的Vehicle、person为例:
表达式 | 效果 |
---|---|
p.setProperty("name", "Envision") | name域的数据设置为"Envision" |
p.setProperty("person.name", "Alice") | 将嵌套的person域下的name数据设置为"Alice" |
p.setProperty("list[1]", "Step2") | list域是一个列表,将第二个数据设置为"Step2" |
p.setProperty("otherInfo[birthday]", "2000-01-01") | otherInfo域是一个Map,将key=birthday、value="2000-01-01"的数据添加到Map中。 |
上面这4条规则可以组合使用,比如p.setProperty("person.otherInfo[license code]", "123456789")。
关于在Java如何使用Properties有很多讨论(比如这篇stackoverflow的问答),BeanWrapper不仅仅是针对资源文件,他还衍生扩展了数据类型转换等等功能。
PropertyEditor转换数据
在JavaBean规范中定义了java.beans.PropertyEditor,他的作用简单的说就是将字符串转换为任意对象结构。
PropertyEditor最早是用来支持java.awt中的可视化接口编辑数据的(详情见Oracle关于IDE数据定制化的介绍)。但是在Spring或其他应用场景中更多的仅仅是用来做字符串到特定数据格式的转换(毕竟java.awt应用不多),所以PropertyEditor提供的BeanWrapper::paintValue之类的支持awt的方法不用太去关心他,主要聚焦在BeanWrapper::setAsText方法上。
BeanWrapper继承了PropertyEditorRegistry接口用于注册PropertyEditor。BeanWrapperImpl已经预设了很多有价值的PropertyEditor,比如上面的例子的代码p.setProperty("age", "25");,age域是一个数字整型,而Properties中的数据都是字符串,在设置数据时会自动启用CustomNumberEditor将字符串转换为数字。
Spring已经提供的PropertyEditor可以看这里的清单。需要注意的是,这些PropertyEditor并不是每一个都默认启用,比如CustomDateEditor必须由开发者提供DateFormat才能使用,所以需要像下面这样将其添加注册到BeanWrapper中:
private void propertyEditor() {
BeanWrapper wrapper = new BeanWrapperImpl(new Person());
// 设定日期转换格式
DateFormat df = new java.text.SimpleDateFormat("yyyy-MM-dd");
// 将Editor与DateFormat进行帮顶,使用指定的格式
CustomDateEditor dateEditor = new CustomDateEditor(df, false);
// 注册dateEditor,将其与Date类进行绑定
wrapper.registerCustomEditor(Date.class, dateEditor);
// CustomNumberEditor执行转换
wrapper.setPropertyValue("age", "18");
// CustomBooleanEditor执行转换
wrapper.setPropertyValue("license", "false");
// dateEditor执行转换
wrapper.setPropertyValue("birtday", "1999-01-30");
print(wrapper.getWrappedInstance());
}
添加之后,设定setPropertyValue("birtday", "1999-01-30")时会自动使用指定的DateFormat转换日期。
自定义PropertyEditor
除了预设的各种PropertyEditor,我们还可以开发自定义的PropertyEditor。Person中有一个类型为Address的成员变量:
public class Address {
private String province; //省
private String city; //市
private String district; //区
// Getter & Setter
}
我们为Address实体添加一个PropertyEditor,将特定格式的字符串转换为Address结构:
public class AddressEditor extends PropertyEditorSupport {
private String[] SPLIT_FLAG = { ",", "-", ";", ":" };
public void setAsText(String text) {
int pos = -1;
Address address = new Address();
for (String flag : SPLIT_FLAG) {
pos = text.indexOf(flag);
if (-1 < pos) {
String[] split = text.split(flag);
address.setProvince(split[0]);
address.setCity(split[1]);
address.setDistrict(split[2]);
break;
}
}
if (-1 == pos) {
throw new IllegalArgumentException("地址格式错误");
}
setValue(address);//设定Address实例
}
}
通过AddressEditor::setAsText方法,可以将输入的字符串最红转换为一个Address实例。通常情况下开发一个Editor转换器不会直接去实现PropertyEditor接口,而是继承PropertyEditorSupport。
然后我们使用AddressEditor来将字符串转换为Address对象:
private BeanManipulationApp propertyEditor() {
//使用预设转换工具和自定义转换工具
BeanWrapper wrapper = new BeanWrapperImpl(new Person());
// 创建AddressEditor实例
AddressEditor addressEditor = new AddressEditor();
// 注册addressEditor,将其与Address类进行绑定
wrapper.registerCustomEditor(Address.class, addressEditor);
// 设置值自动进行转化
wrapper.setPropertyValue("address", "广东-广州-白云");
print(wrapper.getWrappedInstance());
}
按照JavaBean规范,PropertyEditor和对应的JavaBean可以使用命名规则来表示绑定关系,而无需显式的调用注册方法。
绑定的规则是:有一个JavaBean命名为Tyre,在相同的包下(package)有一个实现了PropertyEditor接口并且命名为TyreEditor的类,那么框架认为TyreEditor就是Tyre的Editor,无需调用BeanWrapper::registerCustomEditor方法来声明Tyre和TyreEditor的绑定关系,详情请看源码中chkui.springcore.example.hybrid.beanmanipulation.bean.Tyre的使用。
IoC与数据转换整合
对于Spring的ApplicationContext而言,BeanWrapper、PropertyEditor都是相对比较底层的功能,在使用Spring Ioc容器的时候可以直接将这些功能嵌入到Bean初始化中或MVC的requestContext的数据转换中。
从框架使用者的角度来看,Spring的XML配置数据或者通过MVC接口传递数据都是字符串,因此PropertyEditor在处理这些数据时有极大的用武之地。IoC容器使用后置处理器CustomEditorConfigurer来管理Bean初始化相关的PropertyEditor。通过CustomEditorConfigurer可以使用所有预设的Editor,还可以增加自定义的Editor,下面是使用@Configurable启用CustomEditorConfigurer的例子:
"classpath:hybrid/beanmanipulation/config.xml")
public class BeanManipulationConfig {
CustomEditorConfigurer customEditorConfigurer() {
// 构建CustomEditorConfigurer
CustomEditorConfigurer configurer = new CustomEditorConfigurer();
Map<Class<?>, Class<? extends PropertyEditor>> customEditors = new HashMap<>();
// 添加AddressEditor和Address的绑定
customEditors.put(Address.class, AddressEditor.class);
// 添加绑定列表
configurer.setCustomEditors(customEditors);
// 通过PropertyEditorRegistrar注册PropertyEditor
configurer.setPropertyEditorRegistrars(new PropertyEditorRegistrar[] { new DateFormatRegistrar() });
return configurer;
}
}
(
CustomEditorConfigurer::setCustomEditors和CustomEditorConfigurer::setPropertyEditorRegistrars都可以向容器中添加PropertyEditor,最主要区别在于:
- 前者是直接申明一对绑定关系的类对象(Class<?>),例如customEditors.put(Address.class, AddressEditor.class); 这行代码并没有实例化AddressEditor,而是将实例化交给后置处理器。
- 而后者是提供一个实例化的PropertyEditor,比前者更能实现更复杂的功能。比如下面的DateFormatRegistrar代码,由于需要组装DateFormat和CustomDateEditor,所以使用PropertyEditorRegistrar来实现这个过程更加合理,后置处理器会在某个时候调用这个注册方法。
public class DateFormatRegistrar implements PropertyEditorRegistrar {
public void registerCustomEditors(PropertyEditorRegistry registry) {
DateFormat df = new java.text.SimpleDateFormat("yyyy-MM-dd");
CustomDateEditor editor = new CustomDateEditor(df, false);
registry.registerCustomEditor(Date.class, editor);
}
}
配置好CustomEditorConfigurer之后,就可以直接在配置Bean的时候直接使用预定的格式了,比如:
<beans>
<bean id="person" class="chkui.springcore.example.hybrid.beanmanipulation.bean.Person">
<property name="name" value="XML" />
<!-- 使用CustomNumberEditor转换 -->
<property name="age" value="20" />
<!-- 使用CustomBooleanEditor转换 -->
<property name="license" value="1" />
<!-- 使用CustomDateEditor转换 -->
<property name="birtday" value="1998-12-30" />
<!-- 使用AddressEditor转换 -->
<property name="address" value="广东,深圳,南山" />
</bean>
<bean class="chkui.springcore.example.hybrid.beanmanipulation.bean.Vehicle">
<property name="name" value="Mercedes-Benz C-Class" />
<property name="manufacturer" value="Mercedes-Benz" />
<property name="person" ref="person" />
</bean>
</beans>
此外,在Spring MVC中,可以SimpleFormController::initBinder方法将外部传入的数据和某个Bean进行绑定:
public final class MyController extends SimpleFormController {
// 通过任何方式获取PropertyEditorRegistrar
private MyPropertyEditorRegistrar editorRegistrar;
protected void initBinder(HttpServletRequest request,
ServletRequestDataBinder binder) throws Exception {
// 将Editor与当前Controller进行绑定
this.editorRegistrar.registerCustomEditors(binder);
}
}
Spring MVC并不属于Sring核心功能范畴,这里就不展开了,需要了解的话看看SimpleFormController的JavaDoc文档即可。