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();
				}
			}
			...
			// 省略一万字
		}
	}

在处理异常时,首先要判断当前用的是哪个数据库,然后根据数据库来确定重复主键错误的错误码。相信大家也看出问题来了:

  1. 数据库不可配置。这if else代码着实是丑的一批,而且考虑到数据库辣么多,这根本是个无底洞。当然了,我们只要对项目中可能用到的数据库处理即可。但是上面的if else代码显然不具备扩展性,每次增加新的数据库,都需要改动里面的代码。
  2. 异常不可配置。这里还只处理了重复主键这一种错误,要是再多处理几种,这里面的代码就跟毛线球一样了。毕竟,我们永远不知道意外和需求哪个先来(メ`ロ´)/~

好,是时候表演真正的(翻车)技术了~

蘑菇君翻车时刻

外部矛盾

首先,处理外部矛盾。异常处理逻辑不应该跟我们的业务代码混合在一起,先把这些代码移出去。同时,蘑菇君不打算再用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();
        }
    }
}

好!蘑菇君的改造流程就到这里。自我感觉还阔以,我屁颠屁颠的告诉小伙伴自己的成果︿( ̄︶ ̄)︿
Spring JDBC的优雅设计 - 异常封装(上)_第1张图片

翻车记录

小伙伴小薇看到以后,说:“哎哟,不错哦,看起来很方便嘛!”

第二天,小薇凑过来对我说,”昨天用的时候发现几个问题,我想问问咋解决?"

  1. 我们小组用的SQL Server数据库,实现了你的SQLExceptionParser接口,但是还要修改你的SQLExceptionParserFactory,这不符合开闭原则呀,假如你的代码封装成了jar包,其他组改不了源码…
  2. 异常类型咋扩展啊,我想添加违反外键约束的异常类型,直接改源码也不合适吧…
  3. 我觉得你的异常处理还是对业务逻辑有侵入。每个sql语句的执行代码里,都要手动转换SqlException,多麻烦。能不能自动完成异常类型的转换?

咳咳,面对这夺命连环三问,我只好掏出了枪…
Spring JDBC的优雅设计 - 异常封装(上)_第2张图片
言归正传,上面确实是设计上没考虑到的。咱们来动动蹄子思考一下:

优雅的支持其他数据库

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();	
	}

怎么变呢?

Spring JDBC的优雅设计 - 异常封装(上)_第3张图片
咳咳,咱们需要包装一下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的那些优雅设计)