Spring JDBC的优雅设计 - 异常封装(上)
Posted 蘑菇君520
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring JDBC的优雅设计 - 异常封装(上)相关的知识,希望对你有一定的参考价值。
JDBC里的异常之痛
在上一篇文章 Spring JDBC的优雅设计 - 数据转换 的开头,蘑菇君提到过,用原生JDBC操作数据库,需要自己处理各种异常,很让人头秃。(啥玩意儿?不了解JDBC里的异常?还不快去看Java JDBC的优雅设计)
头秃在哪?我们来复习一下JDBC中的异常基类:
public class SQLException extends java.lang.Exception
implements Iterable<Throwable>
// SQL状态类型,这是SQL标准中定义的
private String SQLState;
// 错误码,这是厂商定义的
private int vendorCode;
public SQLException(String reason, String SQLState, int vendorCode)
super(reason);
this.SQLState = SQLState;
this.vendorCode = vendorCode;
里面有两个属性来标识,数据库出了什么茬子:
SQLState
代表通用的错误类型,这是SQL标准里定义的一些类型码。vendorCode
是厂商定义的错误码,标识了某数据库的具体错误类型。显然,对于同一种错误,不同数据库的错误码很可能不一样。
假如现在有个需求,要针对重复主键这个错误做一些特殊处理。(记录更详细的log以便追踪错误啦,或者返回更精确的报错信息给调用层啦,balabala)
先无脑写出代码:
public void insertCourse(Course course)
Connection connection = getConnection();
PreparedStatement pstmt = null;
try
pstmt = connection.prepareStatement("insert course values(?, ?) ");
pstmt.setInt(1, course.getId());
pstmt.setString(2, course.getName());
pstmt.execute();
catch (SQLException e)
String databaseVendor = getDatabaseVendor(); // 获取数据库厂商名
if (databaseVendor.equals("mysql"))
if (e.getErrorCode() == 1062)
doSomething();
else if (databaseVendor.equals("Oracle"))
if (e.getErrorCode() == 1)
doSomething();
...
// 省略一万字
在处理异常时,首先要判断当前用的是哪个数据库,然后根据数据库来确定重复主键错误的错误码。相信大家也看出问题来了:
- 数据库不可配置。这
if else
代码着实是丑的一批,而且考虑到数据库辣么多,这根本是个无底洞。当然了,我们只要对项目中可能用到的数据库处理即可。但是上面的if else
代码显然不具备扩展性,每次增加新的数据库,都需要改动里面的代码。 - 异常不可配置。这里还只处理了重复主键这一种错误,要是再多处理几种,这里面的代码就跟毛线球一样了。毕竟,我们永远不知道意外和需求哪个先来(メ`ロ´)/~
好,是时候表演真正的(翻车)技术了~
蘑菇君翻车时刻
外部矛盾
首先,处理外部矛盾。异常处理逻辑不应该跟我们的业务代码混合在一起,先把这些代码移出去。同时,蘑菇君不打算再用SQLException这种通用的异常类了,而是重新封装一些异常,更精确的标识异常类型。
public class SQLExceptionParser
public MoguJunSQLException parse(SQLException e)
String databaseVendor = getDatabaseVendor(); // 获取数据库厂商名
if (databaseVendor.equals("MySQL"))
if (e.getErrorCode() == 1062)
return new DuplicateKeyException(e);
else if (databaseVendor.equals("Oracle"))
if (e.getErrorCode() == 1)
return new DuplicateKeyException(e);
String getDatabaseVendor()
return null;
public class MoguJunSQLException extends Exception
public MoguJunSQLException(Throwable cause)
super(cause);
public class DuplicateKeyException extends MoguJunSQLException
public DuplicateKeyException(final Throwable cause)
super(cause);
这样就把业务代码跟异常转换逻辑解耦了:
public void insertCourse(Course course)
SQLExceptionParser parser = new SQLExceptionParser();
try
// 省略
catch (SQLException e)
MoguJunSQLException exception = parser.parse(e);
if (exception instanceof DuplicateKeyException)
doSomething();
这样在处理重复主键异常的时候,只需要通过SQLExceptionParser
转换一次,再判断一次,就可以了。是不是清爽了许多(~ ̄▽ ̄)~
内部纠纷
接下来处理内部纠纷:这么多数据库,这么多错误类型,怎么处理更优雅呢?
蘑菇君首先想到的是,异常转换的过程是类似的,只是不同数据库有不同的错误码。那这异常转换这个动作可以抽象出来,具体实现就交给不同数据库的转换类去实现呗。
public interface SQLExceptionParser
MoguJunSQLException parse(SQLException e);
public class MySQLExceptionParser implements SQLExceptionParser
public MoguJunSQLException parse(SQLException e)
if (e.getErrorCode() == 1062)
return new DuplicateKeyException(e);
// 省略其他异常转换
return new DefaultException(e);
public class OracleExceptionParser implements SQLExceptionParser
public MoguJunSQLException parse(SQLException e)
if (e.getErrorCode() == 1)
return new DuplicateKeyException(e);
// 省略其他异常转换
return new DefaultException(e);
// 兜个底
public class DefaultException extends MoguJunSQLException
public DefaultException(final Throwable cause)
super(cause);
上面的结构看起来就清晰许多了。同时,也有个兜底的实现类,很鲁棒有木有!
那如何选择用哪个转换类呢?咱们可以用个工厂类来表示这个选择的过程:
public class SQLExceptionParserFactory
public SQLExceptionParser create(String databaseVendor)
switch (databaseVendor)
case "MySQL":
return new MySQLExceptionParser();
case "Oracle":
return new OracleExceptionParser();
// 省略其他数据库
return new DefaultSQLExceptionParser();
// 依旧兜个底
public class DefaultSQLExceptionParser implements SQLExceptionParser
@Override
public MoguJunSQLException parse(final SQLException e)
return new DefaultException(e);
咱们送佛送到西,再弄个工具类,将上面的过程封装起来,方便别人调用:
// 异常转换工具类
public class SQLExceptionParserUtil
public static MoguJunSQLException parse(Connection connection, SQLException e)
String databaseVendor = connection.getMetaData().getDatabaseProductName();
SQLExceptionParser parser = new SQLExceptionParserFactory().create(databaseVendor);
return parser.parse(e);
// 调用方调用
public void insertCourse(Course course)
Connection connection = getConnection();
try
// 省略sql语句执行
catch (SQLException e)
MoguJunSQLException exception = SQLExceptionParserUtil.parse(connection, e);
if (exception instanceof DuplicateKeyException)
doSomething();
好!蘑菇君的改造流程就到这里。自我感觉还阔以,我屁颠屁颠的告诉小伙伴自己的成果︿( ̄︶ ̄)︿
翻车记录
小伙伴小薇看到以后,说:“哎哟,不错哦,看起来很方便嘛!”
第二天,小薇凑过来对我说,”昨天用的时候发现几个问题,我想问问咋解决?"
- 我们小组用的SQL Server数据库,实现了你的
SQLExceptionParser
接口,但是还要修改你的SQLExceptionParserFactory
,这不符合开闭原则呀,假如你的代码封装成了jar包,其他组改不了源码… - 异常类型咋扩展啊,我想添加违反外键约束的异常类型,直接改源码也不合适吧…
- 我觉得你的异常处理还是对业务逻辑有侵入。每个sql语句的执行代码里,都要手动转换SqlException,多麻烦。能不能自动完成异常类型的转换?
咳咳,面对这夺命连环三问,我只好掏出了枪…
言归正传,上面确实是设计上没考虑到的。咱们来动动蹄子思考一下:
优雅的支持其他数据库
SQLExceptionParserFactory
是简单工厂模式,用起来简单粗暴。如果是自己项目里的业务逻辑,这么用也简洁。如果是作为基础库提供给他人使用,就得考虑扩展性了。
最理想的方式是,他人实现我们提供的SQLExceptionParser
接口,然后就完事儿了。咱们的异常转换库可以扫描到所有的实现类,并将这些实现类和某个数据库关联起来。
至于如何关联嘛,咱们可以通过配置文件指定,或通过实现类的类名来约定,或给SQLExceptionParser
接口添加一个getDatabaseVendor
的方式指定。
优雅的扩展异常类型
想要加一个新的异常,那就得给所有的异常转换实现类,加上新的异常处理。这显然不可取,改动太大了。
咱们得想办法,把这些改动统一到一处去。
咱们可以将代码里写死的错误码和异常类的映射关系,挪到配置文件里去。其他小伙伴想定义新的异常了,只要实现接口MoguJunSQLException
,再提供一个配置文件,将新的异常类跟错误码绑定即可。
比如,可以定义一个配置文件sql-error-codes.json
"mysql": [
"errorCode": 520,
"class": "wang.mogujun.sqlexception.DuplicateKeyException"
,
"errorCode": 1314,
"class": "wang.mogujun.sqlexception.ForeignKeyViolationException"
],
"oracle": [
"errorCode": 520,
"class": "wang.mogujun.sqlexception.DuplicateKeyException"
,
"errorCode": 1314,
"class": "wang.mogujun.sqlexception.ForeignKeyViolationException"
]
想要添加新的异常类型,小伙伴可以提供一个上面的这种配置文件,咱们异常处理库可以解析到所有的配置。看起来是不是很酷?Spring就是这么做的,咱们下篇再细说。
自动完成异常类型的转换
蘑菇君目前异常处理的方式是这样的:
try
// 执行sql
catch (SQLException e)
MoguJunSQLException exception = SQLExceptionParserUtil.parse(connection, e);
if (exception instanceof DuplicateKeyException)
pleaseBeElegant();
if (exception instanceof ForeignKeyViolationException)
DoNotWu();
想要偷个懒,自动转化异常的话,要变成这样:
try
// 执行sql
catch (DuplicateKeyException e)
pleaseBeElegant();
catch (ForeignKeyViolationException e)
DoNotWu();
怎么变呢?
咳咳,咱们需要包装一下sql语句的执行,将原来执行sql语句抛出来的SqlException
转换一下。类似这样:
public void execute(String sql) throws MoguJunException
try
// 执行sql
catch (SQLException e)
MoguJunSQLException exception = SQLExceptionParserUtil.parse(connection, e);
throw exception;
总结
在这篇文章里,蘑菇君记录了自己封装JDBC中的异常时的心路历程,虽然有些粗糙,但是思考的方向还是基本正确滴。下一篇分析一下Spring是如何做好这件事的。(八成抄袭了我的思路╮( ̄▽ ̄)╭)
题外话
我是蘑菇君,妈个鸡又熬夜了…下次谁熬夜谁是猪!(▼へ▼メ)
以上是关于Spring JDBC的优雅设计 - 异常封装(上)的主要内容,如果未能解决你的问题,请参考以下文章