文章目录
- 第九章 细化XML 语句构建器
- 背景
- 技术背景
- 迪米特法则
- 1. 通俗解释:
- 2. 迪米特法则的要点:
- 3. 举例:
- 违反迪米特法则的代码:
- 改进后的代码(符合迪米特法则):
- 业务背景
- 目标
- 设计
- 实现
- 工程代码
- 类图
- 实现步骤
- 1.解耦映射解析
- 2. 语句构建器
- 3. 脚本语言驱动
- 3-1 定义语言驱动接口
- 3-2 XML语言驱动器实现
- 3-3 XML脚本构建器解析
- 3-4 SQL源码构建器
- 4. DefaultSqlSession 调用调整
- 测试
- 事先准备
- 测试用例
- 单元测试
- 测试结果
- 总结
第九章 细化XML 语句构建器
mybatis" />
背景
技术背景
迪米特法则
迪米特法则(Law of Demeter),也被称为最少知识原则,是一种软件设计原则,旨在减少系统中的类和对象之间的耦合度,提高代码的可维护性和可扩展性。通俗来说,迪米特法则的核心思想就是:一个对象应该对其他对象知道得尽量少,只与它直接交互的对象沟通,不要与它知道的所有对象都进行交互。
1. 通俗解释:
假设你是一个班级的老师,你可以直接与学生沟通,但不需要去了解每个学生家里的事情,也不需要直接与学生的父母沟通。你只要关心学生在学校的表现,与你直接相关的信息就够了。
在编程中,假设你有一个对象A,它与另一个对象B进行交互,B又调用了对象C。如果A直接与C交互,那么A就需要了解C的具体实现细节,这样就违反了迪米特法则。按照迪米特法则,A应该只与B交互,B再决定如何与C交互。
2. 迪米特法则的要点:
- 一个对象应该只与以下类型的对象进行交互:
- 自己(自身对象)
- 传入参数的对象
- 自己创建的对象
- 直接拥有的对象(成员变量)
- 不要和“陌生人”打交道。如果你需要了解某个对象的内部实现,最好通过一个简单的接口来访问,而不是直接操作它。
3. 举例:
假设我们有一个汽车对象,它包含一个发动机对象,而发动机对象又包含一个油箱对象。我们希望通过调用汽车的启动方法来启动整个系统。如果汽车直接去调用油箱的某些细节,就违反了迪米特法则。
违反迪米特法则的代码:
java">class Engine {private FuelTank fuelTank;public Engine(FuelTank fuelTank) {this.fuelTank = fuelTank;}public boolean isFuelEmpty() {return fuelTank.getFuelLevel() == 0;}
}class Car {private Engine engine;public Car(Engine engine) {this.engine = engine;}public void start() {if (engine.isFuelEmpty()) {System.out.println("Not enough fuel!");} else {System.out.println("Car is starting...");}}
}
在上面的代码中,Car
类直接调用了engine.isFuelEmpty()
,而engine
又通过fuelTank.getFuelLevel()
来判断油箱的燃料是否用完。Car
类不应该直接与fuelTank
进行交互,它应该只关心engine
是否准备好。
改进后的代码(符合迪米特法则):
java">class FuelTank {private int fuelLevel;public FuelTank(int fuelLevel) {this.fuelLevel = fuelLevel;}public boolean isEmpty() {return fuelLevel == 0;}
}class Engine {private FuelTank fuelTank;public Engine(FuelTank fuelTank) {this.fuelTank = fuelTank;}public boolean canStart() {return !fuelTank.isEmpty();}
}class Car {private Engine engine;public Car(Engine engine) {this.engine = engine;}public void start() {if (engine.canStart()) {System.out.println("Car is starting...");} else {System.out.println("Not enough fuel!");}}
}
在改进后的代码中,Car
类只与Engine
类交互,Engine
类内部会决定如何与FuelTank
类交互。这样,Car
类不需要知道油箱的具体细节,符合迪米特法则,降低了类之间的耦合度。
业务背景
实现到本章节前,关于 Mybatis ORM 框架的大部分核心结构已经逐步体现出来了,包括:解析、绑定、映射、事务、执行、数据源
等。
但随着更多功能的逐步完善,我们需要对模块内的实现进行细化处理,而不单单只是完成功能逻辑。
这就有点像把 CRUD 使用设计原则进行拆分解耦,满足代码的易维护和可扩展性。
而这里我们首先着手要处理的就是关于 XML 解析
的问题,把之前粗糙的实现进行细化,满足我们对解析时一些参数的整合和处理
。
java"> private void mapperElement(Element mappers) throws Exception {//获取mappers标签下的所有mapper标签.List<Element> mapperList = mappers.elements("mapper");for (Element mapper : mapperList) {//获取mapper标签的resource属性.String resource = mapper.attributeValue("resource");Reader reader = Resources.getResourceAsReader(resource);SAXReader saxReader = new SAXReader();Document document = saxReader.read(new InputSource(reader));Element root = document.getRootElement();//命名空间String namespace = root.attributeValue("namespace");//SELECT 解析语句.List<Element> selectNodes = root.elements("select");for (Element node : selectNodes) {String id = node.attributeValue("id");String parameterType = node.attributeValue("parameterType");String resultType = node.attributeValue("resultType");String sql = node.getText();// ?匹配Map<Integer, String> parameter = new HashMap<>();Pattern pattern = Pattern.compile("(#\\{(.*?)})");Matcher matcher = pattern.matcher(sql);//匹配到的参数替换为?for (int i = 1; matcher.find(); i++) {String g1 = matcher.group(1);String g2 = matcher.group(2);parameter.put(i, g2);sql = sql.replace(g1, "?");}String msId = namespace + "." + id;String nodeName = node.getName();SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));// 构建BoundSql.BoundSql boundSql = new BoundSql(sql, parameter, parameterType, resultType);MappedStatement mappedStatement = new MappedStatement.Builder(configuration, msId, sqlCommandType,boundSql).build();// 添加解析SQLconfiguration.addMappedStatement(mappedStatement);}// 注册Mapper映射器.configuration.addMapper(Resources.classForName(namespace));}}
- 这一部分的解析,是XMLConfigBuilder#mapperElement 方法中的操作。能实现功能,但不够规整。如果按照我们平常开发CRUD的习惯,功能代码揉在一块,虽然什么流程都能处理,但后续代码会越来越混乱,最后难逃重构的结果。
- 本章节把上述部分解析处理,使用设计原则将流程和职责进行解耦,并结合我们的当前诉求,优先
处理静态 SQL 内容
。 - 待框架结构逐步完善,再进行一些动态SQL和更多参数类型的处理,满足读者以后在阅读 Mybatis 源码,或者需要开发自己的 X-ORM 框架的时候,可以有一些经验积累。
目标
基于当前框架实现,使用设计原则将XML解析的流程和职责进行解耦,优先处理静态 SQL 内容。
设计
参照设计原则,对于 XML 信息的读取
,各个功能模块的流程上应该符合单一职责
,而每一个具体的实现又得具备迪米特法则
,这样实现出来的功能才能具有良好的扩展性。通常这类代码也会看着很干净 那么基于这样的诉求,我们则需要给解析过程中,所属解析的不同内容,按照各自的职责类进行拆解和串联调用。整体设计如图 :
- 之前的解析代码,把所有的解析都在一个循环中处理。 现在解析过程中,引入 XMLMapperBuilder解析Mapper,引入XMLStatementBuilder 解析MappedStatement,按照不同的职责分别进行解析。
- 在语句构建器中,引入脚本语言驱动器,默认实现的是 XML语言驱动器 XMLLanguageDriver,这个类来具体操作静态和动态 SQL 语句节点的解析。这部分的解析处理实现方式很多,为了保持与 Mybatis 的统一,我们直接参照源码 Ognl 的方式进行处理。对应的类是 DynamicContext。
- 这里所有的解析铺垫,通过解耦的方式实现,都是为了后续在 executor 执行器中,更加方便的处理 setParameters 参数的设置。后面参数的设置,也会涉及到前面我们实现的元对象反射工具类的使用。
实现
工程代码
类图
- 解耦原 XMLConfigBuilder 中对 XML 的解析,扩展映射构建器、语句构建器,处理 SQL 的提取和参数的包装,整个核心流图以 XMLConfigBuilder#mapperElement 为入口进行串联调用。
- 在 XMLStatementBuilder#parseStatementNode 方法中解析
<select> ...</select>
配置语句内容,提取参数类型、结果类型。 - 这里的语句处理流程稍微较长,因为需要用到脚本语言驱动器,进行解析处理,创建出 SqlSource 语句信息。
- SqlSource 包含了 BoundSql,同时扩展了
ParameterMapping 作为参数包装传递类
,而不是仅仅作为 Map 结构包装。因为通过这样的方式,可以封装解析后的 javaType/jdbcType 信息
。
实现步骤
-
1.解耦映射解析
XMLMapperBuilder
- 提供单独的 XML 映射构建器 XMLMapperBuilder 类,把关于 Mapper 内的 SQL 进行解析处理。
- configuration.isResourceLoaded 资源判断避免重复解析,做了个记录。
- configuration.addMapper 绑定映射器主要是把 namespace
cn.suwg.mybatis.test.dao.IUserDao
绑定到 Mapper 上。也就是注册到映射器注册机里。
java">public class XMLMapperBuilder extends BaseBuilder {private Element element;private String resource;private String currentNamespace;public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource) throws DocumentException {this(new SAXReader().read(inputStream), configuration, resource);}private XMLMapperBuilder(Document document, Configuration configuration, String resource) {super(configuration);this.element = document.getRootElement();this.resource = resource;}/*** 解析.** @throws Exception*/public void parse() throws Exception {// 如果当前资源没有加载过再加载,防止重复加载if (!configuration.isResourceLoaded(resource)) {// 配置mapper元素configurationElement(element);// 标记一下,已经加载过了configuration.addLoadedResource(resource);// 绑定映射器到namespaceconfiguration.addMapper(Class.forName(currentNamespace));}}// <mapper namespace="org.mybatis.example.BlogMapper">// <select id="selectBlog" parameterType="int" resultType="Blog">// select * from Blog where id = #{id}// </select>// </mapper>/*** 配置mapper元素.*/private void configurationElement(Element element) {// 1.配置namespacecurrentNamespace = element.attributeValue("namespace");if (currentNamespace.equals("")) {throw new RuntimeException("Mapper's namespace cannot be empty");}// 2.解析sql语句,配置select | insert | update | deletebuildStatementFromContext(element.elements("select"));}private void buildStatementFromContext(List<Element> list) {for (Element element : list) {final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, element, currentNamespace);statementParser.parseStatementNode();}}}
XMLMapperBuilder
- 在 XMLConfigBuilder#mapperElement 中,把原来流程化的处理进行解耦,调用 XMLMapperBuilder#parse 方法进行解析处理。
java">public class XMLConfigBuilder extends BaseBuilder {private Element root;...private void mapperElement(Element mappers) throws Exception {//获取mappers标签下的所有mapper标签.List<Element> mapperList = mappers.elements("mapper");for (Element mapper : mapperList) {//获取mapper标签的resource属性.String resource = mapper.attributeValue("resource");//获取输入流InputStream inputStream = Resources.getResourceAsStream(resource);XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource);mapperParser.parse();}}
}
-
2. 语句构建器
XMLStatementBuilder
-
XMLStatementBuilder 语句构建器主要解析 XML 中
select|insert|update|delete
中的语句,当前我们先以 select 解析为案例,后续再扩展其他的解析流程。 -
这部分内容的解析,实现了 XMLConfigBuilder中关于 Mapper 语句的解析,通过这样解耦设计,会让整个流程更加清晰。
-
XMLStatementBuilder#parseStatementNode 方法是解析 SQL 语句节点的过程,包括了语句的ID、参数类型、结果类型、命令(
select|insert|update|delete
),以及使用语言驱动器处理和封装SQL信息,当解析完成后写入到 Configuration 配置文件中的Map<String, MappedStatement>
映射语句存放中。
java">public class XMLStatementBuilder extends BaseBuilder {private String currentNamespace;private Element element;public XMLStatementBuilder(Configuration configuration, Element element, String currentNamespace) {super(configuration);this.element = element;this.currentNamespace = currentNamespace;}//解析语句(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();// 添加解析 SQLconfiguration.addMappedStatement(mappedStatement);}}
3. 脚本语言驱动
- 在 XMLStatementBuilder#parseStatementNode 语句构建器的解析中,本章节获取默认语言驱动器并解析SQL的操作。
- 这部分就是 XML 脚本语言驱动器所实现的功能,在 XMLScriptBuilder 中处理静态SQL和动态SQL,不过目前我们只是实现了其中的一部分,待后续这部分框架都完善后在进行扩展,避免一次引入过多的代码。
3-1 定义语言驱动接口
LanguageDriver
- 定义脚本语言驱动接口,提供创建 SQL 信息的方法,入参包括了配置、元素、参数。其实它的实现类一共有3个;
XMLLanguageDriver
、RawLanguageDriver
、VelocityLanguageDriver
,这里我们只是实现了默认的第一个即可。
java">public interface LanguageDriver {SqlSource createSqlSource(Configuration configuration, Element script, Class<?> parameterType);
}
3-2 XML语言驱动器实现
XMLLanguageDriver
- 关于 XML 语言驱动器的实现比较简单,只是封装了对 XMLScriptBuilder 的调用处理。
java">public class XMLLanguageDriver implements LanguageDriver {@Overridepublic SqlSource createSqlSource(Configuration configuration, Element script, Class<?> parameterType) {// 用XML脚本构建器解析XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);return builder.parseScriptNode();}
}
3-3 XML脚本构建器解析
XMLScriptBuilder
- XMLScriptBuilder#parseScriptNode 解析SQL节点的处理其实没有太多复杂的内容,主要是对
RawSqlSource 的包装处理
。其他小细节可以阅读源码进行学习
java">public class XMLScriptBuilder extends BaseBuilder {private Element element;private boolean isDynamic;private Class<?> parameterType;public XMLScriptBuilder(Configuration configuration, Element element, Class<?> parameterType) {super(configuration);this.element = element;this.parameterType = parameterType;}public SqlSource parseScriptNode() {List<SqlNode> contents = parseDynamicTags(element);MixedSqlNode rootSqlNode = new MixedSqlNode(contents);return new RawSqlSource(configuration, rootSqlNode, parameterType);}private List<SqlNode> parseDynamicTags(Element element) {List<SqlNode> contents = new ArrayList<>();// element.getText 拿到 SQLString data = element.getText();contents.add(new StaticTextSqlNode(data));return contents;}}
3-4 SQL源码构建器
SqlSourceBuilder
- BoundSql.parameterMappings 的参数就是来自于 ParameterMappingTokenHandler#buildParameterMapping 方法进行构建处理的。
- 具体的 javaType、jdbcType 会体现到 ParameterExpression 参数表达式中完成解析操作。这个解析过程直接是 Mybatis 自己的源码,整个过程功能较单一,直接对照学习即可
java">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);// 返回静态 SQLreturn new StaticSqlSource(configuration, sql, handler.getParameterMappings());}private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {private List<ParameterMapping> parameterMappings = new ArrayList<>();private Class<?> parameterType;private MetaObject metaParameters;public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) {super(configuration);this.parameterType = parameterType;this.metaParameters = configuration.newMetaObject(additionalParameters);}public List<ParameterMapping> getParameterMappings() {return parameterMappings;}@Overridepublic 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();}}
}
4. DefaultSqlSession 调用调整
本章中调整了解析过程,细化了 SQL 的创建。在 MappedStatement 映射语句中,使用 SqlSource 替换了 BoundSql,所以在 DefaultSqlSession 中也会有相应的调整,主要体现在获取SQL操作
ms.getSqlSource().getBoundSql(parameter)
java">public class DefaultSqlSession implements SqlSession {/*** 配置项.*/private Configuration configuration;private Executor executor;public DefaultSqlSession(Configuration configuration, Executor executor) {this.configuration = configuration;this.executor = executor;}public Configuration getConfiguration() {return configuration;}/*** 根据给定的执行SQL获取一条记录的封装对象.** @param statement* @param <T>* @return*/@Overridepublic <T> T selectOne(String statement) {return this.selectOne(statement, null);}@Overridepublic <T> T selectOne(String statement, Object parameter) {//映射语句MappedStatement mappedStatement = configuration.getMappedStatement(statement);//使用执行器.List<T> list = executor.query(mappedStatement, parameter, Executor.NO_RESULT_HANDLER, mappedStatement.getSqlSource().getBoundSql(parameter));return list.get(0);}@Overridepublic <T> T getMapper(Class<T> type) {return configuration.getMapper(type, this);}}
测试
事先准备
创建库表
-- 建表
CREATE TABLE `my_user` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',`user_id` varchar(9) DEFAULT NULL COMMENT '用户ID',`user_head` varchar(16) DEFAULT NULL COMMENT '用户头像',`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',`update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',`user_name` varchar(64) DEFAULT NULL COMMENT '用户名',`user_password` varchar(64) DEFAULT NULL COMMENT '用户密码',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ;-- 插入数据
INSERT INTO my_user (user_id, user_head, create_time, update_time, user_name, user_password) VALUES('1', '头像', '2024-12-13 18:00:12', '2024-12-13 18:00:12', '小苏', 's123asd');
定义一个数据库接口 IUserDao
IUserDao
java">public interface IUserDao {String queryUserInfoById(String uid);}
配置数据源
- 通过
mybatis-config-datasource.xml
配置数据源信息,包括:driver、url、username、password - 这里DataSource 配置的是 DRUID,因为我们实现的是这个数据源的处理方式。
xml"><?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration><environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="UNPOOLED"> #无池化时配置成这个类型值<dataSource type="POOLED"> #池化时配置城这个类型值<property name="driver" value="com.mysql.cj.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><mappers><mapper resource="mapper/User_Mapper.xml"/></mappers>
</configuration>
定义对应的mapper xml文件
xml"><?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.suwg.mybatis.test.dao.IUserDao"><select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.suwg.mybatis.test.po.User">SELECT id, user_id, user_head, create_timeFROM userwhere id = #{id}</select></mapper>
测试用例
- 单元测试,传递一个 1L 的 long 类型参数,进行方法的调用处理。通过单元测试验证执行器的处理过程,读者在学习的过程中可以进行断点测试,学习每个过程的处理内容。
- mybatis-config-datasource.xml 中 dataSource 数据源类型的调整
dataSource type="DRUID/POOLED/UNPOOLED"
,按需配置验证.
单元测试
- 这里的测试不需要调整,因为我们本章节的开发内容,主要以解耦 XML 的解析,只要能保持和之前章节一样,正常输出结果就可以。
java">public class ApiTest {private Logger logger = LoggerFactory.getLogger(ApiTest.class);// 测试SqlSessionFactory@Testpublic void testSqlSessionFactory() throws IOException {// 1.从xml文件读取mybatis配置项, 从SqlSessionFactory获取SqlSession.Reader reader = Resources.getResourceAsReader("mybatis-config-datasource.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);SqlSession sqlSession = sqlSessionFactory.openSession();// 2.获取映射器对象IUserDao userDao = sqlSession.getMapper(IUserDao.class);// 4.测试验证User user = userDao.queryUserInfoById(1L);logger.info("测试结果:{}", JSON.toJSONString(user));}}
测试结果
- 从输出的结果来看,我们的XML解析处理已经顺利根据职责进行解耦。整个流程更加清晰了,后续我们扩展就更方便了。
总结
- 本章节把一整块实现功能流程的代码块,通过设计原则进行拆分和解耦,运用不用的类来承担不同的职责,完成整个功能的实现。包括:
映射构建器、语句构建器、源码构建器
的综合使用,以及对应的引用脚本语言驱动和脚本构建器解析
,处理我们的 XML 中的 SQL 语句。 - 通过这样的
重构代码
,也能让我们对平常的业务开发中的大片面向过程的流程代码
有所感悟,当你可以细分拆解职责功能到不同的类中去以后,你的代码会更加的清晰并易于维护。 - 后续我们将继续按照现在的扩展结构底座,完成其他模块的功能逻辑开发,因为了这些基础内容的建造,再继续补充功能也会更加容易。当然这些代码还是需要你熟悉以后才能驾驭,在学习的过程中可以尝试断点调试,看看每一个步骤都在完成哪些工作。
参考书籍:《手写Mybatis渐进式源码实践》
书籍源代码:https://github.com/fuzhengwei/book-small-mybatis