手写 MyBatis 框架 - GodBatis 实现上的问题与改进
GodBatis 实现上的问题
GodBatis 在事务管理器的实现上存在问题:一个 SqlSessionFactory
只能对应一个 Transaction
事务管理器。如果 SqlSessionFactory.openSession()
开启多次会话,那么每个会话 SqlSession
底层都是使用的同一个事务管理器 Transaction
,从而每个会话使用的是同一个 Connection
连接对象。
在 MyBatis 中也有 Transaction
接口,也有 JdbcTransaction
和 ManagedTransaction
两个实现类。
假设事务管理器配置是 <transactionManager type="JDBC"/>
,那么每次开启会话 SqlSessionFactory.openSession()
,底层就会相应创建一个 JdbcTransaction
。因此:
- 一个
SqlSessionFactory
能够创建多个 SqlSession
SQL 会话。
- 一个
SqlSession
对应一个 Transaction
事务管理器。
- 一个
Transaction
对应一个 Connection
连接对象。
因此,在 MyBatis 中,一个 SqlSessionFactory
在多次 openSession
后,其实是创建了多个 Transaction
事务管理器,每个事务管理器管理一个独立的自己的 Connection
对象。
但在 GodBatis 中,一个 SqlSessionFactory
只能对应一个 Transaction
事务管理器。这个问题出现的根本原因在于,设计 SqlSessionFactory
类时,直接将 Transaction
作为属性。
GodBatis 问题溯源
学习到 036 - 手写 godbatis 框架第二步 - 分析 SqlSessionFactory 类该有的属性
时,老杜分析说,SqlSessionFactory
的类中应该包含如下属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class SqlSessionFactory {
private Map<String, MappedStatement> mappedStatements; }
|
按我的理解,到这一步还是没什么太大问题的,前提是:
- 事务管理器属性应该对应核心配置文件中的
<transactionManager>
标签,而非一个事务管理器实现。
但在学习到下一集 037 - 手写 godbatis 框架第三步 - 抽取事务管理器接口
就有些不对劲了。
老杜分析说,SqlSessionFactory
中的事务管理器属性应该面向接口编程,应如下定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class SqlSessionFactory {
private Transaction transaction;
private Map<String, MappedStatement> mappedStatements; }
|
其中接口 Transaction
具有两个实现类 JdbcTransaction
和 ManagedTransaction
,定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
public interface Transaction {
void commit();
void rollback();
void close(); }
|
看到这就觉得有些问题了。
在 SqlSessionFactory
中,事务管理器属性应该更像是一个事务管理器工厂,而非一个具体的事务管理器实现。
但在老杜的设计中,事务管理器属性显然是被设计为一个具体的事务管理器实现。
主要体现在 Transaction
接口中提供了 commit
rollback
和 close
这样的事务管理方法。
如果是事务管理器工厂,显然无需提供这样的方法,而是提供类似 newTransaction
这样的方法。
继续往下学习,会越来越确定,GodBatis 的事务管理器就是存在设计问题。
038 - 手写 godbatis 框架第四步 - 事务管理器的实现
中老杜计划重点实现 JdbcTransaction
事务管理器,但 Transaction
接口中的 commit
rollback
和 close
需要 Connection
连接对象,才能完成操作。获取连接对象需要数据源,因此干脆将原先计划定义在 SqlSessionFactory
中的数据源属性,移动到 JdbcTransaction
接口中。
039 - 手写 godbatis 框架第五步 - 数据源的实现
中老杜重点实现了 UnPooledDataSource
数据源。
040 - 手写 godbatis 框架第六步 - 事务管理器改造
中老杜在 JdbcTransaction
中新增了两个属性:连接对象 Connection connection
和自动提交标志 boolean autoCommit
,新增了开启数据库连接的方法 openConnection()
给属性 connection
赋值。最后使用属性 connection
实现了接口中的 commit
rollback
和 close
方法。
到此为止,GodBatis 从 SqlSessionFactory
到 JdbcTransaction
再到 Connection
的整个脉络基本已经清晰,一个 SqlSessionFactory
对象确实只会对应一个 Transaction
,从而只对应一个 Connection
。
后续在实现 SqlSessionFactory
类的 openSession()
方法,是直接通过该类的属性 transaction
来 transaction.openConnection
开启连接,因此,多次开启会话只有第一次开启有用。
SqlSession
的 insert()
和 selectOne()
方法实现中,但凡需要用到连接对象,无一例外,也都是直接从 SqlSessionFactory
类中的 transaction
属性来获取连接对象,因此每个 SqlSession
中的连接其实是同一个,都是第一次开启会话的那个连接。
参考 MyBatis 的实现
先看 SqlSessionFactoryBuilder
,其 build
方法底层实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class SqlSessionFactoryBuilder { public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null); }
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { if (inputStream != null) { inputStream.close(); } } catch (IOException e) { } } }
public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
}
|
可以看到,执行 build
方法,最后实际返回的是 DefaultSqlSessionFactory
(该类是 SqlSessionFactory
接口的实现类)。
创建 DefaultSqlSessionFactory
对象需要一个 Configuration config
,而 config
是 DefaultSqlSessionFactory
中唯一定义的一个属性,该属性即 xml 文件解析的结果,其中包含了所有的配置信息,自然就包括了事务管理器相关的配置信息。
1 2 3 4 5 6 7 8 9 10
| public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) { this.configuration = configuration; } }
|
得到 DefaultSqlSessionFactory factory
后,再看 factory.openSession()
方法的底层实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
@Override public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); }
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } }
|
关注 openSessionFromDataSource
方法,重点梳理一下 try
代码块中的内容。
1 2 3 4 5 6 7 8 9
| final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
|
可以看到,每次 openSession()
执行,实际上底层都会创建一个新的事务管理器,并将该事务管理器封装到 DefaultSqlSession
即 SQL 会话对象中返回(同样 SqlSession
只是接口,DefaultSqlSession
才是具体实现)。
为什么 configuration.getEnvironment()
获取的是默认环境呢?这需要深入到 XML 解析的过程。
具体看 XMLConfigBuilder 中的 environmentsElement 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| private void environmentsElement(XNode context) throws Exception { if (context != null) { if (environment == null) { environment = context.getStringAttribute("default"); } for (XNode child : context.getChildren()) { String id = child.getStringAttribute("id"); if (isSpecifiedEnvironment(id)) { TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); DataSource dataSource = dsFactory.getDataSource(); Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); configuration.setEnvironment(environmentBuilder.build()); break; } } } }
|
综上梳理可得到如下关系:
改进 GodBatis
MyBatis 的设计相对复杂,这里仅借鉴其思想来改进 GodBatis 在事务管理器上的设计问题,改造为:
新增事务管理器工厂相关类和接口
新增 TransactionFactory
接口,接口提供 newTransaction
方法:
1 2 3
| public interface TransactionFactory { Transaction newTransaction(DataSource dataSource, boolean autoCommit); }
|
提供接口实现类 JdbcTransactionFactory
和 ManagedTransactionFactory
:
1 2 3 4 5 6
| public class JdbcTransactionFactory implements TransactionFactory { @Override public Transaction newTransaction(DataSource dataSource, boolean autoCommit) { return new JdbcTransaction(dataSource, autoCommit); } }
|
1 2 3 4 5 6
| public class ManagedTransactionFactory implements TransactionFactory { @Override public Transaction newTransaction(DataSource dataSource, boolean autoCommit) { return null; } }
|
改进 SqlSessionFactory
实现
SqlSessionFactory
中不再直接定义 Transaction
属性(注释掉),而是定义 TransactionFactory
属性。在 openSession()
中,需要 transactionFactory.newInstance()
来创建事务管理器对象,newInstance()
方法需要数据源对象,因此额外新增定义 DataSource
属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| public class SqlSessionFactory {
private TransactionFactory transactionFactory;
private DataSource dataSource;
private Map<String, MappedStatement> mappedStatements;
public SqlSession openSession() { Transaction transaction = transactionFactory.newTransaction(dataSource, false); SqlSession sqlSession = new SqlSession(transaction, mappedStatements); return sqlSession; }
public SqlSessionFactory() { }
public SqlSessionFactory(TransactionFactory transactionFactory, DataSource dataSource, Map<String, MappedStatement> mappedStatements) { this.transactionFactory = transactionFactory; this.dataSource = dataSource; this.mappedStatements = mappedStatements; } }
|
注意,事务管理器 Transaction
是在 openSession()
时才创建,创建后封装到 SqlSession
对象中。
改进 JdbcTransaction
实现
老杜原来的 JdbcTransaction
实现其实没啥大问题,不过既然都看过 Mybatis 源码了,那就尽量和源码保持一致吧。
首先是事务管理器接口 Transaction
:
没必要把 openConnection()
方法也定义到接口中,然后在 openSession()
中手动开启连接,这样显得有些多余了,这个方法完全可以隐藏在 getConnection()
实现中,对外只提供 getConnection()
方法即可(外层更关心的是获取连接对象来执行 SQL 语句)!
所有方法都加上了 SQLException
,异常直接抛出,不再内部捕获打印了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public interface Transaction {
void commit() throws SQLException ;
void rollback() throws SQLException ;
void close() throws SQLException ;
Connection getConnection() throws SQLException; }
|
然后 JdbcTransaction
实现作相应调整:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| public class JdbcTransaction implements Transaction {
private DataSource dataSource;
private boolean autoCommit;
private Connection connection;
public JdbcTransaction(DataSource dataSource, boolean autoCommit) { this.dataSource = dataSource; this.autoCommit = autoCommit; }
@Override public void commit() throws SQLException { if (connection != null && !connection.getAutoCommit()) { connection.commit(); } }
@Override public void rollback() throws SQLException { if (connection != null && !connection.getAutoCommit()) { connection.rollback(); } }
@Override public void close() throws SQLException { if (connection != null) { connection.close(); } }
@Override public Connection getConnection() throws SQLException { if (connection == null) { openConnection(); } return connection; }
private void openConnection() throws SQLException { connection = dataSource.getConnection(); connection.setAutoCommit(autoCommit); } }
|
改进 SqlSessionFactory.build()
实现
SqlSessionFactory
的属性有变化,SqlSessionFactoryBuilder
的 build
方法需要略作调整。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
| public class SqlSessionFactoryBuilder { public SqlSessionFactoryBuilder() { }
public SqlSessionFactory build(InputStream in) { SqlSessionFactory factory = null; try { SAXReader reader = new SAXReader(); Document document = reader.read(in); Element environments = (Element) document.selectSingleNode("/configuration/environments"); String defaultEnvId = environments.attributeValue("default"); Element environment = (Element) document.selectSingleNode("/configuration/environments/environment[@id='" + defaultEnvId + "']");
Element transactionManagerElt = environment.element("transactionManager"); Element dataSourceElt = environment.element("dataSource"); List<String> resourceXmlPathList = environment.selectNodes("//mapper").stream() .map(node -> ((Element) node).attributeValue("resource")) .collect(Collectors.toList());
TransactionFactory transactionFactory = getTransactionFactory(transactionManagerElt); DataSource dataSource = getDataSource(dataSourceElt); Map<String, MappedStatement> mappedStatements = getMappedStatements(resourceXmlPathList);
factory = new SqlSessionFactory(transactionFactory, dataSource, mappedStatements); } catch (Exception e) { e.printStackTrace(); } return factory; }
private DataSource getDataSource(Element dataSourceElt) { DataSource dataSource = null;
String type = dataSourceElt.attributeValue("type").trim().toUpperCase(); List<Element> propertyElts = dataSourceElt.elements(); HashMap<String, String> map = new HashMap<>(); propertyElts.forEach(propertyElt -> { String name = propertyElt.attributeValue("name"); String value = propertyElt.attributeValue("value"); map.put(name, value); });
if (Const.UN_POOLED_DATASOURCE.equals(type)) { dataSource = new UnPooledDataSource(map.get("driver"), map.get("url"), map.get("username"), map.get("password")); } if (Const.POOLED_DATASOURCE.equals(type)) { dataSource = new PooledDataSource(); } if (Const.JNDI_DATASOURCE.equals(type)) { dataSource = new JndiDataSource(); } return dataSource; }
private TransactionFactory getTransactionFactory(Element transactionManagerElt) { TransactionFactory transactionFactory = null;
String type = transactionManagerElt.attributeValue("type").trim().toUpperCase(); if (Const.JDBC_TRANSACTION.equals(type)) { transactionFactory = new JdbcTransactionFactory(); } if (Const.MANAGED_TRANSACTION.equals(type)) { transactionFactory = new ManagedTransactionFactory(); }
return transactionFactory; }
private Map<String, MappedStatement> getMappedStatements(List<String> resourceXmlPathList) { Map<String, MappedStatement> mappedStatements = new HashMap<>();
resourceXmlPathList.forEach(resourceXmlPath -> { try { SAXReader reader = new SAXReader(); Document document = reader.read(Resources.getResourceAsStream(resourceXmlPath)); Element mapper = document.getRootElement(); String namespace = mapper.attributeValue("namespace"); List<Element> elements = mapper.elements(); elements.forEach(element -> { String id = element.attributeValue("id"); String sqlId = namespace + "." + id; String resultType = element.attributeValue("resultType"); String sql = element.getTextTrim(); MappedStatement mappedStatement = new MappedStatement(sql, resultType); mappedStatements.put(sqlId, mappedStatement); }); } catch (Exception e) { e.printStackTrace(); } }); return mappedStatements; } }
|
改进 SqlSession
实现
SqlSession
的实现也需要调整,原来老杜是直接将 SqlSessionFactory
定义为属性。这里修改为,定义 Transaction
和 Map<String, MappedStatement>
两个属性,这样 insert()
和 selectOne()
方法也要相应调整。
另外,insert()
方法中给 jdbcSql
的 ?
占位符传值这块,我是直接用正则表达式来提取 #{}
中的属性名的,区别于老杜那种反复 indexOf
+ subString
的方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
| public class SqlSession {
private Transaction transaction;
private Map<String, MappedStatement> mappedStatements;
public SqlSession(Transaction transaction, Map<String, MappedStatement> mappedStatements) { this.transaction = transaction; this.mappedStatements = mappedStatements; }
public int insert(String sqlId, Object pojo) { int count = 0; try { Connection connection = transaction.getConnection(); String godbatisSql = mappedStatements.get(sqlId).getSql(); String jdbcSql = godbatisSql.replaceAll("#\\{[a-zA-Z0-9_$\\s]+}", "?"); PreparedStatement ps = connection.prepareStatement(jdbcSql);
Pattern pattern = Pattern.compile("#\\{([a-zA-Z0-9_$\\s]+)}"); Matcher matcher = pattern.matcher(godbatisSql); int index = 1; while (matcher.find()) { String propertyName = matcher.group(1).trim(); String getterName = "get" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1); Method getter = pojo.getClass().getDeclaredMethod(getterName); Object propertyValue = getter.invoke(pojo); ps.setString(index, propertyValue.toString()); index++; } count = ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); }
return count; }
public Object selectOne(String sqlId, Object param) { Object obj = null; try { Connection connection = transaction.getConnection();
MappedStatement mappedStatement = mappedStatements.get(sqlId); String resultType = mappedStatement.getResultType(); String godbatisSql = mappedStatement.getSql();
String jdbcSql = godbatisSql.replaceAll("#\\{([a-zA-Z0-9_$]+)}", "?"); PreparedStatement ps = connection.prepareStatement(jdbcSql);
ps.setString(1, param.toString());
ResultSet rs = ps.executeQuery();
if (rs.next()) { Class<?> resultTypeClass = Class.forName(resultType); obj = resultTypeClass.getDeclaredConstructor().newInstance(); ResultSetMetaData metaData = rs.getMetaData(); int columnCount = metaData.getColumnCount(); for (int i = 0; i < columnCount; i++) { String propertyName = metaData.getColumnName(i + 1); String setterName = "set" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1); Method setter = resultTypeClass.getDeclaredMethod(setterName, String.class); setter.invoke(obj, rs.getString(propertyName)); } }
} catch (Exception e) { e.printStackTrace(); } return obj; }
public void commit() { try { transaction.commit(); } catch (SQLException e) { e.printStackTrace(); } }
public void rollback() { try { transaction.rollback(); } catch (SQLException e) { e.printStackTrace(); } }
public void close() { try { transaction.close(); } catch (SQLException e) { e.printStackTrace(); } } }
|
其他
编写测试程序时,需要导入 junit
和 mysql-connector-java
依赖,注意 mysql-connector-java
依赖也要加上 <scope>test</scope>
,表示 msyql 驱动仅用于测试,后续打包时,mysql 驱动不应该被打包进去!后续 godbatis 框架给人使用时,由用户指定使用什么驱动,比如如 oracle 驱动或 mysql 驱动等。
另外,最好将原本放在 main/resources
下的配置文件,放到 test/resources
(而不是删除掉),这些配置文件将用于测试程序,这样打包源码后,测试程序依然可以正常运行。