Spring核心——字符串到实体转换

笼统的说一个系统主要是由3个部分组成的:

  1. 执行程序:主要负责处理业务逻辑,对接用户操作。
  2. 内部数据:嵌套在源码中的数据,用于指导程序运行。
  3. 外部数据:业务数据,外部配置数据。

内部数据本身就是程序的一部分,在Java中这些数据通常停留在类的静态成员变量中。而外部数据往往与代码无关,所以对于程序而言要“读懂”它们需要进行一些前置处理。例如用户在前端页面提交的数据我们从RequestContext中获取的数据类型都是字符串,而我们的业务需要将字符串转换成数字、列表、对象等等,这就引入了我们接下来要介绍的内容——数据类型转换。

JavaBean对于J2SE或者J2EE而言有着非常重要的意义,ORACLE为了统一各个组织对JavaBean的使用方式制定了详尽的JavaBean规范,包括BeanInfoPropertyEditorPropertyEditorSupport等方面的内容。本文会涉及到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的例子:

@Configurable
@ImportResource("classpath:hybrid/beanmanipulation/config.xml")
public class BeanManipulationConfig {

	@Bean
	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::setCustomEditorsCustomEditorConfigurer::setPropertyEditorRegistrars都可以向容器中添加PropertyEditor,最主要区别在于:

  1. 前者是直接申明一对绑定关系的类对象(Class<?>),例如customEditors.put(Address.class, AddressEditor.class); 这行代码并没有实例化AddressEditor,而是将实例化交给后置处理器。
  2. 而后者是提供一个实例化的PropertyEditor,比前者更能实现更复杂的功能。比如下面的DateFormatRegistrar代码,由于需要组装DateFormat和CustomDateEditor,所以使用PropertyEditorRegistrar来实现这个过程更加合理,后置处理器会在某个时候调用这个注册方法。
public class DateFormatRegistrar implements PropertyEditorRegistrar {

	@Override
	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
    @Autowired
    private MyPropertyEditorRegistrar editorRegistrar;

    protected void initBinder(HttpServletRequest request,
            ServletRequestDataBinder binder) throws Exception {
        // 将Editor与当前Controller进行绑定
        this.editorRegistrar.registerCustomEditors(binder);
    }
}

Spring MVC并不属于Sring核心功能范畴,这里就不展开了,需要了解的话看看SimpleFormController的JavaDoc文档即可。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏python爬虫日记

python 下字符串格式时间比较

python 下有多个有关时间的模块,分别是time、datetime、calendar,今天重点讨论下time写法。

1012
来自专栏Java帮帮-微信公众号-技术文章全总结

Java设计模式-模板方式模式

模板方法模式: 定义一个操作中的算法的骨架, 而将一些步骤延迟到子类中. 模板方法使得子类可以在不改变一个算法的结构的前提下重定义该算法的某些特定步骤. ? ...

5028
来自专栏康怀帅的专栏

PHP 变量与常量

本文介绍了 PHP 变量与常量。 官方文档:http://php.net/manual/zh/language.variables.php 官方文档:http:...

3064
来自专栏一名合格java开发的自我修养

springMVC参数绑定

处理器形参中添加如下类型的参数处理注解适配器会默认识别并进行赋值。 1 HttpServletRequest 通过request对象获取请求信息 2 Htt...

1032
来自专栏Java学习之路

02 Spring框架 简单配置和三种bean的创建方式

整理了一下之前学习Spring框架时候的一点笔记。如有错误欢迎指正,不喜勿喷。 上一节学习了如何搭建SpringIOC的环境,下一步我们就来讨论一下如何利...

3225
来自专栏林德熙的博客

C# 金额转中文大写

创建的项目是创建一个 dot net core 的项目,实际上这个项目可以创建为 Stand 的。

1712
来自专栏大数据风控

R中的数据导入与导出

1、数据的导入 导入文本文件 使用read.table函数导入普通文本文件 read.table(file,header=FALSE,sep="",...) ?...

1888
来自专栏MasiMaro 的技术博文

使用MSHTML解析HTML页面

最近在写一个爬虫项目,本来打算用C/C++来实现,在网上查找有关资料的时候发现了微软的这个MSHTML库,最后发现在解析动态页面的时候它的表现实在是太差:在项目...

2783
来自专栏java闲聊

POI之easypoi操作(二)

8205
来自专栏py+selenium

[笨方法学python]习题51自动化测试笔记

本节自动化测试部分看不大懂,自己每步都打印出来,帮助理解。(代码标红部分为自己加入调试为打印变量值所用)

2302

扫码关注云+社区

领取腾讯云代金券