乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

@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) {
        // 数据库查询逻辑
    }
}

常用属性

属性类型默认值说明
valueString""指定 Bean 的名称(id)。默认使用类名首字母小写
// 显式指定 Bean 名称
@Repository("studentJdbcDao")
public class StudentDao {
}

核心原理:持久层异常转换机制

Spring 为 @Repository 类自动注册了一个 BeanPostProcessor——PersistenceExceptionTranslationPostProcessor。该后处理器为所有 @Repository Bean 创建 AOP 代理,拦截方法调用并转换异常:

异常转换流程:

  1. 业务层调用 studentDao.save(student)
  2. AOP 代理拦截调用,执行目标方法
  3. 如果目标方法抛出 SQLException,代理捕获该异常
  4. SQLExceptionTranslator 根据错误码和 SQL 状态分析异常类型
  5. 转换为对应的 DataAccessException 子类(如 DuplicateKeyException、DataIntegrityViolationException)
  6. 业务层统一捕获 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);
            }
        }
    }
}

痛点:

  1. 每个 DAO 方法都声明 throws SQLException,污染方法签名
  2. 业务层需要了解数据库错误码(如 MySQL 的 1062 表示主键冲突),技术耦合
  3. 连接和 Statement 的关闭逻辑重复且易出错
  4. 如果切换为 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,技术无关
资源管理手动关闭连接/StatementJdbcTemplate 自动管理
技术切换切换 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。该后处理器为 @Repository Bean 创建 AOP 代理,自动将底层持久层异常(如 SQLException)转换为 Spring 统一的 DataAccessException 体系,实现技术无关的异常处理。

Q2:为什么 @Repository 能实现异常转换?

Spring 容器在启动时注册 PersistenceExceptionTranslationPostProcessor(通常由 @EnableTransactionManagement 引入)。该 BeanPostProcessor 为所有 @Repository Bean 创建代理,拦截方法调用,捕获 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 切点匹配。

上一页
@Service 详解