ibatis源码学习(二)初始化和配置文件解析

ibatis整体设计和核心流程一文中,我们提到了ibatis框架的初始化过程,本文将深入分析ibatis框架的初始化和配置文件解析过程。本文使用的ibatis版本为2.3.4,不同版本间会略有差异。 

问题 
在详细介绍ibatis初始化过程之前,让我们先来思考几个问题。 

1. ibatis初始化的目标是什么? 
上文中提到过,ibatis初始化的核心目标是构造SqlMapClientImpl对象,主要是其内部重要属性delegate这个代理对象的初始化。delegate这个对象耦合了用户端的操作行为和执行环境,持有执行操作的所需要的所有数据。 

2. 如何解析ibatis的sqlmap配置文件和sqlmap映射文件? 
可以采用通用的xml文件解析工具,如SAX等。 

3. 如何将配置文件中每个属性值注入到SqlMapClientImpl对象中? 
可以给不同类型节点设置对应的handler,遍历节点时,调用handler对象的处理方法,将节点值注入到SqlMapClientImpl对象的属性中。
 

带着上面的这些问题,我们开始探索ibatis的初始化过程。 

核心类图 
初始化过程主要涉及以下几个重要类,理解这些类的含义非常重要。主要类图如下: 
 
1. Nodelet 
该接口是对配置文件中节点处理方式的抽象,该接口仅有一个process()方法,表示对该类节点的处理方式。 

2. NodeletParser 
同SAX类似的一个xml解析器。不同点在于SAX对所有节点采用同样的处理方式,而NodeletParser可以针对不同的节点个性化配置相应的处理方式。 NodeletParser内部维护了一个letMap,这个map维护着节点标识信息XPath和对应的处理方式Nodelet的关系。 

3. XmlParserState 
用于维护配置文件解析过程中的上下文信息,配置文件解析过程中产生的数据都放入XmlParserState中统一维护。 
注意: ibatis2.3.0版本中没有这个类,它使用BaseParser中内部类Variables维护上下文信息。 

4. SqlMapConfigParser 
用于解析sqlMap配置文件,其内部组合了NodeletParser对象,用于完成sqlMap配置文件解析。该对象构造函数中,完成向NodeletParser属性中添加sqlMap配置文件中节点XPath和对应的Nodelet映射关系。比如<typeAlias>节点的处理方式如下: 

Java代码   收藏代码
  1. private void addTypeAliasNodelets() {  
  2.   parser.addNodelet("/sqlMapConfig/typeAlias"new Nodelet() {  
  3.     public void process(Node node) throws Exception {  
  4.       Properties prop = NodeletUtils.parseAttributes(node, vars.properties);  
  5.       String alias = prop.getProperty("alias");  
  6.       String type = prop.getProperty("type");  
  7.       vars.typeHandlerFactory.putTypeAlias(alias, type);  
  8.     }  
  9.   });  
  10. }  


5. SqlMapParser  
用于解析sqlMap映射文件,其内部组合了NodeletParser对象,用于完成sqlMap映射文件解析。和SqlMapConfigParser类似,在构造函数中向NodeletParser属性添加sqlMap映射文件中节点标识信息XPath和对应的Nodelet映射关系。比如<sql>节点的处理方式如下: 
Java代码   收藏代码
  1. private void addSqlNodelets() {  
  2.   parser.addNodelet("/sqlMap/sql"new Nodelet() {  
  3.     public void process(Node node) throws Exception {  
  4.       Properties attributes = NodeletUtils.parseAttributes(node, vars.properties);  
  5.       String id = attributes.getProperty("id");  
  6.       if (vars.useStatementNamespaces) {  
  7.         id = applyNamespace(id);  
  8.       }  
  9.       if (vars.sqlIncludes.containsKey(id)) {  
  10.         // To be upgraded to throwing of a RuntimeException later on  
  11.         log.warn("Duplicate <sql>-include '" + id + "' found.");  
  12.       }  
  13.       else  {  
  14.         vars.sqlIncludes.put(id, node);  
  15.       }  
  16.     }  
  17.   });  
  18. }  


6. SqlStatementParser  
用于生成sql对应的MappedStatement对象。其内部仅包含一个public方法parseGeneralStatement(Node node, GeneralStatement statement),用于生成MappedStatement对象。 

下面以代码中常见的ibatis配置为例,说明ibatis框架的配置文件解析和初始化过程。 
常见ibatis配置  
Biz-data-source.xml代码   收藏代码
  1. <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">  
  2.     <property name="dataSource" ref="dataSource"/>  
  3.     <property name="configLocation" value="classpath/sqlmap-ibatis.xml"/>  
  4. </bean>    

Sqlmap-ibatis.xml代码   收藏代码
  1. <sqlMapConfig>  
  2.     <sqlMap resource="sqlmap/cases/CaseSQL.xml"/>  
  3.         ...  
  4. </sqlMapConfig>  

Casesql.xml代码   收藏代码
  1. <select id="QUERY-CASE-BY-CASE-ID"  parameterClass="map" resultMap="RM-Case">       
  2.     SELECT  ID ,GMT_MODIFIED ,GMT_CREATE ,STATUS   
  3.     FROM AVATAR_CASE   
  4.     WHERE ID = #caseId#   
  5. </select>  


初始化过程  
1) SqlMapClientFactoryBean初始化  
SqlMapClientFactoryBean的初始化方法afterPropertiesSet(),用于构建sqlMapClient对象: 
Java代码   收藏代码
  1. public void afterPropertiesSet() throws Exception {  
  2.     ...  
  3.     this.sqlMapClient = buildSqlMapClient(this.configLocations, this.mappingLocations, this.sqlMapClientProperties);   //初始化核心方法,构建sqlMapClient对象       ...  
  4. }  


其中buildSqlMapClient()的实现: 
Java代码   收藏代码
  1. protected SqlMapClient buildSqlMapClient(  
  2.         Resource[] configLocations, Resource[] mappingLocations, Properties properties)  
  3.         throws IOException {  
  4.                ...  
  5.     SqlMapClient client = null;  
  6.     SqlMapConfigParser configParser = new SqlMapConfigParser();  
  7.     for (int i = 0; i < configLocations.length; i++) {  
  8.         InputStream is = configLocations[i].getInputStream();  
  9.         try {  
  10.             client = configParser.parse(is, properties); //通过SqlMapConfigParser解析配置文件,生成SQLMapClientImpl对象  
  11.         }  
  12.     ...  
  13.     return client;  
  14. }  

上面这段代码中,调用SqlMapConfigParser解析sqlMap配置文件sqlmap-ibatis.xml。 

2) 调用SqlMapConfigParser进行解析  
SqlMapConfigParser.parse(InputStream inputStream, Properties props)方法源码如下: 
Java代码   收藏代码
  1. public SqlMapClient parse(InputStream inputStream, Properties props) {  
  2.   if (props != null) state.setGlobalProps(props);  
  3.   return parse(inputStream);  
  4. }  

其中parse()方法源码如下: 
Java代码   收藏代码
  1. public SqlMapClient parse(InputStream inputStream) {  
  2.     ...  
  3.     parser.parse(inputStream); // 调用NodeletParser解析配置文件  
  4.     return state.getConfig().getClient(); //返回SqlMapClientImpl对象  
  5.     ...  
  6. }  


3) 调用NodeletParser进行解析  
NodeletParser.parse()是配置文件解析的核心方法,这里被SqlMapConfigParser调用,用于解析sqlMap配置文件sqlmap-ibatis.xml。 
Java代码   收藏代码
  1. public void parse(InputStream inputStream) throws NodeletException {  
  2.   try {  
  3.     Document doc = createDocument(inputStream);  
  4.     parse(doc.getLastChild());  //从根节点开始解析  
  5.   }   
  6.   ...  
  7. }  

其中parse(Node node)方法源码如下: 
Java代码   收藏代码
  1. public void parse(Node node) {  
  2.   Path path = new Path();  
  3.   processNodelet(node, "/");    
  4.   process(node, path);  
  5. }  

这个方法中包含两个重要方法,processNodelet()根据当前节点的XPath进行相应处理,process()方法用于处理该节点相关信息(如Element,Attribute,Children等)。 

首先看一下processNodelet(Node node, String pathString)的源码实现: 
Java代码   收藏代码
  1. private void processNodelet(Node node, String pathString) {  
  2.   Nodelet nodelet = (Nodelet) letMap.get(pathString);  
  3.   if (nodelet != null) {  
  4.     try {  
  5.       nodelet.process(node);  
  6.     }   
  7.    ...  
  8.   }  
  9. }  

在SqlMapConfigParser类介绍中,我们提到过NodeletParser中letMap属性维护着节点XPath信息和节点信息处理方式的映射关系,所有的映射关系会在SqlMapConfigParser构造函数中加入。 这里根据节点的XPath,获取对应的处理方式Nodelet,再调用Nodelet.process()处理该节点信息。 

再来看一下NodeletParser.process(Node node, Path path)的源码实现: 
Java代码   收藏代码
  1. private void process(Node node, Path path) {  
  2.   if (node instanceof Element) {  
  3.     //处理Element信息  
  4.     String elementName = node.getNodeName();  
  5.     path.add(elementName);  
  6.     processNodelet(node, path.toString());  
  7.     processNodelet(node, new StringBuffer("//").append(elementName).toString());  
  8.   
  9.     //处理Attribute信息  
  10.     NamedNodeMap attributes = node.getAttributes();  
  11.     int n = attributes.getLength();  
  12.     for (int i = 0; i < n; i++) {  
  13.       Node att = attributes.item(i);  
  14.       String attrName = att.getNodeName();  
  15.       path.add("@" + attrName);  
  16.       processNodelet(att, path.toString());  
  17.       processNodelet(node, new StringBuffer("//@").append(attrName).toString());  
  18.       path.remove();  
  19.     }  
  20.   
  21.     // 处理Children信息  
  22.     NodeList children = node.getChildNodes();  
  23.     for (int i = 0; i < children.getLength(); i++) {  
  24.       process(children.item(i), path); //递归处理子节点  
  25.     }  
  26.     path.add("end()");  
  27.     processNodelet(node, path.toString());  
  28.     path.remove();  
  29.     path.remove();  
  30.   } else if (node instanceof Text) {  
  31.     // Text  
  32.     path.add("text()");  
  33.     processNodelet(node, path.toString());  
  34.     processNodelet(node, "//text()");  
  35.     path.remove();  
  36.   }  
  37. }  

该方法中采用递归方式处理节点的Element、Attribute和Children信息,首先构建当前节点的XPath信息,再调用processNodelet(node, path)处理该节点。 

4) 调用SqlMapParser解析sqlMap映射配置文件CaseSQL.xml。  
当处理sqlMap配置文件sqlmap-ibatis.xml中的sqlMap节点时,会调用下面的Nodelet进行处理: 
Java代码   收藏代码
  1.  protected void addSqlMapNodelets() {  
  2.    parser.addNodelet("/sqlMapConfig/sqlMap"new Nodelet() {  
  3.      public void process(Node node) throws Exception {  
  4.        ...  
  5.        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());  
  6.   
  7.        String resource = attributes.getProperty("resource");  
  8.        String url = attributes.getProperty("url");  
  9.        ...  
  10.        new SqlMapParser(state).parse(reader); //调用SqlMapParser解析sqlMap映射文件  
  11.        }  
  12.      }  
  13.    });  
  14. }  


SqlMapParser的parse()方法实现如下 
Java代码   收藏代码
  1. public void parse(InputStream inputStream) throws NodeletException {  
  2.   parser.parse(inputStream);//调用NodeletParser解析配置文件  
  3. }  

这里和上面的SqlMapConfigParser解析方式类似,都是调用NodeletParser解析配置文件,不同点在于这两个类是针对不同的配置文件解析(SqlMapConfigParser针对sqlMap配置文件,SqlMapParser针对sqlMap映射文件),所以在各自构造函数插入letMap时,使用的key是自己配置文件里节点的XPath。 

5) 调用SqlStatementParser生成MappedStatement。  
当解析sqlMap映射文件的select节点时,将调用SqlStatementParser生成MappedStatement。 
  protected void addStatementNodelets() { 
    ... 
    parser.addNodelet("/sqlMap/select", new Nodelet() { 
      public void process(Node node) throws Exception { 
        statementParser.parseGeneralStatement(node, new SelectStatement()); 
      } 
    
    }); 

SqlStatementParser.parseGeneralStatement()的实现这里就不详述了,主要目的是构建MappedStatement,并将配置文件解析后的信息注入到XmlParserState中。 

小结  
ibatis初始化的核心目标是构建SqlMapClientImpl对象,核心思想如下: 
1. 提供NodeletParser配置文件解析类,它维护了节点XPath和对应处理方式的映射关系。 
2. SqlMapConfigParser和SqlMapParser在构造方法中向NodeletParser中添加节点XPath和对应处理方式的映射关系。 
3. 配置文件解析采用递归方式进行,首先生成当前节点的XPath信息,从NodeletParser获取对应的处理方式并执行。 
4. 整个解析过程中每个节点生成的数据统一注入到XmlParserState,最终通过XmlParserStat获取SqlMapClientImpl对象并返回。

转自:http://learnworld.iteye.com/blog/1450057


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. 引入依赖 在 pom.xml 文件中添加以下依赖: ``` <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-extension</artifactId> <version>3.4.2</version> </dependency> <dependency> <groupId>org.apache.ibatis</groupId> <artifactId>mybatis-redis-cache</artifactId> <version>2.0.1</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> ``` 2. 配置 Redis 在 application.yml 配置文件中添加 Redis 的连接信息: ``` spring: redis: host: 127.0.0.1 port: 6379 password: 123456 database: 0 pool: max-active: 8 max-idle: 8 min-idle: 0 max-wait: -1ms ``` 3. 配置 MyBatis Plus 在 MyBatis Plus 配置文件中开启级缓存并配置 Redis 缓存: ``` mybatis-plus: configuration: cache-enabled: true # 开启级缓存 local-cache-scope: session # 级缓存作用域 lazy-loading-enabled: true # 开启懒加载 multiple-datasource-enabled: true # 开启多数据源 global-config: db-config: id-type: auto # 主键ID类型 field-strategy: not_empty # 字段非空验证 swagger2: true # 是否开启Swagger2 cache: enabled: true # 开启缓存 type: redis # 缓存类型 # 设置缓存前缀,默认是 mybatis:cache: # prefix: mybatisplus: spring: redis: cache: # 过期时间,默认1天 ttl: 86400 # 级缓存前缀,默认是 mybatisplus:cache: key-prefix: mybatis-plus:cache: # 条目数量,默认256个 max-number-of-elements-in-cache: 256 ``` 4. 实体类开启缓存 在需要开启缓存的实体类上添加 `@CacheNamespace` 注解: ``` @Data @NoArgsConstructor @TableName("student") @CacheNamespace(implementation = MybatisRedisCache.class, eviction = MybatisRedisCache.class, flushInterval = 60000) public class Student implements Serializable { @TableId(type = IdType.AUTO) private Long id; @TableField("name") private String name; @TableField("age") private Integer age; } ``` 其中,`implementation` 和 `eviction` 属性的值均为 `MybatisRedisCache.class`,表示该实体类使用 Redis 缓存,并且缓存失效时间为 1 分钟(60 秒)。 5. 注册 Redis 缓存插件 在 Spring Boot 应用启动类中注册 Redis 缓存插件: ``` @Configuration public class MyBatisPlusConfig { @Bean public RedisCachePlugin redisCachePlugin(RedisTemplate<Object, Object> redisTemplate) { return new RedisCachePlugin(redisTemplate); } } ``` 6. 测试缓存 使用以下代码进行测试: ``` @Test public void testRedisCache() { Student student1 = studentService.getById(1L); Student student2 = studentService.getById(1L); System.out.println("student1:" + student1); System.out.println("student2:" + student2); Assert.assertEquals(student1, student2); } ``` 第一次查询会从数据库中获取数据并保存到 Redis 缓存,第次查询会直接从 Redis 缓存中获取数据,输出结果如下: ``` DEBUG [MybatisRedisCache] [Session XX] [Namespace com.example.demo.entity.Student] [Cache INSERT] Student(id=1, name=Tom, age=20) student1:Student(id=1, name=Tom, age=20) student2:Student(id=1, name=Tom, age=20) ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值