Spring中的验证、数据绑定和类型转换
# 一、引言
在现代Web应用程序开发中,数据验证、数据绑定以及类型转换是确保应用健壮性和用户友好性的关键方面。
Spring框架作为一个广泛使用的Java平台开源应用框架,为开发者提供了强大的支持来处理这些问题。无论是构建简单的个人网站还是复杂的企业级应用,正确有效地实现这些功能都能极大地提升用户体验,并减少服务器端的错误和异常。
# 二、Spring验证
# 1. 验证简介
# A. 什么是验证
在软件开发领域,特别是Web应用程序开发中,验证是指确保用户输入的数据符合预期的规则和标准。
这些规则可以是简单的(如必填字段、字符串长度限制)也可以是复杂的(如日期格式、数值范围等)。
Spring框架提供了全面的数据验证支持,使得开发者能够轻松地将验证逻辑整合到他们的应用中。通过使用注解或者实现Validator接口,Spring允许开发者定义各种各样的验证条件,并且可以方便地在控制器层或服务层进行数据校验。
# B. 为什么需要验证
数据验证是任何与用户交互的应用程序不可或缺的一部分,主要原因如下:
- 提高用户体验:及时向用户提供关于输入错误的反馈,可以帮助他们更快地纠正错误,从而提高用户的满意度和体验。
- 保护应用程序的安全性:有效的验证可以防止恶意用户利用不正确的输入对系统进行攻击,例如SQL注入、跨站脚本(XSS)等。
- 保证数据的一致性和准确性:通过验证,可以确保存储在数据库中的数据满足业务规则和完整性约束,这对于维护数据的质量至关重要。
- 减少后端处理的异常:良好的前端验证可以在数据到达服务器之前过滤掉无效或不完整的输入,从而减轻服务器端的负担并降低处理异常的可能性。
在Spring框架中,数据验证通常通过Hibernate Validator等实现,它是Bean Validation规范的一个参考实现。
Spring支持直接在Java类的字段、方法参数上添加注解来定义验证规则,同时也支持自定义验证器以适应特定的业务需求。通过这种方式,Spring不仅简化了验证逻辑的实现,还增强了代码的可读性和可维护性。
# 2. Bean Validation(JSR 380/303)
# A. 基本概念与注解介绍
Bean Validation (opens new window)是Java EE和Java SE中用于验证对象属性的标准。它通过提供一系列的注解,让开发者能够快速地为Java Bean添加验证规则。这些规则可以在对象实例化时、方法调用前或者在Spring MVC控制器层处理用户输入时自动执行。
一些常用的注解包括但不限于:
- @NotNull: 确保字段值不能为null,但允许为空字符串。
- @Size(min=, max=): 验证字符串长度、集合大小等是否在指定范围内。
- @Min(value) 和 @Max(value): 验证数值类型是否大于等于或小于等于指定值。
- @Pattern(regexp): 根据正则表达式来验证字符串格式是否正确。
- @Email: 验证字符串是否符合电子邮件地址格式。
- @AssertTrue 和 @AssertFalse: 验证布尔值是否为true或false。
例如,以下是一个使用了上述注解的简单Java Bean示例:
import javax.validation.constraints.*;
public class User {
@NotNull(message = "用户名不能为空")
private String username;
@Email(message = "请输入有效的电子邮件地址")
private String email;
@Size(min = 6, message = "密码至少需要6个字符")
private String password;
// 构造函数、getter和setter省略
}
# B. 自定义约束的创建与使用
尽管Bean Validation提供了丰富的内置注解,但在某些情况下,我们可能需要根据具体的业务需求创建自定义的验证逻辑。下面是如何创建并使用自定义约束的一个例子:
- 定义注解: 首先,我们需要定义一个新的注解,并指定该注解使用的验证器类。
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = MyCustomValidator.class)
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCustomConstraint {
String message() default "默认错误消息";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@Constraint
注解用于自定义校验,需要指定一个处理验证逻辑的实现类。
- 实现验证器: 接下来,我们需要编写一个实现了
ConstraintValidator
接口的类,这个类将包含具体的验证逻辑。
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class MyCustomValidator implements ConstraintValidator<MyCustomConstraint, String> {
@Override
public void initialize(MyCustomConstraint constraintAnnotation) {
// 初始化代码
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true; // 或者根据你的业务逻辑返回false
}
// 在这里实现自定义的验证逻辑
return value.startsWith("custom");
}
}
- 应用自定义注解: 最后,在需要的地方使用新创建的注解。
public class MyClass {
@MyCustomConstraint(message = "值必须以'custom'开头")
private String myField;
// 构造函数、getter和setter省略
}
通过以上步骤,我们可以轻松扩展Bean Validation的功能,满足特定业务场景下的数据验证需求。
# 3. 在Spring中配置与使用Validator
在Spring框架中,Bean Validation可以通过编程式验证和注解驱动的验证两种方式来集成。下面将分别介绍这两种方法。
# A. 编程式验证
编程式验证允许开发者在代码中手动触发验证逻辑,这在某些特定场景下非常有用,比如在服务层需要根据不同的业务逻辑动态决定是否执行验证时。
要实现编程式验证,首先需要注入javax.validation.Validator
实例,然后使用它来创建一个Set<ConstraintViolation<T>>
对象,其中包含所有违反约束的信息。以下是一个简单的例子:
import org.springframework.beans.factory.annotation.Autowired;
import javax.validation.Validator;
import javax.validation.ConstraintViolation;
import javax.validation.ValidatorFactory;
import java.util.Set;
public class MyService {
private final Validator validator;
@Autowired
public MyService(Validator validator) {
this.validator = validator;
}
public <T> void validateObject(T object) {
Set<ConstraintViolation<T>> violations = validator.validate(object);
if (!violations.isEmpty()) {
// 根据实际情况处理验证错误
for (ConstraintViolation<T> violation : violations) {
System.out.println(violation.getPropertyPath() + ": " + violation.getMessage());
}
} else {
// 执行后续操作
}
}
}
这里传入的T object
可以是上面的User
或者MyClass
对象。
在这个例子中,我们通过Spring的自动装配机制注入了Validator
实例,并使用它来验证传入的对象。如果存在任何违反约束的情况,将会输出相应的错误信息。
# B. 注解驱动的验证
注解驱动的验证是更常见的方式,它利用了Spring对JSR-303/JSR-380(Bean Validation)的支持,使得开发者只需通过注解就能轻松地为Java Bean添加验证规则,而无需编写额外的验证逻辑。
为了启用注解驱动的验证,你需要确保你的Spring项目已经包含了Hibernate Validator等Bean Validation实现库。接下来,在控制器或服务层的方法参数上使用@Valid
或@Validated
注解,以及在需要验证的类字段上添加相应的约束注解。
例如,在控制器中使用注解驱动的验证如下所示:
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public String createUser(@Valid @RequestBody User user) {
// 如果验证失败,Spring会自动返回400 Bad Request状态码以及默认的错误消息
return "用户创建成功";
}
}
这里的User
类应当已经用前面提到的各种约束注解进行了标注。当请求到达createUser
方法时,Spring会自动验证User
对象的所有约束条件。如果验证失败,Spring会立即返回错误响应给客户端,而不会继续执行后续代码。
# 4. @Valid
和 @Validated
的区别
@Valid
和 @Validated
都用于启用 Bean Validation(基于 JSR-303/JSR-380 规范)来对数据进行校验,虽然它们在某些场景下有相似的功能,但它们的含义和使用场景有所区别。
# A. 共同点
- 两者都依赖于 Bean Validation 规范(JSR-303 和 Jakarta Bean Validation)。
- 都可以用于方法参数校验、属性校验或嵌套对象校验。
- 两者都需要一个 Bean Validation 实现(例如 Hibernate Validator)支持。
# B. 区别
# (1) @Valid
特点
来自
jakarta.validation
(或javax.validation
):- 它是 Bean Validation 规范中的注解,定义在
jakarta.validation.Valid
包中。
- 它是 Bean Validation 规范中的注解,定义在
嵌套校验支持:
@Valid
最主要的作用是对嵌套对象进行校验。例如,在对象 A 中引用对象 B(如子对象或集合),可以通过@Valid
触发对 B 的校验。
分组校验不支持:
@Valid
不支持分组校验,也就是说不能配合@GroupSequence
或groups
属性来定义不同的校验组。
使用场景
import jakarta.validation.constraints.NotNull;
import jakarta.validation.Valid;
public class Order {
@NotNull(message = "Order ID cannot be null")
private String orderId;
@NotNull(message = "Customer information is required")
@Valid // 对嵌套对象进行校验
private Customer customer;
// getters and setters
}
public class Customer {
@NotNull(message = "Customer name cannot be null")
private String name;
// getters and setters
}
调用校验方法时:
import jakarta.validation.Validator;
import jakarta.validation.Validation;
public class ValidatorDemo {
public static void main(String[] args) {
Order order = new Order();
order.setOrderId(null);
order.setCustomer(new Customer()); // 设置嵌套对象
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
validator.validate(order).forEach(violation ->
System.out.println(violation.getPropertyPath() + ": " + violation.getMessage()));
}
}
输出:
orderId: Order ID cannot be null
customer.name: Customer name cannot be null
# (2) @Validated
特点
来自 Spring 的扩展注解:
- 它定义在
org.springframework.validation.annotation.Validated
包中,是 Spring 对 Bean Validation 的增强支持。
- 它定义在
支持分组校验:
@Validated
可以指定校验的分组(groups),从而根据不同的业务场景灵活地校验字段。
启用方法级别校验:
@Validated
除了可以用于字段校验,还可以作用于类或接口上,用于启用方法级别校验(配合MethodValidationPostProcessor
使用)。
使用场景
(1)分组校验
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import jakarta.validation.groups.Default;
public class User {
public interface CreateGroup {} // 创建分组
public interface UpdateGroup {}
@NotNull(groups = {CreateGroup.class}, message = "Username cannot be null when creating")
@Size(min = 3, max = 20, groups = {CreateGroup.class, UpdateGroup.class}, message = "Username size must be between 3 and 20")
private String username;
@NotNull(groups = {UpdateGroup.class}, message = "User ID cannot be null when updating")
private Long id;
// Getters and setters
}
在 CreateGroup
或 UpdateGroup
中使用不同的校验:
import jakarta.validation.Validator;
import jakarta.validation.Validation;
public class GroupValidationDemo {
public static void main(String[] args) {
// 初始化 Validator
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
User user = new User(); // 全部为 null
// 调用分组校验
validator.validate(user, User.CreateGroup.class).forEach(violation ->
System.out.println(violation.getPropertyPath() + ": " + violation.getMessage()));
System.out.println("---");
validator.validate(user, User.UpdateGroup.class).forEach(violation ->
System.out.println(violation.getPropertyPath() + ": " + violation.getMessage()));
}
}
输出:
username: Username cannot be null when creating
---
id: User ID cannot be null when updating
(2)方法级别校验
启用方法级别校验时,需要在类上标注 @Validated
注解。
import jakarta.validation.constraints.NotNull;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@Service
@Validated // 启用方法级别的校验
public class UserService {
public void createUser(@NotNull(message = "Username cannot be null") String username) {
System.out.println("User created: " + username);
}
}
调用方法时,Spring 自动对方法参数进行校验。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class UserController {
@Autowired
private UserService userService;
public void test() {
try {
userService.createUser(null); // 会触发参数校验
} catch (Exception e) {
System.out.println("Validation failed: " + e.getMessage());
}
}
}
运行时输出:
Validation failed: createUser.username: Username cannot be null
# 主要区别对比
特性 | @Valid | @Validated |
---|---|---|
来源 | 标准 JSR-303/JSR-380(Bean Validation 规范) | 来自 Spring 的扩展功能 |
分组校验支持 | 不支持 | 支持 |
方法级别校验 | 不支持 | 支持(需与 MethodValidationPostProcessor 配合) |
嵌套对象校验 | 支持(用于嵌套的对象或集合字段) | 也支持(不过更常用于方法级校验或分组校验) |
推荐使用场景 | 简单校验或嵌套对象的校验 | 分组校验、方法级校验和复杂场景 |
# 三、数据绑定
数据绑定是将用户输入的数据(如表单数据)与应用程序中的对象属性进行绑定的过程。在Spring框架中,数据绑定是一个核心功能,它允许开发者将HTTP请求参数、表单数据等直接绑定到Java对象上。
数据绑定简化了开发过程,减少了手动解析和转换数据的代码量。通过数据绑定,开发者可以更专注于业务逻辑的实现,而不必过多关注数据的获取和转换。
# 1、DataBinder
DataBinder是Spring框架中用于数据绑定的核心类。它负责将请求参数绑定到目标对象的属性上,并处理数据转换和验证。
# DataBinder的使用示例
public class User {
private String name;
private int age;
// getters and setters
}
public class UserController {
public void bindUser(HttpServletRequest request) {
User user = new User();
DataBinder binder = new DataBinder(user, "user");
binder.bind(new MutablePropertyValues(request.getParameterMap()));
System.out.println(user.getName() + ", " + user.getAge());
}
}
在这个示例中,DataBinder
将HTTP请求中的参数绑定到User
对象的属性上。
# 构造函数绑定
Spring还支持通过构造函数进行数据绑定。这种方式特别适用于不可变对象,因为它允许在对象创建时直接绑定数据。
public class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
// getters
}
public class UserController {
public void bindUser(HttpServletRequest request) {
DataBinder binder = new DataBinder(null, "user");
binder.setTargetType(User.class);
User user = (User) binder.construct(new MutablePropertyValues(request.getParameterMap()));
System.out.println(user.getName() + ", " + user.getAge());
}
}
在这个示例中,DataBinder
通过构造函数将HTTP请求中的参数绑定到User
对象上。
# 2、BeanWrapper
BeanWrapper是Spring框架中用于操作JavaBean属性的接口。它提供了对JavaBean属性的访问和设置功能,支持嵌套属性的访问。
# BeanWrapper的使用示例
public class Address {
private String city;
private String street;
// getters and setters
}
public class User {
private String name;
private Address address;
// getters and setters
}
public class BeanWrapperExample {
public static void main(String[] args) {
User user = new User();
BeanWrapper wrapper = new BeanWrapperImpl(user);
wrapper.setPropertyValue("name", "John Doe");
wrapper.setPropertyValue("address.city", "New York");
wrapper.setPropertyValue("address.street", "5th Avenue");
System.out.println(user.getName() + ", " + user.getAddress().getCity() + ", " + user.getAddress().getStreet());
}
}
在这个示例中,BeanWrapper
用于设置User
对象及其嵌套属性address
的值。
# 嵌套属性的访问
BeanWrapper支持嵌套属性的访问,这意味着你可以通过点号(.
)来访问嵌套对象的属性。例如,address.city
表示访问User
对象中address
属性的city
属性。
# 3、PropertyEditor
PropertyEditor是Java标准库中的一个接口,用于将字符串转换为特定类型的对象。Spring框架扩展了PropertyEditor的功能,使其能够处理更复杂的数据类型转换。
# PropertyEditor的使用示例
public class CustomDateEditor extends PropertyEditorSupport {
private final DateFormat dateFormat;
public CustomDateEditor(DateFormat dateFormat) {
this.dateFormat = dateFormat;
}
@Override
public void setAsText(String text) throws IllegalArgumentException {
try {
setValue(dateFormat.parse(text));
} catch (ParseException e) {
throw new IllegalArgumentException("Could not parse date: " + e.getMessage(), e);
}
}
@Override
public String getAsText() {
return dateFormat.format((Date) getValue());
}
}
public class PropertyEditorExample {
public static void main(String[] args) {
User user = new User();
BeanWrapper wrapper = new BeanWrapperImpl(user);
wrapper.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), true));
wrapper.setPropertyValue("birthDate", "1990-01-01");
System.out.println(user.getBirthDate());
}
}
在这个示例中,CustomDateEditor
用于将字符串格式的日期转换为Date
对象,并将其绑定到User
对象的birthDate
属性上。
# 内置的PropertyEditor
以下是Spring中一些常见的内置PropertyEditor
及其用途:
PropertyEditor | 用途 |
---|---|
ByteArrayPropertyEditor | 将字符串转换为字节数组。默认由BeanWrapperImpl 注册。 |
ClassEditor | 将字符串表示的类名转换为Class 对象。默认由BeanWrapperImpl 注册。 |
CustomBooleanEditor | 自定义的Boolean 类型编辑器,支持将字符串转换为Boolean 值。默认由BeanWrapperImpl 注册。 |
CustomCollectionEditor | 将字符串或其他集合类型转换为目标集合类型。 |
CustomDateEditor | 自定义的Date 类型编辑器,支持将字符串转换为Date 对象。需要手动注册。 |
CustomNumberEditor | 自定义的Number 类型编辑器,支持将字符串转换为Integer 、Long 、Float 等数字类型。默认由BeanWrapperImpl 注册。 |
FileEditor | 将字符串转换为java.io.File 对象。默认由BeanWrapperImpl 注册。 |
InputStreamEditor | 将字符串转换为InputStream 对象。默认由BeanWrapperImpl 注册。 |
LocaleEditor | 将字符串转换为Locale 对象。默认由BeanWrapperImpl 注册。 |
PatternEditor | 将字符串转换为java.util.regex.Pattern 对象。 |
PropertiesEditor | 将字符串转换为Properties 对象。默认由BeanWrapperImpl 注册。 |
StringTrimmerEditor | 修剪字符串,并可选地将空字符串转换为null 。需要手动注册。 |
URLEditor | 将字符串转换为URL 对象。默认由BeanWrapperImpl 注册。 |
使用场景:
- 基本类型转换:Spring内置的
PropertyEditor
可以处理常见的基本类型转换,例如将字符串转换为Integer
、Boolean
、Date
等。 - 文件处理:
FileEditor
和InputStreamEditor
可以用于处理文件路径和输入流的转换。 - 国际化支持:
LocaleEditor
可以用于处理国际化相关的Locale
对象。 - 正则表达式:
PatternEditor
可以用于将字符串转换为正则表达式对象。
# 自定义PropertyEditor
Spring允许开发者注册自定义的PropertyEditor
来处理特定的数据类型转换。例如,你可以创建一个PropertyEditor
来处理自定义的ExoticType
类型。
public class ExoticTypeEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(new ExoticType(text.toUpperCase()));
}
}
public class ExoticType {
private final String name;
public ExoticType(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
# PropertyEditorRegistrar
PropertyEditorRegistrar
是一个接口,用于注册多个PropertyEditor
实例。它通常与CustomEditorConfigurer
一起使用,以便在Spring容器中注册自定义的PropertyEditor
。
public class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
@Override
public void registerCustomEditors(PropertyEditorRegistry registry) {
registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());
}
}
# 在Spring MVC中使用PropertyEditor
在Spring MVC中,你可以通过@InitBinder
注解来注册自定义的PropertyEditor
。
@Controller
public class RegisterUserController {
private final PropertyEditorRegistrar customPropertyEditorRegistrar;
public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
this.customPropertyEditorRegistrar = propertyEditorRegistrar;
}
@InitBinder
public void initBinder(WebDataBinder binder) {
this.customPropertyEditorRegistrar.registerCustomEditors(binder);
}
// other methods related to registering a User
}
# 四、类型转换
# 1. 类型转换基础
# A. 类型转换的意义与应用场景
类型转换是将一种数据类型转换为另一种数据类型的过程。在Spring框架中,类型转换通常用于将HTTP请求中的字符串参数转换为目标对象所需的类型(如整数、日期、枚举等)。类型转换的意义在于简化数据绑定过程,使开发者无需手动处理数据格式的转换。
应用场景包括:
- 表单提交时,将字符串类型的输入转换为目标对象的属性类型。
- REST API中,将URL参数或请求体中的字符串转换为方法参数的类型。
- 配置文件解析时,将字符串配置值转换为目标类型。
# B. 默认类型转换器
Spring框架提供了许多默认的类型转换器,用于处理常见的数据类型转换。例如:
- 字符串到整数(
String
->Integer
) - 字符串到布尔值(
String
->Boolean
) - 字符串到日期(
String
->Date
)
这些默认转换器由Spring的ConverterRegistry
管理,开发者可以直接使用它们而无需额外配置。
示例:
public class User {
private Integer age;
private Date birthDate;
// getters and setters
}
public class TypeConversionExample {
public static void main(String[] args) {
User user = new User();
BeanWrapper wrapper = new BeanWrapperImpl(user);
wrapper.setPropertyValue("age", "25"); // 字符串 -> Integer
wrapper.setPropertyValue("birthDate", "1990-01-01"); // 字符串 -> Date
System.out.println(user.getAge() + ", " + user.getBirthDate());
}
}
# 2. 自定义类型转换器
# A. 创建自定义转换器
当默认的类型转换器无法满足需求时,开发者可以创建自定义的类型转换器。自定义转换器需要实现org.springframework.core.convert.converter.Converter
接口。
示例:创建一个将字符串转换为自定义PhoneNumber
对象的转换器。
public class PhoneNumber {
private String areaCode;
private String number;
// getters and setters
}
public class StringToPhoneNumberConverter implements Converter<String, PhoneNumber> {
@Override
public PhoneNumber convert(String source) {
PhoneNumber phoneNumber = new PhoneNumber();
String[] parts = source.split("-");
phoneNumber.setAreaCode(parts[0]);
phoneNumber.setNumber(parts[1]);
return phoneNumber;
}
}
# B. 在Spring中注册和使用自定义转换器
要在Spring中使用自定义转换器,需要将其注册到ConversionService
中。
示例:
@Configuration
public class AppConfig {
@Bean
public ConversionService conversionService() {
DefaultConversionService service = new DefaultConversionService();
service.addConverter(new StringToPhoneNumberConverter());
return service;
}
}
public class User {
private PhoneNumber phoneNumber;
// getters and setters
}
public class CustomConverterExample {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
User user = new User();
BeanWrapper wrapper = new BeanWrapperImpl(user);
wrapper.setConversionService(context.getBean(ConversionService.class));
wrapper.setPropertyValue("phoneNumber", "010-12345678");
System.out.println(user.getPhoneNumber().getAreaCode() + "-" + user.getPhoneNumber().getNumber());
}
}
DefaultConversionService
不仅实现了ConversionService
,还实现了ConverterRegistry
,因此可以直接用于注册和管理转换器。
ConversionService
是Spring中用于类型转换的核心接口。它提供了一种统一的方式来执行类型转换,并支持条件判断(是否支持某种类型的转换)。ConverterRegistry
是用于注册和管理类型转换器(Converter
、GenericConverter
或ConverterFactory
)的接口。它允许开发者自定义类型转换逻辑,并将其注册到Spring的转换服务中。
# 3. PropertyEditor与Converter接口对比
# A. PropertyEditor
- 特点:
PropertyEditor
是Java标准库的一部分,主要用于将字符串转换为目标类型。 - 优点:简单易用,适合处理简单的类型转换。
- 缺点:功能有限,不支持双向转换,且线程不安全。
示例:
public class CustomDateEditor extends PropertyEditorSupport {
private final DateFormat dateFormat;
public CustomDateEditor(DateFormat dateFormat) {
this.dateFormat = dateFormat;
}
@Override
public void setAsText(String text) throws IllegalArgumentException {
try {
setValue(dateFormat.parse(text));
} catch (ParseException e) {
throw new IllegalArgumentException("Could not parse date: " + e.getMessage(), e);
}
}
@Override
public String getAsText() {
return dateFormat.format((Date) getValue());
}
}
# B. Converter
- 特点:
Converter
是Spring框架提供的接口,支持双向转换(S
->T
和T
->S
)。 - 优点:功能强大,支持复杂的类型转换,且线程安全。
- 缺点:配置稍复杂,需要注册到
ConversionService
中。
示例:
public class StringToPhoneNumberConverter implements Converter<String, PhoneNumber> {
@Override
public PhoneNumber convert(String source) {
PhoneNumber phoneNumber = new PhoneNumber();
String[] parts = source.split("-");
phoneNumber.setAreaCode(parts[0]);
phoneNumber.setNumber(parts[1]);
return phoneNumber;
}
}
# C. 对比总结
特性 | PropertyEditor | Converter |
---|---|---|
来源 | Java标准库 | Spring框架 |
双向转换 | 不支持 | 支持 |
线程安全 | 不安全 | 安全 |
适用场景 | 简单类型转换 | 复杂类型转换 |
配置复杂度 | 简单 | 较复杂 |
在实际开发中,如果需要进行复杂的类型转换或需要双向转换功能,建议使用Converter
接口;而对于简单的类型转换,PropertyEditor
仍然是一个不错的选择。
# 4. 高级类型转换
# A. ConverterFactory
ConverterFactory
是Spring框架中用于创建一组相关转换器的工厂接口。它允许开发者根据源类型和目标类型动态选择适当的转换器。
示例:创建一个将字符串转换为不同数字类型的ConverterFactory
。
public class StringToNumberConverterFactory implements ConverterFactory<String, Number> {
@Override
public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) {
return new StringToNumberConverter<>(targetType);
}
private static final class StringToNumberConverter<T extends Number> implements Converter<String, T> {
private final Class<T> targetType;
public StringToNumberConverter(Class<T> targetType) {
this.targetType = targetType;
}
@Override
public T convert(String source) {
if (targetType.equals(Integer.class)) {
return (T) Integer.valueOf(source);
} else if (targetType.equals(Long.class)) {
return (T) Long.valueOf(source);
} else if (targetType.equals(Double.class)) {
return (T) Double.valueOf(source);
}
throw new IllegalArgumentException("Unsupported number type: " + targetType);
}
}
}
# B. GenericConverter
GenericConverter
是Spring框架中用于处理复杂类型转换的接口。它支持多对多的类型转换,并且可以处理嵌套属性。
示例:创建一个将Map
转换为User
对象的GenericConverter
。
public class MapToUserConverter implements GenericConverter {
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Map.class, User.class));
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
Map<String, String> map = (Map<String, String>) source;
User user = new User();
user.setName(map.get("name"));
user.setAge(Integer.parseInt(map.get("age")));
return user;
}
}
# C. 编程方式使用 ConversionService
实例
要以编程方式使用 ConversionService
实例,可以像注入其他 Bean 一样注入它的引用。以下示例展示了如何实现这一点:
@Service
public class MyService {
private final ConversionService conversionService;
public MyService(ConversionService conversionService) {
this.conversionService = conversionService;
}
public void doIt() {
this.conversionService.convert(...); // 执行类型转换
}
}
在大多数情况下,可以使用指定目标类型(targetType
)的 convert
方法。然而,对于更复杂的类型(例如参数化元素的集合),这种方法可能不适用。例如,如果要以编程方式将 List<Integer>
转换为 List<String>
,则需要提供源类型和目标类型的正式定义。
幸运的是,TypeDescriptor
提供了多种选项来简化这一过程,如下例所示:
DefaultConversionService cs = new DefaultConversionService();
List<Integer> input = ...; // 输入数据
cs.convert(input,
TypeDescriptor.forObject(input), // List<Integer> 的类型描述符
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class))); // List<String> 的类型描述符
# 五、总结
Spring框架通过强大的验证、数据绑定和类型转换机制,简化了Web应用开发中的数据校验、映射与转换,提升了应用的健壮性、安全性和用户体验,开发者可通过注解、自定义验证器及转换器灵活应对复杂业务需求。
祝你变得更强!