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
相关产品推荐
相关产品推荐

