Mybatis原理分析及插件实现

183 阅读9分钟

Demo

//main-----------------------------------------------------
public class MybatisDemo {
    public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory =
                new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession seesion = sqlSessionFactory.openSession();
        TestMapper testMapper = seesion.getMapper(TestMapper.class);
        Object object = testMapper.selectOne();
        System.out.println(object.toString());
    }
}

//myabtis-config.xml-----------------------------------------------------

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <plugins>
        <plugin interceptor="com.elijah.mybatisdemo.MybatistInterceptor">
        </plugin>
    </plugins>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3307/acme"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/testMapper.xml"/>
    </mappers>
</configuration>

//TestMapper.java-----------------------------------------------------
public interface TestMapper {
    Object selectOne();
}

//TestMapper.xml-----------------------------------------------------

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.elijah.mybatisdemo.TestMapper">
    <select id="selectOne" resultType="object">
        select * from test limit 1;
    </select>
</mapper>

demo是根据官网提供的demo写的,就是加载配置文件初始化,然后获取mapper装载模版sql,然后调用执行。

基础原理

mybatisUML.png
mybatis的原理其实并不难,麻烦的里面用了很多设计模式,对JDBC进行了各种能力增强,实际上就是对JDBC的二次封装。

源码分析

获取配置

首先读取配置文件的文件流,然后构建sqlSessionfactory,一个生成sql会话的工厂类。

这里可以看到通过build方法,生成了一个DefaultSqlSessionFactory,并且通过配置文件的文件流生成了一个Confaguration的实体,来看一下configration里面有啥。

这里可以看到,不仅仅是配置文件里写的数据源信息,并且加载了关联的TestMapper的xml文件的信息也一并加载并且装载进config实例里面了,也就是说这个时候所有的sql已经缓存到config里面了,除了这些里面还有其他所有的可配置项的默认值,所有可以用到的配置项都可以在Configration类中找到身影。

生成会话

到这里我们已经建立起来了核心配置类的实例,并且装载进了sqlsessionfactory,用它来生成以及建立和数据库的会话。通过sqlSessionFactory的openSession方法,创建了一个持有和数据库建立会话的实体类SqlSession,这里面又通过不同的包装来封装不同功能的实体类。

这里可以看到他其实是分了三个步骤:

  1. 通过工厂类创建事务实例,事务实例持有数据库的Connection对象,通过connection对象可以获取和数据库的连接。
  2. 将事务实例设置到Executor对象中,executor其实是实际调用事务执行sql的,executor会将参数和sql模版语句进行拼装,最后使用transaction发送数据库执行。
  3. 最后将executor和configration等信息封装进DefaultSqlSession里面,使用sqlSession包装一次完整的会话。

先来看第一步,因为我们配置文件中指定的类型是JDBC所以通过xml文件出事化的时候会创建JDBCTransactionFactory。通过这个工厂类可以获取一个事务实例,看JdbcTransaction的成员变量,其实就是封装了数据源信息和连接对象,看方法确实是控制了连接的打开,事务的提交回滚等。

再来看第二步,创建Executor执行器,通过配置类Configration.newExecutor()的方法,将之前创建的事务实例传入并且根据执行器的类型,进行选择创建对应类型的执行器,默认的执行器的类型是SIMPLE,所以创建的是SimpleExecutor,SimpleExecutor其实继承了基类BaseExecutor,里面封装了configration、transaction等用于和数据库交互的属性。最重要的是使用包装者模式进行了层层包装,将插件一一包装,变成了一个过滤连,通过所有的插件的逻辑,才能运行到simpleexecutor,所以执行的前后可以添加增强的逻,其实也就是通过JDK的动态代理,新建了一个代理类,调用了Plugin类中的invoke方法,来先运行代理类中的增强逻辑。

第三步就没什么可说的了,就是把配置的中心类Configration和操作类Executor设置到sqlSession中持有。

代理包装

下一步就开始生成代理类了,根据dao层的interface生成包装类,在包装逻辑中,调用数据库。

这里使用了之前创建的sqlsession的会话实例,来生成代理类,具体的实现类是DefaultSqlSession,这里调用了内置的Configuration对象来获取代理类,根据传入的Class类型进行增强,因为jdk的动态代理需要一个接口类来增强。

这里的mapperRegistry在解析配置文件初始化Configuration的时候就已经创建了,缓存了interface的类型,以及生成interface代理类的代理工厂,会使用这个代理工厂来创建代理类。

其实这个MapperProxyFactory的作用就是持有代理的Class的类型,也就是interface的类型,以及缓存了这个interface对应的xml的方法和方法签名以及对应的sql语句。可以参考原理那边儿的UML图。

可以看到,这一路的调用链就是将目标接口进行增强,增强的类就是MapperProxy,MapperProxy继承了InvocationHandler,实现了invoke动态代理的增强方法,这个invoke的增强方法里面可以看到,除了Object基类的方法外,都会使用MapperMethod的execute进行执行,从而实现代理,具体的代理过程就是调用的时候的事了,这里其实就是生成MapperProxy之后就返回这个代理类实例了,用于后面调用。

解析调用

上面已经说明了代理类的生成,实际上调用的是代理类的实例,因为使用jdk代理的,所以调用方法后,会进入到invoke方法。

首先先判断调用的是不是Object的方法,Object方法不代理,直接调用,其他的方法会获取MapperMetod的实例对象,这个类中封装了原始sql,以及sql类型,比如select update等,还包装了方法的签名,比如入参类型出参类型等。分别用SqlCommond,MethodSignature两个类来封装。(可以参考基础原理的图)

获取到MapperMethod实例之后,会把当前的会话和入参传入execute方法中执行。

根据MapperMethod实例对象的Sqlcommand的类型,来执行不同逻辑,以select为例,会判断MethodSignature的返回值类型,运行不同的逻辑,demo里面用的是select。首先会根据缓存的对象获取传入的参数的值,因为有可能有多个参数,所以他获取的有可能是一个map,实际上就是@Param里面的参数名,和对应的参数值。因为之前判断的返回值的类型没有特书表明是map还是什么,所以直接调用sqlsession里面的selectOne方法。

这里首先通过statement的值,也就是方法签名com.elijah.mybatisdemo.TestMapper.selectOne来获取Configration中早已经加载的这个方法对应的sql语句,然后将这个包装的sql语句的对象,和入参等信息,传入到executor中,也就是之前创建的SimpleExecutor,但是simpleExecutor只实现了BaseExecutor的模版方法,所以会先进入baseExecutor的代码中。

这里会直接进行从数据库中获取数据。

这里就会调用模版方法也就是进入simpleExecutor里面去执行了。

这里首先会生成一个statmenthandler,默认是使用prepareStatmentHandler,这个对象里面封装了Configuration、MappedStatement、parameter、resulthandler等一切与执行sql有关的东西,直接会调用jdbc的接口,和jdbc打交道。

然后会调用SimpleExecutor的prepareStatement方法。这个方法首先会调用getConnection,实际上是调用了executor中缓存的transaction对象,获取了一个Connection对象连接数据库;然后会将connection对象传入到之前的preparestatement的preaper进行方法的预编译。

这里的prepare方法会调用connection发送未设置参数的sql语句給mysql,一是为了mysql将语句预编译,还可以为防止sql注入。

在预编译结束之后,会把参数和sql语句拼装起来,得到完整的sql语句设置到statment中返回,然后在返回到SimpleExecutor的doQuery方法中,调用query方法,实际真正的执行sql。

执行结束后再处理返回参数的拼装。

到这里就分析完了整个sql的执行过程以及架构。

插件原理

插件的主要目的就是增强或者修改一些东西,其实上面分析过程中已经可以看到Executor创建过程和statment的创建过程中都已经有interceptorChain.pluginAll的方法了,其实这个就是用动态代理设置的一个责任链。

因为利用的是动态代理的原理,所以首先需要实现一个interceptor的接口用于增强。

@Intercepts({
        @Signature(type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class})
})
public class MybatistInterceptor  implements Interceptor {
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("this is MybatistInterceptor");
        return invocation.proceed();

    }

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

    public void setProperties(Properties properties) {

    }
}
  • intercept方法就是增强方法可以在前面后面进行增强,然后invocation对象的proceed方法,将会继续处理任务,就是一个AOP。
  • plugin方法,是为了生成代理对象,target是被拦截的对象,所以一般就是直接返回Plugin.wrap,这个方法会调用动态代理生成代理对象,增强的逻辑就是intercept方法中的内容。
  • setProperties方法在mybatis根据配置文件初始化的时候会调用,可以设置配置文件中写的property的属性值。

再来说一下这个方法上面的注解,这个其实就是为了确定要拦截的对象。

  • executor是执行SQL的全过程,包括参数组装接口及返回和sql执行的过程。
  • statmenthandler是执行sql的过程,是常用的拦截对象。
  • parameterhandler是用于拦截sql的参数组装。
  • resultsetHandler,用于拦截执行结果的组装。 其他的参数是确定拦截的方法以及来接的参数。

其实说白了就是在装载这几个对象的时候会先调用interceptorChain的pluginall的方法,会先看看那些拦截器是作用在哪些对象上的。拦截器的实现类的Class对象,其实在config文件初始化的时候就已经被加载进来了,只不过是在创建Executor、statmenthandler、parameterhandler、resultsethandler的时候,才是用动态代理生成新的代理对象包装起来形成责任链的。

可以看到只有这四个对象调用了pluginAll,所以也只能在这四个地方进行增强。