JdbcTemplate CRUD
简介
Spring 框架对 JDBC 进行了封装,提供了 JdbcTemplate 工具类,大大简化了传统 JDBC 的开发流程。它处理了资源的创建与释放、异常处理等繁琐操作,让开发者能更专注于 SQL 业务逻辑。
准备工作
- 创建子模块
新建名为 spring-jdbc-tx 的子模块,专门用于学习 Spring 的 JDBC 和事务管理功能。
- 添加依赖
在 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>
- 配置数据库连接
在 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
- 配置 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>
- 准备测试数据库
执行以下 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. 分析数据库表结构
// 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() 方法便于调试
}
- 配置测试环境
首先需要创建测试类并装配 JdbcTemplate。通过 Spring 测试框架整合 JUnit,可以方便地注入和使用 JdbcTemplate。
// 1. 创建测试类
public class JDBCTemplateTest {
// 2. 使用注解加载 Spring 配置文件
@SpringJUnitConfig(locations = "classpath:beans.xml")
// 3. 注入 JdbcTemplate
@Autowired
private JdbcTemplate jdbcTemplate;
}
各种CRUD操作
- 增删改操作
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);
}
- 查询返回单个对象
当查询结果期望返回单条记录时,可以使用 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);
}
- 查询返回多条记录的集合
当查询结果可能包含多条记录时,使用 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():可能返回多条记录
- 查询返回单个值
对于返回单行单列的查询(如 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接口调用,都需要对传入的参数进行合法性验证。
传统校验方式的问题
传统的数据校验方式通常存在以下问题:
- 代码耦合度高:校验逻辑与业务逻辑紧密耦合,难以分离
- 代码重复:相同的校验规则需要在多个地方重复编写
- 维护困难:校验规则分散在各个业务类中,修改时需要多处调整
- 可读性差:校验代码与业务代码混杂,影响代码清晰度
Spring Validation的优势
Spring Validation通过统一的数据校验框架解决了上述问题,主要特点包括:
- 关注点分离:将校验逻辑与业务逻辑分离,提高代码的可维护性
- 注解驱动:通过声明式注解定义校验规则,代码简洁直观
- 标准化:基于JSR-380(Bean Validation 2.0)标准,与Hibernate Validator兼容
- 易于扩展:支持自定义校验规则和错误消息国际化
Spring Validation的实现方式
Spring Validation提供了多种数据校验方式,可以根据不同场景选择使用:
- Validator接口实现:通过编程方式实现校验逻辑
- Bean Validation注解:通过声明式注解定义校验规则
- 方法级别校验:对方法参数和返回值进行校验
- 自定义校验器:扩展自定义的校验规则
Validator接口实现
这是Spring Validation中最基础、最传统的一种校验方式。通过实现Spring提供的Validator接口,开发者可以完全控制校验逻辑,适合复杂或特殊的校验需求。
项目搭建步骤
第一步:创建子模块 spring6-validator
在实际开发中,我们通常会将不同功能的模块分开管理。创建一个独立的子模块可以更好地组织代码结构,方便后续的维护和扩展。
创建步骤:
- 在父工程下新建Maven模块
- 模块名称为
spring6-validator - 确保模块继承了父工程的配置
第二步:引入相关依赖
为了使用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");
}
}
}
核心方法解析:
- supports方法:
- 作用:设置当前校验器仅支持的类型,用于判断某一类型是否能使用当前校验器
- 设置方式:
return 支持检查的类名.class.equals(clazz); - 返回值为
true:支持对该类型进行校验 - 返回值为
false:不支持对该类型进行校验
- 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
- 至少有一个违规,校验失败,返回
第四步:编写测试类验证功能
创建测试类,验证两种校验方式的效果。
写法:
- 使用
@Test注解标记测试方法 - 创建Spring容器:
new AnnotationConfigApplicationContext(配置类.class) - 从容器获取服务对象Bean:
context.getBean(校验逻辑的服务类名.class) - 创建测试对象并设置属性值
- 调用校验方法:
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(面向切面编程)的思想,但专门用于校验
第二步:创建实体类,使用注解设置校验规则
- 在字段上直接添加需要的校验注解
- 注意注解的包:
jakarta.validation.constraints.*(或javax.validation.constraints.*) - 可以为注解指定
message属性来自定义错误消息 - 多个注解可以同时用在同一个字段上(如手机号既有格式要求又不能为空)
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方法...
}
新的校验注解介绍:
-
@Pattern:正则表达式校验regexp:正则表达式字符串message:自定义错误消息- 示例中的正则:手机号以1开头,第二位是3、4、5、7、8之一,后面9位数字
-
@NotBlank:比@NotNull更严格- 不能为null
- 不能为空字符串""
- 不能全是空白字符(如空格、制表符)
第三步:定义Service类,通过注解操作对象
- 类上添加
@Service注解(让Spring管理) - 类上添加
@Validated注解(启用方法校验) - 在方法参数前添加校验注解
- 如果参数是对象且需要校验其内部字段,必须加
@Valid
package com.atguigu.spring6.validation.method3;
@Service
@Validated // 关键注解:告诉Spring这个类的方法需要被校验
public class MyService {
public String testParams(@NotNull @Valid User user) { // 校验规则方法
return user.toString();
}
}
关键点解析:
@Validated注解:
- 必须加在类上(这里是@Service类)
- 告诉Spring:"请对我的方法参数进行校验"
- Spring会为此类创建代理,拦截方法调用
- 方法参数上的注解:
@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();
}
}
注解说明:
@Target:指定注解可以应用的目标元素类型。@Retention:指定注解的保留策略。@Documented:表示该注解应该被javadoc工具记录。@Constraint:表示这是一个校验注解,并通过validatedBy属性指定对应的校验器类。public @interface 自定义注解名 {}:创建一个新的注解标签,名为自定义注解名。- 注解中的三个方法:
message、groups、payload是校验注解必须的三个方法,含义如下:
数据类型 message() default:用于提供默认的错误消息。groups:用于分组校验,可以将注解分为不同的组,在不同的场景下使用不同的组。payload:用于传递元数据,比如可以定义严重程度。
- 内部注解
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;
}
}
校验器类说明:
-
校验器类必须实现
ConstraintValidator<A, T>接口,其中A是要处理的注解类型,T是要校验的数据类型。这里我们校验的是字符串,所以是String。 -
initialize方法:在校验器被初始化时调用,可以获取注解中的属性值。如果不需要,可以不重写(默认空实现)。
示例:- 给注解添加自定义属性
// 修改CannotBlank注解,添加两个自定义属性 public @interface CannotBlank { String message() default "不能包含空格"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; // ========== 新增的两个属性 ========== int maxLength() default 100; // 最大长度限制 boolean allowChinese() default true; // 是否允许中文 // ================================ } - 在校验器中获取并使用这些属性
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); }
- 给注解添加自定义属性
-
isValid方法:实际的校验逻辑。参数value是要校验的值,context是校验上下文,可以用来构建错误消息。- 注意:当
value为null时,我们选择不校验,返回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失败 } }
注意事项:
- 自定义注解和校验器需要配合使用,注解通过
@Constraint指定校验器。 - 校验器类需要被Spring管理(如果是Spring项目,确保能被扫描到或者通过配置注册)。
- 在使用自定义注解时,可以像使用内置注解一样,在需要校验的字段或参数上添加即可。
资源操作: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接口核心方法详解:
- exists() - 检查资源是否物理存在,存在返回true。
boolean exists();
- 用于避免对不存在资源的操作
- 对于某些资源类型(如ClassPathResource),
exists()只检查类加载器是否能找到资源,不验证资源内容是否损坏
使用场景:
Resource resource = new ClassPathResource("config.properties");
if (resource.exists()) {
// 安全地处理资源
InputStream is = resource.getInputStream();
} else {
// 处理资源不存在的情况
System.out.println("配置文件不存在");
}
注意事项:
- 资源存在不一定可读
- 对于网络资源,可能会有延迟或网络问题导致误判
- 某些资源(如动态生成的)可能永远返回true
- isReadable() - 检查对资源是否有读取权限
boolean isReadable();
- 用于检查资源是否可读(是否有读取权限),但与文件是否被锁定无关(被锁定时不可读)
使用场景:
Resource resource = new FileSystemResource("/var/log/app.log");
if (resource.isReadable()) {
// 读取日志文件操作
} else {
System.out.println("没有读取权限或文件被锁定");
}
- isOpen() - 检查资源本身是否是打开的流
boolean isOpen();
- 所有非流资源(FileSystemResource、ClassPathResource、UrlResource等大多数资源)都返回 false
- 只有
InputStreamResource返回 true - 返回true的资源只能读取一次,需要及时关闭
- 用于防止资源重复读取
- isFile() - 检查资源是否能表示为文件系统路径(即是否是文件系统文件)
boolean isFile();
- 如果是,就能转换为File对象,用于为后续操作做准备
Resource resource = // 获取资源
if (resource.isFile()) {
File file = resource.getFile(); // 安全转换
// 使用File API操作
} else {
// 使用流方式操作
InputStream is = resource.getInputStream();
}
- 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(); // 这是另一个流
- 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();
- 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("此资源不是文件系统文件");
}
- readableChannel() - 获取NIO通道
ReadableByteChannel readableChannel() throws IOException;
- 用于使用NIO方式读取资源
- 对于文件系统资源,返回
FileChannel - 对于其他资源,通常包装
InputStream为ReadableByteChannel - 适合大文件读取,性能更好
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();
}
}
- contentLength() - 获取资源内容的字节数
long contentLength() throws IOException;
- 用于进度显示、内存分配
- 对于某些资源,长度未知,可能返回-1
long size = resource.contentLength();
System.out.println("文件大小: " + size + " 字节");
// 根据大小分配缓冲区
byte[] buffer = new byte[(int) Math.min(size, 8192)];
- lastModified() - 获取资源最后修改的时间戳
long lastModified() throws IOException;
- 用于缓存控制、版本管理
- 如果不能确定修改时间,返回0(而不是抛出异常)
long lastModified = resource.lastModified();
Date modifyDate = new Date(lastModified);
System.out.println("最后修改时间: " + modifyDate);
// 缓存检查
if (lastModified > lastCacheTime) {
// 资源已更新,重新加载
}
- 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
- getFilename() - 获取资源的文件名部分(不含路径)
String getFilename();
- 用于显示、日志、保存操作
Resource resource = new FileSystemResource("/home/user/documents/report.pdf");
String filename = resource.getFilename(); // "report.pdf"
// 根据扩展名处理
if (filename.endsWith(".pdf")) {
// PDF处理逻辑
}
- 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依赖
下面是提供的示例代码,我们先添加注释,再详细解析:
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");
}
关键点解析:
- URL协议前缀的使用:
// 注意这里的"file:"前缀 loadAndReadUrlResource("file:atguigu.txt"); // 对比其他形式: // "file:atguigu.txt" - 相对路径(相对于当前工作目录) // "file:///C:/atguigu.txt" - Windows绝对路径 // "file:///home/user/atguigu.txt" - Linux/Mac绝对路径 - 为什么使用UrlResource而不是FileSystemResource?
- 统一访问方式:无论资源在哪里,都用相同API
- 代码复用:可以使用同一段代码处理多种资源
- 配置灵活:通过改变URL前缀切换资源位置
ClassPathResource 访问类路径资源
类路径就是Java程序在运行时查找.class文件和资源文件(如配置文件)的所有位置集合。
类路径包含:
- 项目编译输出目录(通常是
target/classes或bin) - 依赖的JAR包(
lib目录或Maven依赖) - 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关系模型来表示
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)的路径解析规则
- 根据路径前缀自动选择Resource类型
| 路径格式 | 创建的Resource类型 | 示例 |
|---|---|---|
classpath: | ClassPathResource | classpath:config.properties |
file: | FileSystemResource | file:/home/user/config.xml |
http: 或 https: | UrlResource | http://example.com/data.json |
| 无前缀(默认) | 根据上下文决定 | data/file.txt |
2. 无前缀时的默认行为
- 在 Web应用 中:通常创建 ServletContextResource
- 在 非Web应用 中:通常创建 ClassPathResource 或 FileSystemResource(取决于具体实现)
使用演示
实验一: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 → 根据上下文环境决定