在上一篇文章 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();
}
}
}
好!蘑菇君的改造流程就到这里。自我感觉还阔以,我屁颠屁颠的告诉小伙伴自己的成果︿( ̄︶ ̄)︿
小伙伴小薇看到以后,说:“哎哟,不错哦,看起来很方便嘛!”
第二天,小薇凑过来对我说,”昨天用的时候发现几个问题,我想问问咋解决?"
SQLExceptionParser
接口,但是还要修改你的SQLExceptionParserFactory
,这不符合开闭原则呀,假如你的代码封装成了jar包,其他组改不了源码…咳咳,面对这夺命连环三问,我只好掏出了枪…
言归正传,上面确实是设计上没考虑到的。咱们来动动蹄子思考一下:
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是如何做好这件事的。(八成抄袭了我的思路╮( ̄▽ ̄)╭)
我是蘑菇君,妈个鸡又熬夜了…下次谁熬夜谁是猪!(▼へ▼メ)