作者:小傅哥 博客:https://bugstack.cn
❝沉淀、分享、成长,让自己和他人都能有所收获!😜❞
你只是在解释过程,而他是在阐述高度!
如果不是长时间的沉淀、积累和储备,我一定也没有办法用更多的维度和更多的视角来对一个问题进行多方面阐述。就像你我;越过峭壁山川,才知枕席还师的通达平坦。领略过雷声千嶂落,雨色万峰来,才闻到八表流云澄夜色,九霄华月动春城的宁静。
所以引申到编程开发,往简单了说就是写写代码,改改bug。但如果就局限在只是写写代码,其实很难领略到那些众多设计思想和复杂问题中,庖丁解牛般的酣畅淋漓。而这些酣畅的体验,都需要你对技术的拓展学习和深度探索,从众多的优秀源码框架中吸收经验。反复揣摩、反复尝试,终有那么一个时间点,你会有种悟了的感觉。而这些一个个感觉的积累,就能帮助你以后在面试、述职、答辩、分享、汇报等场景中,说出更有深度的技术思想和类比设计对照,站在更高的角度俯视业务场景的走向和给出长远的架构方案。
实现到本章节前,关于 Mybatis ORM 框架的大部分核心结构已经逐步体现出来了,包括;解析、绑定、映射、事务、执行、数据源等。但随着更多功能的逐步完善,我们需要对模块内的实现进行细化处理,而不单单只是完成功能逻辑。这就有点像把 CRUD 使用设计原则进行拆分解耦,满足代码的易维护和可扩展性。而这里我们首先着手要处理的就是关于 XML 解析的问题,把之前粗糙的实现进行细化,满足我们对解析时一些参数的整合和处理。
图 9-1 ORM框架XML解析映射关系
参照设计原则,对于 XML 信息的读取,各个功能模块的流程上应该符合单一职责,而每一个具体的实现又得具备迪米特法则,这样实现出来的功能才能具有良好的扩展性。通常这类代码也会看着很干净 那么基于这样的诉求,我们则需要给解析过程中,所属解析的不同内容,按照各自的职责类进行拆解和串联调用。整体设计如图 9-2
图 9-2 XML 配置构建器解析过程
映射构建器
和语句构建器
,按照不同的职责分别进行解析。mybatis-step-08
└── src
├── main
│ └── java
│ └── cn.bugstack.mybatis
│ ├── binding
│ ├── builder
│ │ ├── xml
│ │ │ ├── XMLConfigBuilder.java
│ │ │ ├── XMLMapperBuilder.java
│ │ │ └── XMLStatementBuilder.java
│ │ ├── BaseBuilder.java
│ │ ├── ParameterExpression.java
│ │ ├── SqlSourceBuilder.java
│ │ └── StaticSqlSource.java
│ ├── datasource
│ ├── executor
│ │ ├── resultset
│ │ │ ├── DefaultResultSetHandler.java
│ │ │ └── ResultSetHandler.java
│ │ ├── statement
│ │ │ ├── BaseStatementHandler.java
│ │ │ ├── PreparedStatementHandler.java
│ │ │ ├── SimpleStatementHandler.java
│ │ │ └── StatementHandler.java
│ │ ├── BaseExecutor.java
│ │ ├── Executor.java
│ │ └── SimpleExecutor.java
│ ├── io
│ ├── mapping
│ │ ├── BoundSql.java
│ │ ├── Environment.java
│ │ ├── MappedStatement.java
│ │ ├── ParameterMapping.java
│ │ ├── SqlCommandType.java
│ │ └── SqlSource.java
│ ├── parsing
│ │ ├── GenericTokenParser.java
│ │ └── TokenHandler.java
│ ├── reflection
│ ├── session
│ │ ├── defaults
│ │ │ ├── DefaultSqlSession.java
│ │ │ └── DefaultSqlSessionFactory.java
│ │ ├── Configuration.java
│ │ ├── ResultHandler.java
│ │ ├── SqlSession.java
│ │ ├── SqlSessionFactory.java
│ │ ├── SqlSessionFactoryBuilder.java
│ │ └── TransactionIsolationLevel.java
│ ├── transaction
│ └── type
│ ├── JdbcType.java
│ ├── TypeAliasRegistry.java
│ ├── TypeHandler.java
│ └── TypeHandlerRegistry.java
└── test
├── java
│ └── cn.bugstack.mybatis.test.dao
│ ├── dao
│ │ └── IUserDao.java
│ ├── po
│ │ └── User.java
│ └── ApiTest.java
└── resources
├── mapper
│ └──User_Mapper.xml
└── mybatis-config-datasource.xml
工程源码:公众号「bugstack虫洞栈」,回复:手写Mybatis,获取完整源码
XML 语句解析构建器,核心逻辑类关系,如图 9-3 所示
图 9-3 XML 语句解析构建器,核心逻辑类关系
<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">...</select>
配置语句,提取参数类型、结果类型,而这里的语句处理流程稍微较长,因为需要用到脚本语言驱动器,进行解析处理,创建出 SqlSource 语句信息。SqlSource 包含了 BoundSql,同时这里扩展了 ParameterMapping 作为参数包装传递类,而不是仅仅作为 Map 结构包装。因为通过这样的方式,可以封装解析后的 javaType/jdbcType 信息提供单独的 XML 映射构建器 XMLMapperBuilder 类,把关于 Mapper 内的 SQL 进行解析处理。提供了这个类以后,就可以把这个类的操作放到 XML 配置构建器,XMLConfigBuilder#mapperElement 中进行使用了。具体我们看下如下代码。
源码详见:cn.bugstack.mybatis.builder.xml.XMLMapperBuilder
public class XMLMapperBuilder extends BaseBuilder {
/**
* 解析
*/
public void parse() throws Exception {
// 如果当前资源没有加载过再加载,防止重复加载
if (!configuration.isResourceLoaded(resource)) {
configurationElement(element);
// 标记一下,已经加载过了
configuration.addLoadedResource(resource);
// 绑定映射器到namespace
configuration.addMapper(Resources.classForName(currentNamespace));
}
}
// 配置mapper元素
// <mapper namespace="org.mybatis.example.BlogMapper">
// <select id="selectBlog" parameterType="int" resultType="Blog">
// select * from Blog where id = #{id}
// </select>
// </mapper>
private void configurationElement(Element element) {
// 1.配置namespace
currentNamespace = element.attributeValue("namespace");
if (currentNamespace.equals("")) {
throw new RuntimeException("Mapper's namespace cannot be empty");
}
// 2.配置select|insert|update|delete
buildStatementFromContext(element.elements("select"));
}
// 配置select|insert|update|delete
private void buildStatementFromContext(List<Element> list) {
for (Element element : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, element, currentNamespace);
statementParser.parseStatementNode();
}
}
}
在 XMLMapperBuilder#parse 的解析中,主要体现在资源解析判断、Mapper解析和绑定映射器到;
cn.bugstack.mybatis.test.dao.IUserDao
绑定到 Mapper 上。也就是注册到映射器注册机里。配置构建器,调用映射构建器,源码详见:cn.bugstack.mybatis.builder.xml.XMLMapperBuilder
public class XMLConfigBuilder extends BaseBuilder {
/*
* <mappers>
* <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
* <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
* <mapper resource="org/mybatis/builder/PostMapper.xml"/>
* </mappers>
*/
private void mapperElement(Element mappers) throws Exception {
List<Element> mapperList = mappers.elements("mapper");
for (Element e : mapperList) {
String resource = e.attributeValue("resource");
InputStream inputStream = Resources.getResourceAsStream(resource);
// 在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource);
mapperParser.parse();
}
}
}
XMLStatementBuilder 语句构建器主要解析 XML 中 select|insert|update|delete
中的语句,当前我们先以 select 解析为案例,后续再扩展其他的解析流程。
源码详见:cn.bugstack.mybatis.builder.xml.XMLStatementBuilder
public class XMLStatementBuilder extends BaseBuilder {
//解析语句(select|insert|update|delete)
//<select
// id="selectPerson"
// parameterType="int"
// parameterMap="deprecated"
// resultType="hashmap"
// resultMap="personResultMap"
// flushCache="false"
// useCache="true"
// timeout="10000"
// fetchSize="256"
// statementType="PREPARED"
// resultSetType="FORWARD_ONLY">
// SELECT * FROM PERSON WHERE ID = #{id}
//</select>
public void parseStatementNode() {
String id = element.attributeValue("id");
// 参数类型
String parameterType = element.attributeValue("parameterType");
Class<?> parameterTypeClass = resolveAlias(parameterType);
// 结果类型
String resultType = element.attributeValue("resultType");
Class<?> resultTypeClass = resolveAlias(resultType);
// 获取命令类型(select|insert|update|delete)
String nodeName = element.getName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 获取默认语言驱动器
Class<?> langClass = configuration.getLanguageRegistry().getDefaultDriverClass();
LanguageDriver langDriver = configuration.getLanguageRegistry().getDriver(langClass);
SqlSource sqlSource = langDriver.createSqlSource(configuration, element, parameterTypeClass);
MappedStatement mappedStatement = new MappedStatement.Builder(configuration, currentNamespace + "." + id, sqlCommandType, sqlSource, resultTypeClass).build();
// 添加解析 SQL
configuration.addMappedStatement(mappedStatement);
}
}
select|insert|update|delete
),以及使用语言驱动器处理和封装SQL信息,当解析完成后写入到 Configuration 配置文件中的 Map<String, MappedStatement>
映射语句存放中。在 XMLStatementBuilder#parseStatementNode 语句构建器的解析中,可以看到这么一块,获取默认语言驱动器并解析SQL的操作。其实这部分就是 XML 脚步语言驱动器所实现的功能,在 XMLScriptBuilder 中处理静态SQL和动态SQL,不过目前我们只是实现了其中的一部分,待后续这部分框架都完善后在进行扩展,避免一次引入过多的代码。
源码详见:cn.bugstack.mybatis.scripting.LanguageDriver
public interface LanguageDriver {
SqlSource createSqlSource(Configuration configuration, Element script, Class<?> parameterType);
}
XMLLanguageDriver
、RawLanguageDriver
、VelocityLanguageDriver
,这里我们只是实现了默认的第一个即可。源码详见:cn.bugstack.mybatis.scripting.xmltags.XMLLanguageDriver
public class XMLLanguageDriver implements LanguageDriver {
@Override
public SqlSource createSqlSource(Configuration configuration, Element script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
}
源码详见:cn.bugstack.mybatis.scripting.xmltags.XMLScriptBuilder
public class XMLScriptBuilder extends BaseBuilder {
public SqlSource parseScriptNode() {
List<SqlNode> contents = parseDynamicTags(element);
MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
return new RawSqlSource(configuration, rootSqlNode, parameterType);
}
List<SqlNode> parseDynamicTags(Element element) {
List<SqlNode> contents = new ArrayList<>();
// element.getText 拿到 SQL
String data = element.getText();
contents.add(new StaticTextSqlNode(data));
return contents;
}
}
源码详见:cn.bugstack.mybatis.builder.SqlSourceBuilder
public class SqlSourceBuilder extends BaseBuilder {
private static final String parameterProperties = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";
public SqlSourceBuilder(Configuration configuration) {
super(configuration);
}
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
// 返回静态 SQL
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
@Override
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
// 构建参数映射
private ParameterMapping buildParameterMapping(String content) {
// 先解析参数映射,就是转化成一个 HashMap | #{favouriteSection,jdbcType=VARCHAR}
Map<String, String> propertiesMap = new ParameterExpression(content);
String property = propertiesMap.get("property");
Class<?> propertyType = parameterType;
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
return builder.build();
}
}
}
因为以上整个设计和实现,调整了解析过程,以及细化了 SQL 的创建。那么在 MappedStatement 映射语句中,则使用 SqlSource 替换了 BoundSql,所以在 DefaultSqlSession 中也会有相应的调整。
源码详见:cn.bugstack.mybatis.session.defaults.DefaultSqlSession
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
private Executor executor;
@Override
public <T> T selectOne(String statement, Object parameter) {
MappedStatement ms = configuration.getMappedStatement(statement);
List<T> list = executor.query(ms, parameter, Executor.NO_RESULT_HANDLER, ms.getSqlSource().getBoundSql(parameter));
return list.get(0);
}
}
ms.getSqlSource().getBoundSql(parameter)
这样获取后,后面的流程就没有多少变化了。在我们整个解析框架逐步完善后,就会开始对各个字段的属性信息添加进行设置操作。创建一个数据库名称为 mybatis 并在库中创建表 user 以及添加测试数据,如下:
CREATE TABLE
USER
(
id bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
userId VARCHAR(9) COMMENT '用户ID',
userHead VARCHAR(16) COMMENT '用户头像',
createTime TIMESTAMP NULL COMMENT '创建时间',
updateTime TIMESTAMP NULL COMMENT '更新时间',
userName VARCHAR(64),
PRIMARY KEY (id)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into user (id, userId, userHead, createTime, updateTime, userName) values (1, '10001', '1_04', '2022-04-13 00:00:00', '2022-04-13 00:00:00', '小傅哥');
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
mybatis-config-datasource.xml
配置数据源信息,包括:driver、url、username、password<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">
SELECT id, userId, userName, userHead
FROM user
where id = #{id}
</select>
@Test
public void test_SqlSessionFactory() throws IOException {
// 1. 从SqlSessionFactory中获取SqlSession
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
// 2. 获取映射器对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 3. 测试验证
User user = userDao.queryUserInfoById(1L);
logger.info("测试结果:{}", JSON.toJSONString(user));
}
测试结果
07:26:15.049 [main] INFO c.b.m.d.pooled.PooledDataSource - Created connection 1138410383.
07:26:15.192 [main] INFO cn.bugstack.mybatis.test.ApiTest - 测试结果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
Disconnected from the target VM, address: '127.0.0.1:54797', transport: 'socket'
Process finished with exit code 0
- END -
你好,我是小傅哥。一线互联网java
工程师、架构师,开发过交易&营销、写过运营&活动、设计过中间件也倒腾过中继器、IO板卡。不只是写Java语言,也搞过C#、PHP,是一个技术活跃的折腾者。
2022年在知识星球【码农会锁】开发完成基于 DDD 四层架构设计的,《分布式实战项目抽奖系统》。此项目以互联网开发常用技术为主,包括:SpringBoot、Mybatis、Dubbo、MQ、Redis、分库分表、ELK、Docker等,以及大量的真实场景案例和对应的设计模式实战,解决每一个细节问题,非常适合学习实践。