You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring未将SQLException转换为DataAccessException问题排查

问题:Spring未将SQLException转换为DataAccessException

我尝试理解为何与Spring文档所述相悖,SQLException未被转换为DataAccessException。

我的Spring配置

@Configuration
@ComponentScan
public class AppConfig  {
    @Bean
    DataSource getDataSource() {
        String dbUrl = mySqlUrl;
        String dbUser = "user";
        String dbPassword = "password";

        MysqlDataSource mysqlDS = null;
        
        mysqlDS = new MysqlDataSource();
        mysqlDS.setURL(dbUrl);
        mysqlDS.setUser(dbUser);
        mysqlDS.setPassword(dbPassword);

        return mysqlDS;
    }

    @Bean
    static public PersistenceExceptionTranslationPostProcessor translation() {
        return new PersistenceExceptionTranslationPostProcessor();
    }
}

@Repository类代码

@Repository
public class UsersDAO {

    DataSource dataSource;
    
    @Autowired 
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    void insertUser(User user) {
        try(Connection conn = this.dataSource.getConnection()) {
            try(PreparedStatement pstmt = conn.prepareStatement("insert into users (id, name) values (?, ?)")) {
                pstmt.setInt(1, user.getId());
                pstmt.setString(2, user.getName());

                int result = pstmt.executeUpdate();

            } 
        } catch(SQLException e) {
            throw new RuntimeException(e);
        }
        
    }
}

编辑补充

我也尝试了不捕获并重抛异常的写法:

void insertUser(User user) throws SQLException {
        try(Connection conn = this.dataSource.getConnection()) {
            try(PreparedStatement pstmt = conn.prepareStatement("insert into users (id, name) values (?, ?)")) {
                pstmt.setInt(1, user.getId());
                pstmt.setString(2, user.getName());

                int result = pstmt.executeUpdate();

            } 
        }
}

应用主类代码

public class App {
    
    public static void main(String[] args) throws SQLException {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

        UsersDAO dao = (UsersDAO)context.getBean("usersDAO");
        
        User john = new User(5, "John");
        dao.insertUser(john);
   }
}

异常信息

当我运行两次程序(插入同一主键用户,触发DuplicateKey异常)时,得到的是java.sql类型的异常:

Exception in thread "main" java.lang.RuntimeException: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '5' for key 'users.PRIMARY'
        at org.example.UsersDAO.insertUser(UsersDAO.java:58)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:354)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:768)
        at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:138)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:768)
        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:720)
        at org.example.UsersDAO$$SpringCGLIB$$0.insertUser(<generated>)
        at org.example.App.main(App.java:27)
Caused by: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '5' for key 'users.PRIMARY'
        at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:118)
        at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
        at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:916)
        at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1061)
        at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1009)
        at com.mysql.cj.jdbc.ClientPreparedStatement.executeLargeUpdate(ClientPreparedStatement.java:1320)
        at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdate(ClientPreparedStatement.java:994)
        at org.example.UsersDAO.insertUser(UsersDAO.java:54)
        ... 14 more

说明:我未使用JdbcTemplate,因业务场景需使用原生JDBC,现做POC验证Spring是否能转换SQL异常。

请问为何Spring未将SQLException(或其子类)转换为DataAccessException(或其子类)?


解答

核心原因分析

1. 捕获SQLException并包装为RuntimeException的场景

Spring的PersistenceExceptionTranslationInterceptor仅能识别并转换方法直接抛出的受检异常(如SQLException)。你在代码中捕获了SQLException并将其包装为RuntimeException抛出,拦截器无法识别这个非受检异常与数据库异常的关联,因此不会执行转换逻辑。

2. 直接抛出SQLException的场景

这个场景下未转换的核心问题是方法访问权限限制:你的insertUser方法是包访问权限(无public修饰符),而Spring AOP默认仅对public方法创建代理并执行拦截逻辑。尽管栈中显示存在CGLIB代理痕迹,但拦截器并未实际处理非public方法抛出的异常。

解决方案

方案1:修正方法访问权限(最简单)

将insertUser方法改为public修饰,确保Spring AOP能正常拦截方法调用并执行异常转换:

@Repository
public class UsersDAO {
    // ... 其他代码
    public void insertUser(User user) throws SQLException {
        try(Connection conn = this.dataSource.getConnection()) {
            try(PreparedStatement pstmt = conn.prepareStatement("insert into users (id, name) values (?, ?)")) {
                pstmt.setInt(1, user.getId());
                pstmt.setString(2, user.getName());
                pstmt.executeUpdate();
            } 
        }
    }
}

方案2:手动转换异常(保留原生JDBC且方法权限不变)

如果必须保留包访问权限,可以手动注入SQLExceptionTranslator来转换异常:

@Repository
public class UsersDAO {
    DataSource dataSource;
    private final SQLExceptionTranslator exceptionTranslator;

    @Autowired 
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
        // 基于当前DataSource自动适配对应数据库的异常翻译器
        this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
    }

    void insertUser(User user) {
        try(Connection conn = this.dataSource.getConnection()) {
            try(PreparedStatement pstmt = conn.prepareStatement("insert into users (id, name) values (?, ?)")) {
                pstmt.setInt(1, user.getId());
                pstmt.setString(2, user.getName());
                pstmt.executeUpdate();
            } 
        } catch(SQLException e) {
            // 手动转换为DataAccessException子类
            throw exceptionTranslator.translate("执行用户插入操作", "insert into users (id, name) values (?, ?)", e);
        }
    }
}

内容的提问来源于stack exchange,提问作者YoavKlein

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 19:19:51