本文從 <mvc:annotation-driven /> 切入,系統講解 SpringMVC 的類型轉換體系、格式化支援、JSR-303 驗證與 Spring Validator 兩種驗證方式,以及錯誤訊息前端顯示機制。
RequestMappingHandlerMapping、RequestMappingHandlerAdapter、ExceptionHandlerExceptionResolverConversionService 支援表單參數類型轉換@Valid 支援 JSR-303 驗證@RequestBody/@ResponseBody 支援 REST/JSON位於 org.springframework.core.convert,透過 ConversionServiceFactoryBean 在 Spring 容器中配置實例。<mvc:annotation-driven /> 預設註冊 FormattingConversionServiceFactoryBean,支援日期/數字格式化。
實作 Converter<S, T> 介面:
public class PersonConverter implements Converter<String, Person> {
@Override
public Person convert(String source) {
// 解析 "name:lily|age:23" → Person 物件
String[] parts = source.split("\\|");
Person p = new Person();
for (String part : parts) {
String[] kv = part.split(":");
Field field = Person.class.getDeclaredField(kv[0]);
field.setAccessible(true);
if (field.getType().equals(Integer.class)) {
field.set(p, Integer.parseInt(kv[1]));
} else {
field.set(p, kv[1]);
}
}
return p;
}
}
<bean id="formattingConversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="com.example.converter.PersonConverter"/>
<bean class="com.example.converter.String2MapConverter"/>
</set>
</property>
</bean>
<mvc:annotation-driven conversion-service="formattingConversionService"/>
關鍵點:使用 FormattingConversionServiceFactoryBean 而非 ConversionServiceFactoryBean,避免覆蓋預設日期/數字格式化器。
public class Employee {
@DateTimeFormat(pattern = "yyyy-MM-dd")
@Past
private Date birth;
@NumberFormat(pattern = "#,###,###.##")
private Double salary;
// getter/setter...
}
請求 birth=1992-12-23&salary=1,234,567.89 自動綁定為 Date 與 Double。
public class PersonValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Person.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmpty(errors, "name", "name.empty", "姓名不能為空");
Person p = (Person) target;
if (p.getAge() < 0) errors.rejectValue("age", "negative", "年齡不能為負");
else if (p.getAge() > 110) errors.rejectValue("age", "too.old", "年齡不可超過110");
}
}
@InitBinder
public void initBinder(DataBinder binder) {
binder.setValidator(new PersonValidator());
}
@RequestMapping("/test")
public String test(@Valid Person person, BindingResult result) {
if (result.hasErrors()) {
for (ObjectError e : result.getAllErrors()) {
System.out.println(e.getDefaultMessage());
}
}
return "success";
}
注意:@InitBinder("person") 可限定僅對特定模型屬性生效;同一 @InitBinder 方法只能設定一個 Validator,多個會覆蓋。
需引入 Hibernate Validator 實作:
validation-api-1.1.0.Final.jar hibernate-validator-5.x.jar jboss-logging-3.x.jar classmate-1.x.jar
public class Person {
@NotBlank(message = "姓名不能為空")
private String name;
@Min(value = 10, message = "最小10歲", groups = First.class)
@Max(value = 110, message = "最大110歲", groups = Second.class)
private int age;
// getter/setter...
}
@RequestMapping("/test")
public String test(@Validated({First.class}) Person person, BindingResult result) {
// 無需 @InitBinder,支援同一 Controller 多種 Bean 同時驗證
...
}
public interface First {}groups = First.class@Validated({First.class, Second.class}) 指定分組,按順序驗證@Target({FIELD, METHOD})
@Retention(RUNTIME)
@Constraint(validatedBy = NumberValidator.class)
public @interface Number {
String message() default "必須為數字";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class NumberValidator implements ConstraintValidator<Number, String> {
@Override public void initialize(Number ann) {}
@Override public boolean isValid(String value, ConstraintValidatorContext ctx) {
try { Integer.parseInt(value); return true; }
catch (NumberFormatException e) { return false; }
}
}
Spring MVC 標籤庫:<form:errors path="fieldName"/>
@Pattern 驗證 password 為例):Pattern.user.password → Pattern.password → Pattern.java.lang.String → PatterntypeMismatchrequiredmethodInvocation<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="i18n"/>
</bean>
建立 i18n.properties / i18n_zh_TW.properties 等檔案,鍵對應上述錯誤代碼。
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.setDisallowedFields("name"); // 禁止綁定 name 欄位
binder.setAllowedFields("age", "email"); // 僅允許綁定指定欄位
binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), true));
}
工具推薦:中文 ↔ Unicode 在線轉換 atool.org