在上一节中,我们解析了mapperXML文件中的SQL配置信息,将其填充如Configuration中的MapperRegistry和mappedStatements中,可以在调用代理对象的方法返回其在XML文件中配置的sql语句
我们今天的需求是:
- 解析配置文件中关于数据源的信息
- JDBC事务管理接口(一个成熟的数据库访问操作应该具有事务管理能力,比如commit、close、rollback),并用工厂方法模式包装JDBC事务实现,为每一个事务实现提供一个对应工厂
- 引入 Druid 连接池
- 在 SqlSession 执行SQL操作时顺利访问数据源。
0 依赖介绍
Druid
Druid 是阿里巴巴开发的数据库连接池和数据库监控组件,用于管理和优化 Java 应用程序中的数据库连接,可以用于增强 JDBC 的功能。
主要职责:
- 数据库连接池管理:Druid 作为数据库连接池,能够高效地管理数据库连接的创建、分配和销毁,从而提升应用的并发性能和资源利用效率。
- SQL 监控:Druid 内置了 SQL 监控功能,支持实时查看 SQL 执行频率、执行时间、慢查询等统计数据,可以帮助开发人员优化 SQL 性能。
- 防 SQL 注入:Druid 可以检测并拦截 SQL 注入攻击,增强数据库访问的安全性。
- 扩展 JDBC:Druid 兼容 JDBC API,可以无缝替换 JDBC 使用的 DriverManager,通过代理模式拦截 JDBC 操作,加入监控、日志等功能。
1 事务管理接口设计与JDBC事务实现
定义标准的事务管理接口,可以有不同的事务实现方式。比如:
- JDBC事务
- 托管事务(框架提供的)
public interface Transaction {
Connection getConnection() throws SQLException;
void rollback() throws SQLException;
void close() throws SQLException;
void commit() throws SQLException;
}
JDBC事务实现
JDBC 事务是基于 JDBC API 的原生事务管理方式,直接在代码中控制数据库连接的事务行为。
- 原理:开发者在代码中通过 JDBC 的 Connection 对象手动开始、提交和回滚事务。通过设置 Connection 的 autoCommit 属性为 false,可以手动控制事务的开始和结束。
回顾一下 JDBC事务隔离级别:
事务并发执行时可能会遇到脏读、不可重复读、幻读的现象:
- 脏读:读到其他事务未提交的数据
- 不可重复读:在一个事务中前后读取的数据不一致
- 幻读:在一个事务中读区的记录数量不一致(就像出现幻觉一样)
JDBC提供5种事务隔离级别接口(具体的事务管理由InnoDB存储引擎执行)来规避这些现象,隔离级别越高,性能效率越低:
- NONE:没有事务管理,不提供事务隔离。每条 SQL 语句都独立执行,立即生效,无法rollback或commit。
- READ_UNCOMMITTED:读未提交,允许读取未提交的更改,可能发生脏读。
- READ_COMMITTED:读提交,只能读取已提交的更改,防止脏读。
- REPEATABLE_READ:可重复读,防止脏读和不可重复读,但可能发生幻读。
- SERIALIZABLE:完全隔离的级别,防止脏读、不可重复读和幻读,是最严格的隔离级别。
在这里我们创建枚举对象来管理JDBC提供的事务隔离级别,方便代码中清晰便捷使用。
【工厂方法模式】为每一种事务实现提供工厂
如果不提供工厂,每次创建 Transaction 实例都需要直接调用构造方法,这样做会导致下面问题:
- 高耦合性:调用方必须直接依赖 Transaction 的具体实现,这会导致代码难以维护或扩展。例如,如果要切换 Transaction 的实现,则需要修改所有调用的地方。
- 重复代码:每次创建 Transaction 实例时都要重复设置隔离级别等属性,增加代码重复性和维护成本。
- 难以扩展:未来若要增加新的 Transaction 类型或配置方案,就需要大幅度修改现有代码,而不是通过简单地添加新的工厂实现。
public interface TransactionFactory {
/**
* 根据 Connection 创建 Transaction
* @param conn Existing database connection
* @return Transaction
*/
Transaction getTransaction(Connection conn);
/**
* 根据数据源和事务隔离级别创建 Transaction
* @param dataSource DataSource to take the connection from
* @param level Desired isolation level
* @param autoCommit Desired autocommit
* @return Transaction
*/
Transaction getTransaction(DataSource dataSource, Boolean autoCommit, TransactionIsolationLevel level);
}
简单工厂模式 v.s. 工厂方法模式
这里用到的是工厂方法模式,工厂方法模式的优越之处体现在符合开闭原则,且提供更高的耦合。
- 在简单工厂模式下,一个简单工厂对应所有接口实现。如果要新增新的实现,就要修改这个简单工厂中的switch代码;
- 而在工厂方法模式下,一个工厂对应一个实现。如果要新增新的实现,只需要新增新的接口实现和工厂即可。
方面 | 简单工厂模式 | 工厂方法模式 |
---|---|---|
扩展性 | 不符合开闭原则,修改工厂类 | 符合开闭原则,新增工厂类即可 |
复杂度 | 简单,适合少量产品 | 较复杂,适合产品种类多的场景 |
适用场景 | 产品少,且变化不频繁 | 产品多且频繁变化 |
典型使用情况 | 通常在简单场景使用 | 在较大系统或灵活需求场景中使用 |
2 类型别名注册器
创建一个类型别名注册器,将事务工厂和数据源工厂(比如JDBC、DRUID等)注册进去,方便解析配置时候直接在别名注册器中通过别名找到对应的类型。
3 XML 数据源配置信息解析
- 在 Configuration 对象中加入相应字段
environment
,存储事务管理配置和数据源配置。
private void datasourceElement(Element environments) throws Exception {
String defaultEnvironment = environments.attributeValue("default");
List<Element> environmentList = environments.elements("environment");
for (Element e : environmentList) {
String id = e.attributeValue("id");
if (defaultEnvironment.equals(id)) {
// 找到了配置的环境
// 事务管理器
String transactionManagerType = e.element("transactionManager").attributeValue("type");
TransactionFactory transactionFactory = (TransactionFactory) typeAliasRegistry.resolveAlias(transactionManagerType).newInstance();
// 数据源
Element dataSourceElement = e.element("dataSource");
DataSourceFactory dataSourceFactory = (DataSourceFactory) typeAliasRegistry.resolveAlias(dataSourceElement.attributeValue("type")).newInstance();
// 解析配置
List<Element> propertyList = dataSourceElement.elements("property");
Properties props = new Properties();
for (Element property : propertyList) {
props.setProperty(property.attributeValue("name"), property.attributeValue("value"));
}
dataSourceFactory.setProperties(props);
DataSource dataSource = dataSourceFactory.getDataSource();
// 构建环境 用建造者模式,保证Environment实例的完整性
Environment.Builder builder = new Environment.Builder(id)
.dataSource(dataSource)
.transactionFactory(transactionFactory);
configuration.setEnvironment(builder.build());
}
}
}
在mybatis中Configuration对象起到流程中心的作用,负责管理和维护 MyBatis 各个模块的配置信息。可以看到mapper映射 mapperRegistry、方法映射 mappedStatements、事务管理和数据源配置 Environment 都保存在Configuration 中。之后将Configuration作为依赖注入到SqlSession,提供完整的SQL操作上下文,创建高度定制化的SQL会话。
4 SqlSession 提供完整Sql操作(select)
SqlSession按照传入的Configuration
配置创建数据库连接,匹配并执行数据库操作。
这里暂时将语句写死,后面再进行完善。
@Override
public <T> T selectOne(String statement, Object parameter) {
try {
MappedStatement mappedStatement = configuration.getMappedStatement(statement);
Environment environment = configuration.getEnvironment();
Connection conn = environment.getDataSource().getConnection();
BoundSql boundSql = mappedStatement.getBoundSql();
PreparedStatement preparedStatement = conn.prepareStatement(boundSql.getSql());
// todo:暂时将sql写死在这里
preparedStatement.setLong(1, Long.parseLong(((Object[]) parameter)[0].toString()));
ResultSet resultSet = preparedStatement.executeQuery();
List<T> objList = resultSet2Obj(resultSet, Class.forName(boundSql.getResultType()));
return objList.get(0);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
5 效果测试
@Test
public void test_MapperProxyFactory_day4() throws IOException {
// 获取到配置文件的Reader
Reader reader = Resources.getResourceAsReader("mybatis-config-datasource.xml");
// 通过sqlSessionFactory的建造者 按照配置文件Reader 得到 sqlSessionFactory;
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
/**
* sqlSessionFactoryBuilder.build() 去解析配置文件 装配出Configuration
* 将提供给sqlSessionFactory,作为定制化SqlSession的需求
*/
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(reader);
// 得到sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 得到代理对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
User result = userDao.queryUserInfoById("10001");
logger.info("测试结果:{}", JSON.toJSONString(result));
}
完成本节后,我们可以解析配置文件中的数据源和事务管理器配置,包装成Environment对象,并注入到Configuration中。由此,SqlSession工厂创建出类的SqlSession中的Configuration内含数据源、事务的上下文信息,可以创建数据源连接并执行相应的sql操作。