轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java

  • Spring

    • 基础

    • 框架

      • Spring容器初始化过程和Bean生命周期探究
      • Spring容器:从依赖管理到注解配置全解析
      • Spring事件探秘
      • Spring AOP的应用
      • Spring 事务管理
      • Spring中的资源访问:Resource接口
      • Spring中的验证、数据绑定和类型转换
        • 一、引言
        • 二、Spring验证
          • 1. 验证简介
          • A. 什么是验证
          • B. 为什么需要验证
          • 2. Bean Validation(JSR 380/303)
          • A. 基本概念与注解介绍
          • B. 自定义约束的创建与使用
          • 3. 在Spring中配置与使用Validator
          • A. 编程式验证
          • B. 注解驱动的验证
          • 4. @Valid 和 @Validated的区别
          • A. 共同点
          • B. 区别
          • (1) @Valid
          • (2) @Validated
          • 主要区别对比
        • 三、数据绑定
          • 1、DataBinder
          • DataBinder的使用示例
          • 构造函数绑定
          • 2、BeanWrapper
          • BeanWrapper的使用示例
          • 嵌套属性的访问
          • 3、PropertyEditor
          • PropertyEditor的使用示例
          • 内置的PropertyEditor
          • 自定义PropertyEditor
          • PropertyEditorRegistrar
          • 在Spring MVC中使用PropertyEditor
        • 四、类型转换
          • 1. 类型转换基础
          • A. 类型转换的意义与应用场景
          • B. 默认类型转换器
          • 2. 自定义类型转换器
          • A. 创建自定义转换器
          • B. 在Spring中注册和使用自定义转换器
          • 3. PropertyEditor与Converter接口对比
          • A. PropertyEditor
          • B. Converter
          • C. 对比总结
          • 4. 高级类型转换
          • A. ConverterFactory
          • B. GenericConverter
          • C. 编程方式使用 ConversionService 实例
        • 五、总结
      • Spring表达式语言(SpELl)
      • Spring中的属性占位符
      • Spring数据缓冲区与编解码器详解
      • Spring对于原生镜像和AOT的支持
      • Spring中的数据访问:JDBC、R2DBC、ORM、Object-XML
      • Spring中的Web访问:Servlet API支持
      • Spring中的Web访问:WebSocket支持
      • Spring中的Web访问:响应式栈 WebFlux
      • Spring中的集成测试与单元测试
      • Spring与多种技术的集成
      • Spring框架版本新特性
    • Spring Boot

    • 集成

  • 其他语言

  • 工具

  • 后端
  • Spring
  • 框架
轩辕李
2024-01-30
目录

Spring中的验证、数据绑定和类型转换

# 一、引言

在现代Web应用程序开发中,数据验证、数据绑定以及类型转换是确保应用健壮性和用户友好性的关键方面。

Spring框架作为一个广泛使用的Java平台开源应用框架,为开发者提供了强大的支持来处理这些问题。无论是构建简单的个人网站还是复杂的企业级应用,正确有效地实现这些功能都能极大地提升用户体验,并减少服务器端的错误和异常。

# 二、Spring验证

# 1. 验证简介

# A. 什么是验证

在软件开发领域,特别是Web应用程序开发中,验证是指确保用户输入的数据符合预期的规则和标准。

这些规则可以是简单的(如必填字段、字符串长度限制)也可以是复杂的(如日期格式、数值范围等)。

Spring框架提供了全面的数据验证支持,使得开发者能够轻松地将验证逻辑整合到他们的应用中。通过使用注解或者实现Validator接口,Spring允许开发者定义各种各样的验证条件,并且可以方便地在控制器层或服务层进行数据校验。

# B. 为什么需要验证

数据验证是任何与用户交互的应用程序不可或缺的一部分,主要原因如下:

  1. 提高用户体验:及时向用户提供关于输入错误的反馈,可以帮助他们更快地纠正错误,从而提高用户的满意度和体验。
  2. 保护应用程序的安全性:有效的验证可以防止恶意用户利用不正确的输入对系统进行攻击,例如SQL注入、跨站脚本(XSS)等。
  3. 保证数据的一致性和准确性:通过验证,可以确保存储在数据库中的数据满足业务规则和完整性约束,这对于维护数据的质量至关重要。
  4. 减少后端处理的异常:良好的前端验证可以在数据到达服务器之前过滤掉无效或不完整的输入,从而减轻服务器端的负担并降低处理异常的可能性。

在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提供了丰富的内置注解,但在某些情况下,我们可能需要根据具体的业务需求创建自定义的验证逻辑。下面是如何创建并使用自定义约束的一个例子:

  1. 定义注解: 首先,我们需要定义一个新的注解,并指定该注解使用的验证器类。
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 注解用于自定义校验,需要指定一个处理验证逻辑的实现类。

  1. 实现验证器: 接下来,我们需要编写一个实现了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");
    }
}
  1. 应用自定义注解: 最后,在需要的地方使用新创建的注解。
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

特点

  1. 来自 jakarta.validation(或 javax.validation):

    • 它是 Bean Validation 规范中的注解,定义在 jakarta.validation.Valid 包中。
  2. 嵌套校验支持:

    • @Valid 最主要的作用是对嵌套对象进行校验。例如,在对象 A 中引用对象 B(如子对象或集合),可以通过 @Valid 触发对 B 的校验。
  3. 分组校验不支持:

    • @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

特点

  1. 来自 Spring 的扩展注解:

    • 它定义在 org.springframework.validation.annotation.Validated 包中,是 Spring 对 Bean Validation 的增强支持。
  2. 支持分组校验:

    • @Validated 可以指定校验的分组(groups),从而根据不同的业务场景灵活地校验字段。
  3. 启用方法级别校验:

    • @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应用开发中的数据校验、映射与转换,提升了应用的健壮性、安全性和用户体验,开发者可通过注解、自定义验证器及转换器灵活应对复杂业务需求。

祝你变得更强!

编辑 (opens new window)
上次更新: 2025/01/31
Spring中的资源访问:Resource接口
Spring表达式语言(SpELl)

← Spring中的资源访问:Resource接口 Spring表达式语言(SpELl)→

最近更新
01
Spring Boot版本新特性
09-15
02
Spring框架版本新特性
09-01
03
Spring Boot开发初体验
08-15
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式