spring6(JdbcTemplate、Validation和Resources)

37 阅读38分钟

JdbcTemplate CRUD

简介

Spring 框架对 JDBC 进行了封装,提供了 JdbcTemplate 工具类,大大简化了传统 JDBC 的开发流程。它处理了资源的创建与释放、异常处理等繁琐操作,让开发者能更专注于 SQL 业务逻辑。

准备工作

  1. 创建子模块

新建名为 spring-jdbc-tx 的子模块,专门用于学习 Spring 的 JDBC 和事务管理功能。

  1. 添加依赖

pom.xml 中添加必要依赖:

  • spring-jdbc:Spring 持久化层核心支持
  • mysql-connector-java:MySQL 数据库驱动
  • druid:高性能数据库连接池
<dependencies>
    <!-- Spring JDBC 支持 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>6.0.2</version>
    </dependency>
    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.30</version>
    </dependency>
    <!-- Druid 数据源 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.15</version>
    </dependency>
</dependencies>
  1. 配置数据库连接

src/main/resources 下创建 jdbc.properties,配置数据库连接信息:

jdbc.user=root
jdbc.password=root
jdbc.url=jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false
jdbc.driver=com.mysql.cj.jdbc.Driver
  1. 配置 Spring 配置文件

创建 beans.xml,配置数据源和 JdbcTemplate:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 加载外部属性文件 -->
    <context:property-placeholder location="classpath:jdbc.properties" />

    <!-- 配置 Druid 数据源 -->
    <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="username" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!-- 配置 JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="druidDataSource"/>
    </bean>

</beans>
  1. 准备测试数据库

执行以下 SQL 创建数据库和测试表:

CREATE DATABASE `spring`;

USE `spring`;

CREATE TABLE `t_emp` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `sex` varchar(2) DEFAULT NULL COMMENT '性别',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

实现 CRUD 操作

  1. 创建实体类
// 1. 分析数据库表结构
// t_emp表有:id、name、age、sex 四个字段

// 2. 创建对应的 Java 类
public class Emp {
    // 3. 为每个字段创建属性
    private Integer id;
    private String name;
    private Integer age;
    private String sex;
    
    // 4. 生成 getter 和 setter 方法
    // 5. 生成 toString() 方法便于调试
}
  1. 配置测试环境

首先需要创建测试类并装配 JdbcTemplate。通过 Spring 测试框架整合 JUnit,可以方便地注入和使用 JdbcTemplate。

// 1. 创建测试类
public class JDBCTemplateTest {
    // 2. 使用注解加载 Spring 配置文件
    @SpringJUnitConfig(locations = "classpath:beans.xml")
    // 3. 注入 JdbcTemplate
    @Autowired
    private JdbcTemplate jdbcTemplate;
}

各种CRUD操作

  1. 增删改操作

JdbcTemplate 的 update() 方法用于执行 INSERT、UPDATE 和 DELETE 语句,返回受影响的行数。

增删改代码模板:

@Test
public void test操作类型() {
    // 1. 写 SQL 语句(字符串)
    String sql = "SQL语句模板";
    
    // 2. 调用 jdbcTemplate.update()
    // 参数1:SQL 字符串
    // 参数2及以后:SQL 中的问号对应的值
    int result = jdbcTemplate.update(sql, 参数1, 参数2, ...);
    
    // 3. (可选)处理结果
    System.out.println("影响行数:" + result);
}

示例:

@Test
public void testUpdate(){
    // 添加数据
    String sql = "insert into t_emp values(null,?,?,?)";
    int result = jdbcTemplate.update(sql, "张三", 23, "男");
    
    // 修改数据
    // String sql = "update t_emp set name=? where id=?";
    // int result = jdbcTemplate.update(sql, "张三atguigu", 1);

    // 删除数据
    // String sql = "delete from t_emp where id=?";
    // int result = jdbcTemplate.update(sql, 1);
}
  1. 查询返回单个对象

当查询结果期望返回单条记录时,可以使用 queryForObject() 方法。这里有两种常用方式:

  • 方式一:使用 RowMapper 接口(手动映射)
    通过实现 RowMapper 接口,手动将 ResultSet 的每一行映射到对象。
  • 方式二:使用 BeanPropertyRowMapper(自动映射)
    BeanPropertyRowMapper 会自动将数据库列名与 JavaBean 属性名进行匹配,要求列名与属性名一致或遵循命名约定。
// 方法一:手动映射(理解原理)
@Test
public void testSelectObject1() {
    // 1. 写 SQL
    String sql = "SELECT * FROM t_emp WHERE id=?";
    
    // 2. 调用 queryForObject() 方法,使用 Lambda 表达式简化
    Emp emp = jdbcTemplate.queryForObject(sql,
        // 参数2:Lambda 表达式(替代 RowMapper 匿名内部类)
        // (参数1, 参数2) -> {方法体}
        (rs, rowNum) -> {
            // 这个方法会被自动调用
            // 参数 rs:查询结果集
            // 参数 rowNum:当前行号
            
            // 3. 创建实体对象
            Emp emp = new Emp();
            
            // 4. 从结果集取出数据,设置到对象中
            // rs.getXXX("字段名"):获取指定字段的值
            emp.setId(rs.getInt("id"));
            emp.setName(rs.getString("name"));
            emp.setAge(rs.getInt("age"));
            emp.setSex(rs.getString("sex"));
            
            return emp;
        },
        // 参数3:SQL 问号对应的值
        1);
    
    System.out.println(emp);
}

// 方法二:自动映射(实际使用)
@Test
public void testSelectObject2() {
    // 1. 写 SQL
    String sql = "SELECT * FROM t_emp WHERE id=?";
    
    // 2. 使用 BeanPropertyRowMapper
    // 它自动将数据库字段映射到 JavaBean 属性
    // 要求:数据库字段名和 JavaBean 属性名一致
    Emp emp = jdbcTemplate.queryForObject(sql,
        new BeanPropertyRowMapper<>(Emp.class), 1);
    
    System.out.println(emp);
}
  1. 查询返回多条记录的集合

当查询结果可能包含多条记录时,使用 query() 方法返回 List 集合。

@Test
public void testSelectList() {
    // 1. 写 SQL(没有条件)
    String sql = "SELECT * FROM t_emp";
    
    // 2. 调用 query() 方法(不是 queryForObject)
    // query() 返回 List
    List<Emp> list = jdbcTemplate.query(sql, 
        new BeanPropertyRowMapper<>(Emp.class));
    
    System.out.println(list);
}

query() 和 queryForObject() 的区别:

  • queryForObject():期望返回一条记录
  • query():可能返回多条记录
  1. 查询返回单个值

对于返回单行单列的查询(如 COUNT、MAX、MIN 等聚合函数),可以直接指定返回类型。

@Test
public void selectCount() {
    // 1. 写 SQL(聚合函数)
    String sql = "SELECT COUNT(id) FROM t_emp";
    
    // 2. 调用 queryForObject(),指定返回类型
    // 第二个参数:Class 类型,表示期望返回的数据类型
    Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
    
    System.out.println("总记录数:" + count);
}

适用场景:

  • COUNT():统计数量
  • MAX():求最大值
  • MIN():求最小值
  • SUM():求和
  • AVG():求平均值

数据校验:Validation

Spring Validation概述

在实际项目开发中,数据校验是保证系统健壮性和数据完整性的重要环节。无论是用户注册、表单提交还是API接口调用,都需要对传入的参数进行合法性验证。

传统校验方式的问题
传统的数据校验方式通常存在以下问题:

  1. 代码耦合度高:校验逻辑与业务逻辑紧密耦合,难以分离
  2. 代码重复:相同的校验规则需要在多个地方重复编写
  3. 维护困难:校验规则分散在各个业务类中,修改时需要多处调整
  4. 可读性差:校验代码与业务代码混杂,影响代码清晰度

Spring Validation的优势
Spring Validation通过统一的数据校验框架解决了上述问题,主要特点包括:

  1. 关注点分离:将校验逻辑与业务逻辑分离,提高代码的可维护性
  2. 注解驱动:通过声明式注解定义校验规则,代码简洁直观
  3. 标准化:基于JSR-380(Bean Validation 2.0)标准,与Hibernate Validator兼容
  4. 易于扩展:支持自定义校验规则和错误消息国际化

Spring Validation的实现方式
Spring Validation提供了多种数据校验方式,可以根据不同场景选择使用:

  1. Validator接口实现:通过编程方式实现校验逻辑
  2. Bean Validation注解:通过声明式注解定义校验规则
  3. 方法级别校验:对方法参数和返回值进行校验
  4. 自定义校验器:扩展自定义的校验规则

Validator接口实现

这是Spring Validation中最基础、最传统的一种校验方式。通过实现Spring提供的Validator接口,开发者可以完全控制校验逻辑,适合复杂或特殊的校验需求。

项目搭建步骤

第一步:创建子模块 spring6-validator

在实际开发中,我们通常会将不同功能的模块分开管理。创建一个独立的子模块可以更好地组织代码结构,方便后续的维护和扩展。

创建步骤:

  1. 在父工程下新建Maven模块
  2. 模块名称为spring6-validator
  3. 确保模块继承了父工程的配置
image-20221206221002615.png

第二步:引入相关依赖

为了使用Spring Validation功能,需要在pom.xml中添加必要的依赖:

<dependencies>
    <!-- Hibernate Validator:实现了Bean Validation规范的校验框架 -->
    <dependency>
        <groupId>org.hibernate.validator</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>7.0.5.Final</version>
    </dependency>

    <!-- Jakarta EL:用于解析验证注解中的表达式 -->
    <dependency>
        <groupId>org.glassfish</groupId>
        <artifactId>jakarta.el</artifactId>
        <version>4.0.1</version>
    </dependency>
</dependencies>

实现步骤详解

第三步:创建实体类

首先创建一个需要被校验的实体类,定义需要校验的属性和对应的getter/setter方法。

package com.atguigu.spring6.validation.method1;

public class Person {
    private String name;
    private int age;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

第四步:实现Validator接口

创建校验器类,实现org.springframework.validation.Validator接口,并重写两个核心方法。

package com.atguigu.spring6.validation.method1;

public class PersonValidator implements Validator {

    // 1. supports方法:指定此校验器支持的校验类型
    @Override
    public boolean supports(Class<?> clazz) {
        return Person.class.equals(clazz);   // 告诉系统该校验器只能检查Person类型
    }

    // 2. validate方法:编写具体的校验逻辑
    @Override
    public void validate(Object object, Errors errors) {
        // 使用ValidationUtils简化非空校验
        ValidationUtils.rejectIfEmpty(errors, "name", "name.empty");
        
        // 手动编写复杂校验逻辑
        Person p = (Person) object;  // 将传入的Object类型对象转换为Person类型
        if (p.getAge() < 0) {
            errors.rejectValue("age", "error value < 0");
        } else if (p.getAge() > 110) {
            errors.rejectValue("age", "error value too old");
        }
    }
}

核心方法解析:

  1. supports方法
  • 作用:设置当前校验器仅支持的类型,用于判断某一类型是否能使用当前校验器
  • 设置方式:return 支持检查的类名.class.equals(clazz);
  • 返回值为true:支持对该类型进行校验
  • 返回值为false:不支持对该类型进行校验
  1. validate方法
  • object参数:需要校验的目标对象
  • errors参数:用于存储校验结果的容器
  • 关于强制类型转换,传进validate方法的对象只能是Object类型,必须将传入的Object类型对象强制转换回校验器支持的类型(即原类型),以便后续能调用原类型特有的方法
  • 使用ValidationUtils.rejectIfEmpty:快速实现非空校验
  • 使用errors.rejectValue:将校验错误信息存入Errors对象

第五步:使用校验器进行测试

创建测试类,通过DataBinder将校验器与目标对象绑定,执行校验并获取结果。

package com.atguigu.spring6.validation.method1;

import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;

public class TestMethod1 {

    public static void main(String[] args) {
        // 创建待校验的Person对象
        Person person = new Person();
        person.setName("lucy");
        person.setAge(-1);  // 设置非法年龄值
        
        // 创建DataBinder,将Person对象绑定
        DataBinder binder = new DataBinder(person);

        // 设置校验器
        binder.setValidator(new PersonValidator());

        // 执行校验
        binder.validate();

        // 获取校验结果
        BindingResult results = binder.getBindingResult();
        System.out.println(results.getAllErrors());
    }
}

实际运用中,将案例中的Person及其对象换成其他类型及其对象。

Bean Validation注解实现

Bean Validation是JavaEE/Jakarta EE的标准规范,通过声明式注解的方式定义数据校验规则。Spring对Bean Validation进行了集成和封装,使得在Spring应用中使用注解校验变得更加简便。

工作原理

Spring通过LocalValidatorFactoryBean将Bean Validation的实现(如Hibernate Validator)集成到Spring容器中。这个类同时实现了Jakarta Validation的Validator接口和Spring的Validator接口,因此可以支持两种不同的校验方式。

实现步骤详解

第一步:创建配置类,配置LocalValidatorFactoryBean

创建Spring配置类,将LocalValidatorFactoryBean注册到容器中。这是连接Spring和Bean Validation的桥梁。

@Configuration  // 声明这是一个Spring配置类
@ComponentScan("com.atguigu.spring6.validation.method2")  // 扫描指定包下的组件
public class ValidationConfig {

    @Bean  // 将LocalValidatorFactoryBean实例注册为Spring Bean
    public LocalValidatorFactoryBean validator() {
        return new LocalValidatorFactoryBean();
    }
}

第二步:创建实体类,使用注解定义校验规则

在实体类的属性上使用Bean Validation注解声明校验规则,实现声明式校验。

  • 在需要校验的字段上直接添加注解
package com.atguigu.spring6.validation.method2;

public class User {

    @NotNull  // 校验规则:不能为null
    private String name;

    @Min(0)   // 校验规则:最小值0
    @Max(120) // 校验规则:最大值120
    private int age;

    // Getter和Setter方法
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

Bean Validation常用注解说明

注解作用适用类型
@NotNull限制必须不为null任意类型
@NotEmpty字符串不为空且长度不为0字符串、集合、数组
@NotBlank字符串不为空且trim()后不为空串字符串
@DecimalMax(value)限制必须不大于指定值的数字数字类型
@DecimalMin(value)限制必须不小于指定值的数字数字类型
@Max(value)限制必须不大于指定值的数字数字类型
@Min(value)限制必须不小于指定值的数字数字类型
@Pattern(value)限制必须符合指定的正则表达式字符串
@Size(max,min)限制字符长度在min到max之间字符串、集合、数组
@Email验证元素值是Email格式字符串

第三步:实现校验逻辑的两种方式

方式一:使用jakarta.validation.Validator(标准Bean Validation方式)
这种方式使用标准的Jakarta Validation API,与Spring解耦,适合需要在非Spring环境下复用的场景。

package com.atguigu.spring6.validation.method2;

@Service
public class MyService1 {

    @Autowired
    private jakarta.validation.Validator jakartaValidator;  // 标准API,注入校验器

    public boolean validator(User user){
        Set<ConstraintViolation<User>> sets = jakartaValidator.validate(user);
        return sets.isEmpty();
    }
}

技术要点:

  • 注入jakarta.validation.Validator接口
  • 执行校验,返回违规结果集:Set<ConstraintViolation<被校验的类名>> sets = validator.validate(被校验的对象);
  • isEmpty()方法:当对象内容为空时返回 true,反之返回 false。用于判断校验是否通过,如果没有违规,则对象内容为空,返回true

方式二:使用org.springframework.validation.Validator(Spring集成方式,更常用)
这种方式使用Spring封装的Validator接口,与Spring框架深度集成,适合在Spring MVC等场景中使用。

package com.atguigu.spring6.validation.method2;

@Service
public class MyService2 {

    @Autowired
    private org.springframework.validation.Validator springValidator;  // Spring API,自动注入Spring提供的校验器

    public boolean validaPersonByValidator(User user) {
        // 创建BindException对象,用于存储校验错误
        BindException bindException = new BindException(user, user.getName());
        
        // 执行校验
        springValidator.validate(user, bindException);
        
        // 返回是否有错误
        return bindException.hasErrors();
    }
}

技术要点:

  • 注入org.springframework.validation.Validator接口
  • 创建BindException对象,用于存储校验错误
    • 第一个参数:被校验的对象(这里是user
    • 第二个参数:对象的标识符(这里用user.getName(),名称为null有风险)
  • validate()方法需要传入被校验的对象BindException对象
  • BindException对象.hasErrors()方法:用于判断校验是否通过
    • 至少有一个违规,校验失败,返回true
    • 所有规则都符合,校验通过,返回false

第四步:编写测试类验证功能

创建测试类,验证两种校验方式的效果。

写法:

  1. 使用@Test注解标记测试方法
  2. 创建Spring容器:new AnnotationConfigApplicationContext(配置类.class)
  3. 从容器获取服务对象Bean:context.getBean(校验逻辑的服务类名.class)
  4. 创建测试对象并设置属性值
  5. 调用校验方法:boolean validator = 服务对象.validator(测试对象);
package com.atguigu.spring6.validation.method2;

public class TestMethod2 {

    @Test  // 标准Bean Validation方式
    public void testMyService1() {
        // 1. 创建Spring容器
        ApplicationContext context = new AnnotationConfigApplicationContext(ValidationConfig.class);
        
        // 2. 从容器中获取服务对象
        MyService1 myService = context.getBean(MyService1.class);
        
        // 3. 创建测试数据(设置非法年龄)
        User user = new User();
        user.setAge(-1);
        
        // 4. 调用校验方法,保持与MyService1的校验方法相同
        boolean validator = myService.validator(user);
        
        // 5. 输出结果(输出为true,表示有错误)
        System.out.println(validator);
    }

    @Test  // Spring集成方式
    public void testMyService2() {
        ApplicationContext context = new AnnotationConfigApplicationContext(ValidationConfig.class);
        MyService2 myService = context.getBean(MyService2.class);
        
        // 创建测试数据(姓名不为空,但年龄超出范围)
        User user = new User();
        user.setName("lucy");
        user.setAge(130);  // 超出@Max(120)的限制
        
        // 调用校验方法,保持与MyService2的校验方法相同
        boolean validator = myService.validaPersonByValidator(user);
        System.out.println(validator);  // 输出为true,表示有错误
    }
}

基于方法实现校验

这种方式与前面最大的不同是:校验发生在方法被调用时,有异常则自动抛出异常,而非在方法内部手动调用校验器。Spring会自动拦截方法调用,对参数进行校验,非常方便!

实现步骤详解

第一步:创建配置类,配置MethodValidationPostProcessor

这是基于方法校验的核心配置,比之前多了一个MethodValidationPostProcessor

@Configuration
@ComponentScan("com.atguigu.spring6.validation.method3")
public class ValidationConfig {

    // 显式声明Validator,避免因Bean命名问题导致依赖注入失败
    @Bean
    public LocalValidatorFactoryBean validator() {
        return new LocalValidatorFactoryBean();
    }
    
    // 必须配置:方法校验处理器
    @Bean
    public MethodValidationPostProcessor validationPostProcessor() {
        // 注意:Spring会自动将上面的validator注入到这个处理器中
        return new MethodValidationPostProcessor();
    }
}

MethodValidationPostProcessor校验器原理

  • 在Spring容器启动时,这个后置处理器会对带有@Validated注解的Bean进行增强,为这些Bean创建代理,拦截方法调用,在执行方法前进行参数校验
  • 类似于AOP(面向切面编程)的思想,但专门用于校验

第二步:创建实体类,使用注解设置校验规则

  1. 在字段上直接添加需要的校验注解
  2. 注意注解的包:jakarta.validation.constraints.*(或javax.validation.constraints.*
  3. 可以为注解指定message属性来自定义错误消息
  4. 多个注解可以同时用在同一个字段上(如手机号既有格式要求又不能为空)
package com.atguigu.spring6.validation.method3;

public class User {

    @NotNull  // 基本注解:不能为null
    private String name;

    @Min(0)   // 最小值限制
    @Max(120) // 最大值限制
    private int age;

    @Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$", message = "手机号码格式错误")
    @NotBlank(message = "手机号码不能为空")  // 注意:NotBlank包含非空和非空字符串检查
    private String phone;

    // getter和setter方法...
}

新的校验注解介绍:

  1. @Pattern:正则表达式校验

    • regexp:正则表达式字符串
    • message:自定义错误消息
    • 示例中的正则:手机号以1开头,第二位是3、4、5、7、8之一,后面9位数字
  2. @NotBlank:比@NotNull更严格

    • 不能为null
    • 不能为空字符串""
    • 不能全是空白字符(如空格、制表符)

第三步:定义Service类,通过注解操作对象

  1. 类上添加@Service注解(让Spring管理)
  2. 类上添加@Validated注解(启用方法校验)
  3. 在方法参数前添加校验注解
  4. 如果参数是对象且需要校验其内部字段,必须加@Valid
package com.atguigu.spring6.validation.method3;

@Service
@Validated  // 关键注解:告诉Spring这个类的方法需要被校验
public class MyService {
    
    public String testParams(@NotNull @Valid User user) {  // 校验规则方法
        return user.toString();
    }
}

关键点解析:

  1. @Validated注解
  • 必须加在类上(这里是@Service类)
  • 告诉Spring:"请对我的方法参数进行校验"
  • Spring会为此类创建代理,拦截方法调用
  1. 方法参数上的注解
  • @NotNull:确保传入的user参数不为null
  • @Valid:对user对象本身进行校验(检查其字段上的注解)
  • 这两个注解可以组合使用

常见的参数校验注解:

public void exampleMethod(
    @NotNull String name,           // 不能为null
    @Size(min=2, max=10) String code, // 长度限制
    @Min(18) int age,               // 最小值
    @Max(100) Integer score,        // 最大值
    @Email String email,           // 邮箱格式
    @Pattern(regexp="\\d+") String number, // 正则匹配
    @Valid User user               // 校验对象内部字段
) {
    // 方法体
}

第四步:测试

package com.atguigu.spring6.validation.method3;

public class TestMethod3 {

    @Test
    public void testMyService1() {
        ApplicationContext context = new AnnotationConfigApplicationContext(ValidationConfig.class);
        MyService myService = context.getBean(MyService.class);
        
        User user = new User();
        user.setAge(-1);  // 设置非法年龄
        
        myService.testParams(user);  // 这里会抛出异常,且没有处理异常!
    }
}

注意

只有从外部调用校验规则方法才会被代理并触发校验,类内部调用不会触发校验。

  • 如果调用是通过Spring容器获取Bean后调用其方法(即,通过@Autowired注入的Bean执行的) → 外部调用 → 触发校验
  • 如果调用是在同一个Bean内部一个方法调用另一个方法(通过this.或直接方法名执行的) → 内部调用 → 不触发校验

实现自定义校验

实现步骤详解

第一步:自定义校验注解

package com.atguigu.spring6.validation.method4;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

// 自定义注解-CannotBlank:用于校验字符串中不能包含空格
// 使用@Constraint注解指定校验器为CannotBlankValidator
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})  // 指定注解可以应用的目标元素类型:方法、字段、注解、构造器、参数等。
@Retention(RetentionPolicy.RUNTIME)  // 表示注解在运行时可以通过反射获取。
@Documented
@Constraint(validatedBy = {CannotBlankValidator.class})  // 指定校验器类为CannotBlankValidator.class
public @interface CannotBlank {
    // 默认错误消息,当校验失败时,如果未指定消息,则使用此消息
    String message() default "不能包含空格";

    // 分组,用于区分不同场景下的校验
    Class<?>[] groups() default {};

    // 负载,用于携带元数据,比如严重程度等
    Class<? extends Payload>[] payload() default {};

    // 用于在同一元素上重复使用@CannotBlank注解
    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {
        CannotBlank[] value();  // 重复的注解名[] value();
    }
}

注解说明:

  1. @Target:指定注解可以应用的目标元素类型。
  2. @Retention:指定注解的保留策略。
  3. @Documented:表示该注解应该被javadoc工具记录。
  4. @Constraint:表示这是一个校验注解,并通过validatedBy属性指定对应的校验器类。
  5. public @interface 自定义注解名 {}:创建一个新的注解标签,名为自定义注解名
  6. 注解中的三个方法:messagegroupspayload是校验注解必须的三个方法,含义如下:
  • 数据类型 message() default:用于提供默认的错误消息。
  • groups:用于分组校验,可以将注解分为不同的组,在不同的场景下使用不同的组。
  • payload:用于传递元数据,比如可以定义严重程度。
  1. 内部注解List:这是为了支持在同一元素上重复使用该注解。当需要多个相同的注解时,可以用@CannotBlank.List来包含多个@CannotBlank注解。

第二步:编写真正的校验类

package com.atguigu.spring6.validation.method4;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
// 校验器实现类:实现ConstraintValidator接口,泛型中第一个参数为对应的注解,第二个为校验的数据类型
public class CannotBlankValidator implements ConstraintValidator<CannotBlank, String> {

        // 初始化方法,可以在校验开始前获取注解中的属性
        @Override
        public void initialize(CannotBlank constraintAnnotation) {
                // 这里可以获取注解中的属性,如果有需要可以保存起来供后续使用
        }

        // 校验逻辑,返回true表示校验通过,false表示失败
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
                // 如果值为null,我们不进行校验,因为null校验可以通过@NotNull等注解完成
                // 这里我们只校验非null且包含空格的情况
                if (value != null && value.contains(" ")) {
                        // 获取默认的提示信息(即注解中message的默认值)
                        String defaultConstraintMessageTemplate = context.getDefaultConstraintMessageTemplate();
                        System.out.println("default message :" + defaultConstraintMessageTemplate);
                        // 禁用默认的提示信息,因为我们想要自定义提示信息
                        context.disableDefaultConstraintViolation();
                        // 构建新的提示信息,并添加到上下文
                        context.buildConstraintViolationWithTemplate("can not contains blank").addConstraintViolation();
                        return false;
                }
                return true;
        }
}

校验器类说明:

  1. 校验器类必须实现ConstraintValidator<A, T>接口,其中A是要处理的注解类型,T是要校验的数据类型。这里我们校验的是字符串,所以是String

  2. initialize方法:在校验器被初始化时调用,可以获取注解中的属性值。如果不需要,可以不重写(默认空实现)。
    示例

    1. 给注解添加自定义属性
      // 修改CannotBlank注解,添加两个自定义属性
      public @interface CannotBlank {
          String message() default "不能包含空格";
          Class<?>[] groups() default {};
          Class<? extends Payload>[] payload() default {};
      
          // ========== 新增的两个属性 ==========
          int maxLength() default 100;      // 最大长度限制
          boolean allowChinese() default true; // 是否允许中文
          // ================================
      }
      
    2. 在校验器中获取并使用这些属性
      public class CannotBlankValidator implements ConstraintValidator<CannotBlank, String> {
      
          // 定义两个变量,用来"保存"注解中的属性值
          private int maxLength;
          private boolean allowChinese;
      
          @Override
          public void initialize(CannotBlank constraintAnnotation) {
              // 这里获取注解中的属性值并保存到变量中
              this.maxLength = constraintAnnotation.maxLength();  // 获取maxLength属性的值
              this.allowChinese = constraintAnnotation.allowChinese(); // 获取allowChinese属性的值
      
              System.out.println("初始化校验器:maxLength=" + maxLength + ", allowChinese=" + allowChinese);
          }
      
  3. isValid方法:实际的校验逻辑。参数value是要校验的值,context是校验上下文,可以用来构建错误消息。

    • 注意:当valuenull时,我们选择不校验,返回true。这是因为null值应该由@NotNull等注解来处理,避免重复报错。
    • 当值不为null且包含空格时,我们进行如下操作:
      a. 获取默认的错误消息(即注解中message的默认值)并打印。
      b. 禁用默认的错误消息,因为我们想要自定义错误消息。
      c. 使用context.buildConstraintViolationWithTemplate("自定义消息")来创建新的错误消息,并添加到上下文中。
      d. 返回false表示校验失败。
    • 如果校验通过,返回true

编写自定义校验器的模板:

public class 校验器类名 implements ConstraintValidator<对应的注解, 校验的数据类型> {

    // 可选:注解中的属性,用于保存初始化时的注解信息
    // private 类型 属性名;

    @Override
    public void initialize(对应的注解 constraintAnnotation) {
        // 如果需要,可以从注解中获取属性值并保存
        // this.属性名 = constraintAnnotation.属性名();
    }

    @Override
    public boolean isValid(校验的数据类型 value, ConstraintValidatorContext context) {
        // 1. 如果值不为null(或者根据需求,null值是否校验),进行校验
        // 2. 校验逻辑,根据业务要求判断是否通过
        // 3. 如果校验失败,可以自定义错误消息
        //    context.disableDefaultConstraintViolation();
        //    context.buildConstraintViolationWithTemplate("自定义消息").addConstraintViolation();
        // 4. 返回校验结果:true通过,false失败
    }
}

注意事项:

  1. 自定义注解和校验器需要配合使用,注解通过@Constraint指定校验器。
  2. 校验器类需要被Spring管理(如果是Spring项目,确保能被扫描到或者通过配置注册)。
  3. 在使用自定义注解时,可以像使用内置注解一样,在需要校验的字段或参数上添加即可。

资源操作:Resources

Spring Resources概述

在Java中,我们通常使用java.net.URL类来定位和访问资源。然而,URL类在处理一些常见的资源路径时存在局限性,例如:

  • 访问类路径(classpath)下的资源。
  • 访问相对于ServletContext的资源。

此外,URL类也缺少一些实际开发中需要的功能,比如检查资源是否存在、无法处理相对路径到绝对路径的转换、缺少对文件系统资源的细粒度控制等。

为了弥补这些不足,Spring框架提供了Resource接口。Resource接口是Spring用于访问低级(low-level)资源的核心接口,它提供了比URL更强大的资源访问能力。

通过Resource接口,Spring能够以统一的方式访问各种资源,包括文件系统资源、类路径资源、URL资源等。这使得在Spring应用中处理资源变得更加方便和灵活。

在接下来的章节中,我们将详细介绍Resource接口及其使用。

Resource接口

Spring的Resource接口位于org.springframework.core.io包中,它是Spring框架资源抽象的核心。这个接口的设计目的是统一所有资源的访问方式,无论资源来自文件系统、类路径、网络还是其他位置。

Resource接口核心方法详解

  1. exists() - 检查资源是否物理存在,存在返回true。
boolean exists();
  • 用于避免对不存在资源的操作
  • 对于某些资源类型(如ClassPathResource),exists()只检查类加载器是否能找到资源,不验证资源内容是否损坏

使用场景:

Resource resource = new ClassPathResource("config.properties");
if (resource.exists()) {
    // 安全地处理资源
    InputStream is = resource.getInputStream();
} else {
    // 处理资源不存在的情况
    System.out.println("配置文件不存在");
}

注意事项:

  • 资源存在不一定可读
  • 对于网络资源,可能会有延迟或网络问题导致误判
  • 某些资源(如动态生成的)可能永远返回true
  1. isReadable() - 检查对资源是否有读取权限
boolean isReadable();
  • 用于检查资源是否可读(是否有读取权限),但与文件是否被锁定无关(被锁定时不可读)

使用场景:

Resource resource = new FileSystemResource("/var/log/app.log");
if (resource.isReadable()) {
    // 读取日志文件操作
} else {
    System.out.println("没有读取权限或文件被锁定");
}
  1. isOpen() - 检查资源本身是否是打开的
boolean isOpen();
  • 所有非流资源(FileSystemResource、ClassPathResource、UrlResource等大多数资源)都返回 false
  • 只有InputStreamResource返回 true
  • 返回true的资源只能读取一次,需要及时关闭
  • 用于防止资源重复读取
  1. isFile() - 检查资源是否能表示为文件系统路径(即是否是文件系统文件)
boolean isFile();
  • 如果是,就能转换为File对象,用于为后续操作做准备
Resource resource = // 获取资源
if (resource.isFile()) {
    File file = resource.getFile();  // 安全转换
    // 使用File API操作
} else {
    // 使用流方式操作
    InputStream is = resource.getInputStream();
}
  1. getInputStream() - 获取资源输入流
InputStream getInputStream() throws IOException;
  • 这是最核心的方法,用于实际读取资源内容。
  • 如果资源不存在,抛出FileNotFoundException,不是返回null
// 正确用法:使用try-with-resources确保关闭
try (InputStream is = resource.getInputStream()) {
    // 读取和处理数据
    byte[] buffer = new byte[1024];
    int bytesRead;
    while ((bytesRead = is.read(buffer)) != -1) {
        // 处理数据
    }
} catch (IOException e) {
    // 处理异常
}

// ⚠️ 注意:每次调用都返回新的InputStream
InputStream is1 = resource.getInputStream();
InputStream is2 = resource.getInputStream();  // 这是另一个流
  1. getURL()getURI() - 获取资源定位符
URL getURL() throws IOException;
URI getURI() throws IOException;

区别与用途:

  • getURL():获取标准的URL对象
  • getURI():获取更通用的URI对象(支持更多协议)
Resource resource = new UrlResource("https://example.com/data");
URL url = resource.getURL();  // 获取URL
URI uri = resource.getURI();   // 获取URI

// 使用示例
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
  1. getFile() - 获取File对象
File getFile() throws IOException;
  • 不是所有资源都能转换为File
  • 即使isFile()返回true,也不保证getFile()一定成功(可能因权限问题失败)
  • 如果无法转换,会抛出FileNotFoundException
try {
    File file = resource.getFile();
    // 使用File API
    long size = file.length();
    Date lastModified = new Date(file.lastModified());
} catch (FileNotFoundException e) {
    // 处理不能转换的情况
    System.out.println("此资源不是文件系统文件");
}
  1. readableChannel() - 获取NIO通道
ReadableByteChannel readableChannel() throws IOException;
  • 用于使用NIO方式读取资源
  • 对于文件系统资源,返回FileChannel
  • 对于其他资源,通常包装InputStreamReadableByteChannel
  • 适合大文件读取,性能更好
try (ReadableByteChannel channel = resource.readableChannel();
     FileChannel fileChannel = (channel instanceof FileChannel) ? (FileChannel) channel : null) {
    
    if (fileChannel != null) {
        // 使用FileChannel的高级功能
        fileChannel.lock(0, Long.MAX_VALUE, true);  // 共享锁
    }
    
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    while (channel.read(buffer) != -1) {
        buffer.flip();
        // 处理数据
        buffer.clear();
    }
}
  1. contentLength() - 获取资源内容的字节数
long contentLength() throws IOException;
  • 用于进度显示、内存分配
  • 对于某些资源,长度未知,可能返回-1
long size = resource.contentLength();
System.out.println("文件大小: " + size + " 字节");

// 根据大小分配缓冲区
byte[] buffer = new byte[(int) Math.min(size, 8192)];
  1. lastModified() - 获取资源最后修改的时间戳
long lastModified() throws IOException;
  • 用于缓存控制、版本管理
  • 如果不能确定修改时间,返回0(而不是抛出异常)
long lastModified = resource.lastModified();
Date modifyDate = new Date(lastModified);
System.out.println("最后修改时间: " + modifyDate);

// 缓存检查
if (lastModified > lastCacheTime) {
    // 资源已更新,重新加载
}
  1. createRelative() - 基于当前资源创建相对路径的资源
Resource createRelative(String relativePath) throws IOException;
  • 用于避免路径拼接错误,非常实用
Resource baseResource = new ClassPathResource("config/");
Resource appConfig = baseResource.createRelative("application.properties");
Resource dbConfig = baseResource.createRelative("database.properties");

// 相当于:config/application.properties 和 config/database.properties
  1. getFilename() - 获取资源的文件名部分(不含路径)
String getFilename();
  • 用于显示、日志、保存操作
Resource resource = new FileSystemResource("/home/user/documents/report.pdf");
String filename = resource.getFilename();  // "report.pdf"

// 根据扩展名处理
if (filename.endsWith(".pdf")) {
    // PDF处理逻辑
}
  1. getDescription() - 获取资源的描述信息(通常是完整路径)
String getDescription();
  • 用于调试和错误日志
try {
    resource.getInputStream();
} catch (IOException e) {
    // 在错误信息中包含资源描述
    logger.error("无法读取资源: " + resource.getDescription(), e);
}

常见问题解答

Q:什么时候需要自己实现Resource接口?
A:当你的资源不在标准位置(文件系统、类路径、网络),比如存储在数据库、Redis、云存储中时。

Q:Resource和InputStream有什么区别?
A:Resource是更高级的抽象,包含了资源的元数据(是否存在、文件名等),而InputStream只关注数据流。

Q:如何处理不能转换为File的资源?
A:使用getInputStream()readableChannel()方法,这些方法适用于所有类型的资源。

Resource的实现类

Resource接口是Spring资源访问策略的抽象,具体的资源访问由它的实现类完成。每个实现类代表一种资源访问策略。常见的实现类有:

  • UrlResource:访问网络资源的实现类。
  • ClassPathResource:访问类路径下资源的实现类。
  • FileSystemResource:访问文件系统资源的实现类。
  • ServletContextResource:访问Web应用上下文资源的实现类(用于Web应用)。
  • InputStreamResource:访问输入流资源的实现类。
  • ByteArrayResource:访问字节数组资源的实现类。

UrlResource访问网络资源

UrlResource是Spring Resource接口的一个重要实现类,专门用于访问各种基于URL协议的资源。它像一个"万能资源访问器",能够处理多种不同类型的远程或本地资源。

URL协议支持

  • http/https - 访问网页、API接口等网络资源
  • ftp - 访问FTP服务器上的文件
  • file - 访问本地文件系统
  • jar - 访问JAR包内的文件
  • 以及其他标准URL协议

实验:访问基于HTTP协议的网络资源

创建一个maven子模块spring6-resources,配置Spring依赖

image-20221207102315185.png

下面是提供的示例代码,我们先添加注释,再详细解析:

package com.atguigu.spring6.resources;

import org.springframework.core.io.UrlResource;

public class UrlResourceDemo {

    /**
     * 加载并读取URL资源
     * @param path 资源路径,可以是http://、ftp://、file://等URL格式
     */
    public static void loadAndReadUrlResource(String path) {
        // 创建一个 Resource 对象
        UrlResource url = null;
        try {
            // 1.创建UrlResource实例
            // 这里根据传入的path字符串自动识别协议类型
            url = new UrlResource(path);
            
            // 2.获取资源名称
            // 对于网络资源,通常是URL的最后一部分
            System.out.println(url.getFilename());
            
            // 3.获取资源的URI
            // 统一资源标识符,比URL更通用的标识方式
            System.out.println(url.getURI());
            
            // 4.获取资源描述
            // 通常包含完整的URL路径,用于日志和错误信息
            System.out.println(url.getDescription());
            
            // 5.获取资源内容(读取第一个字节)
            // getInputStream()是核心方法,打开连接获取数据流
            System.out.println(url.getInputStream().read());
            
        } catch (Exception e) {
            // 6.异常处理
            // URL资源访问可能抛出多种异常:网络异常、文件不存在等
            throw new RuntimeException(e);
        }
    }
    
    public static void main(String[] args) {
        // 实验一:访问网络资源(百度首页)
        // 注意:实际运行可能因为网络或权限问题失败
        loadAndReadUrlResource("http://www.baidu.com");
    }
}

三种创建UrlResource的方式

// 方式1:直接使用URL字符串
UrlResource resource1 = new UrlResource("http://example.com/file.txt");

// 方式2:使用java.net.URL对象
URL url = new URL("http://example.com/file.txt");
UrlResource resource2 = new UrlResource(url);

// 方式3:使用URI对象(更通用)
URI uri = new URI("http://example.com/file.txt");
UrlResource resource3 = new UrlResource(uri);

实验二:使用UrlResource来访问本地文件系统资源(在项目根路径下创建文件)

UrlResource主要设计用于网络资源,但通过file:协议前缀,也能很好地处理本地文件。

public static void main(String[] args) {
    // 1 访问网络资源(注释掉了)
    // loadAndReadUrlResource("http://www.atguigu.com");
    
    // 2 访问文件系统资源
    loadAndReadUrlResource("file:atguigu.txt");
}

关键点解析

  1. URL协议前缀的使用
    // 注意这里的"file:"前缀
    loadAndReadUrlResource("file:atguigu.txt");
    
    // 对比其他形式:
    // "file:atguigu.txt"           - 相对路径(相对于当前工作目录)
    // "file:///C:/atguigu.txt"      - Windows绝对路径
    // "file:///home/user/atguigu.txt" - Linux/Mac绝对路径
    
  2. 为什么使用UrlResource而不是FileSystemResource?
  • 统一访问方式:无论资源在哪里,都用相同API
  • 代码复用:可以使用同一段代码处理多种资源
  • 配置灵活:通过改变URL前缀切换资源位置

ClassPathResource 访问类路径资源

类路径就是Java程序在运行时查找.class文件和资源文件(如配置文件)的所有位置集合。

类路径包含:

  1. 项目编译输出目录(通常是target/classesbin
  2. 依赖的JAR包lib目录或Maven依赖)
  3. Web应用的WEB-INF/classes目录

为什么需要ClassPathResource?

问题场景:

假设你有一个配置文件config.properties,在开发、测试、生产环境中,它的位置可能不同:

  • 开发时:在项目的src/main/resources/目录下
  • 打包后:在JAR包的根目录下
  • 部署时:在WEB-INF/classes目录下

传统方式的局限:

// 方式1:使用绝对路径(不可移植)
File file = new File("C:/project/config.properties");

// 方式2:使用相对路径(依赖当前工作目录)
File file = new File("./config.properties");

// 问题:程序换个地方部署就找不到文件了!

ClassPathResource的解决方案

// 无论文件在哪里,只要在类路径中就能找到
ClassPathResource resource = new ClassPathResource("类路径下的资源文件路径");
InputStream is = resource.getInputStream(); // 总能找到

实验:在类路径下创建文件atguigu.txt,使用ClassPathResource 访问

package com.atguigu.spring6.resources;

import org.springframework.core.io.ClassPathResource;
import java.io.InputStream;

public class ClassPathResourceDemo {

    /**
     * 加载并读取类路径资源
     * @param path 类路径下的资源路径,相对于类路径根目录的路径
     * @throws Exception 可能抛出IO异常等
     */
    public static void loadAndReadUrlResource(String path) throws Exception {
        // 1.创建ClassPathResource对象
        // 参数path同上
        ClassPathResource resource = new ClassPathResource(path);
        
        // 2.获取文件名
        System.out.println("resource.getFileName = " + resource.getFilename());
        
        // 3.获取资源描述
        // 返回资源的描述信息,通常包含完整路径,用于调试和日志
        System.out.println("resource.getDescription = "+ resource.getDescription());
        
        // 4.获取文件内容流
        // 这是核心方法,通过类加载器获取资源的输入流
        // 注意:这个流需要手动关闭(这里代码没有关闭,实际使用时应该关闭)
        InputStream in = resource.getInputStream();
        
        // 5.读取文件内容
        // 这里使用字节数组读取,适用于文本和二进制文件
        byte[] b = new byte[1024];
        int bytesRead;
        
        // 循环读取直到文件末尾(read返回-1)
        while((bytesRead = in.read(b)) != -1) {
            // 将字节转换为字符串并打印
            // 注意:这里每次读取后,整个数组都被转换,包括可能的上次读取的剩余数据
            System.out.println(new String(b));
        }
        
        // 问题:这里没有关闭流!实际使用应该用try-with-resources
    }

    public static void main(String[] args) throws Exception {
        // 测试:读取类路径根目录下的atguigu.txt文件
        // 文件位置:src/main/resources/atguigu.txt(Maven项目标准位置)
        loadAndReadUrlResource("atguigu.txt");
    }
}
  • 2.获取文件名中,getFilename()返回资源的文件名部分(不包含路径)
    例如:"atguigu.txt" -> "atguigu.txt";"config/database.properties" -> "database.properties"

代码中的问题和改进

  • 问题1:流未关闭(资源泄漏)
  • 问题2:字符串转换可能包含垃圾数据
// 原始代码的问题
InputStream in = resource.getInputStream();
// ... 使用流
// 没有关闭!可能导致内存泄漏

// 正确做法:使用try-with-resources
// 资源声明在try后面的括号中
try (InputStream in = resource.getInputStream()) {
    byte[] b = new byte[1024];
    int bytesRead;
    while ((bytesRead = in.read(b)) != -1) {
        // 只转换实际读取的部分
        System.out.println(new String(b, 0, bytesRead));
    }
}   // 这里会自动调用in.close()

记住:流读取时,要求只处理实际读取的部分,而不是整个数组!

FileSystemResource 文件系统资源访问详解

FileSystemResource 是 Spring 框架提供的一个类,用于访问文件系统资源(即硬盘上的文件)。

尽管Java已经提供了File类来访问文件系统,但使用FileSystemResource的好处在于它符合Spring的资源抽象,可以和其他Resource实现一样,通过统一的getInputStream()方法获取输入流,而不需要关心资源的具体类型。这样,如果你的应用程序需要支持多种资源类型(如类路径、URL、文件系统),使用Spring的Resource抽象会更加方便。

实验:使用FileSystemResource 访问文件系统资源

package com.atguigu.spring6.resources;

import org.springframework.core.io.FileSystemResource;
import java.io.InputStream;

public class FileSystemResourceDemo {
    public static void loadAndReadFileSystemResource(String path) throws Exception{
        // 1. 创建FileSystemResource对象 - 使用相对路径
        // 相对路径:相对于当前项目的工作目录(不是类路径)
        FileSystemResource resource = new FileSystemResource("atguigu.txt");
        
        // 2. 也可以使用绝对路径(注意Windows路径需要转义,使用双反斜杠 \\ 或正斜杠 /)
        // FileSystemResource resource = new FileSystemResource("C:\\atguigu.txt");
        
        // 3. 获取文件名
        System.out.println("resource.getFileName = " + resource.getFilename());
        
        // 4. 获取资源描述信息(通常是完整路径)
        System.out.println("resource.getDescription = "+ resource.getDescription());
        
        // 5. 获取文件内容的输入流(核心操作)
        InputStream in = resource.getInputStream();
        byte[] b = new byte[1024];
        
        // 6. 读取并输出文件内容
        while(in.read(b)!=-1) {
            System.out.println(new String(b));
        }
    }

    public static void main(String[] args) throws Exception {
        loadAndReadUrlResource("atguigu.txt");
    }
}

ServletContextResource

特点:

  • 用于访问ServletContext资源,即Web应用程序根目录下的资源。
  • 支持流访问和URL访问。
  • 只有Web应用以解压形式部署(资源在文件系统上)时才允许java.io.File访问。
  • 依赖于Servlet容器,无论资源是在文件系统上还是在JAR等中。

适用场景:  在Web应用程序中,需要访问相对于Web应用根目录的资源时使用。

InputStreamResource

特点:

  • 是对一个已打开的输入流(InputStream)的封装。
  • 适用于没有特定Resource实现时,作为通用的Resource使用。
  • 由于它是对已打开流的描述,所以isOpen()返回true,表示流已经打开。
  • 注意:不能多次读取,因为流可能已经消耗。如果需要多次读取,应该使用其他Resource实现(如ByteArrayResource)。

适用场景:  当已经有一个输入流,并且需要将其作为Resource接口来传递时使用。但是要注意,一旦流被读取,就无法再次读取。

ByteArrayResource

特点:

  • 是基于字节数组的Resource实现。
  • 内部通过字节数组创建ByteArrayInputStream,所以可以多次读取。
  • 适用于从字节数组加载内容,且不需要担心流被关闭或只能读取一次的问题。

适用场景:  当资源内容已经存在于字节数组中,或者需要多次读取资源内容时使用。

Resource类图

上述Resource实现类与Resource顶级接口之间的关系可以用下面的UML关系模型来表示

image-20221206232920494.png

ResourceLoader 接口

ResourceLoader 概述

核心概念

ResourceLoader 是 Spring 框架中的一个资源加载器接口,它帮助我们根据资源路径字符串选择适当的Resource实现类,创建合适的 Resource 对象,从而将应用程序从具体的资源访问策略中解耦出来。

因此,ResourceLoader充当了一个资源访问策略的选择器,它根据我们提供的资源位置信息,选择适当的Resource实现类,从而将应用程序从具体的资源访问策略中解耦出来。

接口内唯一方法

Resource getResource(String location)

  • 根据资源位置字符串,返回对应的Resource对象
  • 工作逻辑:
    输入:资源路径字符串(如 "classpath:config.properties")
          ↓
    ResourceLoader分析路径前缀
          ↓
    根据前缀创建对应的Resource对象
          ↓
    输出:Resource实例(如ClassPathResource、FileSystemResource等)
    

使用ResourceLoader的主要目的——解耦

我们在代码中不需要直接实例化具体的Resource类(如ClassPathResource、FileSystemResource等),而是通过ResourceLoader的getResource方法来获取Resource实例
这样,资源访问策略(即具体使用哪种Resource实现类)就由ResourceLoader根据传入的资源位置字符串(可能包含前缀如classpath:、file:等)来决定,或者由ApplicationContext的类型(如果是通过ApplicationContext获取Resource)来决定默认策略。

解耦设计的实际优势——灵活切换资源加载方式
假设我们有一个应用,原来从类路径加载资源,现在需要改为从文件系统加载。如果没有ResourceLoader,我们可能需要修改代码,将 new ClassPathResource(...) 改为 new FileSystemResource(...)
但使用ResourceLoader,我们只需要修改资源路径字符串(加上file:前缀)或者更换ResourceLoader的实现(例如,从ClassPathXmlApplicationContext换为FileSystemXmlApplicationContext),而不需要修改获取资源的代码。

ApplicationContext——ResourceLoader 接口的实现者

所有 ApplicationContext 接口(Spring应用上下文)的实现类都实现了 ResourceLoader 接口。

这意味着:

  • ApplicationContext 本身就是一个 ResourceLoader
  • 可以直接通过 ApplicationContext 获取 Resource 对象
  • 无需额外创建 ResourceLoader

常见的 ApplicationContext 实现

  • ClassPathXmlApplicationContext - 类路径下的XML配置文件上下文
  • FileSystemXmlApplicationContext - 文件系统中的XML配置文件上下文
  • AnnotationConfigApplicationContext - 注解配置的上下文
  • WebApplicationContext - Web应用的上下文

ResourceLoaderAware 接口

ResourceLoaderAware 是一个回调接口,它的实现类的实例能够自动获得一个ResourceLoader的引用

应用场景:假设你有一个自定义的Bean,它需要加载一些资源文件,但你不知道当前应用使用的是哪种ResourceLoader。通过实现ResourceLoaderAware接口,Spring容器会自动把合适的ResourceLoader注入给你,你就不用自己创建或查找了。

getResource(String location)的路径解析规则

  1. 根据路径前缀自动选择Resource类型
路径格式创建的Resource类型示例
classpath:ClassPathResourceclasspath:config.properties
file:FileSystemResourcefile:/home/user/config.xml
http:https:UrlResourcehttp://example.com/data.json
无前缀(默认)根据上下文决定data/file.txt

2. 无前缀时的默认行为

  • Web应用 中:通常创建 ServletContextResource
  • 非Web应用 中:通常创建 ClassPathResourceFileSystemResource(取决于具体实现)

使用演示

实验一:ClassPathXmlApplicationContext

package com.atguigu.spring6.resouceloader;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.Resource;

public class Demo1 {

    public static void main(String[] args) {
        // 创建基于类路径的Spring容器
        ApplicationContext ctx = new ClassPathXmlApplicationContext();
        
        // 通过ApplicationContext访问资源
        // ApplicationContext实例获取Resource实例时,默认采用与ApplicationContext相同的资源访问策略
        Resource res = ctx.getResource("atguigu.txt");
        System.out.println(res.getFilename());
    }
}

关键理解:

  • 创建的是 ClassPathXmlApplicationContext,本身就是一个 ResourceLoader实现类,这个容器的特点是:从类路径(classpath)中查找配置文件(它名字里就有 "ClassPath")
  • 所以,当使用 ctx.getResource("atguigu.txt") 时,它默认会从类路径中查找这个文件
  • 相当于自动使用了 ClassPathResource

实验二:FileSystemXmlApplicationContext

package com.atguigu.spring6.resouceloader;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.core.io.Resource;

public class Demo2 {

    public static void main(String[] args) {
        // 创建基于文件系统的Spring容器
        ApplicationContext ctx = new FileSystemXmlApplicationContext();
        
        // 同上
        Resource res = ctx.getResource("atguigu.txt");
        System.out.println(res.getFilename());
    }
}

关键理解:同上

ResourceLoader 总结

实践建议

Q1:什么时候用 ResourceLoader?什么时候直接用 Resource 实现类?

  • 使用 ResourceLoader:当你只有资源路径字符串,想让Spring自动选择合适Resource类型时
  • 直接使用 Resource 实现类:当你明确知道资源类型和位置时(如明确知道是文件系统上的文件)

Q2:ResourceLoaderAware 是必须的吗?

  • 不是必须的,但它是Spring提供的一种便捷方式
  • 如果你的Bean确实需要ResourceLoader,实现这个接口可以让Spring自动注入,无需手动获取

Q3:在Web应用中,ResourceLoader会创建什么类型的Resource?

  • 对于无前缀路径,通常会创建 ServletContextResource
  • 你可以通过 classpath: 前缀强制使用类路径资源
  • 可以通过 file: 前缀强制使用文件系统资源

一句话总结:ResourceLoader是Spring提供的"资源定位器",你告诉它资源在哪(路径字符串),它就把资源对象(Resource)交给你,而ApplicationContext就是最常用的ResourceLoader。

ResourceLoader 的智能选择机制

Spring将采用和ApplicationContext相同的策略来访问资源。

你使用的ApplicationContext类型 → 决定了默认的资源访问策略
      ↓
ClassPathXmlApplicationContext → 默认使用ClassPathResource
      ↓
FileSystemXmlApplicationContext → 默认使用FileSystemResource
      ↓
其他ApplicationContext → 根据上下文环境决定