福建网站开发企业,欧美风格网站设计,自己做都网站怎么发朋友圈,pre_get_posts wordpress文章目录 前言一、SqlSessionFactoryBuilder1.1、XMLConfigBuilder1.2、parse 二、mappers标签的解析2.1、cacheElement2.1.1、缓存策略 2.2、buildStatementFromContext2.2.1、sql的解析 前言 本篇主要介绍MyBatis源码中的配置文件解析部分。MyBatis是对于传统JDBC的封装… 文章目录 前言一、SqlSessionFactoryBuilder1.1、XMLConfigBuilder1.2、parse 二、mappers标签的解析2.1、cacheElement2.1.1、缓存策略 2.2、buildStatementFromContext2.2.1、sql的解析 前言 本篇主要介绍MyBatis源码中的配置文件解析部分。MyBatis是对于传统JDBC的封装屏蔽了传统JDBC与数据库进行交互组装参数获取查询结果并自己封装成对象的繁琐过程。 原生MyBatis首先需要配置mybatis-config.xml
?xml version1.0 encodingUTF-8 ?
!DOCTYPE configuration PUBLIC -//mybatis.org//DTD Config 3.0//ENhttp://mybatis.org/dtd/mybatis-3-config.dtdconfigurationproperties resourcejdbc.properties/environments defaultdevenvironment iddevtransactionManager typeJDBC/dataSource typePOOLEDproperty namedriver value${jdbc.driver}/property nameurl value${jdbc.url}/property nameusername value${jdbc.username}/property namepassword value${jdbc.password}//dataSource/environment/environmentsmappersmapper resourcemapper/UserMapper.xml//mappers
/configuration并且指定数据源jdbc.properties
jdbc.drivercom.mysql.cj.jdbc.Driver
jdbc.urljdbc:mysql://localhost:3306/test?useSSLfalseserverTimezoneUTC
jdbc.usernameroot
jdbc.password123456创建数据库访问层接口
public interface UserMapper {ListUser selectAll();User selectById(int id);void insert(User user);void update(User user);void delete(int id);
}以及对应的xml文件
?xml version1.0 encodingUTF-8 ?
!DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//ENhttp://mybatis.org/dtd/mybatis-3-mapper.dtdmapper namespacecom.example.mybatis.mapper.UserMappercache/resultMap iduserResultMap typecom.example.mybatis.entity.Userid propertyid columnid/result propertyname columnname/result propertyage columnage//resultMapselect idselectAll resultMapuserResultMapSELECT * FROM users/selectselect idselectById resultMapuserResultMap parameterTypeintSELECT * FROM users WHERE id #{id}/selectinsert idinsert parameterTypecom.example.mybatis.entity.UserINSERT INTO users (name, age) VALUES (#{name}, #{age})/insertupdate idupdate parameterTypecom.example.mybatis.entity.UserUPDATE users SET name #{name}, age #{age} WHERE id #{id}/updatedelete iddelete parameterTypeintDELETE FROM users WHERE id #{id}/delete
/mappermybatis-config.xml常见的标签
标签作用settings控制 MyBatis 全局行为缓存、懒加载、日志等typeAliases设置类型别名简化 Mapper XML 中类名书写typeHandlers自定义类型转换器Java类型 ↔ JDBC类型plugins注册插件如分页插件、SQL打印等objectFactory自定义对象创建逻辑environments配置数据库环境及事务管理mappers注册 Mapper 映射文件或 Mapper 接口 原生MyBatis的使用其中读取配置文件并进行解析主要体现在SqlSessionFactoryBuilder的build方法中
public class Main {public static void main(String[] args) throws Exception {//将xml构筑成configuration配置类Reader reader Resources.getResourceAsReader(mybatis-config.xml);//解析xml注册成SqlSessionFactorySqlSessionFactory sqlSessionFactory new SqlSessionFactoryBuilder().build(reader);try (SqlSession session sqlSessionFactory.openSession()) {User user session.selectOne(com.example.mybatis.mapper.UserMapper.selectById, 1);System.out.println(user);}}
}一、SqlSessionFactoryBuilder
1.1、XMLConfigBuilder 在调用SqlSessionFactoryBuilder的build方法时首先会去创建一个XMLConfigBuilder目的是构建一个XML配置文件解析器对象。 其中的核心代码这段代码的作用是注册别名将配置文件中的 “JDBC”、POOLED等关键词和实际的类型进行绑定。
别名实际类用途JDBCJdbcTransactionFactoryJDBC事务管理器默认事务方式MANAGEDManagedTransactionFactory受容器管理的事务如 SpringJNDIJndiDataSourceFactory从 JNDI 获取数据源POOLEDPooledDataSourceFactory数据库连接池MyBatis 内置UNPOOLEDUnpooledDataSourceFactory不使用连接池的数据源PERPETUALPerpetualCache永久缓存FIFOFifoCache先进先出缓存LRULruCache最近最少使用缓存SOFTSoftCache基于 SoftReference 的缓存WEAKWeakCache基于 WeakReference 的缓存DB_VENDORVendorDatabaseIdProvider根据数据库类型自动切换 SQLXMLXMLLanguageDriverMyBatis 默认的 XML SQL 语言驱动器RAWRawLanguageDriver原生 SQL 写法语言驱动器SLF4JSlf4jImpl使用 SLF4J 的日志输出COMMONS_LOGGINGJakartaCommonsLoggingImpl使用 Commons Logging 日志LOG4JLog4jImpl使用 Log4j 日志LOG4J2Log4j2Impl使用 Log4j2 日志JDK_LOGGINGJdk14LoggingImpl使用 JDK 内建日志STDOUT_LOGGINGStdOutImpl输出日志到控制台NO_LOGGINGNoLoggingImpl不输出日志CGLIBCglibProxyFactory使用 CGLIB 动态代理JAVASSISTJavassistProxyFactory使用 Javassist 动态代理
1.2、parse 真正解析配置文件的是利用上一步构造出的XMLConfigBuilder的parse方法首先会进行判断如果已经解析过则抛出异常不会重复解析 否则就将标记设置为true。并且执行parseConfiguration方法从根节点进行解析 每一行都对应了一个 mybatis-config.xml 中的标签逐步填充 Configuration 对象内容
/*** 解析 configuration 根节点的各个子标签并将配置信息填充到 Configuration 对象中*/
private void parseConfiguration(XNode root) {try {// 【1】先解析 properties 标签必须最优先解析以便后续标签中的占位符 ${} 能被正确替换propertiesElement(root.evalNode(properties));// 【2】解析 settings 标签将其内容转换为 Properties 对象Properties settings settingsAsProperties(root.evalNode(settings));// 【3】解析 settings 中的 vfsImpl 属性如果配置了自定义 VFS 实现类loadCustomVfs(settings);// 【4】解析 settings 中的 logImpl 属性设置日志实现类如 LOG4J、STDOUT_LOGGING 等loadCustomLogImpl(settings);// 【5】解析 typeAliases 标签注册用户自定义的别名或包扫描别名typeAliasesElement(root.evalNode(typeAliases));// 【6】解析 plugins 标签注册 MyBatis 插件如分页插件、SQL 拦截器等pluginElement(root.evalNode(plugins));// 【7】解析 objectFactory 标签设置自定义对象工厂用于实例化结果对象objectFactoryElement(root.evalNode(objectFactory));// 【8】解析 objectWrapperFactory 标签自定义对象包装器封装结果对象属性访问行为objectWrapperFactoryElement(root.evalNode(objectWrapperFactory));// 【9】解析 reflectorFactory 标签自定义反射器工厂高级反射行为控制reflectorFactoryElement(root.evalNode(reflectorFactory));// 【10】将 settings 中的配置项应用到 Configuration 对象中settingsElement(settings);// 【11】解析 environments 标签注册事务管理器和数据源配置必须在 objectFactory 之后执行environmentsElement(root.evalNode(environments));// 【12】解析 databaseIdProvider 标签支持数据库厂商识别如区分 MySQL、OracledatabaseIdProviderElement(root.evalNode(databaseIdProvider));// 【13】解析 typeHandlers 标签注册自定义类型处理器TypeHandlertypeHandlerElement(root.evalNode(typeHandlers));// 【14】解析 mappers 标签加载 Mapper 映射器包括 XML 和接口方式mapperElement(root.evalNode(mappers));} catch (Exception e) {// 如果解析过程中发生异常则封装为 BuilderException 抛出throw new BuilderException(Error parsing SQL Mapper Configuration. Cause: e, e);}
}当解析完成后会得到一个configuration对象其中就包含了配置文件中的各种值。相当于此时的xml配置文件已经转化为了configuration对象。最后还会将其再次包装成SqlSessionFactory后续会利用SqlSessionFactory进行sql相关逻辑的执行。 其中最关键的是mappers标签的解析。
二、mappers标签的解析 mapperElement方法首先会拿到mappers根标签然后进行解析。
/*** 解析 mappers 标签支持三种加载方式package、resource/url、class*/
private void mapperElement(XNode parent) throws Exception {if (parent ! null) {// 遍历 mappers 下的所有子节点可能是 package 或 mapperfor (XNode child : parent.getChildren()) {// 情况1package namecom.xxx.mapper/批量注册包下所有 Mapper 接口if (package.equals(child.getName())) {String mapperPackage child.getStringAttribute(name);// 自动扫描指定包下的所有接口并注册到 Configuration 中configuration.addMappers(mapperPackage);} else {// 情况2~4单个 mapper 节点通过 resource/url/class 指定加载方式String resource child.getStringAttribute(resource); // 从 classpath 中加载 Mapper XMLString url child.getStringAttribute(url); // 从网络路径加载 Mapper XMLString mapperClass child.getStringAttribute(class); // 直接加载 Mapper 接口类// 情况2只指定 resource加载 Mapper XML 文件if (resource ! null url null mapperClass null) {ErrorContext.instance().resource(resource); // 设置错误上下文信息try (InputStream inputStream Resources.getResourceAsStream(resource)) {XMLMapperBuilder mapperParser new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());mapperParser.parse(); // 解析 Mapper XML注册语句映射}// 情况3只指定 url加载远程 Mapper XML 文件} else if (resource null url ! null mapperClass null) {ErrorContext.instance().resource(url);try (InputStream inputStream Resources.getUrlAsStream(url)) {XMLMapperBuilder mapperParser new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());mapperParser.parse(); // 同样调用解析逻辑}// 情况4只指定 class注册 Mapper 接口类无 XML 时适用} else if (resource null url null mapperClass ! null) {Class? mapperInterface Resources.classForName(mapperClass);configuration.addMapper(mapperInterface); // 注册接口类到 MapperRegistry// 情况5配置冲突三种方式只能选一种否则抛异常} else {throw new BuilderException(A mapper element may only specify a url, resource or class, but not more than one.);}}}}
} 案例中对应的是情况2首先会注册一个mapper解析器然后调用其parse方法对案例中UserMapper.xml进行解析在该方法中首先会进行判断如果已经进行过解析则不会重复解析。 解析的核心方法在于configurationElement同样是对于xml中的各种标签再次分类解析 这里重点看一下cacheElement以及buildStatementFromContext。
2.1、cacheElement cacheElement和Mybatis的二级缓存有关。简单的说Mybatis有两级缓存
一级缓存是SqlSession 级别的并且默认开启。二级缓存是Mapper 映射级别默认不开启如果需要应该在某个mapper.xml中使用cache标签开启。 cacheElement方法正是解析mapper.xml中的cache标签
/*** 解析 cache 标签构建二级缓存对象并注册到 Configuration 中。*/
private void cacheElement(XNode context) {// 1. 判断 cache 标签是否存在if (context ! null) {// 2. 解析缓存类型默认是 PERPETUAL即 PerpetualCacheString type context.getStringAttribute(type, PERPETUAL);Class? extends Cache typeClass typeAliasRegistry.resolveAlias(type);// 3. 解析缓存淘汰策略默认是 LRU即最近最少使用String eviction context.getStringAttribute(eviction, LRU);Class? extends Cache evictionClass typeAliasRegistry.resolveAlias(eviction);// 4. 缓存刷新间隔可选指定自动清空缓存的时间毫秒Long flushInterval context.getLongAttribute(flushInterval);// 5. 缓存大小可选最大缓存对象个数Integer size context.getIntAttribute(size);// 6. 是否为读写缓存readOnlyfalse 表示使用序列化true 表示共享引用// readWrite true 表示开启对象副本确保线程安全boolean readWrite !context.getBooleanAttribute(readOnly, false);// 7. 是否阻塞当缓存正在被其他线程刷新时是否阻塞等待boolean blocking context.getBooleanAttribute(blocking, false);// 8. 获取 cache 中配置的其他 property 子节点Properties props context.getChildrenAsProperties();// 9. 构建缓存并注册到 Configuration封装为 MapperBuilderAssistant.useNewCache()builderAssistant.useNewCache(typeClass, // 缓存类型类如 PerpetualCacheevictionClass, // 淘汰策略类如 LruCacheflushInterval, // 缓存刷新间隔size, // 缓存容量readWrite, // 是否使用读写模式blocking, // 是否阻塞模式props // 自定义属性);}
}在useNewCache中最终会调用CacheBuilder的build方法 build方法中运用到了装饰器模式所有的Cache都实现了一个共同的父类Cache。 在**cache newCacheDecoratorInstance(decorator, cache);这一行代码中传入LruCache和当前的Cache实例PERPETUAL将PERPETUAL包装到LRU中LruCache的delegate属性指向的是传入的PerpetualCache实例 然后继续执行到cache setStandardDecorators(cache);**这一行代码会继续进行装饰器的包装 setStandardDecorators方法对于Cache实例层层包装赋值给各自的delegate属性 包装完成的层次SynchronizedCache线程同步缓存区-LoggingCache统计命中率以及打印日志-SerializedCache序列化-LruCache最少使用-PerpetualCache默认。
2.1.1、缓存策略 默认的PerpetualCache使用的是HashMap进行存储。 而LruCache为了实现最近最少使用的机制使用了LinkedHashMap的数据结构并且重写了它的removeEldestEntry方法关键在于LinkedHashMap构造时第三个参数为 true 表示按访问顺序排列:
LruCache cache new LruCache(new PerpetualCache(myCache));
cache.setSize(3);cache.put(A, 1); // A
cache.put(B, 2); // A B
cache.put(C, 3); // A B C
cache.get(A); // B C A A 被访问过移到尾部
cache.put(D, 4); // C A DB 被淘汰 SynchronizedCache每个方法上通过加synchronized保证线程安全 LoggingCache会记录日志以及统计缓存命中次数
2.2、buildStatementFromContext buildStatementFromContext是用来解析 select、insert、update、delete 标签中sql语句的方法首先会解析出这些节点然后进行循环获取到XMLStatementBuilder后执行parseStatementNode方法 在parseStatementNode方法中有几个关键点这一段代码会判断当前的标签是否为select如果是select标签则不会清除一级缓存增删改会清除以及判断是否使用二级缓存默认 select 使用
2.2.1、sql的解析 真正执行解析sql的是下图中的代码 同样地会先去构建一个XMLScriptBuilder然后调用其parseScriptNode方法进行解析 在parseScriptNode方法中首先会解析 SQL 标签中的所有子标签然后去进行判断
包含动态 SQL即是否包含 if、choose、${} 等动态节点构建 DynamicSqlSource运行时动态拼接 SQL不包含动态 SQL即是否包含 if、choose、${} 等动态节点构建 RawSqlSource直接编译成静态 SQL提升效率 MixedSqlNode对象实现了SqlNode接口SqlNode是所有动态 SQL节点的统一接口而MixedSqlNode代表了 一整个 SQL 脚本块比如select标签中所有内容就会变成一个 MixedSqlNode。 SqlNode 接口 │ ├── MixedSqlNode // 组合节点 ├── StaticTextSqlNode // 静态文本节点普通 SQL 字符串 ├── TextSqlNode // 动态文本节点包含 ${} ├── IfSqlNode // if 标签 ├── ChooseSqlNode // choose/when/otherwise ├── ForEachSqlNode // foreach ├── WhereSqlNode // where ├── TrimSqlNode // trim ├── SetSqlNode // set └── BindSqlNode // bind 用一个案例说明假如我在mapper.xml中定义了如下的sql语句
select idfindUser parameterTypemap resultTypeUserSELECT * FROM userwhereif testname ! nullAND name #{name}/ifif testage ! nullAND age #{age}/if/where
/select则生成的结构如下 MixedSqlNode ├── StaticTextSqlNode(“SELECT * FROM user”) └── WhereSqlNode └── MixedSqlNode ├── IfSqlNode(test“name ! null”) → TextSqlNode(“AND name #{name}”) └── IfSqlNode(test“age ! null”) → TextSqlNode(“AND age #{age}”)