Spring筆記 - 驗證、數據綁定和類型轉換

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>
相關文章
相關標籤/搜索