1. 驗證Validation
1.1 基本用法
// 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部分介紹
1.2 在消息格式化中進行驗證
[參考]
java
#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()); }
1.3 BeanValidation規範JSR303, JSR409的支持
- Spring完整支持JSR303 (BeanValidation 1.0),該規範定義了基於註解方式的JavaBean驗證元數據模型和API,也能夠經過XML進行元數據定義,但註解將覆蓋XML的元數據定義。git
- Spring也支持JSR409 (BeanValidation 1.1),該規範標準化了Java平臺的約束定義、描述和驗證,其實現例如Hibernate Validatorweb
- JSR303主要是對JavaBean進行驗證,如方法級別(方法參數/返回值)、依賴注入等的驗證是沒有指定的。所以又有了JSR-349規範的產生。正則表達式
[參考]
spring
1.3.1 JSR303
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 | 過去的日期 |
1.3.2 自定義限制
// 定義限制 @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(); } }
1.3.3 配置Bean Validation Provider
<!-- 配置LocalValidatorFactoryBean,Spring會自動查找並加載類路徑下的provider,例如Hibernate Validator --> <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/> <!-- 配置後,就能夠經過注入validator的方式使用validator -->
1.3.4 Spring驅動的方法驗證
對方法的參數、返回值進行驗證 [參考]
app
// 沒有方法驗證時的作法 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); } }
1.4 在數據綁定中使用validator
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();
2. 操做Bean
2.1 BeanWrapper
- 封裝一個bean的行爲,諸如設置和獲取屬性值等編輯器
- 根據JavaDoc中的說明,BeanWrapper提供了設置和獲取屬性值(單個的或者是批量的),獲取屬性描述信息、查詢只讀或者可寫屬性等功能。不只如此,BeanWrapper還支持嵌套屬性,設置子屬性的值。BeanWrapper無需任何輔助代碼就能夠支持標準JavaBean的PropertyChangeListeners和VetoableChangeListeners。此外,還支持設置索引屬性。一般不直接使用BeanWrapper而是使用DataBinder 和BeanFactoryide
// 被操做的類 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");
2.2 PropertyEditor
- Spring大量使用了PropertyEditor以在Object和 String之間進行轉化工具
- PE原本是Java爲IDE可視化設置JavaBean屬性而準備的,Spring對此進行了封裝以簡化使用[參考]
2.2.1 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) |
2.2.2 自定義屬性編輯器
2.2.2.1 擴展PropertyEditorSupport
// 定義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>
2.2.2.2 實現PropertyEditorRegistrar接口
在不一樣狀況下(如編寫一個相應的註冊器而後在多種狀況下重用它)須要使用相同的屬性編輯器時該接口特別有用
// 如今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>
3. 類型轉換
3.1 概述
- Spring容器使用轉換器進行屬性注入;SpEL和DataBinder使用轉換器綁定值
- 不一樣於Property的Object和String之間的轉換,類型轉換能夠有更多的格式互轉
- 轉換器Converter可實現如下接口:
非泛型的強類型轉換用Converter接口
一個源類型轉成指定基類的子類型用ConverterFactory接口
多個源類型轉成多個目標類型用GenericConverter接口,優先用前面兩個
- ConversionService接口實現做爲無狀態的工具在運行時提供轉換服務,可供多個線程共享,可在其中設置各類轉換器.
3.2 Converter接口
將一個源類型轉換成一個目標類型
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); } }
3.3 ConverterFactory接口
一個源類型轉成指定基類的子類型
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 ... }
3.4 GenericConverter接口
- 多個源類型轉成多個目標類型
- 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); } }
3.5 Conversion Service
- 無狀態的工具在運行時提供轉換服務,可供多個線程共享,可在其中設置各類轉換器
- 內置GenericConversionService實現ConvensionService接口
- 也能夠配置ConversionServiceFactoryBean提供轉換服務
3.5.1 GenericConversionService
<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))); } // 也能夠建立轉換器實例,一般不須要
3.5.2 ConversionServiceFactoryBean
<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
4. 格式化
4.1 概述
Spring格式化Formatter與Converter相似,但能夠指定Locale,根據不一樣的Locale進行不一樣的雙向格式化 (print/parse)
4.2 Formatter接口
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); } }
4.3 基於註解的格式化
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" }
4.4 註冊Formatter
- 實現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>
4.5 註冊全局日期和時間格式
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>