// Bean public class Person { private String name; private int age; //setter & getter, constructor略 } // Validator public class PersonValidator implements Validator { public boolean supports(Class<?> aClass) { return Person.class.isAssignableFrom(aClass); } public void validate(Object o, Errors errors) { ValidationUtils.rejectIfEmpty(errors, "name", "person.name.null", "Person name cannot be null."); Person person = (Person) o; if (person.getAge() < 0) errors.reject("person.age.negative", "Age cannot be negative."); } } // 验证 @Test public void testPersonValidator() { Person person = new Person("张三", -10); PersonValidator validator = new PersonValidator(); Assert.assertTrue(validator.supports(Person.class)); Errors errors = new DirectFieldBindingResult(person, "person"); //亦可使用ValidationUtils.invokeValidator(...) validator.validate(person, errors); for (ObjectError objectError : errors.getAllErrors()) { System.out.println("Error Code is: " + objectError.getCode() + " | " + "Error DefaultMessage is: " + objectError.getDefaultMessage()); } } // web容器环境的用法在SpringMVC部分介绍
[参考]
#properties消息文件 person.age.negative=\u5e74\u9f84\u4e0d\u80fd\u5c0f\u4e8e\u0030+ person.name.null=\u7528\u6237\u540d\u4e0d\u80fd\u4e3a\u7a7a
// 自定义MessageCodesResolver @Component public class MyMessageCodesResolver implements MessageCodesResolver { @Autowired ApplicationContext context; public String[] resolveMessageCodes(String errorCode, String objectName) { return new String[]{context.getMessage(errorCode, null, null)}; } public String[] resolveMessageCodes(String errorCode, String objectName, String field, Class<?> fieldType) { return resolveMessageCodes(errorCode, objectName); } } // 验证 BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(person, "person"); bindingResult.setMessageCodesResolver(myMessageCodesResolver); ValidationUtils.invokeValidator(new PersonValidator(), person, bindingResult); for (ObjectError objectError : bindingResult.getAllErrors()) { System.out.println("Error Code is: " + objectError.getCode() + " | " + "Error DefaultMessage is: " + objectError.getDefaultMessage()); }
- Spring完整支持JSR303 (BeanValidation 1.0),该规范定义了基于注解方式的JavaBean验证元数据模型和API,也可以通过XML进行元数据定义,但注解将覆盖XML的元数据定义。
- Spring也支持JSR409 (BeanValidation 1.1),该规范标准化了Java平台的约束定义、描述和验证,其实现例如Hibernate Validator
- JSR303主要是对JavaBean进行验证,如方法级别(方法参数/返回值)、依赖注入等的验证是没有指定的。因此又有了JSR-349规范的产生。
[参考]
JSR-303原生支持的限制有如下几种
限制 | 说明 |
@Null | |
@NotNull | |
@AssertFalse | |
@AssertTrue | |
@DecimalMax(value) | 不大于指定值的数字 |
@DecimalMin(value) | 不小于指定值的数字 |
@Digits(integer,fraction) | 小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction |
@Max(value) | 不大于指定值的数字 |
@Min(value) | 不小于指定值的数字 |
@Pattern(value) | 符合指定的正则表达式 |
@Size(max,min) | 字符长度必须在min到max之间 |
@Future | 将来的日期 |
@Past | 过去的日期 |
// 定义限制 @Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy=MoneyValidator.class) public @interface Money { String message() default "不是金额形式"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } // 定义限制验证器 public class MoneyValidator implements ConstraintValidator<Money, Double> { private String moneyReg = "^\\d+(\\.\\d{1,2})?$";//表示金额的正则表达式 private Pattern moneyPattern = Pattern.compile(moneyReg); public boolean isValid(Double value, ConstraintValidatorContext arg1) { if (value == null) return true; return moneyPattern.matcher(value.toString()).matches(); } }
<!-- 配置LocalValidatorFactoryBean,Spring会自动查找并加载类路径下的provider,例如Hibernate Validator --> <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/> <!-- 配置后,就可以通过注入validator的方式使用validator -->
对方法的参数、返回值进行验证 [参考]
// 没有方法验证时的做法 public UserModel get(Integer uuid) { //前置条件 Assert.notNull(uuid); Assert.isTrue(uuid > 0, "uuid must lt 0"); //获取 User Model UserModel user = getUserModel(uuid); //从数据库获取 //后置条件 Assert.notNull(user); return user; } // 有方法验证时的做法 // a. 使用方法验证 @Validated // 告诉MethodValidationPostProcessor此Bean需要开启方法级别验证支持 public class UserService { public @NotNull UserModel getUserModel(@NotNull @Min(value = 1) Integer uuid) { //声明前置条件/后置条件 if(uuid > 100) {//方便后置添加的判断(此处假设传入的uuid>100 则返回null) return null; } return getUserModel(uuid); //从数据库获取 } } // b. 配置方法验证的后处理器
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>
// c. 测试用例 @RunWith(value = SpringJUnit4ClassRunner.class) @ContextConfiguration(value = {"classpath:spring-config-method-validator.xml"}) public class MethodValidatorTest { @Autowired UserService userService; @Test public void testConditionSuccess() { // 正常流程 userService.getUserModel(1); } @Test(expected = org.hibernate.validator.method.MethodConstraintViolationException.class) public void testPreCondtionFail() { // 错误的uuid(即前置条件不满足) userService.getUserModel(0); } @Test(expected = org.hibernate.validator.method.MethodConstraintViolationException.class) public void testPostCondtionFail() { // 不满足后置条件的返回值 userService.getUserModel(10000); } }
Foo target = new Foo(); DataBinder binder = new DataBinder(target); binder.setValidator(new FooValidator1()); binder.addValidators(new FooValidator2()); binder.replaceValidators(new FooValidator3()); // 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();
- 封装一个bean的行为,诸如设置和获取属性值等
- 根据JavaDoc中的说明,BeanWrapper提供了设置和获取属性值(单个的或者是批量的),获取属性描述信息、查询只读或者可写属性等功能。不仅如此,BeanWrapper还支持嵌套属性,设置子属性的值。BeanWrapper无需任何辅助代码就可以支持标准JavaBean的PropertyChangeListeners和VetoableChangeListeners。此外,还支持设置索引属性。通常不直接使用BeanWrapper而是使用DataBinder 和BeanFactory
// 被操作的类 class Engine { private String name; //setter & getter ... } // 被操作的类 class Car { private String name; private Engine engine; //setter & getter ... } // 创建并设置Bean BeanWrapper engine = BeanWrapperImpl(new Engine()); engine.setPropertyValue("name", "N73B68A"); // 也可以这样设置 engine.setPropertyValue(new PropertyValue("name", "N73B68A"); // 创建并设置Bean BeanWrapper car = BeanWrapperImpl(new Car()); car.setPropertyValue("name", "劳斯莱斯幻影"); car.setPropertyValue("engine", engine.getWrappedInstance()); // 获取嵌套属性 String engineName = (String) car.getPropertyValue("engine.name");
- Spring大量使用了PropertyEditor以在Object和 String之间进行转化
- PE本来是Java为IDE可视化设置JavaBean属性而准备的,Spring对此进行了封装以简化使用[参考]
类型 | 内建PropertyEditor (是否已在BeanWrapperImpl注册) |
基础数据 | ByteArrayProperty (Y)、CustomBoolean (Y)、CustomDate (N)、CustomNumber (Y) |
集合 | CustomCollection (?) |
资源 | Class (Y)、File (Y)、InputStream (Y)、Locale (Y)、Pattern (?)、Properties (Y)、URL (Y)、StringTrimmer (N) |
// 定义Editor class EngineEditor extends PropertyEditorSupport { public void setAsText(String text){ if(text == null) throw new IllegalArgumentException("设置的字符串格式不正确"); Engine engine = new Engine(); engine.setName(text); setValue(engine); } }
// 注册Editor // 如果自定义Editor和被处理的类在同一包下面,则无需xml注册,会被自动识别 <bean class="org.springframework.beans.factory.config.CustomEditorConfigurer"> <property name="customEditors"> <map> <entry key="x.y.z.Engine" value="x.y.z.EngineEditor"/> </map> </property> </bean> // 使用Editor <bean id="car" class="x.y.z.Car" p:name="劳斯莱斯幻影"> <property name="engine" value="N73B68A" /> </bean>
在不同情况下(如编写一个相应的注册器然后在多种情况下重用它)需要使用相同的属性编辑器时该接口特别有用
// 现在Java代码里面注册 public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar { public void registerCustomEditors(PropertyEditorRegistry registry) { registry.registerCustomEditor(Engine.class, new EngineEditor()); } }
<!-- 再到XML注册 --> <bean id="customPropertyEditorRegistrar" class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/> <bean class="org.springframework.beans.factory.config.CustomEditorConfigurer"> <property name="propertyEditorRegistrars"> <list> <ref bean="customPropertyEditorRegistrar"/> </list> </property> </bean>
- Spring容器使用转换器进行属性注入;SpEL和DataBinder使用转换器绑定值
- 不同于Property的Object和String之间的转换,类型转换可以有更多的格式互转
- 转换器Converter可实现以下接口:
非泛型的强类型转换用Converter接口
一个源类型转成指定基类的子类型用ConverterFactory接口
多个源类型转成多个目标类型用GenericConverter接口,优先用前面两个
- ConversionService接口实现作为无状态的工具在运行时提供转换服务,可供多个线程共享,可在其中设置各种转换器.
将一个源类型转换成一个目标类型
public interface Converter<S, T> { T convert(S source); } final class StringToCarConverter implements Converter<String, Car> { public Car convert(String source) { return new Car(source); } }
一个源类型转成指定基类的子类型
public interface ConverterFactory<S, R> { <T extends R> Converter<S, T> getConverter(Class<T> targetType); } // given Car & Truck extends Auto final class StringToAutoConverterFactory implements ConverterFactory<String, Auto> { public <T extends Auto> Converter<String, T> getConverter(Class<T> targetType) { if(targetType.getType() == Car.class) return new StringToCarConverter(); else if(targetType.getType() == Truck.class) return new StringToTruckConverter(); else throw new ConversionFailedException(String.getType(), targetType, null, new Throwable("不支持的转换类型")); } private final class StringToCarConverter implements Converter<String, Car> { public Car convert(String source) { return new Car(source); } } // StringToTruckConverter ... }
- 多个源类型转成多个目标类型
- ConditionalGenericConverter接口继承该接口,增加了boolean matches(...)方法
public interface GenericConverter { Set<ConvertiblePair> getConvertibleTypes(); Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); public static final class ConvertiblePair { public ConvertiblePair(Class<?> sourceType, Class<?> targetType) { Assert.notNull(sourceType, "Source type must not be null"); Assert.notNull(targetType, "Target type must not be null"); this.sourceType = sourceType; this.targetType = targetType; } private final Class<?> sourceType; private final Class<?> targetType; // getter & setter } } final class GiftGenericConverter implements GenericConverter { public Set<ConvertiblePair> getConvertibleTypes() { Set<ConvertiblePair> pairs = new HashSet<ConvertiblePair>(); paris.add(new ConvertiblePair(String.class, Flower.class)); paris.add(new ConvertiblePair(Integer.class, Toy.class)); return pairs; } Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null || sourceType == TypeDescriptor.NULL || targetType == TypeDescriptor.NULL) { throw new ConversionFailedException(sourceType, targetType, null, new IllegalArgumentException("A null value cannot be assigned to a primitive type")); } if(targetType.getType() == String.class) return new Flower(source); else if(targetType.getType() == Integer.class) return new Toy(source); } }
- 无状态的工具在运行时提供转换服务,可供多个线程共享,可在其中设置各种转换器
- 内置GenericConversionService实现ConvensionService接口
- 也可以配置ConversionServiceFactoryBean提供转换服务
<bean id="conversionService" class="org.springframework.core.convert.support.GenericConversionService"/>
// 注入转换Bean就可以使用转换服务了 @Autowired ConversionService conversionService; public void doSth() { conversionService.convert(source, targetType); List<Integer> input = .... conversionService.convert(input, TypeDescriptor.forObject(input), // List<Integer> type descriptor TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class))); } // 也可以创建转换器实例,通常不需要
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"> <property name="converters"> <set> <bean class="x.y.z.StringToCarConverter"/> <bean class="x.y.z.StringToAutoConverterFactory"/> <bean class="x.y.z.GiftGenericConverter"/> </set> </property> </bean>
// 使用方法同GenericConversionService
Spring格式化Formatter与Converter类似,但可以指定Locale,根据不同的Locale进行不同的双向格式化 (print/parse)
public interface Formatter<T> extends Printer<T>, Parser<T> { } public final class MyDateFormatter implements Formatter<Date> { public String print(Date object, Locale locale) { return new SimpleDateFormat("yyyy-MM-dd", locale).format(object); } public Date parse(String text, Locale locale) throws ParseException { return new SimpleDateFormat("yyyy-MM-dd", locale).parse(text); } }
[参考]
public interface AnnotationFormatterFactory<A extends Annotation> { Set<Class<?>> getFieldTypes(); Printer<?> getPrinter(A annotation, Class<?> fieldType); Parser<?> getParser(A annotation, Class<?> fieldType); } // a. 创建注解 @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface PhoneNumber { } // b. 要格式化的模型 public class PhoneNumberModel { private int areaCode, userNumber; // getter & setter, constructor ... } // c. 实现工厂接口 public class PhoneNumberFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<PhoneNumber> { public Set<Class<?>> getFieldTypes() { return fieldTypes; } public Parser<?> getParser(PhoneNumber annotation, Class<?> fieldType) { return formatter; } public Printer<?> getPrinter(PhoneNumber annotation, Class<?> fieldType) { return formatter; } private final Set<Class<?>> fieldTypes; private final PhoneNumberFormatter formatter; public PhoneNumberFormatAnnotationFormatterFactory() { Set<Class<?>> set = new HashSet<Class<?>>(); set.add(PhoneNumberModel.class); this.fieldTypes = set; this.formatter = new PhoneNumberFormatter(); // 之前定义的Formatter实现 } } // d. 在需要格式化的字段前面加注解 public class contact { @PhoneNumber private PhoneNumberModel phoneNumber; // other fields ... } // e. 测试使用。这个用例有些牵强,更多是注册后,在属性注入、SpEL、数据绑定时,由容器调用自动完成格式化 @Test public void test() throws SecurityException, NoSuchFieldException { DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); // 创建格式化服务 conversionService.addFormatterForFieldAnnotation(new PhoneNumberFormatAnnotationFormatterFactory()); // 添加自定义的注解格式化工厂 TypeDescriptor phoneNumberDescriptor = new TypeDescriptor(FormatterModel.class.getDeclaredField("phoneNumber")); TypeDescriptor stringDescriptor = TypeDescriptor.valueOf(String.class); PhoneNumberModel value = (PhoneNumberModel) conversionService.convert("010-12345678", stringDescriptor, phoneNumberDescriptor); // 解析字符串"010-12345678"--> PhoneNumberModel ContactModel contact = new ContactModel(); contact.setPhoneNumber(value); Assert.assertEquals("010-12345678", conversionService.convert(contact.getPhoneNumber(), phoneNumberDescriptor, stringDescriptor)); // 格式化PhoneNumberModel-->"010-12345678" }
- 实现FormatterRegistry接口,或使用内置实现FormattingConversionService,通常使用FormattingConversionServiceFactoryBean进行配置
- 实现FormatterRegistrar接口
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="formatters"> <list> <bean class="x.y.z.PhoneNumberFormatAnnotationFormatterFactory"/> </list> </property> <property name="converters"> <set> <bean class="org.example.MyConverter"/> </set> </property> <property name="formatterRegistrars"> <set> <bean class="org.example.MyFormatterRegistrar"/> </set> </property> </bean>
Spring默认使用DateFormat.SHORT进行日期和时间格式化,在DefaultFormattingConversionService未被注册的情况下,可以自定义全局日期和时间格式
// a. 以JavaConfig的方式注册全局日期格式yyyyMMdd @Bean public FormattingConversionService conversionService() { // 使用但不注册DefaultFormattingConversionService DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false); // 确保@NumberFormat被支持 conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory()); // 注册全局格式 DateFormatterRegistrar registrar = new DateFormatterRegistrar(); registrar.setFormatter(new DateFormatter("yyyyMMdd")); registrar.registerFormatters(conversionService); return conversionService; }
<!-- b. 以xml的方式注册全局日期格式yyyyMMdd,且用到了Joda-Time第三方库 <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>