备战面试日记(4.3)- (框架.Mybatis)

本人本科毕业,21届毕业生,一年工作经验,简历专业技能如下,现根据简历,并根据所学知识复习准备面试。

记录日期:2022.1.15

大部分知识点只做大致介绍,具体内容根据推荐博文链接进行详细复习。

文章目录

  • 框架原理 - Mybatis
    • 概念
      • 简介
      • 作用
      • 优点
      • 缺点
    • Mybatis配置文件【可阅】
      • properties
      • settings
      • typeAliases
      • typeHandlers
        • 处理枚举类型
      • objectFactory
      • plugins
      • environments
        • 数据源(dataSource)
          • UNPOOLED
          • POOLED
          • JNDI
      • databaseIdProvider
      • mappers
    • Mybatis源码浅析
      • 引入
      • SqlSessionFactoryBuilder
        • XMLConfigBuilder#parse()
          • XMLConfigBuilder#parseConfiguration()
            • 插件注册
            • mapper扫描与解析
      • SqlSession
      • Mapper代理
      • 执行查询语句
      • 插件开发
        • 引入
        • 四大对象
          • Mybatis插件接口 - Interceptor
          • 实际运用举例
        • 插件原理
    • Mybatis面试问题
      • #{} 和 ${} 的区别?
      • Mybatis 一级缓存 和 二级缓存?
        • 一级缓存
        • 二级缓存
        • 缓存失效
          • 一级缓存
          • 二级缓存
      • Mybatis插件运行原理?
        • 编写插件
        • 插件运行原理
        • 举例:PageHelper
          • 配置过程
          • 使用步骤
            • 普通使用
            • Service中使用
          • 原理说明
      • Xml映射文件和内部数据结构之间的映射?
        • configuration
        • resultMap
        • mappedStatment
      • Mybatis中用到了哪些设计模式?

框架原理 - Mybatis

推荐阅读面试文章【推荐】:MyBatis面试题总结

推荐阅读功能使用文章:MyBatis详解

推荐阅读解析源码文章:MyBatis系列之Mybatis源码解读

推荐囧辉的文章:面试题:mybatis 中的 DAO 接口和 XML 文件里的 SQL 是如何建立关系的?

其他推荐:Mybatis知识点整理

概念

Mybatis官网:mybatis – MyBatis 3 | 入门

简介

Mybatis 是一个半ORM框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。

MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。

通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。

作用

帮助程序员将数据存入到数据库中。

传统的JDBC代码太复杂了。简化、框架、自动化。

优点

  1. 基于SQL语句编程,不会对应用程序或者数据库的现有设计造成任何影响,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,重用性高。
  2. 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
  3. 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
  4. 能够与Spring很好的集成。
  5. 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。

缺点

  1. SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
  2. SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

Mybatis配置文件【可阅】

对配置文件不熟悉的可以阅读一下。

参考博客链接:【MyBatis】第二章:MyBatis xml配置文件详解

在使用mybatis框架时,首先导入其对应的jar包,并进行相应的配置,所以得对配置文件的每个参数都得了解。一个完全的mybatis配置文件结构如下:


DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    
    <properties>properties>
    
    <settings>
       
       <setting name="" value=""/> 
    settings>
    
    <typeAliases>typeAliases>
    
    <typeHandlers>typeHandlers>
    
    <objectFactory type="">objectFactory>
    
    <plugins>
       <plugin interceptor="">plugin>
    plugins>
    
    <environments default="">
       
       <environment id="">
          
          <transactionManager type="">transactionManager>
          
          <dataSource type="">dataSource>
       environment> 
    environments>
    
    <databaseIdProvider type="">databaseIdProvider>
    
    <mappers>mappers>
configuration>

properties

properties元素主要是用来定义配置外在化,比如数据库的连接属性等。这些属性都是可外部配置且可动态替换的,既可以在典型的Java属性文件中配置,亦可以通过properties元素的子元素来传递。例如:

<properties resource="org/mybatis/example/config.properties">
  <property name="username" value="root"/>
  <property name="password" value="123456"/>
properties>

其中的属性就可以在整个配置文件中使用来替换需要动态配置的属性值。比如在数据源中使用的例子:

<dataSource type="POOLED">
  <property name="driver" value="${driver}"/>
  <property name="url" value="${url}"/>
  <property name="username" value="${username}"/>
  <property name="password" value="${password}"/>
dataSource>

这个例子中的 usernamepassword 将会由 properties 元素中设置的相应值来替换。driverurl 属性将会由config.properties文件中对应的值来替换。这样就为配置提供了诸多灵活选择。属性也可以被传递到SqlSessionBuilder.build()方法中。例如:

SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, props);
 // or
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, environment, props);

但是,这也就涉及到了优先级的问题,如果属性不只在一个地方配置,那么mybatis将会按照下面的顺序来加载:

  1. 在properties元素体内指定的属性首先被读取。
  2. 然后根据properties元素中的resource属性读取类路径下属性文件或根据url属性指定的路径读取属性文件,并覆盖已读取的同名属性。
  3. 最后读取作为方法参数传递的属性,并覆盖已读取的同名属性。

因此,通过方法参数传递的属性具有最高优先级,resource/url属性中指定的配置文件次之,最低优先级的是properties属性中指定的属性。

settings

setting是指定MyBatis的一些全局配置属性,这是MyBatis中极为重要的调整设置,它们会改变MyBatis的运行时行为,所以我们需要清楚的知道这些属性的作用及默认值。

设置参数 描述 有效值 默认值
cacheEnabled 该配置影响的所有映射器中配置的缓存的全局开关 true 、false true
lazyLoadingEnabled 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态 true、false false
aggressiveLazyLoading 当启用时,对任意延迟属性的调用会使带有延迟加载属性的对象完整加载;反之,每种属性将会按需加载。 true、false true
multipleResultSetsEnabled 是否允许单一语句返回多结果集(需要兼容驱动)。 true、flase true
useColumnLabel 使用列标签代替列名。不同的驱动在这方面会有不同的表现, 具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果。 true、false true
useGeneratedKeys 允许 JDBC 支持自动生成主键,需要驱动兼容。 如果设置为 true 则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作(比如 Derby)。 true、false false
autoMappingBehavior 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示取消自动映射;PARTIAL 只会自动映射没有定义嵌套结果集映射的结果集。 FULL 会自动映射任意复杂的结果集(无论是否嵌套)。 NONE, PARTIAL, FULL PARTIAL
defaultExecutorType 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。 SIMPLE REUSE BATCH SIMPLE
defaultStatementTimeout 设置超时时间,它决定驱动等待数据库响应的秒数。 Any positive integer Not Set (null)
defaultFetchSize Sets the driver a hint as to control fetching size for return results. This parameter value can be override by a query setting. Any positive integer Not Set (null)
safeRowBoundsEnabled 允许在嵌套语句中使用分页(RowBounds)。 true、false false
mapUnderscoreToCamelCase 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。 true、false false
localCacheScope MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。 SESSION、STATEMENT SESSION
jdbcTypeForNull 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。 某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 JdbcType enumeration. Most common are: NULL, VARCHAR and OTHER OTHER
lazyLoadTriggerMethods 指定哪个对象的方法触发一次延迟加载。 A method name list separated by commas equals,clone,hashCode,toString
defaultScriptingLanguage 指定动态 SQL 生成的默认语言。 A type alias or fully qualified class name. org.apache.ibatis.scripting.xmltags.XMLDynamicLanguageDriver
callSettersOnNulls 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这对于有 Map.keySet() 依赖或 null 值初始化的时候是有用的。注意基本类型(int、boolean等)是不能设置成 null 的。 true、false false
logPrefix 指定 MyBatis 增加到日志名称的前缀。 Any String Not set
logImpl 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING Not set
proxyFactory 指定 Mybatis 创建具有延迟加载能力的对象所用到的代理工具。 CGLIB、JAVASSIST JAVASSIST (MyBatis 3.3 or above)

一个完整的settings元素示例如下:

<settings>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="multipleResultSetsEnabled" value="true"/>
  <setting name="useColumnLabel" value="true"/>
  <setting name="useGeneratedKeys" value="false"/>
  <setting name="autoMappingBehavior" value="PARTIAL"/>
  <setting name="defaultExecutorType" value="SIMPLE"/>
  <setting name="defaultStatementTimeout" value="25"/>
  <setting name="defaultFetchSize" value="100"/>
  <setting name="safeRowBoundsEnabled" value="false"/>
  <setting name="mapUnderscoreToCamelCase" value="false"/>
  <setting name="localCacheScope" value="SESSION"/>
  <setting name="jdbcTypeForNull" value="OTHER"/>
  <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
settings>

typeAliases

类型别名是为Java类型设置一个短的名字。它只和xml配置有关,存在的意义仅在于用来减少类完全限定名的冗余,例如:

<typeAliases>
  <typeAlias alias="Author" type="domain.blog.Author"/>
  <typeAlias alias="Blog" type="domain.blog.Blog"/>
  <typeAlias alias="Comment" type="domain.blog.Comment"/>
  <typeAlias alias="Post" type="domain.blog.Post"/>
  <typeAlias alias="Section" type="domain.blog.Section"/>
  <typeAlias alias="Tag" type="domain.blog.Tag"/>
typeAliases>

当这样配置时,Blog可以用在任何使用domain.blog.Blog的地方。

也可以指定一个包名,MyBatis会在包名下搜索需要的JavaBean,比如:

<typeAliases>
  <package name="domain.blog"/>
typeAliases>

每一个在包domain.blog中的JavaBean,在没有注解的情况下,会使用Bean的首字母小写的非限类名来作为它的别名。比如domain.blog.Author的别名为author;若有注解,则别名为注解值。看下面的例子:

@Alias("author")
public class Author {
	...
}

已经为许多常见的Java类型内建了相应的类型别名。它们都是大小写不敏感的,需要注意的是有基本类型名称重复导致的特殊处理。

别名 映射的类型
_byte byte
_long long
_short short
_int int
_integer int
_double double
_float float
__boolean boolean
string String
byte Byte String
long Long
short Short
int Integer
integer Integer
double Double
float Float
boolean Boolean
date Date
decimal BigDecimal
bigdecimal BigDecimal
object Object
map Map
hashmap HashMap
list List
arraylist ArrayList
collection Collection
iterator Iterator

typeHandlers

无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。下表描述了一些默认的类型处理器。

类型处理器 Java 类型 JDBC 类型
BooleanTypeHandler java.lang.Boolean, boolean 数据库兼容的 BOOLEAN
ByteTypeHandler java.lang.Byte, byte 数据库兼容的 NUMERIC 或 BYTE
ShortTypeHandler java.lang.Short, short 数据库兼容的 NUMERIC 或 SHORT INTEGER
IntegerTypeHandler java.lang.Integer, int 数据库兼容的 NUMERIC 或 INTEGER
LongTypeHandler java.lang.Long, long 数据库兼容的 NUMERIC 或 LONG INTEGER
FloatTypeHandler java.lang.Float, float 数据库兼容的 NUMERIC 或 FLOAT
DoubleTypeHandler java.lang.Double, double 数据库兼容的 NUMERIC 或 DOUBLE
BigDecimalTypeHandler java.math.BigDecimal 数据库兼容的 NUMERIC 或 DECIMAL
StringTypeHandler java.lang.String CHAR, VARCHAR
ClobTypeHandler java.lang.String CLOB, LONGVARCHAR
NStringTypeHandler java.lang.String NVARCHAR, NCHAR
NClobTypeHandler java.lang.String NCLOB
ByteArrayTypeHandler byte[] 数据库兼容的字节流类型
BlobTypeHandler byte[] BLOB, LONGVARBINARY
DateTypeHandler java.util.Date TIMESTAMP
DateOnlyTypeHandler java.util.Date DATE
TimeOnlyTypeHandler java.util.Date TIME
SqlTimestampTypeHandler java.sql.Timestamp TIMESTAMP
SqlDateTypeHandler java.sql.Date DATE
SqlTimeTypeHandler java.sql.Time TIME
ObjectTypeHandler Any OTHER 或未指定类型
EnumTypeHandler Enumeration Type VARCHAR-任何兼容的字符串类型,存储枚举的名称(而不是索引)
EnumOrdinalTypeHandler Enumeration Type 任何兼容的 NUMERIC 或 DOUBLE 类型,存储枚举的索引(而不是名称)。

可以重写类型处理器或创建自己的类型处理器来处理不支持的或非标准的类型。具体的做法为:实现org.apache.ibatis.type.TypeHandler接口,或继承一个很便利的类org.apache.ibatis.type.BaseTypeHandler,然后可以选择性地将它映射到一个JDBC类型。比如:

// ExampleTypeHandler.java
@MappedJdbcTypes(JdbcType.VARCHAR)
public class ExampleTypeHandler extends BaseTypeHandler<String> {
 
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, parameter);
    }
 
    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException
    {
        return rs.getString(columnName);
    }
 
    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException
    {
        return rs.getString(columnIndex);
    }
 
    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return cs.getString(columnIndex);
    }
    
}

并且还需要在配置文件里面加上:


<typeHandlers>
  <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
typeHandlers>

使用这个的类型处理器将会覆盖已经存在的处理Java 的 String 类型属性和 VARCHAR 参数及结果的类型处理器。要注意MyBatis不会窥探数据库元信息来决定使用哪种类型,所以必须在参数和结果映射中指明是 VARCHAR 类型字段,以使其能绑定到正确的类型处理器上。这是因为,MyBatis直到语句被执行才清楚数据类型。

通过类型处理器的泛型,MyBatis可以得知该类型处理器的Java类型,不过这种行为可以通过两种方法来指定被关联的JDBC类型:

  • 在类型处理器的元素(typeHandler element)上增加一个javaType属性(比如,javaType=“String”);
  • 在类型处理器的类上(TypeHandler class)增加一个@MappedTypes注解来指定与其关联的Java类型列表。如果在javaType属性中也同时制定,则注解方式将被忽略。

最后,还可以让MyBatis查找类型处理器:


<typeHandlers>
  <package name="org.mybatis.example"/>
typeHandlers>

注意在使用自动检索(autodiscovery)功能的时候,只能通过注解的方式来指定JDBC类型。

你能创建一个泛型类型处理器,它可以处理多于一个类。为达到此目的,需要增加一个接收该类作为参数的构造器,这样在构造一个类型处理器的时候MyBatis就会传入一个具体的类。

//GenericTypeHandler.java
public class GenericTypeHandler<E extends MyObject> extends BaseTypeHandler<E> {
    private Class<E> type;
    public GenericTypeHandler(Class<E> type) {
        if (type == null) 
            throw new IllegalArgumentException("Type argument cannot be null");
        this.type = type;
    }
  ...
}

EnumTypeHandlerEnumOrdinalTypeHandler都是泛型处理器(generic TypeHandlers),接下来的部分详细探讨。

处理枚举类型

若想映射枚举类型Enum,则需要从EnumTypeHandler 或者 EnumOrdinalTypeHandler 中选一个来使用

比如说我们想存储近似值时用到的舍入模式。默认情况下,MyBatis会利用EnumTypeHandler 来把Enum 值转换成对应的名字。

注意 EnumTypeHandler 在某种意义上来说是比较特别的,其他的处理器只针对某个特定的类,而它不同,它会处理任意继承了Enum 的类。

不过,我们可能不想存储名字,相反我们的DBA会坚持使用整形值代码。那也一样轻而易举;在配置文件中把EnumOrdinalTypeHandler 加到typeHandlers 中即可,这样每个RoundingMode 将通过他们的序数值来映射成对应的整形。


<typeHandlers>
  <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="java.math.RoundingMode"/>
typeHandlers>

但是怎么样能将同样的 Enum 既映射成字符串又映射成整形呢?

自动映射器( auto-mapper )会自动选用 EnumOrdinalTypeHandler 来处理,所以如果我们想用普通的EnumTypeHandler ,就非要为那些SQL语句显示地设置要用到的类型处理器不可。

DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 
 
<mapper namespace="org.apache.ibatis.submitted.rounding.Mapper">
	<resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap">
		<id column="id" property="id"/>
		<result column="name" property="name"/>
		<result column="funkyNumber" property="funkyNumber"/>
		<result column="roundingMode" property="roundingMode"/>
	resultMap>
 
 
	<select id="getUser" resultMap="usermap">
		select * from users
	select>
	<insert id="insert">
        insert into users (id, name, funkyNumber, roundingMode)
        values
	    (#{id},#{name},#{funkyNumber},#{roundingMode})
	insert>
		
	<resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap2">
		<id column="id" property="id"/>
		<result column="name" property="name"/>
		<result column="funkyNumber" property="funkyNumber"/>
		<result column="roundingMode" property="roundingMode" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/>
	resultMap>
	<select id="getUser2" resultMap="usermap2">
		select * from users2
	select>
	<insert id="insert2">
        insert into users2 (id, name, funkyNumber, roundingMode) values(
	    	#{id}, #{name}, #{funkyNumber}, #{roundingMode, typeHandler=
	    org.apache.ibatis.type.EnumTypeHandler})
	insert>
 
 
mapper>

注意,这里的select语句强制使用resultMap来代替resultType。

objectFactory

MyBatis每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过参数构造方法来实例化。如果想覆盖对象工厂的行为,则可以通过创建自己的对象工厂来实现,比如:

//ExampleObjectFactory.java
public class ExampleObjectFactory extends DefaultObjectFactory {
    public Object create(Class type) {
        return super.create(type);
    }
    public Object create(Class type, List<Class> constructorArgTypes, List<Object> constructorArgs) {
        return super.create(type, constructorArgTypes,constructorArgs);
    }
    
    public void setProperties(Properties properties) {
        super.setProperties(properties);
    }
    public <T> boolean isCollection(Class<T> type) {
        return Collection.class.isAssignableFrom(type);
    }
}

<objectFactory type="org.mybatis.example.ExampleObjectFactory">
  <property name="someProperty" value="100"/>
objectFactory>

ObjectFactory接口很简单,它包含两个创建用的方法,一个是处理默认构造方法的,另外一个是处理带参数的构造方法。最后setProperties方法可以被用来配置ObjectFactory,初始化你的ObjectFactory实例后,objectFactory元素体内定义的属性会被传递给setProperties方法。

plugins

MyBatis允许你在已映射的语句执行过程中的某一点进行拦截调用。默认情况下,Mybatis允许使用插件来拦截的方法调用包括:

  • Executor(update,query,flushStatements,commit,rollback,getTransaction,close,isClosed)
  • ParameterHandler(getParameterObejct,setParameters)
  • ResultSetHandler(handlerResultSets,handlerOutputParameters)
  • StatementHandler(prepare,parameterize,batch,update,query)

这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看MyBatis的发行包中的源代码。假设你想做的不仅仅是方法的调用,那么你应该很好的了解正在重写的方法的行为。因为如果在视图修改或重写已有方法的行为的时候,你很有可能在破坏MyBatis的核心模块。这些都是更低层的类和方法,所以使用插件的时候要特别担心。

通过MyBatis提供强大的机制,使用插件是非常简单的,只需要实现Interceptor接口,并指定想要拦截的方法签名即可。

// ExamplePlugin.java
@Intercepts(
    {
        @Signature(
            type= Executor.class,
            method = "update",
  			args = {
      			MappedStatement.class,Object.class
  			}
         )
    }
)
public class ExamplePlugin implements Interceptor {
    public Object intercept(Invocation invocation) throws Throwable {
        return invocation.proceed();
    }
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);
    }
    public void setProperties(Properties properties) {

    }
}

<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  plugin>
plugins>

上面的插件将会拦截Executor实例中所有的“update”方法调用,这里的Executor是负责执行底层映射语句的内部对象。
覆盖配置类
除了用插件来修改MyBatis核心行为之外,还可以通过完全覆盖配置类来达到目的。只需继承后覆盖其中的每个方法,再把它传递到sqlSessionFactoryBuilder.build(myConfig)方法即可。再次重申,这可能会严重影响Mybatis的行为,务请慎之又慎!

environments

MyBatis可以配置成适应多种环境,这种机制有助于将sql映射应用于多种数据库中,现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者共享相同的Schema的多个生产数据库,想使用相同的sql映射。许多类似的用例。
尽管可以配置多个环境,但是每个SqlSessionFactory实例只能选择其一。

所以,如果想连接两个数据库,就需要创建两个SqlSessionFactory实例,每个数据库对应一个。而如果是三个数据库,就需要三个实例,依此类推。

每个数据库对应一个SqlSessionFactory实例。

为了指定创建哪种环境,只要将它作为可选参数传递给SqlSessionFactoryBuilder即可。可以接受环境配置的两个方法签名是:

SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, environment);
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, environment,properties);

如果忽略了环境参数,那么默认环境将会被加载,如下所示:

SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader);
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader,properties);

环境元素定义了如何配置环境:

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC">
      <property name="..." value="..."/>
    transactionManager>
    <dataSource type="POOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    dataSource>
  environment>
environments>

注意这里的关键点:

  • 默认环境的ID(比如:default=“development”)
  • 每个environment元素定义的环境ID(比如:id=“development”)
  • 事务管理器的配置(比如:type=“JDBC”)
  • 数据源的配置(比如:type=“POOLED”)

默认的环境和环境ID是一目了然的。随你怎么命名,只要保证默认环境要匹配其中一个环境ID事务管理器(transactionManager)。

在MyBatis中有两种类型的事务管理器(也就是 type="[JDBC|MANAGED]")

  1. JDBC:这个配置就是直接使用了JDBC的提交和回滚设置,它依赖于从数据源得到的连接来管理事务范围。

  2. MANAGED:这个配置几乎没做什么。它从来不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如JEE应用服务器上下文)。默认情况下它会关闭连接,然而一些容器并不希望这样,因此需要将closeConnection属性设置为false来阻止它默认的行为。例如:

    <transactionManager type="MANAGED">
      <property name="closeConnection" value="false"/>
    transactionManager>
    

如果正在使用Spring+MyBatis,则没有必要配置事务管理器,因为Spring模块会使用自带的管理器来覆盖前面的配置。
这两种事务管理器类型都不需要任何属性。它们只不过是类型别名,换句话说,你可以使用TransactionFactory接口的实现类的完全限定名或类型别名替代它们。

public interface TransactionFactory {
    
  void setProperties(Properties props);   
    
  Transaction newTransaction(Connection conn);
    
  Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);   

}

任何在xml中配置的属性在实例化之后将会被传递给setProperties方法。你也需要创建一个Transaction接口的实现类,这个接口也很简单。

 public interface Transaction{
     
  Connection getConnection() throws SQLException;
     
  void commit() throws SQLException;
     
  void rollback() throws SQLException;
     
  void close() throws SQLException;
     
}

使用这两个接口,完全可以自定义MyBatis对事务的处理。

数据源(dataSource)

dataSource元素使用了标准的JDBC数据源接口来配置JDBC连接对象的资源。

许多MyBatis的应用程序将会按示例中的例子来配置数据源。然而它并不是必须的。要知道为了方便使用延迟加载,数据源才是必须的。

有三种内建的数据源类型(也就是 type="[UNPOOLED|POOLED|JNDI]"):

UNPOOLED

这个数据源的实现只是被请求时打开和关闭连接。虽然有一点慢,它对在及时可用连接方面没有性能要求的简单应用是一个很好的选择。不同的数据库在这方面表现也是不一样的,所以对某些数据库来说使用连接池并不重要,这个配置也是理想。UNPOOLED类型的数据源仅仅需要配置以下5种属性:

  • driver – 这是JDBC驱动的Java类的完全限定名(并不是JDBC驱动中可能包含的数据源类)
  • url – 这是数据库的JDBC URL 地址。
  • username – 登录数据库的用户名。
  • password – 登录数据库的密码。
  • defaultTransactionIsolationLevel – 默认的连接事务隔离级别。

作为可选项,可以传递属性给数据库驱动。要这样做,属性的前缀为"driver.",例如:

  • driver.encoding=UTF-8

这将通过DriverManager,getConnection(url,driverProperties)方法传递值为UTF-8的encoding属性给数据库驱动。

POOLED

这种数据源的实现利用“池”的概念将JDBC连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。这是一种使得并发web应用快速响应请求的流行处理方式。

除了上述提到UNPOOLED下的属性外,会有更多属性用来配置POOLED的数据源:

  • poolMaximumActiveConnections – 在任意时间可以存在的活动(也就是正在使用)连接数量,默认值10。
  • poolMaximumIdleConnections – 任意时间可能存在的空闲连接数。
  • poolMaximumCheckoutTime – 在被强制返回之前,池中连接被检出(checked out)时间,默认值:20000毫秒(即20秒)。
  • poolTimeToWait – 这是一个底层设置,如果获取连接花费的相当长的时间,它会给连接池打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直安静的失败),默认值20000毫秒(即20秒)。
  • poolPingQuery – 发送到数据库的侦测查询,用来检验连接是否处在正常的工作秩序中,并且准备接受请求。默认是"NOT PING QUERY SET",这会导致多数数据库连接失败时带有一个恰当的错误信息。
  • poolPingEnabled – 是否启用侦测。若开启,也必须使用一个可执行的SQL语句设置poolPingQuery属性(最好是一个非常快的SQL),默认值:false。
  • poolPingConnectionsNotUsedFor – 配置poolPingQuery使用的频度。这可以被设置成匹配具体的数据库连接超时时间,来避免不必要的侦测,默认值:0(即所有连接每一时刻都被侦测 – 当然仅当 poolPingEnabled为true时适用)。
JNDI

这个数据源的实现是为了能在如EJB或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个JNDI上下文的引用。这种数据源配置只要两个属性:

  • initial_context – 这个属性用来在InitialContext中寻找上下文(即,initialContext.lookup(initial_context))。这是个可选属性,如果忽略,那么data_source属性将会直接从InitialContext中寻找。
  • data_source – 这是引用数据源实例位置的上下文的路径。提供了 initial_context配置时会在其返回的上下文中进行查找,没有提供时则直接在InitialContext中查找。

和其他数据源配置类似,可以通过添加前缀"env."直接把属性传递给初始上下文。比如:

  • env.encoding=UTF-8

这会在初始上下文(InitialContext)实例化时往它的构造方法传递值为UTF-8的encoding属性。

通过需要实现接口 org.apache.ibatis.datasource.DataSourceFactory,也可使用任何第三方数据源:

 public interface DataSourceFactory {
     
  void setProperties(Properties props);
     
  DataSource getDataSource();
     
}

org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory 可被用作父类来构建新的数据源适配器,比如下面这段插入C3P0所必需的代码:

import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory;
import com.mchange.v2.c3p0.ComboPooledDataSource;

public class C3P0DataSourceFactory extends UnpooledDataSourceFactory {
    public C3P0DataSourceFactory() {
        this.dataSource =  new ComboPooledDataSource();
    }
}

为了令其工作,为每个需要MyBatis调用的setter方法中增加一个属性。下面是一个可以连接到PostgreSQL数据库的例子:

<dataSource type="org.myproject.C3P0DataSourceFactory">
  <property name="driver" value="org.postgresql.Driver"/>
  <property name="url" value="jdbc:postgresql:mydb"/>
  <property name="username" value="postgres"/>
  <property name="password" value="root"/>
dataSource>

databaseIdProvider

MyBatis可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的databaseId属性。MyBatis会加载不带databaseId属性和带有匹配当前数据库databaseId属性的所有语句。如果同时找到带有databaseId和不带databaseId的相同语句,则后者被舍弃。为支持多厂商特性,只要像下面这样在mybatis-config.xml文件中加入databaseIdProvider即可:

<databaseIdProvider type="DB_VENDOR" />

这里的DB_VENDOR会通过DatabaseMetaData#getDatabaseProductName()返回的字符串进行设置。由于通常情况下这个字符串都非常长而且相同产品的不同版本会返回不同的值,所以最好通过设置属性别名来使其变短,如下:

<databaseIdProvider type="DB_VENDOR">
    <property name="SQL Server" value="sqlserver"/>
    <property name="DB2" value="db2"/>         
    <property name="Oracle" value="oracle" />
databaseIdProvider>

在有properties时,DB_VENDOR databaseIdProvider的将被设置为第一个能匹配数据库产品名称的属性键值对应的值,如果没有匹配的属性将会设置为”null“。在这个例子中,如果getDatabaseProductName()返回”Oracle(DataDirect)“,databaseId将被设置为"oracle"。
可以通过实现接口org.apache.ibatis.mapping.DatabaseIdProvider并在mybatis-config.xml中注册来构建自己的DatabaseIdProvider

public interface DatabaseIdProvider {
    
    void setProperties(Properties p);
    
    String getDatabaseId(DataSource dataSource) throws SQLException;
    
}

mappers

既然MyBatis的行为已经由上述元素配置完了,现在就要定义SQL映射语句了。但是首先需要告诉MyBatis到哪里去找到这些语句。Java在自动查找这方面没有提供一个很好的方法,所以最佳的方式是告诉MyBatis到哪里去找映射文件。可以使用相对于类路径的资源引用、或完全限定资源定位符(包括file:///的URL),或类名和包名等等。例如:


<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
mappers>

<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
mappers>

<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
mappers>

<mappers>
  <package name="org.mybatis.builder"/>
mappers>

这些配置会告诉了MyBatis去哪里找映射文件,剩下的细节就应该是每个SQL映射文件了。

Mybatis源码浅析

这部分内容仅涉及SqlSessionFactoryBuilderSqlSessionMapperProxy插件等源码部分,对于日志cache等仅提供博客链接参考。

本段落参考博客链接:Mybatis运行原理及源码解析

引入

项目的搭建我们可以参照官方文档。本次基于mybatis-3.5.3.jar版本搭建分析,不加入Spring的整合。参考官方文档,我们只需要创建mybatis-config.xmlmapper.xml 文件以及对应的 mapper 接口。为了方便大家阅读,搭建源码如下:

mybatis-config.xml


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="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/test"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            dataSource>
        environment>
    environments>
    <mappers>
        <mapper resource="mybatis/mapper/UserMapper.xml"/>
    mappers>
configuration>

UserMapper.xml


DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mapper.UserMapper">
 
    <resultMap id="User" type="pojo.User">
        <result column="user_id" property="userId" jdbcType="INTEGER" javaType="Integer"/>
        <result column="user_account" property="userAccount" jdbcType="VARCHAR" javaType="String"/>
        <result column="user_createtime" property="userCreatetime" jdbcType="TIMESTAMP"
                javaType="java.time.LocalDateTime"/>
    resultMap>
    <sql id="userColumn">
        user_id,
        user_account,
        user_createtime
    sql>
    <select id="getUserById" parameterType="java.lang.String" resultMap="User">
        SELECT
        <include refid="userColumn"/>
        FROM `user`
        where
        user_id = #{userId}
    select>
mapper>

UserMapper.java

public interface UserMapper {
    WxUser getUserById(String userId);
}

MybatisTest.java入口测似类

public static void main(String[] args) {
    try {
        String resource = "mybatis-config.xml";
        // 通过classLoader获取到配置文件
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        // 把配置文件和mapper文件封装到Configuration实体类
        SqlSessionFactory sqlSessionFactory = builder.build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 动态代理方式
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = mapper.getUserById("8");
        System.out.println("通过动态代理返回结果" + user.getUserAccount());

        // 不用动态代理的方式直接statement获取查询
        User u2 = sqlSession.selectOne("mapper.UserMapper.getUserById", "8");
        System.out.println("通过statement返回结果" + u2.getUserAccount());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

SqlSessionFactoryBuilder

由于 SqlSessionFactory 初始化需要的参数比较多,所以Mybatis这里采用了构造者模式通过xml的方式实例化一个 SqlSessionFactory 对象。具体的属性和配置可以查看官方文档。通过查看 SqlSessionFactoryBuilder 的build()方法分析源码,主要逻辑看代码注释(最好结合源码对照查看)。

public class SqlSessionFactoryBuilder {
    
	public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
        try {
            // 主要是把配置文件构造成XMLConfigBuilder对象 
        	XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
        	return build(parser.parse());
        } 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.
          	}
        }
  	}
    
    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    	try {
            // 主要是把配置文件构造成XMLConfigBuilder对象 
      		// 通俗的说就是拿到config.xml的inputStream,然后解析xml,把配置的信息封装到parser对象里面
      		XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      		return build(parser.parse());
    	} catch (Exception e) {
      		throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    	} finally {
      		ErrorContext.instance().reset();
        	try {
        		inputStream.close();
      		} catch (IOException e) {
        		// Intentionally ignore. Prefer previous error.
      		}
        }
    }
    
    public SqlSessionFactory build(Configuration config) {
    	return new DefaultSqlSessionFactory(config);
  	}
}

XMLConfigBuilder#parse()

// 解析config.xml和所有的mapper.xml 封装成Configuration对象返回
public Configuration parse() {
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}
XMLConfigBuilder#parseConfiguration()
private void parseConfiguration(XNode root) {
    try {
        //issue #117 read properties first
        /**解析配置文件中的各种属性*/
        propertiesElement(root.evalNode("properties"));
        /**解析mybatis的全局设置信息*/
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        loadCustomVfs(settings);
        loadCustomLogImpl(settings);
        /**解析别名配置*/
        typeAliasesElement(root.evalNode("typeAliases"));
        /**解析插件配置*/
        pluginElement(root.evalNode("plugins"));
        /**解析对象工厂元素*/
        objectFactoryElement(root.evalNode("objectFactory"));
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        reflectorFactoryElement(root.evalNode("reflectorFactory"));
        settingsElement(settings);
        // read it after objectFactory and objectWrapperFactory issue #631
        /**解析mybatis的环境配置*/
        environmentsElement(root.evalNode("environments"));
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        /**解析类型处理器配置信息*/
        typeHandlerElement(root.evalNode("typeHandlers"));
        /**解析mapper配置信息*/
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

实际上就是解析主配置文件中的各个节点,然后保存在 Configuration 当中,然后使用 Configuration 创建出一个 DefaultSqlsessionFactory 对象。

此处,我们可以重点关注如下两个地方,看看具体在做了什么动作:

pluginElement(root.evalNode("plugins"));
mapperElement(root.evalNode("mappers"));
插件注册
pluginElement(root.evalNode("plugins"));

点进去查看详细实现:

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // 获取到内容 比如com.github.pagehelper.PageHelper
            String interceptor = child.getStringAttribute("interceptor");
            // 获取配置的属性信息
            Properties properties = child.getChildrenAsProperties();
            // 创建的拦截器实例
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
            // 将属性和拦截器绑定
            interceptorInstance.setProperties(properties);
            /** 将实例化的拦截器类放到configuration中的interceptorChain中 */
            configuration.addInterceptor(interceptorInstance);
        }
    }
}

实际上就是通过interceptor标签,解析出拦截器类,然后将其实例化并保存到Configuration类中的InterceptorChain中,以备后用。

public void addInterceptor(Interceptor interceptor) {
    // 将拦截器添加到了 拦截器链中 而拦截器链本质上就是一个List有序集合
    this.interceptorChain.addInterceptor(interceptor);
}
mapper扫描与解析
mapperElement(root.evalNode("mappers"));

点进去查看详细实现:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        // 遍历config.xml所有的
      	//                       
      	//                   
        for (XNode child : parent.getChildren()) {
            /*如果子节点是配置的,那么进行包自动扫描处理*/
            if ("package".equals(child.getName())) {
                String mapperPackage = child.getStringAttribute("name");
                configuration.addMappers(mapperPackage);
            } else {
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                /**如果子节点配置的是resource、url、mapperClass,本文我们使用的是resource*/
                if (resource != null && url == null && mapperClass == null) {
                    ErrorContext.instance().resource(resource);
                    // 获取到mapper.xml文件
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    // 把xml封装成对象
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    // 解析封装到Configuration对象中
                    mapperParser.parse();
                } else if (resource == null && url != null && mapperClass == null) {
                    ErrorContext.instance().resource(url);
                    InputStream inputStream = Resources.getUrlAsStream(url);
                    /**解析resource引入的另外一个xml文件*/
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                    mapperParser.parse();
                } else if (resource == null && url == null && mapperClass != null) {
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

下面我们具体看一下他是如何解析另一个xml文件的:

// mapperParser.parse()
public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        /*解析sql语句*/
        configurationElement(parser.evalNode("/mapper"));
        configuration.addLoadedResource(resource);
        /*
        	绑定mapper的namespace
        	即解析名称空间,实际上就是对应绑定的接口类
        */
        bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

下面我们来看一下 configurationElement(parser.evalNode("/mapper"))到底做了什么:

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.setCurrentNamespace(namespace);
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        sqlElement(context.evalNodes("/mapper/sql"));
        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);
    }
}

我们来看一下buildStatementFromContext(context.evalNodes("select|insert|update|delete"))到底做了什么:

private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}

我们来看一下buildStatementFromContext()的重载方法:

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

来看一下statementParser.parseStatementNode()方法里:

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        return;
    }

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre:  and  were parsed and removed)
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
        keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
        keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
                                                   configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
            ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
        resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                                        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                                        resultSetTypeEnum, flushCache, useCache, resultOrdered,
                                        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

来看一下添加参数返回MappedStatementaddMappedStatement(...)方法:

public MappedStatement addMappedStatement(
    String id,
    SqlSource sqlSource,
    StatementType statementType,
    SqlCommandType sqlCommandType,
    Integer fetchSize,
    Integer timeout,
    String parameterMap,
    Class<?> parameterType,
    String resultMap,
    Class<?> resultType,
    ResultSetType resultSetType,
    boolean flushCache,
    boolean useCache,
    boolean resultOrdered,
    KeyGenerator keyGenerator,
    String keyProperty,
    String keyColumn,
    String databaseId,
    LanguageDriver lang,
    String resultSets) {

    if (unresolvedCacheRef) {
        throw new IncompleteElementException("Cache-ref not yet resolved");
    }

    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
        statementBuilder.parameterMap(statementParameterMap);
    }

    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
}

通过解析一个个的标签,最终将sql语句的所有信息封装成MappedStatement对象,然后存储在configuration对象中。

那么bindMapperForNamespace()又做了什么呢?

private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
        Class<?> boundType = null;
        try {
            boundType = Resources.classForName(namespace);
        } catch (ClassNotFoundException e) {
            //ignore, bound type is not required
        }
        if (boundType != null) {
            if (!configuration.hasMapper(boundType)) {
                // Spring may not know the real resource name so we set a flag
                // to prevent loading again this resource from the mapper interface
                // look at MapperAnnotationBuilder#loadXmlResource
                configuration.addLoadedResource("namespace:" + namespace);
                configuration.addMapper(boundType);
            }
        }
    }
}

实际上就是解析该sql对应的class,并把该class放到configuration中的mapperRegistry中。实际上mybatis的所有配置信息以及运行时的配置参数全部都保存在configuration对象中。

所以整个流程可以用如下的时序图表示:

备战面试日记(4.3)- (框架.Mybatis)_第1张图片

SqlSession

SqlSession 的获取主要是通过 SqlSessionFactory 的默认实现类 DefaultSqlSessionFactoryopenSessionFromDataSource 封装一个 DefaultSqlSession (实现 SqlSession 接口)返回。

当执行 openSession() 操作的时候,实际上执行的代码如下:

@Override
public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        // 从配置对象获取数据库链接信息和事物对象
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        // 创建一个Executor对象,用于后面执行SQL脚本
        final Executor executor = configuration.newExecutor(tx, execType); // 【核心代码】
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        closeTransaction(tx); // may have fetched a connection so lets call close()
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

从代码可以知道,openSession() 操作会创建 Mybatis 四大对象之一的 Executor 对象,创建过程如下:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }
    /**如果开启了二级缓存,executor会被CachingExecutor包装一次*/
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }
    /*
        尝试将executor使用interceptorChain中的每个interceptor包装一次(根据配置),
        这里是对Mybatis强大的插件开发功能做支持
    */
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

默认情况下会返回一个SimpleExecutor对象。然后SimpleExecutor被封装到DefaultSqlSession

这里我们需要注意一下,在Executor创建完毕之后,会根据配置是否开启了二级缓存,来决定是否使用CachingExecutor包装一次Executor。最后尝试将executor使用interceptorChain中的每个interceptor包装一次(根据配置),这里是对Mybatis强大的插件开发功能做支持。

Mapper代理

当我们使用如下代码:

UserMapper mapper = session.getMapper(UserMapper.class);

来获取 UserMapper 的时候,实际上是从 configuration 当中的 MapperRegistry 当中获取 UserMapper 的代理对象:

/**
  * 可以看到我们是从 Configuration 对象中的 MapperRegistry 对象通过类对象作为key获取
  * MapperProxyFactory 然后通过jdk的动态代理生成代理对象
  (这里也就解释了为什么我们要创建一个 Mapper 接口而不是实体类)  
  * 里面的 addMapper() 方法是不是似曾相识。
  */

// DefaultSqlSession 289
@Override
public <T> T getMapper(Class<T> type) {
    return configuration.getMapper(type, this);
}

// Configuration 778
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}

// MapperRegistry
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

// MapperRegistry 44
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
        return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
        throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
}

我们来看一下 MapperProxyFactory 的实现:

public class MapperProxyFactory<T> {

    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public Class<T> getMapperInterface() {
        return mapperInterface;
    }

    public Map<Method, MapperMethod> getMethodCache() {
        return methodCache;
    }

    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }
	
    // 上方代码中,调用的是这个方法
    // knownMappers属性里面的值,实际上就是我们在mappers扫描与解析的时候放进去的。
    public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }

}
public class MapperProxy<T> implements InvocationHandler, Serializable {
    ...
}

并且MapperProxy实现了InvocationHandler接口,从以上代码可以看出,实际上使用的就是jdk的动态代理,给UserMapper接口生成一个代理对象。实际上就是MapperProxy的一个对象,如下图调试信息所示:

备战面试日记(4.3)- (框架.Mybatis)_第2张图片

所以整个代理对象生成过程可以用如下时序图表示:

备战面试日记(4.3)- (框架.Mybatis)_第3张图片

执行查询语句

我们知道,我们获取到的UserMapper实际上是代理对象MapperProxy,所以我们执行查询语句的时候实际上执行的是MapperProxy的invoke方法:

// MapperProxy 78
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        /**如果调用的是Object原生的方法,则直接放行*/
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else if (method.isDefault()) {
            if (privateLookupInMethod == null) {
                return invokeDefaultMethodJava8(proxy, method, args);
            } else {
                return invokeDefaultMethodJava9(proxy, method, args);
            }
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
}

我们再来看看cachedMapperMethod方法:

private MapperMethod cachedMapperMethod(Method method) {
    return methodCache.computeIfAbsent(method,
        k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
  }

可以看到,先根据方法签名,从方法缓存中获取方法,如果为空,则生成一个MapperMethod放入缓存并返回。

所以最终执行查询的是MapperMethod的execute方法:

// MapperMethod 57
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
        case INSERT: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.insert(command.getName(), param));
            break;
        }
        case UPDATE: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.update(command.getName(), param));
            break;
        }
        case DELETE: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.delete(command.getName(), param));
            break;
        }
         /**select查询语句*/
        case SELECT:
            /**当返回类型为空*/
            if (method.returnsVoid() && method.hasResultHandler()) {
                executeWithResultHandler(sqlSession, args);
                result = null;
            /**当返回many的时候*/
            } else if (method.returnsMany()) {
                result = executeForMany(sqlSession, args);
            /**当返回值类型为Map的时候*/
            } else if (method.returnsMap()) {
                result = executeForMap(sqlSession, args);
                
            } else if (method.returnsCursor()) {
                result = executeForCursor(sqlSession, args);
            } else {
                /**除去以上情况,执行这里的步骤*/
                Object param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.selectOne(command.getName(), param);
                if (method.returnsOptional()
                    && (result == null || !method.getReturnType().equals(result.getClass()))) {
                    result = Optional.ofNullable(result);
                }
            }
            break;
        case FLUSH:
            result = sqlSession.flushStatements();
            break;
        default:
            throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
        throw new BindingException("Mapper method '" + command.getName()
                                   + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
}

我们示例中执行的分支语句是:

 Object param = method.convertArgsToSqlCommandParam(args);
 result = sqlSession.selectOne(command.getName(), param);

这里有一个查询参数的解析过程:

// MapperMethod 308
public Object convertArgsToSqlCommandParam(Object[] args) {
    return paramNameResolver.getNamedParams(args);
}

// ParamNameResolver 110
public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
        return null;
        /**参数没有标注@Param注解,并且参数个数为一个*/
    } else if (!hasParamAnnotation && paramCount == 1) {
        return args[names.firstKey()];
        /**否则执行这个分支*/
    } else {
        final Map<String, Object> param = new ParamMap<>();
        int i = 0;
        for (Map.Entry<Integer, String> entry : names.entrySet()) {
            param.put(entry.getValue(), args[entry.getKey()]);
            // add generic param names (param1, param2, ...)
            final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
            // ensure not to overwrite parameter named with @Param
            if (!names.containsValue(genericParamName)) {
                param.put(genericParamName, args[entry.getKey()]);
            }
            i++;
        }
        return param;
    }
}

这里代码的意思是:这里参数解析如果判断参数一个只有一个(一个单一参数或者是一个集合参数),并且没有标注@Param注解,那么直接返回这个参数的值,否则会被封装为一个Map,然后再返回。

封装的样式如下用几个示例解释:

例1:

/**接口为*/
User selectByNameSex(String name, int sex);
/**我们用如下格式调用*/
userMapper.selectByNameSex("张三",0);
/**参数会被封装为如下格式:*/
0 ---> 张三
1 ---> 0
param1 ---> 张三
param2 ---> 0

备战面试日记(4.3)- (框架.Mybatis)_第4张图片

例2:

/**接口为*/
User selectByNameSex(@Param("name") String name, int sex);
/**我们用如下格式调用*/
userMapper.selectByNameSex("张三",0);
/**参数会被封装为如下格式:*/
name ---> 张三
1 ---> 0
param1 ---> 张三
param2 ---> 0

备战面试日记(4.3)- (框架.Mybatis)_第5张图片

参数处理完接下来就是调用执行过程,最终调用执行的是DefaultSqlSession中的selectList方法:

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
        MappedStatement ms = configuration.getMappedStatement(statement);
        return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

这里调用SimpleExecutor的query方法执行查询操作,接着调用doQuery方法:

// SimpleExecutor 57
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
        Configuration configuration = ms.getConfiguration();
        /**这里出现了Mybatis四大对象中的StatementHandler*/
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        stmt = prepareStatement(handler, ms.getStatementLog());
        return handler.query(stmt, resultHandler);
    } finally {
        closeStatement(stmt);
    }
}

// Configuration 591
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) 
        /**创建StatementHandler并应用到插件支持*/   
        interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

在创建StatementHandler的同时,应用插件功能,同时创建了Mybatis四大对象中的另外两个对象:

protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
   ……
   ……
   ……
 /**Mybatis四大对象中的ParameterHandler*/     
  this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
  /**Mybatis四大对象中的ResultSetHandler*/ 
  this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    	……
        ……
        ……
        interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
                                            ResultHandler resultHandler, BoundSql boundSql) {
    	……
        ……
        ……
        interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
}

接下来就是执行正常的JDB C查询功能了,参数设置由ParameterHandler操作,结果集处理由ResultSetHandler处理。至此,整个查询流程结束。

整个查询阶段的时序图可以用如下图表示:

备战面试日记(4.3)- (框架.Mybatis)_第6张图片

整个查询流程,可以总结如下图:

备战面试日记(4.3)- (框架.Mybatis)_第7张图片

插件开发

引入

首先我们知道一次查询的过程为:

  1. 根据配置文件(全局,sql映射)初始化出 Configuration 对象

  2. 创建一个 DefaultSqlSession 对象,它里面包含Configuration 以及 Executor (根据全局配置文件的defaultExecutorType创建出对应的Executor)

  3. DefaultSqlSession.getMapper() 拿到 Mapper 接口对应的 MapperProxy

  4. MapperProxy 里面有 DefaultSqlSession

  5. 执行增删改查方法:

    • (代理对象)调用 DefaultSqlSession 的增删改查(Executor)

    • 创建一个 StatementHandler 对象,同时也创建ParameterHandlerResultSetHandler

    • 调用 StatementHandler 的预编译参数(使用 ParameterHandler 设置参数值)

    • 调用 StatementHandler 的增删改查方法

    • ResultSetHandler 封装结果

四大对象

Mybatis四大对象指的是:ExecutorStatementHandlerParamaterHandlerResultSetHandler

  • ParameterHandler:处理 SQL 的参数对象。
  • ResultSetHandler:处理 SQL 的返回结果集。
  • StatementHandler:数据库的处理对象,用于执行 SQL 语句。
  • Executor:MyBatis 的执行器,用于执行增删改查操作。

Mybatis允许我们在四大对象执行的过程中对其指定方法进行拦截,这样就可以很方便了进行功能的增强,这个功能跟 Spring 的切面编程非常类似。上文我们都有提到过,在四大对象创建的时候,都进行了插件增强,下面我们就来讲解一下其实现原理。

Mybatis插件接口 - Interceptor

首先我们需要理解 Intercaptor 接口。

  1. Intercept 方法,插件的核心方法。
  2. plugin 方法,生成 target 的代理对象。
  3. setProperties 方法,传递插件所需参数。

看如下代码的方法实现以及参数注释得以理解:

/** 插件签名,告诉mybatis单钱插件用来拦截那个对象的哪个方法 **/
@Intercepts({@Signature(type = ResultSetHandler.class,method ="handleResultSets",args = Statement.class)})
public class MyFirstInterceptor implements Interceptor {

     /** @Description 拦截目标对象的目标方法 **/
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("拦截的目标对象:"+invocation.getTarget());
        Object object = invocation.proceed();
        return object;
    }
     /**
      * @Description 包装目标对象 为目标对象创建代理对象
      * @Param target为要拦截的对象
      * @Return 代理对象
      */
    @Override
    public Object plugin(Object target) {
        System.out.println("将要包装的目标对象:"+target);
        return Plugin.wrap(target,this);
    }
    /** 获取配置文件的属性 **/
    @Override
    public void setProperties(Properties properties) {
        System.out.println("插件配置的初始化参数:"+properties);
    }
}

然后再在 mybatis.xml 中配置插件。


<plugins>
    <plugin interceptor="mybatis.interceptor.MyFirstInterceptor">
        
        <property name="name" value="Bob"/>
    plugin>
plugins>

调用查询方法,查询方法会返回 ResultSet

public class MyBatisTest {
    
    public static SqlSessionFactory sqlSessionFactory = null;

    public static SqlSessionFactory getSqlSessionFactory() {
        if (sqlSessionFactory == null) {
            String resource = "mybatis-config.xml";
            try {
                Reader reader = Resources.getResourceAsReader(resource);
                sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return sqlSessionFactory;
    }

    public void testGetById() {
        SqlSession sqlSession = this.getSqlSessionFactory().openSession();
        PersonMapper personMapper = sqlSession.getMapper(PersonMapper.class);
        Person person=personMapper.getById(2001);
        System.out.println(person.toString());
    }

public static void main(String[] args) {
        new MyBatisTest().testGetById();
    }
}

最后输出结果为:

插件配置的初始化参数:{name=Bob}
将要包装的目标对象:org.apache.ibatis.executor.CachingExecutor@754ba872
将要包装的目标对象:org.apache.ibatis.scripting.defaults.DefaultParameterHandler@192b07fd
将要包装的目标对象:org.apache.ibatis.executor.resultset.DefaultResultSetHandler@7e0b0338
将要包装的目标对象:org.apache.ibatis.executor.statement.RoutingStatementHandler@1e127982
拦截的目标对象:org.apache.ibatis.executor.resultset.DefaultResultSetHandler@7e0b0338
Person{id=2001, username='Tom', email='email@0', gender='F'}
实际运用举例

例如:我们希望在 sql 语句之前前后进行时间打印,计算出 sql 执行的时间。此功能我们就可以拦截StatementHandler。这里我们需要时间Mybatis提供的 Intercaptor 接口。

/**该注解签名告诉此拦截器拦截四大对象中的哪个对象的哪个方法,以及方法的签名信息*/
@Intercepts(
    {
        @Signature(
            type = StatementHandler.class,
            method = "query",
            args = {Statement.class,ResultHandler.class}
        )
	}
)
public class SqlLogPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long begin = System.currentTimeMillis();
        try {
            return invocation.proceed();
        } finally {
            long time = System.currentTimeMillis() - begin;
            System.out.println("sql 运行了 :" + time + " ms");
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

接下来我们需要在mybatis-config.xml配置该拦截器:

<plugins>
    <plugin interceptor="com.test.mybatis.intercaptor.SqlLogPlugin">
        <property name="参数1" value="root"/>
        <property name="参数2" value="123456"/>
    plugin>
plugins>

此时,拦截器的配置就完成了,运行结果如下:

DEBUG 11-24 17:51:34,877 ==>  Preparing: select * from user where id = ?   (BaseJdbcLogger.java:139) 
DEBUG 11-24 17:51:34,940 ==> Parameters: 1(Integer)  (BaseJdbcLogger.java:139) 
DEBUG 11-24 17:51:34,990 <==      Total: 1  (BaseJdbcLogger.java:139) 
sql 运行了 :51 ms
User{id=1, name='张三', age=42, sex=0}

插件原理

Mybatis的插件原理,即Mybatis的插件借助于责任链的模式进行对拦截的处理;使用动态代理对目标对象进行包装,达到拦截的目的;作用于Mybatis的作用域对象之上。

那么插件具体是如何拦截并附加额外的功能的呢?

我们先以 ParameterHandler 来说:

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object object, BoundSql sql, InterceptorChain interceptorChain){
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}

其中的pluginAll实现如下:

public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
        target = interceptor.plugin(target);
    }
    return target;
}

interceptorChain 保存了所有的拦截器(interceptors),是 Mybatis 初始化的时候创建的。调用拦截器链中的拦截器依次的对目标进行拦截或增强。interceptor.plugin(target) 中的 target 就可以理解为 Mybatis 中的四大对象。返回的 target 是被重重代理后的对象。

那我们再从上面的实际运用示例里来看:

public Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

该代码是对目标对象的包装,实际运行的时候,是使用的包装之后的类,运行的时候执行的是intercept方法。那么现在我们来看下它是怎么进行包装的:

public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
        // 返回的是JDK动态代理类,代理类增强了目标类
        return Proxy.newProxyInstance(
            type.getClassLoader(),
            interfaces,
            new Plugin(target, interceptor, signatureMap));
    }
    return target;
}

Plugin类实现了InvocationHanlder接口:

public class Plugin implements InvocationHandler {
    // ...
}

显然这里使用的就是JDK的动态代理,对目标对象包装了一层,重写invoke()方法。

// Plugin.invoke
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        Set<Method> methods = signatureMap.get(method.getDeclaringClass());
        // 如果是定义的拦截的方法 就执行intercept方法
        if (methods != null && methods.contains(method)) {
            //该方法增强
            return interceptor.intercept(new Invocation(target, method, args));
        }
        // 不是需要拦截的方法 直接执行
        return method.invoke(target, args);
    } catch (Exception e) {
        throw ExceptionUtil.unwrapThrowable(e);
    }
}

假如一个对象被多个拦截器进行了多次包装,那么后包装的在最外层会先执行。

Mybatis面试问题

#{} 和 ${} 的区别?

  • ${}properties 文件中的 变量占位符它可以用于标签属性值和sql内部,属于静态文本替换。
  • #{}sql参数占位符Mybatis会将 sql 中的 #{} 替换为 ? 号,在 sql 执行前会使用 PreparedStatement 的参数设置方法,按序给 sql 的 ? 号占位符设置参数值。

举例说明:

# ${param} 传递的参数会被当成sql语句中的一部分,举例:
order by ${param}
# 则解析成的sql为:
order by id
 
# #{param} 传入的数据都当成一个字符串,会对自动传入的数据加一个双引号,举例:
select * from table where name = #{param}
# 则解析成的sql为:
select * from table where name =   "id"

所以一般都是使用 #{}来进行变量替换。

Mybatis 一级缓存 和 二级缓存?

备战面试日记(4.3)- (框架.Mybatis)_第8张图片

  • 一级缓存:基于 PerpetualCacheHashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,Mybatis默认打开一级缓存,一级缓存存放在 BaseExecutorlocalCache 变量中。
  • 二级缓存:机制与一级缓存相同,默认也是采用 PerpetualCacheHashMap 存储,不同在于其存储作用域为 Mapper(Namespace)级别。Mybatis默认不打开二级缓存,可以在 config 文件中xml开启全局的二级缓存,但并不会为所有的 Mapper 设置二级缓存,每个 mapper.xml 文件中使用标签来开启当前mapper的二级缓存,二级缓存存放在 MappedStatementcache 变量中。

一级缓存

代码如下:

public abstract class BaseExecutor implements Executor {
    // ...
    protected PerpetualCache localCache;
	// ...
    
    // 构造方法
    protected BaseExecutor(Configuration configuration, Transaction transaction) {
		// ...
        this.localCache = new PerpetualCache("LocalCache"); // 默认初始化
        // ...
    }

    // query查询方法
    @SuppressWarnings("unchecked")
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
            clearLocalCache();
        }
        List<E> list;
        try {
            queryStack++;
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; // 获取缓存
            if (list != null) {
                handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else {
                list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            queryStack--;
        }
        if (queryStack == 0) {
            for (DeferredLoad deferredLoad : deferredLoads) {
                deferredLoad.load();
            }
            // issue #601
            deferredLoads.clear();
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                // issue #482
                clearLocalCache();
            }
        }
        return list;
    }
    
    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        localCache.putObject(key, EXECUTION_PLACEHOLDER); // 执行占位符,只是一个单例枚举类
        try {
            list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            localCache.removeObject(key);
        }
        localCache.putObject(key, list); // 存缓存
        if (ms.getStatementType() == StatementType.CALLABLE) {
            localOutputParameterCache.putObject(key, parameter);
        }
        return list;
    }
}

二级缓存

二级缓存默认是不开启的,需要手动开启二级缓存,实现二级缓存的时候,MyBatis要求返回的 POJO 必须是可序列化的。

开启二级缓存的条件也是比较简单,通过直接在 MyBatis 配置文件中设置:

<settings>
	<setting name = "cacheEnabled" value = "true" />
settings>

还需要在 Mapper.xml 配置文件中加入 标签。

缓存失效

一级缓存

Mybatis 一级缓存(会话级别缓存:sqlSession)默认是开启的。

Mybatis 一级缓存失效的情况:

  1. 非同一个sqlSession(两个不同的sqlSession)
  2. 同一个sqlSession,查询条件不同(查询条件不同,相应的数据还没有放到缓存)
  3. 同一个sqlSession,两次查询之间做了更新操作(新增、删除、修改)
  4. 同一个sqlSession,两次查询之间做了缓存清除:sqlSession.clearCache()
二级缓存

当执行一条查询sql时,先从二级缓存进行查询,没有则进入一级缓存查询,再没有就执行sql进行数据库查询,在会话(sqlSession)结束前把缓存放到一级缓存,会话结束后放到二级缓存中(需要开启二级缓存)。

@Options(flushCache = FlushCachePolicy.TRUE, useCache = false)
/*
1. flushCache:是否清除缓存,默认是DEFAULT,可以直接设置FlushCachePolicy.TRUE
    1.1 FlushCachePolicy.DEFAULT:如果是查询(select)操作,则是false,其它更新(insert、update、delete)操作则是true
2. useCache:是否将查询结果放到二级缓存
*/

Mybatis插件运行原理?

上面应该讲过了吧,再稍微提一下。

编写插件

  1. 实现Interceptor接口方法
  2. 确定拦截的签名
  3. 在配置文件中配置插件

插件运行原理

在创建三个重要的Handler(StatementHandler、ParameterHandler、ResultSetHandler)时通过插件数组包装了三大Handler:

resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);

获取到所有的 Interceptor(拦截器,即插件需要实现的接口),调用:

interceptor.plugin(target);

返回 target 包装后的对象,最后回调回自定义插件的 intercept() 方法执行插件内的代码逻辑。

可拦截的接口和方法一览:

  • Executor(update、query 、 flushStatment 、 commit 、 rollback 、 getTransaction 、 close 、 isClose)
  • StatementHandler(prepare 、 paramterize 、 batch 、 update 、 query)
  • ParameterHandler( getParameterObject 、 setParameters )
  • ResultSetHandler( handleResultSets 、 handleCursorResultSets 、 handleOutputParameters )

举例:PageHelper

配置过程

相关依赖如下:

<dependency>
    <groupId>org.mybatisgroupId>
    <artifactId>mybatisartifactId>
    <version>3.2.8version>
dependency>
<dependency>
    <groupId>com.github.pagehelpergroupId>
    <artifactId>pagehelperartifactId>
    <version>1.2.15version>
dependency>

首先要在myabtis.xml的全局文件上进行添加插件plugin,如下:


DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <plugins>
        
        <plugin interceptor="com.github.pagehelper.PageHelper">
            <property name="dialect" value="mysql" />
            
            
            
            <property name="offsetAsPageNum" value="true" />
            
            
            <property name="rowBoundsWithCount" value="true" />
            
            
            <property name="pageSizeZero" value="true" />
            
            
            
            <property name="reasonable" value="false" />
            
            
            
            
            <property name="params" value="pageNum=start;pageSize=limit;" />
            
            <property name="returnPageInfo" value="check" />
        plugin>
    plugins>
configuration>

使用步骤
普通使用
// 获取配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml");
// 通过加载配置文件获取SqlSessionFactory对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取SqlSession对象
SqlSession session = factory.openSession();
PageHelper.startPage(1, 5);
session.selectList("com.bobo.UserMapper.query");
Service中使用
@Service("orderService")
public class OrderServiceImpl implements OrderService {
    
	@Autowired
    private OrderDao orderDao;

    @Override
    public List<Order> findAll(int page, int size) throws Exception {
        // pageNum 是页码值,pageSize是每页显示条数
        PageHelper.startPage(page, size);
        return orderDao.findAll();
    }
}
原理说明

根据上面在 SqlSessionFactoryBuilder 使用 XMLConfigBuilder#parseConfiguration() 解析配置文件的插件注册中,我们知道 PageHelper 会被封装成一个 Interceptor 注册进拦截器链。

那么我们这里只要关注它的具体注册的拦截信息就好了。

我们来看下 PageHelper 的源代码的头部定义:

@SuppressWarnings("rawtypes")
// 定义的是拦截 Executor对象中的
// 拦截的方法是:query(MappedStatement ms,Object o,RowBounds ob ResultHandler rh)
@Intercepts(
    @Signature(
        type = Executor.class, 
        method = "query", 
        args = {MappedStatement.class
            , Object.class
                , RowBounds.class
                    , ResultHandler.class
                    }))
public class PageHelper implements Interceptor {
    
    //sql工具类
    private SqlUtil sqlUtil;
    //属性参数信息
    private Properties properties;
    //配置对象方式
    private SqlUtilConfig sqlUtilConfig;
    //自动获取dialect,如果没有setProperties或setSqlUtilConfig,也可以正常进行
    private boolean autoDialect = true;
    //运行时自动获取dialect
    private boolean autoRuntimeDialect;
    //多数据源时,获取jdbcurl后是否关闭数据源
    private boolean closeConn = true;
    
}

具体就不分析了。。PageHelper 分页的实现其实是在我们执行 SQL 语句之前动态的将 SQL 语句拼接了分页的语句,从而实现了从数据库中分页获取的过程。

Xml映射文件和内部数据结构之间的映射?

根据我在文首引入的第一篇文章拿的图,简单看一下吧。

configuration

备战面试日记(4.3)- (框架.Mybatis)_第9张图片

resultMap

备战面试日记(4.3)- (框架.Mybatis)_第10张图片

mappedStatment

备战面试日记(4.3)- (框架.Mybatis)_第11张图片

Mybatis中用到了哪些设计模式?

  • 日志模块:代理模式、适配器模式
  • 数据源模块:代理模式、工厂模式
  • 缓存模块:装饰器模式
  • 初始化阶段:建造者模式
  • 代理阶段:策略模式
  • 数据读写阶段:模板模式
  • 插件化开发:责任链模式

这块可以引申介绍设计模式,其实对设计模式熟悉的可以通过一种设计模式映射到很多源码中的实现。

你可能感兴趣的:(面试准备,面试,java,职场和发展)