@Repository 详解
本章定位:深入理解数据访问层组件的语义化注解。
@Repository不仅是@Component的派生,还额外提供了持久层异常转换机制,是 Spring 数据访问统一抽象的关键一环。
定义与作用
@Repository 是 Spring 提供的数据访问层组件注解,用于标记与数据库、文件系统或其他持久化存储交互的类。
解决的痛点:在纯 JDBC 开发中,数据访问代码需要处理大量受检异常(SQLException、IOException 等),这些异常与业务层代码耦合,导致业务方法签名臃肿:
// 痛点:业务层被迫处理 SQLException
public class StudentService {
public void enrollStudent(Student student) throws SQLException {
studentDao.save(student); // 如果 save 抛 SQLException,业务层必须处理或继续抛
}
}
@Repository 通过 Spring 的持久层异常转换机制,将底层技术相关的异常(如 SQLException)转换为 Spring 统一的 DataAccessException 体系,实现技术无关的异常处理。
与 @Component 的关键差异
@Repository 在 @Component 的基础上,额外启用了 PersistenceExceptionTranslationPostProcessor:
适用位置与常用属性
适用位置
@Repository 只能标注在类级别,通常用于 DAO(Data Access Object)或 Repository 层的类。
@Repository
public class StudentDao {
public Student findById(int id) {
// 数据库查询逻辑
}
}
常用属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value | String | "" | 指定 Bean 的名称(id)。默认使用类名首字母小写 |
// 显式指定 Bean 名称
@Repository("studentJdbcDao")
public class StudentDao {
}
核心原理:持久层异常转换机制
Spring 为 @Repository 类自动注册了一个 BeanPostProcessor——PersistenceExceptionTranslationPostProcessor。该后处理器为所有 @Repository Bean 创建 AOP 代理,拦截方法调用并转换异常:
异常转换流程:
- 业务层调用
studentDao.save(student) - AOP 代理拦截调用,执行目标方法
- 如果目标方法抛出
SQLException,代理捕获该异常 SQLExceptionTranslator根据错误码和 SQL 状态分析异常类型- 转换为对应的
DataAccessException子类(如DuplicateKeyException、DataIntegrityViolationException) - 业务层统一捕获
DataAccessException,无需关心底层是 JDBC、JPA 还是 MyBatis
完整示例:飞翔科技公司的学生数据访问层
场景简述
飞翔科技的学生管理系统需要对学生数据进行 CRUD 操作。后端开发小崔负责实现数据访问层,架构师白歌要求使用 @Repository 标注 DAO 类,以利用 Spring 的异常转换和统一异常体系。
操作前:纯 JDBC,异常处理混乱
// 操作前:StudentDao.java —— 纯 JDBC,原始异常暴露
public class StudentDao {
private DataSource dataSource;
public StudentDao(DataSource dataSource) {
this.dataSource = dataSource;
}
public void save(Student student) throws SQLException {
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(
"INSERT INTO students(name, age, class_name) VALUES(?, ?, ?)"
);
ps.setString(1, student.getName());
ps.setShort(2, student.getAge());
ps.setString(3, student.getClassName());
ps.executeUpdate();
ps.close();
conn.close();
}
public Student findById(int id) throws SQLException {
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(
"SELECT id, name, age, class_name FROM students WHERE id = ?"
);
ps.setInt(1, id);
ResultSet rs = ps.executeQuery();
Student student = null;
if (rs.next()) {
student = new Student();
student.setId(rs.getInt("id"));
student.setName(rs.getString("name"));
student.setAge(rs.getShort("age"));
student.setClassName(rs.getString("class_name"));
}
rs.close();
ps.close();
conn.close();
return student;
}
}
// 操作前:StudentService.java —— 被迫处理 SQLException
public class StudentService {
private final StudentDao studentDao;
public StudentService(StudentDao studentDao) {
this.studentDao = studentDao;
}
public void enrollStudent(Student student) {
try {
studentDao.save(student);
} catch (SQLException e) {
// 业务层被迫处理数据库异常,且无法区分是主键冲突还是连接断开
if (e.getErrorCode() == 1062) {
throw new RuntimeException("学生姓名重复");
} else {
throw new RuntimeException("数据库错误", e);
}
}
}
}
痛点:
- 每个 DAO 方法都声明
throws SQLException,污染方法签名 - 业务层需要了解数据库错误码(如 MySQL 的 1062 表示主键冲突),技术耦合
- 连接和 Statement 的关闭逻辑重复且易出错
- 如果切换为 JPA 或 MyBatis,
SQLException将变成PersistenceException或MyBatisException,业务层需要重写异常处理
操作后:使用 @Repository + JdbcTemplate
// 操作后:StudentDao.java —— @Repository 标注,使用 JdbcTemplate
@Repository
public class StudentDao {
private static final Logger logger = LoggerFactory.getLogger(StudentDao.class);
private final JdbcTemplate jdbcTemplate;
public StudentDao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void save(Student student) {
jdbcTemplate.update(
"INSERT INTO students(name, age, class_name) VALUES(?, ?, ?)",
student.getName(), student.getAge(), student.getClassName()
);
logger.debug("保存学生:{}", student.getName());
}
public Student findById(int id) {
return jdbcTemplate.queryForObject(
"SELECT id, name, age, class_name FROM students WHERE id = ?",
new BeanPropertyRowMapper<>(Student.class),
id
);
}
public boolean existsByName(String name) {
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM students WHERE name = ?",
Integer.class,
name
);
return count != null && count > 0;
}
public List<Student> findAll() {
return jdbcTemplate.query(
"SELECT id, name, age, class_name FROM students",
new BeanPropertyRowMapper<>(Student.class)
);
}
}
// 操作后:StudentService.java —— 统一捕获 DataAccessException
@Service
public class StudentService {
private static final Logger logger = LoggerFactory.getLogger(StudentService.class);
private final StudentDao studentDao;
public StudentService(StudentDao studentDao) {
this.studentDao = studentDao;
}
@Transactional
public void enrollStudent(Student student) {
try {
studentDao.save(student);
logger.info("学生 {} 入学成功", student.getName());
} catch (DuplicateKeyException e) {
// Spring 自动转换的异常,语义清晰
logger.warn("学生姓名重复:{}", student.getName());
throw new BusinessException("该学生已存在,请勿重复入学");
} catch (DataAccessException e) {
// 统一捕获所有数据访问异常
logger.error("数据库操作失败", e);
throw new BusinessException("系统繁忙,请稍后重试");
}
}
}
// 操作后:AppConfig.java
@Configuration
@ComponentScan("com.feixiang.student")
@EnableTransactionManagement
public class AppConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/student_db");
config.setUsername("root");
config.setPassword("secret");
return new HikariDataSource(config);
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
运行结果及分析
场景一:正常保存
[main] DEBUG o.s.j.c.JdbcTemplate - Executing prepared SQL update [INSERT INTO students...]
[main] DEBUG o.s.j.c.JdbcTemplate - SQL update affected 1 rows
[main] INFO c.f.s.service.StudentService - 学生 小崔 入学成功
场景二:主键冲突(异常转换生效)
[main] DEBUG o.s.j.c.JdbcTemplate - Executing prepared SQL update [INSERT INTO students...]
[main] ERROR o.s.j.s.SQLErrorCodeSQLExceptionTranslator -
Duplicate entry '小崔' for key 'students.name'
[main] WARN c.f.s.service.StudentService - 学生姓名重复:小崔
Exception: BusinessException: 该学生已存在,请勿重复入学
改进点总结:
| 维度 | 操作前(纯 JDBC) | 操作后(@Repository + JdbcTemplate) |
|---|---|---|
| 异常处理 | 原始 SQLException 暴露 | 自动转换为 DataAccessException 体系 |
| 错误码耦合 | 业务层需知悉 MySQL 错误码 1062 | 直接捕获 DuplicateKeyException,技术无关 |
| 资源管理 | 手动关闭连接/Statement | JdbcTemplate 自动管理 |
| 技术切换 | 切换 ORM 需重写异常处理 | 统一 DataAccessException,底层技术透明 |
| 代码量 | 每个方法 10+ 行样板代码 | JdbcTemplate 一行完成查询/更新 |
DataAccessException 异常体系
Spring 将底层持久技术异常统一映射到 DataAccessException 树:
DataAccessException
├── NonTransientDataAccessException
│ ├── DataIntegrityViolationException ← 主键冲突、唯一约束违反
│ ├── DuplicateKeyException ← 重复键(MySQL 1062)
│ └── PermissionDeniedDataAccessException
├── TransientDataAccessException
│ ├── ConcurrencyFailureException
│ ├── QueryTimeoutException
│ └── TransientDataAccessResourceException
├── DataRetrievalFailureException
│ └── IncorrectResultSizeDataAccessException
└── InvalidDataAccessApiUsageException
业务层只需捕获顶层的 DataAccessException,或根据具体场景捕获子类:
@Service
public class StudentService {
@Transactional
public void batchEnroll(List<Student> students) {
for (Student student : students) {
try {
studentDao.save(student);
} catch (DuplicateKeyException e) {
// 单个重复,跳过继续
logger.warn("跳过重复学生:{}", student.getName());
}
}
}
}
易错场景与面试考点
反例一:忘记配置 PersistenceExceptionTranslationPostProcessor
小崔使用纯注解配置,但发现 @Repository 的异常转换没有生效:
// ❌ 错误:缺少 @EnableTransactionManagement 或 PersistenceExceptionTranslationPostProcessor
@Configuration
@ComponentScan("com.feixiang.student")
public class AppConfig {
// 没有 @EnableTransactionManagement!
}
原理:PersistenceExceptionTranslationPostProcessor 通常由 @EnableTransactionManagement 间接引入。如果既不使用声明式事务,也不手动注册该后处理器,@Repository 的异常转换机制不会生效。
纠正方案一:使用 @EnableTransactionManagement(推荐):
@Configuration
@ComponentScan("com.feixiang.student")
@EnableTransactionManagement
public class AppConfig {
}
纠正方案二:手动注册后处理器:
@Configuration
@ComponentScan("com.feixiang.student")
public class AppConfig {
@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
return new PersistenceExceptionTranslationPostProcessor();
}
}
反例二:在 @Repository 中编写业务逻辑
小崔把分数计算逻辑也写进了 DAO:
// ❌ 错误:@Repository 越权处理业务逻辑
@Repository
public class StudentDao {
private final JdbcTemplate jdbcTemplate;
public void save(Student student) {
// ① 数据访问(正确)
jdbcTemplate.update("INSERT INTO students...", ...);
// ② 业务逻辑(错误!应该在 @Service 中)
if (student.getScore() >= 90) {
student.setGrade("A");
} else if (student.getScore() >= 80) {
student.setGrade("B");
}
jdbcTemplate.update("UPDATE students SET grade = ? WHERE id = ?", ...);
}
}
纠正:数据访问层只负责 CRUD,业务规则在 @Service 中处理:
// ✅ 正确:@Repository 只负责数据访问
@Repository
public class StudentDao {
public void save(Student student) {
jdbcTemplate.update("INSERT INTO students...", ...);
}
public void updateGrade(int studentId, String grade) {
jdbcTemplate.update("UPDATE students SET grade = ? WHERE id = ?", grade, studentId);
}
}
// ✅ 正确:@Service 负责业务规则
@Service
public class StudentService {
private final StudentDao studentDao;
@Transactional
public void recordScore(int studentId, int score) {
String grade;
if (score >= 90) grade = "A";
else if (score >= 80) grade = "B";
else if (score >= 70) grade = "C";
else grade = "D";
studentDao.updateGrade(studentId, grade);
}
}
反例三:@Repository 类未被扫描到
小崔把 DAO 类放在 repository 包下,但 @ComponentScan 只扫描了 service 包:
// ❌ 错误:扫描路径不包含 repository 包
@Configuration
@ComponentScan("com.feixiang.student.service") // 漏了 dao/repository 包
public class AppConfig {
}
// 包路径:com.feixiang.student.repository.StudentDao
@Repository
public class StudentDao {
}
启动报错:
NoSuchBeanDefinitionException: No qualifying bean of type 'com.feixiang.student.repository.StudentDao' available
纠正:确保 @ComponentScan 的扫描路径覆盖所有标注层:
// ✅ 正确:扫描父包,自动包含所有子包
@Configuration
@ComponentScan("com.feixiang.student")
public class AppConfig {
}
面试高频题
Q1:@Repository 和 @Component 有什么区别?
@Repository在@Component的基础上,额外启用了PersistenceExceptionTranslationPostProcessor。该后处理器为@RepositoryBean 创建 AOP 代理,自动将底层持久层异常(如SQLException)转换为 Spring 统一的DataAccessException体系,实现技术无关的异常处理。
Q2:为什么 @Repository 能实现异常转换?
Spring 容器在启动时注册
PersistenceExceptionTranslationPostProcessor(通常由@EnableTransactionManagement引入)。该BeanPostProcessor为所有@RepositoryBean 创建代理,拦截方法调用,捕获SQLException并通过SQLExceptionTranslator转换为对应的DataAccessException子类。
Q3:JdbcTemplate 相比纯 JDBC 的优势是什么?
① 自动管理连接、Statement 和 ResultSet 的生命周期;② 自动将
SQLException转换为DataAccessException;③ 提供queryForObject、query、update等模板方法,消除样板代码;④ 支持命名参数(NamedParameterJdbcTemplate)和批量操作。
Q4:如果项目使用 MyBatis,还需要 @Repository 吗?
需要。虽然 MyBatis-Spring 集成已经将
PersistenceException转换为MyBatisException,但@Repository仍然有价值:① 语义标识数据访问层;② 如果同时存在纯 JDBC 代码,@Repository确保其异常也被转换;③ 团队规范统一,便于 AOP 切点匹配。