Myabtis 소스 코드 분석 5 - Mybatis 구성 로딩 완료 다이어그램, 빌더 모드 사용

​함께 쓰는 습관을 만들어보세요! '너겟 데일리 뉴플랜·4월 업데이트 챌린지' 참여 10일차 입니다. [이벤트 상세보기 클릭]

1. Mybatis 실행 프로세스 개요

Mybatis의 실행 과정에 익숙해지기 위해 먼저 코드를 살펴보자.

public class MybatisDemo {
	

	private SqlSessionFactory sqlSessionFactory;
	
	@Before
	public void init() throws IOException {
		//--------------------第一步:加载配置---------------------------
	    // 1.读取mybatis配置文件创SqlSessionFactory
		String resource = "mybatis-config.xml";
		InputStream inputStream = Resources.getResourceAsStream(resource);
		// 1.读取mybatis配置文件创SqlSessionFactory
		sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
		inputStream.close();
	}
	
	@Test
	// 快速入门
	public void quickStart() throws IOException {
		//--------------------第二部,创建代理对象---------------------------
		// 2.获取sqlSession	
		SqlSession sqlSession = sqlSessionFactory.openSession();
		// 3.获取对应mapper
		TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);
		
		//--------------------第三步:获取数据---------------------------
		// 4.执行查询语句并返回单条数据
		TUser user = mapper.selectByPrimaryKey(2);
		System.out.println(user);
		
		System.out.println("----------------------------------");
		
		// 5.执行查询语句并返回多条数据
//		List<TUser> users = mapper.selectAll();
//		for (TUser tUser : users) {
//			System.out.println(tUser);
//		}
	}
}
复制代码

위는 mybatis를 사용하여 데이터에 액세스하는 데모입니다.빠른 시작 코드 분석을 통해 MyBatis의 실행 프로세스는 세 단계로 나눌 수 있습니다.

  1. 초기화 단계 : XML 구성 파일 및 주석의 구성 정보를 읽고 구성 개체를 만들고 각 모듈의 초기화를 완료합니다.
  2. 프록시 캡슐화 단계 : iBatis의 프로그래밍 모델을 캡슐화하고 매퍼 인터페이스에서 개발한 초기화 작업을 사용합니다.
  3. 데이터 접근 단계 : SqlSession을 통한 완전한 SQL 구문 분석, 매개변수 매핑, SQL 실행 및 결과 구문 분석 프로세스;

오늘 우리는 Mybatis가 아래의 첫 번째 단계에서 구성을 읽는 방법을 소개합니다.

둘째, 구성 로딩의 핵심 클래스

2.1 빌더의 세 가지 핵심 클래스

MyBatis에서 구성 파일을 로드하는 역할을 하는 세 가지 핵심 클래스가 있으며, 클래스 다이어그램은 다음과 같습니다.

  • BaseBuilder: 구성 파일 인스턴스를 포함한 모든 파서의 상위 클래스 및 파일 구문 분석을 위한 몇 가지 공통 방법.
  • XMLConfigBuilder: 주로 mybatis-config.xml 구문 분석을 담당합니다.
  • XMLMapperBuilder: 매핑 구성 Mapper.xml 파일의 구문 분석을 주로 담당합니다.
  • XMLStatementBuilder: 매핑 구성 파일에서 SQL 노드 구문 분석을 주로 담당합니다.

XMLConfigBuilder, XMLMapperBuilder 및 XMLStatementBuilder의 세 가지 클래스는 구성 파일 로드 프로세스에서 매우 중요하며, 구체적인 작업 분업은 다음 그림과 같습니다.

这三个类使用了建造者模式对 configuration 对象进行初始化,但是没有使用建造者模式
的“肉体”(流式编程风格),只用了灵魂(屏蔽复杂对象的创建过程),把建造者模式演绎
成了工厂模式;后面还会对这三个类源码进行分析;

居然这三个对象使用的是建造者模式,那么我们稍后介绍下什么是建造者模式

三、建造者模式

3.1 什么是建造者模式

建造者模式(BuilderPattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

建造者模式类图如下:

各要素如下:

  • Product:要创建的复杂对象
  • Builder:给出一个抽象接口,以规范产品对象的各个组成成分的建造。这个接口规定要实现复杂对象的哪些部分的创建,并不涉及具体的对象部件的创建;
  • ConcreteBuilder:实现 Builder 接口,针对不同的商业逻辑,具体化复杂对象的各部分的创建。 在建造过程完成后,提供产品的实例;
  • Director:调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建;

应用举例:红包的创建是个复杂的过程,可以使用构建者模式进行创建

代码示例:

1、红包对象RedPacket 


public class RedPacket {
	
	private String publisherName; //发包人

    private String acceptName; //收包人

    private BigDecimal packetAmount; //红包金额

    private int packetType; //红包类型

    private Date pulishPacketTime; //发包时间

    private Date openPacketTime; //抢包时间

    public RedPacket(String publisherName, String acceptName, BigDecimal packetAmount, int packetType, Date pulishPacketTime, Date openPacketTime) {
        this.publisherName = publisherName;
        this.acceptName = acceptName;
        this.packetAmount = packetAmount;
        this.packetType = packetType;
        this.pulishPacketTime = pulishPacketTime;
        this.openPacketTime = openPacketTime;
    }

	public String getPublisherName() {
		return publisherName;
	}

	public void setPublisherName(String publisherName) {
		this.publisherName = publisherName;
	}

	public String getAcceptName() {
		return acceptName;
	}

	public void setAcceptName(String acceptName) {
		this.acceptName = acceptName;
	}

	public BigDecimal getPacketAmount() {
		return packetAmount;
	}

	public void setPacketAmount(BigDecimal packetAmount) {
		this.packetAmount = packetAmount;
	}

	public int getPacketType() {
		return packetType;
	}

	public void setPacketType(int packetType) {
		this.packetType = packetType;
	}

	public Date getPulishPacketTime() {
		return pulishPacketTime;
	}

	public void setPulishPacketTime(Date pulishPacketTime) {
		this.pulishPacketTime = pulishPacketTime;
	}

	public Date getOpenPacketTime() {
		return openPacketTime;
	}

	public void setOpenPacketTime(Date openPacketTime) {
		this.openPacketTime = openPacketTime;
	}

	@Override
	public String toString() {
		return "RedPacket [publisherName=" + publisherName + ", acceptName="
				+ acceptName + ", packetAmount=" + packetAmount
				+ ", packetType=" + packetType + ", pulishPacketTime="
				+ pulishPacketTime + ", openPacketTime=" + openPacketTime + "]";
	}
   
}
复制代码

2、构建对象

public class Director {
	
	public static void main(String[] args) {
		RedPacket redPacket = RedPacketBuilderImpl.getBulider().setPublisherName("DK")
				                                               .setAcceptName("粉丝")
                                                               .setPacketAmount(new BigDecimal("888"))
                                                               .setPacketType(1)
                                                               .setOpenPacketTime(new Date())
                                                               .setPulishPacketTime(new Date()).build();

		System.out.println(redPacket);
	}

}
复制代码

PS:流式编程风格越来越流行,如 zookeeper 的 Curator、JDK8 的流式编程等等都是例子。流式编程的优点在于代码编程性更高、可读性更好,缺点在于对程序员编码要求更高、不太利于调试。建造者模式是实现流式编程风格的一种方式;

3.2 与工厂模式区别

建造者模式应用场景如下:

  • 需要生成的对象具有复杂的内部结构,实例化对象时要屏蔽掉对象代码与复杂对象的实例化过程解耦,可以使用建造者模式;简而言之,如果“遇到多个构造器参数时要考虑用构建器”;
  • 对象的实例化是依赖各个组件的产生以及装配顺序,关注的是一步一步地组装出目标对
  • 象,可以使用建造器模式;

建造者模式与工程模式的区别在于:

设计模式 形象比喻 对象复杂度 客户端参与程度
工厂模式 生产大众版 关注的是一个产品整体,无须关心产品的各部分是如何创建出来的; 客户端对产品的创建过程参与度低,对象实例化时属性值相对比较固定;
建造者模式 生产定制版 建造的对象更加复杂,是一个复合产品,它由各个部件复合而成,部件不同产品对象不同,生成的产品粒度细; 客户端参与了产品的创建,决定了产品的类型和内容,参与度高;适合实例化对象时属性变化频繁的场景;

四、Configuration 对象介绍

实例化并初始化 Configuration 对象是第一个阶段的最终目的,所以熟悉 configuration 对
象是理解第一个阶段代码的核心;configuration 对象的关键属性解析如下:

  • MapperRegistry:mapper 接口动态代理工厂类的注册中心。在 MyBatis 中,通过mapperProxy 实现 InvocationHandler 接口,MapperProxyFactory 用于生成动态代理的实例对象;
  • ResultMap:用于解析 mapper.xml 文件中的 resultMap 节点,使用 ResultMapping 来封装id,result 等子元素;
  • MappedStatement:用于存储 mapper.xml 文件中的 select、insert、update 和 delete 节点,同时还包含了这些节点的很多重要属性;
  • SqlSource:用于创建 BoundSql,mapper.xml 文件中的 sql 语句会被解析成 BoundSql 对象,经过解析 BoundSql 包含的语句最终仅仅包含?占位符,可以直接提交给数据库执行;

Configuration对象图解:

需要特别注意的是 Configuration 对象在 MyBatis 中是单例的,生命周期是应用级的,换句话说只要 MyBatis 运行 Configuration 对象就会独一无二的存在;在 MyBatis 中仅在
org.apache.ibatis.builder.xml.XMLConfigBuilder.XMLConfigBuilder(XPathParser, String, Properties)中有实例化 configuration 对象的代码,如下图:

 Configuration 对象的初始化(属性复制),是在建造 SqlSessionfactory 的过程中进行的,接下
来分析第一个阶段的内部流程;

 五、配置加载流程解析

5.1 配置加载过程

可以把第一个阶段配置加载过程分解为四个步骤,四个步骤如下图:


第一步:通过 SqlSessionFactoryBuilder 建造 SqlSessionFactory,并创建 XMLConfigBuilder 对
象 读 取 MyBatis 核 心 配 置 文 件 , 见 源码方 法 :
org.apache.ibatis.session.SqlSessionFactoryBuilder.build(Reader, String, Properties):

  public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      //读取配置文件
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());//解析配置文件得到configuration对象,并返回SqlSessionFactory
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        reader.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }
复制代码

第二步:进入 XMLConfigBuilder 的 parseConfiguration 方法,对 MyBatis 核心配置文件的各个
元素进行解析,读取元素信息后填充到 configuration 对象。在 XMLConfigBuilder 的
mapperElement()方法中通过 XMLMapperBuilder 读取所有 mapper.xml 文件;见方法:
org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration(XNode);

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
     //解析<properties>节点
      propertiesElement(root.evalNode("properties"));
      //解析<settings>节点
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      //解析<typeAliases>节点
      typeAliasesElement(root.evalNode("typeAliases"));
      //解析<plugins>节点
      pluginElement(root.evalNode("plugins"));
      //解析<objectFactory>节点
      objectFactoryElement(root.evalNode("objectFactory"));
      //解析<objectWrapperFactory>节点
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      //解析<reflectorFactory>节点
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);//将settings填充到configuration
      // read it after objectFactory and objectWrapperFactory issue #631
      //解析<environments>节点
      environmentsElement(root.evalNode("environments"));
      //解析<databaseIdProvider>节点
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //解析<typeHandlers>节点
      typeHandlerElement(root.evalNode("typeHandlers"));
      //解析<mappers>节点
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
复制代码

第三步:XMLMapperBuilder 的核心方法为 configurationElement(XNode),该方法对 mapper.xml 配置文件的各个元素进行解析,读取元素信息后填充到 configuration 对象。

private void configurationElement(XNode context) {
    try {
    	//获取mapper节点的namespace属性
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      //设置builderAssistant的namespace属性
      builderAssistant.setCurrentNamespace(namespace);
      //解析cache-ref节点
      cacheRefElement(context.evalNode("cache-ref"));
      //重点分析 :解析cache节点----------------1-------------------
      cacheElement(context.evalNode("cache"));
      //解析parameterMap节点(已废弃)
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      //重点分析 :解析resultMap节点----------------2-------------------
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      //解析sql节点
      sqlElement(context.evalNodes("/mapper/sql"));
      //重点分析 :解析select、insert、update、delete节点 ----------------3-------------------
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }
复制代码

在 XMLMapperBuilder 解析过程中,有四个点需要注意:

  1. resultMapElements(List)方法用于解析 resultMap 节点,这个方法非常重要, 一定要跟源码理解;解析完之后数据保存在 configuration 对象的 resultMaps 属性中;如下图
  2. 2XMLMapperBuilder 中在实例化二级缓存(见 cacheElement(XNode))、实例化 resultMap (见 resultMapElements(List))过程中都使用了建造者模式,而且是建造者模 式的典型应用;
  3. XMLMapperBuilder 和 XMLMapperStatmentBuilder 有 自 己 的 “ 秘 书 ” MapperBuilderAssistant。XMLMapperBuilder 和 XMLMapperStatmentBuilder 负责解析 读取配置文件里面的信息,MapperBuilderAssistant 负责将信息填充到 configuration。 将文件解析和数据的填充的工作分离在不同的类中,符合单一职责原则;
  4. 在 buildStatementFromContext(List)方法中,创建 XMLStatmentBuilder 解析 Mapper.xml 中 select、insert、update、delete 节点

第四步:在 XMLStatmentBuilder 的 parseStatementNode()方法中,对 Mapper.xml 中 select、 insert、update、delete 节点进行解析,并调用 MapperBuilderAssistant 负责将信息填充到 configuration。在理解 parseStatementNod()方法之前,有必要了解 MappedStatement,这个 类 用 于 封 装 select 、 insert 、 update 、 delete 节 点 的 信 息 ; 如 下 图 所 示 :

 至此,整个Mybatis的配置即加载完毕,整个加载流程图如下:

\

рекомендация

отjuejin.im/post/7085375589627985934