最近有在看Spring的官方文档,然后百度了一下中文版的,发现有大神已经翻译了前七章,本着学习的目的,我想继续翻译下去,由于我看到第九章了,所以我会先翻译第九章,望共勉!
9 验证,数据绑定和类型转换
9.1 简介
将验证视为业务逻辑有利有弊,Spring提供的验证(和数据绑定)也不能例外。验证不应该被绑定在web层,它应该是局部的,能插入到任何可用的验证器中。考虑到上述所说的,Spring提出了一个Validator接口,在应用中的每一层既基础又非常有用。
数据绑定是用于允许用户输入动态绑定到应用程序的域模型(domain model)。Spring提供一个所谓的DateBinder去做这个。Validator和DateBinder组成了Validation的包,它不仅限于用到MVC框架。(还有Springboot?)
BeanWrapper是Spring框架的基础内容,它用于很多地方,但是你可能不会直接的用到它。因为它是参考文档,我们将在本章解释一下BeanWrapper,如果用到它,你很可能是在绑定数据中用到。(官方语气。。。)
Spring的DataBinder和低级的BeanWrapper都使用PropertyEditors解析和格式化属性值。将要在本章解释的PropertyEditor,其概念也是JavaBeans规范的一部分。Spring 3引入的“core.convert”包,提供了通用的类型转换,以及用于格式化UI字段值更高级的格式包。本章将讲到这些新的包可能替代了PropertyEditor。
9.2 用Spring的Validator接口进行验证
Spring提供了一个Validator接口,你可以用这个接口去验证对象。接口有一个Errors对象,以便于验证中,验证器可以将验证的错误信息报告给这个对象。
看一个简单的数据对象:
public class Person {
private String name;
private int age;
// the usual getters and setters...
}
我们将给Person类提供验证功能,通过实现org.springframework.validation.Validator接口的两个方法:
- supports(Class) - 是否能验证提供类的实例?
- validate(Object, org.springframework.validation.Errors) - 验证给定对象
继承Validator是相当简单的,尤其是当你知道Spring框架也提供了ValidationUtils。
public class PersonValidator implements Validator {
/**
* This Validator validates *just* Person instances
*/
public boolean supports(Class clazz) {
return Person.class.equals(clazz);
}
public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
Person p = (Person) obj;
if (p.getAge() < 0) {
e.rejectValue("age", "negativevalue");
} else if (p.getAge() > 110) {
e.rejectValue("age", "too.darn.old");
}
}
}
如上所示,ValidationUtils类中的静态方法rejectIfEmpty,当”name”属性为null或空字符串时,就会拒绝这个属性。(真心觉得翻译出来很怪,其实不就是为空的时候会提示有错么)可以去看一下ValidationUtils的文档,看一下上个例子中么有提到的功能。
固然可以用一个验证器去验证一个复杂对象(rich object)中引入的每个对象,但是最好是每个引入的对象都有一个单独封装好的验证器(不造这么说准不准确,英文好绕)。复杂对象的一个简单例子:Customer类由两个String属性(first,second name)和一个复杂类Address对象。Address对象可以跟Customer对象分开使用,所以可以单独实现一个AddressValidator验证器。如果你想使用AddressValidator验证器,可以在CustomerValidator中依赖注入或实例化AddressValidator,如下所示:
public class CustomerValidator implements Validator {
private final Validator addressValidator;
public CustomerValidator(Validator addressValidator) {
if (addressValidator == null) {
throw new IllegalArgumentException("The supplied [Validator] is " +
"required and must not be null.");
}
if (!addressValidator.supports(Address.class)) {
throw new IllegalArgumentException("The supplied [Validator] must " +
"support the validation of [Address] instances.");
}
this.addressValidator = addressValidator;
}
/**
* This Validator validates Customer instances, and any subclasses of Customer too
*/
public boolean supports(Class clazz) {
return Customer.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
Customer customer = (Customer) target;
try {
errors.pushNestedPath("address");
ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
} finally {
errors.popNestedPath();
}
}
}
验证的错误信息会报告给Errors对象。Spring Web MVC中,你可以用spring:bind标签去查看错误信息,更多详情请查看文档信息。
9.3 解决错误信息
前面已经讨论过了数据绑定和数据验证,而验证错误所输出的信息则是我们最后需要研究的。在前面的例子中,我们主要验证了name和age属性。如果你想通过用MessageSource输出错误信息,当拒绝这些属性时,我们会给出error code(这一句翻不出来)。当你从Errors接口调用rejectValue或其它的reject方法时,潜在的实现不仅能记录你传入的code,还能记录一些额外的error code,这个error code取决于你用的MessageCodesResolver。就像默认的DefaultMessageCodesResolver,不仅能记录传入的code,也记录验证的属性名称。也就是说在rejectValue(“age”, “too.darn.old”)中,除了记录too.darn.old,还有too.darn.old.age和too.darn.old.age.int(也就是名称和属性类型),这么做的目的是帮助开发者定位错误信息(这一句有点乱)。
9.4 Bean操作和BeanWrapper
org.springframework.beans包依赖于Oracle提供的JavaBeans标准。一个JavaBean就是一个有默认构造器的,遵循命名规范的类,举个例子,如果有一个属性bingoMadness,就要有setter方法和getter方法。更多关于JavaBeans的信息,请参考Oracle的官网。
在beans包里一个重要的类是BeanWrapper接口以及和它相关的实现类(BeanWrapperImpl)。据文档引用的说,BeanWrapper提供这样一个功能:set和get属性值,获取属性描述符,查询属性以决定他们是可读还是可写。BeanWrapper也适用于嵌套的属性,可以将子属性的属性设置为无限制深度(无限深度是什么鬼?)。BeanWrapper还提供PropertyChangeListeners和VetoableChangeListeners。它也能设置带有索引的属性。BeanWrapper通常不是直接在程序中使用,都是以DateBinder和BeanFactory方式使用。
BeanWrapper就像它名称寓意的一样:它包装一个bean,履行这个bean的行为,就像设置和获取属性。
9.4.1 Setting和getting基本和嵌套属性
Setting和getting属性是用setPropertyValue(s)和getPropertyValue(s)方法,这两个方法都一些重载变体。它们在Spring附带的文档中有详细的描述。重要的是要知道他们有一些约定来表示一个对象的属性。一下是几个例子:
表 9.1 属性的例子
表达式 | 解释 |
---|---|
name | 表示name属性的相关方法有getName(),isName()和setName(..) |
account.name | 表示它的属性方法有getAccount().setName()和getAccount().getName() |
account[2] | 这种的可以将属性定义为array,list或其他集合类型 |
account[COMPANYNAME] | 可以将属性定义为Map类型 |
下面你可以看到一些用**BeanWrapper**get和set属性的例子。
(这括号里的意思就是你可以跳过这个,直接看PropertyEditors,其实看一下也么有损失。。。)
看一下下面两个类:(最喜欢复制粘贴代码啦~)
public class Company {
private String name;
private Employee managingDirector;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Employee getManagingDirector() {
return this.managingDirector;
}
public void setManagingDirector(Employee managingDirector) {
this.managingDirector = managingDirector;
}
}
public class Employee {
private String name;
private float salary;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public float getSalary() {
return salary;
}
public void setSalary(float salary) {
this.salary = salary;
}
}
下面这个代码片段就是展示实例化上面两个类:
BeanWrapper company = new BeanWrapperImpl(new Company());
// setting the company name..
company.setPropertyValue("name", "Some Company Inc.");
// ... can also be done like this:
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);
// ok, let's create the director and tie it to the company:
BeanWrapper jim = new BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());
// retrieving the salary of the managingDirector through the company
Float salary = (Float) company.getPropertyValue("managingDirector.salary");
9.4.2 内置PropertyEditor实现
Spring用PropertyEditor的概念来实现Oject和String之间的转换。其实仔细想想,它有时可以以另外一种方式而不是它本身来表示属性。比如,Date可以显示我们可读的方式(如’2017-03-25’),同时我们也可以将我们看到的字符串时间转成Date类型。这种行为可以通过java.beans.PropertyEditor的自定义编辑器获得。BeanWrapper(?)中的注册自定义编辑器或在之前提到的一个特定IoC容器中,都可以将属性转成想要的类型。想了解更多关于PropertyEditor的知识,可以查阅Oracle官网文档。
Spring中使用属性编辑的例子:
- setting properties on beans 是用的PropertyEditor。当你在XML文件中声明属性值为java.lang.String时,Spring将用ClassEditor尝试将参数分解为Class对象。
- 在Spring MVC框架中parsing HTTP request parameters 用到了各种PropertyEditors,你可以手动绑定CommandController的所有子类。
Spring有很多方便的内置PropertyEditors,下面展示了每个编辑器,它们位于org.springframework.beans.propertyeditors包。有一些被BeanWrapperImpl默认注册过,在某些方面,你也可以自己去覆盖默认的去注册自己的需求:
表 9.2 内置PropertyEditors
类 | 解释 |
---|---|
ByteArrayPropertyEditor | 字节数组。字符串将转化为相应的字节表示。BeanWrapperImpl默认注册。 |
ClassEditor | 将字符串解析为实际类或其他类。当找不到这个类的时候抛出异常IllegalArgumentException |
CustomBooleanEditor | Boolean属性的自定义属性编辑器。可以自定义注册 |
CustomCollectionEditor | 集合的属性编辑器 |
CustomDateEditor | Date类型的自定义属性编辑器,用户要写自己需要的格式 |
CustomNumberEditor | 自定义属性编辑器比如Integer,Long,Float,Double。可以自定义注册 |
FileEditor | 能够将字符串解析为java.io.File对象。 |
InputStreamEditor | 单向属性编辑器,能够获取字符串文本并生成(通过中间的ResourceEditor和Resource)InputStream。 |
LocaleEditor | 将字符串解析为Locale对象。 |
PatternEditor | 将字符串解析为java.util.regex.Pattern对象,反之亦然。 |
PropertiesEditor | 将字符串转为Properties对象。 |
StringTrimmerEditor | 截取字符串属性编辑器,需要用户根据需求自己定义。 |
URLEditor | 能将表示URL的字符串解析为一个真正的URL对象。 |
Spring用java.beans.PropertyEditorManager来设置属性编辑器可能需要的搜索路径。搜索路径包括sun.bean.editors,其中包括用于Font,Color等原始类型的PropertyEditor实现。还要注意的是,如果编辑器类与它们所处理的类在同一个包下,JavaBeans标准会自动发现PropertyEditor类,例如,你可以有以下类和包结构,FooEditor 类可以被识别为Foo类型的属性编辑器。
com
chank
pop
Foo
FooEditor // the PropertyEditor for the Foo class
你也可以在这里用JavaBean的BeanInfo标准,下面这个例子,用BeanInfo机制注册一个或多个PropertyEditor实例。
com
chank
pop
Foo
FooBeanInfo // the BeanInfo for the Foo class
这是引用FooBeanInfo的源码,它将Foo类的age属性与CustomNumberEditor关联起来了。
public class FooBeanInfo extends SimpleBeanInfo {
public PropertyDescriptor[] getPropertyDescriptors() {
try {
final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Foo.class) {
public PropertyEditor createPropertyEditor(Object bean) {
return numberPE;
};
};
return new PropertyDescriptor[] { ageDescriptor };
}
catch (IntrospectionException ex) {
throw new Error(ex.toString());
}
}
}
注册其他自定义PropertyEditors
当bean属性值是字符串时,Spring IoC 容器最终会用JavaBeans**PropertyEditors** 的标准将字符串转成属性的复杂类型。Spring预先注册一些自定义PropertyEditors。此外,Java的标准JavaBeans PropertyEditor的寻找机制,允许一个类的PropertyEditor可以简单命名,并放在与类相同的包中,以便于自动被找到。
如果有需要注册之前自定义的PropertyEditors,这里也有几个可用的机制。最简单的但也不方便和建议的,就是用ConfigurableBeanFactory接口的registerCustomEditor()方法,当然前提是你有引用BeanFactory。另外一个更方便的机制是用一个特殊的post-processor bean factory,叫做CustomEditorConfigurer。虽然bean factory post-processor 可以与BeanFactory 一起实现,但是CustomEditorConfigurer具有嵌套属性设置,所以强烈建议和ApplicationContext一起使用。它可以以相似的方式部署到任何其他的bean,并可以自动检测和应用。
值得注意的是,所有的bean工厂和应用上下文都会使用一些内置的属性编辑器,通过使用BeanWrapper处理属性转换。BeanWrapper的标准属性标记在上一个章节已经讲到。此外,ApplicationContexts也以适合特定应用上下文的方式重写或者添加一些额外的编辑器来处理资源搜索。
标准JavaBeans PropertyEditor 实例是用于将表示为字符串的属性值转为实际复杂的类型。bean factory post-processor 的 CustomEditorConfigurer 可以方便的向ApplicationContext添加额外的PropertyEditor实例。
话不多说看代码:
package example;
public class ExoticType {
private String name;
public ExoticType(String name) {
this.name = name;
}
}
public class DependsOnExoticType {
private ExoticType type;
public void setType(ExoticType type) {
this.type = type;
}
}
我们希望type属性是字符串,而PropertyEditor 将在后台将它转成实际ExoticType类型。
<bean id="sample" class="example.DependsOnExoticType">
<property name="type" value="aNameForExoticType"/>
</bean>
PropertyEditor实现与此相似:
// converts string representation to ExoticType object
package example;
public class ExoticTypeEditor extends PropertyEditorSupport {
public void setAsText(String text) {
setValue(new ExoticType(text.toUpperCase()));
}
}
最后我们用CustomEditorConfigurer 在ApplicationContext中注册新的PropertyEditor ,以备不时之需:(么有用过XML配置文件,对于XML配置接受有点慢)
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
</map>
</property>
</bean>
使用PropertyEditorRegistrars
另一个用Spring容器之策属性编辑器的机制是创建PropertyEditorRegistrar,这个接口在你需要在几种不同的情况下使用同一组属性编辑器时特别有用:在每种情况下写入相应的注册器并重新使用。PropertyEditorRegistrars与PropertyEditorRegistry接口配合作用,这个接口是Spring的BeanWrapper是实现的。当与CustomEditorConfigurer一起使用时,PropertyEditorRegistrars 有一个属性setPropertyEditorRegistrars(..):以这种方式添加到CustomEditorConfigurer中的PropertyEditorRegistrars,可以很容易的与DataBinder和Spring MVC控制器共享。此外,它避免了在自定义编辑器上同步的需要:PropertyEditorRegistrars被期望与为每个创建的bean新创一个PropertyEditor 实例。
可以通过一个例子来说明PropertyEditorRegistrar。首先,先实现PropertyEditorRegistrar 接口:
package com.foo.editors.spring;
public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
public void registerCustomEditors(PropertyEditorRegistry registry) {
// it is expected that new PropertyEditor instances are created
registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());
// you could register as many custom property editors as are required here...
}
}
另外也可以参考org.springframework.beans.support.ResourceEditorRegistrar中PropertyEditorRegistrar 实现的例子。需要注意的是registerCustomEditors(..)方法中是如何实现为每个属性编辑器创建实例的。
接下来,我们配置一个CustomEditorConfigurer ,并将CustomPropertyEditorRegistrar 的一个实例注进去:
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="propertyEditorRegistrars">
<list>
<ref bean="customPropertyEditorRegistrar"/>
</list>
</property>
</bean>
<bean id="customPropertyEditorRegistrar"
class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>
最后,我们稍微讲点与本章重点有所偏离的知识,对于那些使用Spring MVC框架的人,将PropertyEditorRegistrars 与数据绑定Controller一起配合使用会更方便。在下面的例子中,使用PropertyEditorRegistrar 实现initBinder(..)方法:
public final class RegisterUserController extends SimpleFormController {
private final PropertyEditorRegistrar customPropertyEditorRegistrar;
public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
this.customPropertyEditorRegistrar = propertyEditorRegistrar;
}
protected void initBinder(HttpServletRequest request,
ServletRequestDataBinder binder) throws Exception {
this.customPropertyEditorRegistrar.registerCustomEditors(binder);
}
// other methods to do with registering a User
}
这种风格使代码看着简洁,并将常用的PropertyEditor 封装在一类中,有需要的话可以在多个Controllers之间共享。
9.5 Spring 类型转换
Spring 3引用了一个通用的类型转化系统包core.convert,这个系统定义了一个SPI来实现类型转换逻辑以及一个API在运行时执行类型转换。在Spring容器中,这个系统可以替换PropertyEditor,转换外部bean属性值的字符串类型为需要的类型。Public API可以用到任何需要类型转换的程序中。
9.5.1 转换器SPI
实现类型转换逻辑的SPI时简单的和强类型的:
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
创建自己的转换器只需要实现上述接口,其中参数S是目前的类型(被转换的类型),而T是要转成的类型。如果S的类型为集合或数组,要转成T的类型为数组或集合,这个转换器也是可以用的,前提是要定义一个集合或数组转换器(DefaultConversionService默认提供)。
调用convert(S)时,参数不能为空,调用失败时也需要抛出异常;参数值无效时,要抛出IllegalArgumentException异常。而且要保证Converter实现类时线程安全的。
为了方便,core.convert.support包提供了一些转换器实现类,包括Strings转Numbers,以及其它常用的类型。StringToInteger是Converter实现类中的一个典型例子:
package org.springframework.core.convert.support;
final class StringToInteger implements Converter<String, Integer> {
public Integer convert(String source) {
return Integer.valueOf(source);
}
}
9.5.2 ConverterFactory
当需要从String类型转成java.lang.Enum对象时,继承ConverterFactory即可:
package org.springframework.core.convert.converter;
public interface ConverterFactory<S, R> {
<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}
参数同上,然后实现getConverter(Class<T>)
方法,其中T时R的子类。
下面是StringToEnum
ConverterFactory的例子:
package org.springframework.core.convert.support;
final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {
public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
return new StringToEnumConverter(targetType);
}
private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> {
private Class<T> enumType;
public StringToEnumConverter(Class<T> enumType) {
this.enumType = enumType;
}
public T convert(String source) {
return (T) Enum.valueOf(this.enumType, source.trim());
}
}
}
9.5.3 Generic Converter
当需要实现复杂的转换时,可以考虑Generic Converter接口,这个接口支持多个source和target 类型之间转换,更灵活,弱类型。此外,Generic Converter可以在你实现转换逻辑时提供source和target的应用上下文。这些上下文允许类型转换由字段注释货在字段签名上声明的通用信息来驱动。
package org.springframework.core.convert.converter;
public interface GenericConverter {
public Set<ConvertiblePair> getConvertibleTypes();
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
GenericConverter接口中有getConvertibleTypes()方法,返回类型为souce-target类型对,然后通过实现convert(Object, TypeDescriptor, TypeDescriptor) 方法来实现转换逻辑。Source TypeDescriptor提供对正在转换的source字段的访问;Target TypeDescriptor提供对将要转换的target字段的访问。
GenericConverter的一个好的例子是Java Array和Collection之间的转换。这样一个ArrayToCollectionConverter会介绍声明target集合类型以解析集合元素类型的字段,允许source数组中的每个元素在在target字段设置之前转成集合元素类型。
ConditionalGenericConverter
有时候可能只需要在特定情况下执行转换器,比如,在target字段有特定注解时至性转换器。再比如在target类中定义一个特殊方法(static valueof方法)时执行转换器。ConditionalGenericConverter是GenericConverter和ConditionalConverter接口的联合,可以自定义匹配条件:
public interface ConditionalGenericConverter
extends GenericConverter, ConditionalConverter {
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}
ConditionalGenericConverter的一个很好的例子是EntityConverter,是在persistent entity identifier和entity reference之间转换。EntityConverter只匹配当target声明一个静态方法,比如findAccount(Long) 方法时。
9.5.4 ConversionService API
ConversionService定义了一个用于在运行时执行类型转换逻辑的统一API。转换器通常在这个接口之后执行:
package org.springframework.core.convert;
public interface ConversionService {
boolean canConvert(Class<?> sourceType, Class<?> targetType);
<T> T convert(Object source, Class<T> targetType);
boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
大多数ConversionService实现类也实现ConverterRegistry,它提供注册转换器的SPI。在内部,ConversionService委托其注册转换器来执行类型转换逻辑。
强大的ConversionService功能是core.convert.support包提供的,GenericConversionService适用于到多数环境下的通用实现,ConversionServiceFactory为创建常用的ConversionService配置提供便利的工厂。
9.5.5 配置ConversionService
ConversionService是一个无状态对象,它的作用是在程序启动时出实例化,然后在多个线程中共享。在Spring应用中,每个Spring容器(或应用上下文)配置一个ConversionService,然后在框架有类型转换需求时使用它。也可以将ConversionService注入beans中直接调用。
Spring默认的ConversionService,添加以下bean定义:
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"/>
默认的ConversionService可以在通用类型中转换,而自定义转换器需要补充或重写默认转换器来设置转换器属性。属性值可以实现Converter,ConverterFactory或GenericConverter接口。
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="example.MyCustomConverter"/>
</set>
</property>
</bean>
在Spring MVC应用中使用ConversionService也很常见。详情请参考Spring MVC章节。
在某些情况下,转换期间也会需要应用格式化,请参考下一小节(9.6.3)。
9.5.6 使用ConversionService
在程序中使用ConversionService,只需像其它bean注入就行:
@Service
public class MyService {
@Autowired
public MyService(ConversionService conversionService) {
this.conversionService = conversionService;
}
public void doIt() {
this.conversionService.convert(...)
}
}
对于大部分情况下,可以使用指定的targetType的convert方法,但它不适用于更复杂的类型,比如集合类型。例如,程序中转换List类型,需要提供source和target类型的formal定义。
不过幸运的是,为方便TypeDescriptor提供了各种各样的选项:
DefaultConversionService cs = new DefaultConversionService();
List<Integer> input = ....
cs.convert(input,
TypeDescriptor.forObject(input), // List<Integer> type descriptor
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));
要注意的是DefaultConversionService会自动注册适用于大多数环境下的转换器,包括集合转换器,标量转换器以及Object到String的转换器。可以使用DefaultConversionService类中的静态方法 addDefaultConverters向任何ConverterRegistry注册相同的转换器。
9.6 Spring Field Formatting
之前章节讲过,core.conver是通用的类型转换系统,为实现从一种类型到另外一种类型转换逻辑,它提供一个统一的API和一个强类型转换器SPI。Spring使用这个系统绑定bean属性值。此外,Spring Expression Language (SpEL)和DataBinder也使用这个系统绑定字段值。例如,当SpEL需要强制Short到Long的转换以完成expression.setValue(Object bean, Object value)功能时,就会把这个任务交给core.convert系统来完成。
现在考虑一个经典客户端环境(如Web或桌面应用)的类型转换。在这种环境下,需要将String类型转为支持客户端传递回来的类型,以及转为支持页面渲染的String类型。此外,需要经常本地化String值。通用的core.convert转换器SPI不直接解决这种格式化要求。为直接解决这个问题,Spring 3引入了一个便捷的Formatter SPI,为客户端环境提供了一个简单而强大的PropertyEditors替代方案。
一般情况下,当需要实现通用类型转换时,比如, java.util.Date与java.lang.Long之间的转换,可以使用转换器SPI。当在客户端环境(如Web应用程序)时,使用Formatter SPI,同时也需要解析和输出本地字段值。ConversionService为这两个SPIs提供了一个统一的类型转换器API。
9.6.1 Formatter SPI
Formatter SPI实现字段格式化逻辑是简单和强类型的:
package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
其中,继承了Printer和Formatter接口:
public interface Printer<T> {
String print(T fieldValue, Locale locale);
}
import java.text.ParseException;
public interface Parser<T> {
T parse(String clientValue, Locale locale) throws ParseException;
}
创建自己的Formatter只需实现上述的Formatter接口即可,其中参数T是需要格式的类型,比如java.util.Date
。实现print()
操作是为了输出客户端区域显示的T的参数实例;实现parse()操作是为了解析从客户端区域返回的格式化表达式的实例。如果解析失败,需要抛出ParseException或IllegalArgumentException异常。要保证你的Formatter实现是线程安全的。
为方便format
下的子包提供了一些Formatter的实现类。number
包提供了NumberFormatter
,CurrencyFormatter
和PercentFormatter
三个类,通过java.text.NumberFormat
来格式化java.lang.Number
对象。datetime
包提供了一个DateFormatter
类,通过使用java.text.DateFormat
格式化java.util.Date
对象。在基于Joda Time library的基础上,datetime.joda
提供了支持全面的datetime formatting。
以DateFormatter
为例来介绍一下Formatter
的实现:
package org.springframework.format.datetime;
public final class DateFormatter implements Formatter<Date> {
private String pattern;
public DateFormatter(String pattern) {
this.pattern = pattern;
}
public String print(Date date, Locale locale) {
if (date == null) {
return "";
}
return getDateFormat(locale).format(date);
}
public Date parse(String formatted, Locale locale) throws ParseException {
if (formatted.length() == 0) {
return null;
}
return getDateFormat(locale).parse(formatted);
}
protected DateFormat getDateFormat(Locale locale) {
DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale);
dateFormat.setLenient(false);
return dateFormat;
}
}
9.6.2 Annotation-driven Formatting
正如你所知,field formatting可以通过字段类型或注解来配置,为在formatter上绑定一个可以注解,需实现AnnotationFormatterFactory:
package org.springframework.format;
public interface AnnotationFormatterFactory<A extends Annotation> {
Set<Class<?>> getFieldTypes();
Printer<?> getPrinter(A annotation, Class<?> fieldType);
Parser<?> getParser(A annotation, Class<?> fieldType);
}
参数A是格式逻辑相关的注解类型,例如org.springframework.format.annotation.DateTimeFormat
。getFieldTypes()
返回可以使用注解的字段类型;getPrinter()
返回一个Printer类型以输出注解字段的值;getParser()
返回一个Parser类型以解析字符字段的clientValue。下面的示例AnnotationFormatterFactory实现@NumberFormat注释绑定到格式化程序。此注释允许指定的数字样式或模式:
public final class NumberFormatAnnotationFormatterFactory
implements AnnotationFormatterFactory<NumberFormat> {
public Set<Class<?>> getFieldTypes() {
return new HashSet<Class<?>>(asList(new Class<?>[] {
Short.class, Integer.class, Long.class, Float.class,
Double.class, BigDecimal.class, BigInteger.class }));
}
public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation, fieldType);
}
public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation, fieldType);
}
private Formatter<Number> configureFormatterFrom(NumberFormat annotation,
Class<?> fieldType) {
if (!annotation.pattern().isEmpty()) {
return new NumberFormatter(annotation.pattern());
} else {
Style style = annotation.style();
if (style == Style.PERCENT) {
return new PercentFormatter();
} else if (style == Style.CURRENCY) {
return new CurrencyFormatter();
} else {
return new NumberFormatter();
}
}
}
}
为触发这种格式化,添加注解@NumberFormat:
public class MyModel {
@NumberFormat(style=Style.CURRENCY)
private BigDecimal decimal;
}
Format Annotation API
在org.springframework.format.annotation包中存在一个便捷式格式注解API。用@NumberFormat格式化java.lang.Number字段;@DateTimeFormat格式化java.util.Date, java.util.Calendar, java.util.Long, or Joda Time字段。
以下这个例子用@DateTimeFormat将java.util.Date转成ISO时间(yyyy-MM-dd):
public class MyModel {
@DateTimeFormat(iso=ISO.DATE)
private Date date;
}
9.6.3 FormatterRegistry SPI
FormatterRegistry是注册格式和转换器的SPI,而FormattingConversionService
是在大多数环境下都适用的实现类。这个实现类使用FormattingConversionServiceFactoryBean以程序或声明的方式配置。因为,它还实现了ConversionService,可以直接配置,然后跟Spring DataBinder和Spring Expression Language (SpEL)一起使用。
重新看一下FormatterRegistry SPI:
package org.springframework.format;
public interface FormatterRegistry extends ConverterRegistry {
void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
void addFormatterForFieldType(Formatter<?> formatter);
void addFormatterForAnnotation(AnnotationFormatterFactory<?, ?> factory);
}
由此看出,fieldType和注解可以注册Formatters。
FormatterRegistry SPI允许集中配置格式化规则,而不是在控制器上复制此类配置。 例如,你可以强制以某种方式执行所有日期字段格式化,或者具有特定注释的字段以某种方式进行格式化。 使用共享的FormatterRegistry,可以定义这些规则,并且在需要格式化时应用它们。
9.6.4 FormatterRegistrar SPI
FormatterRegistrar是通过FormatterRegistry注册格式化器和转换器的SPI:
package org.springframework.format;
public interface FormatterRegistrar {
void registerFormatters(FormatterRegistry registry);
}
在为给定的格式化类别注册多个相关转换器和格式化程序(如日期格式化)时,FormatterRegistrar很有用。 在声明注册不足的情况下也是有用的。 例如,当格式化程序需要在与其自己的<T>
不同的特定字段类型下索引时,或者在注册打印机/解析器对时进行索引。
9.7 配置全局日期和时间格式
如果日期和时间没有使用@DateTimeFormat
注解,默认下使用DateFormat.SHORT
样式从字符串转换。当然你也可以定义自己的全局样式。
首先需要确保Spring不会注册默认的格式化,然后手动的注册所有的格式化。根据是否使用Joda Time库,可以自行考虑用以下两个类:org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar
和org.springframework.format.datetime.DateFormatterRegistrar
。
例如,下面这个java配置例子将注册一个全局‘yyyyMMdd‘格式(不依赖Joda Time库):
@Configuration
public class AppConfig {
@Bean
public FormattingConversionService conversionService() {
// Use the DefaultFormattingConversionService but do not register defaults
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);
// Ensure @NumberFormat is still supported
conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
// Register date conversion with a specific global format
DateFormatterRegistrar registrar = new DateFormatterRegistrar();
registrar.setFormatter(new DateFormatter("yyyyMMdd"));
registrar.registerFormatters(conversionService);
return conversionService;
}
}
以下是XML配置的例子,用到FormattingConversionServiceFactoryBean
,例子中用到了Joda Time:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd>
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="registerDefaultFormatters" value="false" />
<property name="formatters">
<set>
<bean class="org.springframework.format.number.NumberFormatAnnotationFormatterFactory" />
</set>
</property>
<property name="formatterRegistrars">
<set>
<bean class="org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar">
<property name="dateFormatter">
<bean class="org.springframework.format.datetime.joda.DateTimeFormatterFactoryBean">
<property name="pattern" value="yyyyMMdd"/>
</bean>
</property>
</bean>
</set>
</property>
</bean>
</beans>
如果使用的是Spring MVC,请记住要明确所使用的转换服务。使用基于@Configuration
的Java,意味着要扩展WebMvcConfigurationSupport
类和覆盖 mvcConversionService()
方法。而在XML配置文件中,需要用mvc:annotation-driven
元素下的conversion-service
属性。
9.8 Spring 验证
Spring 3对验证进行了一些加强,第一,完全支持SR-303 Bean Validation API;第二,在程序中,Spring的DataBinder可以验证它所绑定的对象;第三,Spring MVC现在支持声明式的验证。
9.8.1 JSR-303 Bean Validation API概述
JSR-303为Java平台的验证约束和声明都有一定的标准化。使用这个API,可以注解验证限制的域模型,并在运行时强制运行他们。有很多内置的限制可以用,当然你也可以定义自己的限制。
一个简单model:
public class PersonForm {
private String name;
private int age;
}
JSR-303允许在属性上加一些限制:
public class PersonForm {
@NotNull
@Size(max=64)
private String name;
@Min(0)
private int age;
}
当一个类的实例被JSR-303 Validator验证时,这些验证会被强制执行。
9.8.2 配置Bean Validation Provider
Spring支持Bean Validation API,这包括支持SR-303/JSR-349 Bean Validation为pring bean提供引导。它允许javax.validation.ValidatorFactory
或javax.validation.Validator
注入到验证需要的地方。
用LocalValidatorFactoryBean配置一个默认的Spring bean验证:
<bean id="validator"
class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
上述这个基本配置会触发Bean Validation用它的默认引导机制进行实例化。JSR-303/JSR-349提供的,比如Hibernate Validator,存在于类路径中,并且能自动检测。
注入验证器
LocalValidatorFactoryBean
实现了javax.validation.ValidatorFactory
和javax.validation.Validator
接口,同样还有Spring的org.springframework.validation.Validator
接口,你可以将任何一个接口注入到需要验证的bean中。
如果直接使用Bean Validation API,可以注入javax.validation.Validator
:
import javax.validation.Validator;
@Service
public class MyService {
@Autowired
private Validator validator;
}
如果bean需要Spring Validation API时,引用org.springframework.validation.Validator:
import org.springframework.validation.Validator;
@Service
public class MyService {
@Autowired
private Validator validator;
}
配置自定义限制
每个Bean验证限制由两个部分组成,第一,@Constraint
注解声明约束和可配置属性;第二,javax.validation.ConstraintValidator
接口可以实现限制的行为。要将声明与实现相关联,每个@Constraint
注释引用相应的ValidationConstraint
实现类。在运行时,当您的域模型中遇到约束注释时,ConstraintValidatorFactory
会实例化引用的实现。
默认下,LocalValidatorFactoryBean
会配置SpringConstraintValidatorFactory
工厂,用Spring创建一个ConstraintValidator实例,就像其他Spring bean一样,自定义的ConstraintValidators可以使用依赖注入。
下面展示一个自定义@Constraint,还有一个实现这个依赖的ConstraintValidator实现类:
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}
import javax.validation.ConstraintValidator;
public class MyConstraintValidator implements ConstraintValidator {
@Autowired;
private Foo aDependency;
...
}
如上所述,像其他Spring bean一样,ConstraintValidator实现类也可以使用@Autowired注解。
Spring-driven Method Validation
可以通过MethodValidationPostProcessor的bean定义,将Bean Validation 1.1和自定义的Hibernate Validator 4.3融合进Spring上下文中:
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>
为了符合Spring驱动方法验证的资格,所有目标类都需要使用Spring的@Validated
注释进行注释。 使用Hibernate Validator和Bean Validation 1.1提供程序查看MethodValidationPostProcessor
文档的设置详细信息。
**
9.8.3 配置DataBinder
**
自Spring 3开始,DataBinder实例可以通过Validator配置,一旦配置了,可以通过调用binder.validate()来调用Validator。 任何验证错误都会自动添加到绑定器的BindingResult中。
以编程方式使用DataBinder时,可以在绑定到目标对象之后调用验证逻辑:
Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());
// bind to the target object
binder.bind(propertyValues);
// validate the target object
binder.validate();
// get BindingResult that includes any validation errors
BindingResult results = binder.getBindingResult();
DataBinder还可以通过dataBinder.addValidators和dataBinder.replaceValidators配置多个Validator实例。 当将全局配置的Bean验证与在DataBinder实例上本地配置的Spring验证程序组合时,非常有用。
(第九章的翻译就这些了,翻译中其实还有很多不理解的地方,希望通过实际开发中,或者查看书籍时有什么启发,也会回来改一改。督促自己学习中。。。)