Mybatis内部原理与插件原理

Posted OverZeal

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mybatis内部原理与插件原理相关的知识,希望对你有一定的参考价值。

 

Mybatis的运行分为两大问题,第一部分是读取配置文件保存在Configuration对象中,用以创建SqlSessionFactory,第二部分是SqlSession的执行过程。相对而言SqlSessionFactory创建比较容易,而SqlSession的执行过程就没那么简单了。

构建SqlSessionFactory

Mybatis采用构造模式去创建SqlSessionFactory,我们可以直接使用SqlSessionFactoryBuilder去创建,其内部原理分为两步:

第一步,通过org.apache.ibatis.builder.xml.XMLConfigBuilder解析配置XML文件读出配置信息,并将读取的数据保存入这个org.apacher.ibatis.session.Configuration类中。Mybatis中所有的配置都是存在这里的。

第二步,使用Configuration对象去创建SqlSessionFactory。Mybatis的SqlSessionFactory是一个接口,而不是实现类,为此Mybatis提供了一个默认的SqlSessionFactory实现类,我们一般都会使用org.apache.ibatis.session.default.DefaultSqlSessionFactory。注意,在大多数情况下我们都没必要自己去创建SqlSessionFactory的实现类。

使用SqlSessionFactoryBuilder创建SqlSessionFactory的过程就是一种Builder模式。对于复杂的对象而言,直接使用构造方法构建是有困难的,会导致大量的逻辑放在构造函数中,由于对象的复杂性,在构建的时候,我们希望一步一步有秩序的去构建它,从而降低其复杂性。

构建Configuration

Mybatis的配置XML文件全部都会被读入这里并保存为一个单例,Configuration是通过XMLConfigBuilder去构建的。首先,Mybatis会读取所有的配置XML配置信息,然后,将这些信息保存到Configuration类的单例中。例如,properties全局参数,settings设置,typeAliases别名,typeHandler类型处理器,ObjectFactory对象工厂,plugin插件,environment环境,DatabaseIdProvider数据库标识,Mapper映射器

使用Builder模式

SqlSessionfactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);

我们使用Builder模式去创建SqlSessionFactory就简单多了,这个SqlSessionFactory会根据Configuration中的配置信息去创建对应的工厂

映射器的内部原理

一般而言,一个映射器由3个部分组成:

  • MappedStatement,它保存映射器一个节点(insert/delete/update/select)。包括我们配置retultMap,parameterType,缓存等内容。
  • SqlSource,它是MappedStatement的一个属性,它会提供BoundSql
  • BoundSql,它是建立SQL和参数的地方。它有3个常用的属性:SQL,parameterObject,parameterMappings

MappedStatement涉及东西较多,一般都不会修改它。SqlSource是一个接口,它的作用是根据参数和其他规则组装SQL(包括动态SQL),Mybatis本身也实现了,我们也不需要去修改。对于参数和SQL而言,主要规则都反映在BoundSql类对象上,插件往往需要拿到它进而可以拿到当前SQL和参数以及参数规则,做出适当修改。

  • SQL:就是我们预编译后的SQL
  • parameterObject:就是参数本身,我们会在parameterHandler中设置参数
  • parameterMappings:它是一个List,这个参数会描述我们的参数。参数包括属性,javaType,jdbcType,typeHandler等信息

SqlSession运行过程

SqlSession可以说是Mybatis中的重点和难点,SqlSession是一个接口(默认情况下创建的是DafaultsqlSession这个实现类),创建和使用它并不复杂。我们使用SqlSessionFactory的openSession就可以很轻松的拿到SqlSession了。SqlSession给出了查询,插入,更新,删除的方法,在旧版的ibatis中常常使用这些接口方法,而在新版的Mybatis中我们建议使用Mapper。

为什么Mybatis只用Mapper接口就可以运行SQL?

因为映射器的XML文件命名空间对应的便是这个接口的全路径,那么它根据全路径和方法名能够绑定起来,通过动态代理技术生成代理对象,让这个接口运行起来,然后底层使用的还是SqlSession接口的方法去执行操作。正是有了这层封装,使我们编程更为简单。

这个代理对象就是MapperProxy,它是通过MapperProxyFactory来生成的。我们来看它是如何运行的

一旦Mapper是一个代理对象,那么它就会运行到invoke方法里,首先会判断是否是一个类,而Mapper是一个接口所以判断失效,那么就会通过cachedMapperMethod方法初始化MapperMethod对象,然后执行execute方法,把sqlSession和参数传递进去。我们再来看看这个MapperMethod类的execute方法:

这里有个command.getType()返回的是SqlCommandType枚举类,看一下这个枚举类可取的值:

这不正是映射XML文件写SQL可使用的标签嘛,MapperMethod类的execute方法就是在根据这些值进行switch判断,然后调用不同的执行方法。那么解析配置文件使用的是XMLConfigBuilder,解析映射文件的标签使用的又是什么呢? 使用的是XMLStatementBuilder和XMLMapperBuilder,例如,我们找找如何解析出sqlCommandType(XMLStatementBuilder类的parseStatementNode()):

将节点名使用英文转换为大写,然后与SqlCommandType枚举类的值进行匹配,匹配上了就可以为SqlCommandType赋值

注意一下: 我们再看看MapperMethod类的execute方法中调用convertArgsToSqlCommandParam 这个方法,在增删改返回成功与否之前都会调用这个方法去设置参数,点进去看到MapperMethod类的convertArgsToSqlCommandParam方法又调用ParamNameResolver类的getNamedParams方法,这个就是在封装参数,我们之前说底层封的是一个Map,可以传一个Map;或者@param注解标识参数;又或者交给Mybatis来为我们自动封装,而我们在Mapper映射XML文件中使用#{param1}~#{paramN}/#{0}~#{N}取出,原因就在于此:

首先看names的值:

然后,我们来看如果根据names这个参数引用map来封装参数

在判断需要调用CRUD哪个操作,我们可以看到insert,update,delete都是直接调用sqlSession的API方法去操作的,那么查询是不是呢?我们随便点击一个 executeForMany() 方法看看:

没错,查询也是使用sqlSession的API执行,也就是旧式ibatis的方式,所以新式Mapper接口的底层就是在调用sqlSession中的API方法。

SqlSession下的四大对象

映射器生成的其实就是一个动态代理实现类,进入到MapperMethod的execute方法中经过简单判断就可以进到sqlSession的CRUD方法。那么这些方法又是如何执行的?

我们知道了通过类名和方法名就可以匹配到我们配置的SQL,这些SQL的执行需要依靠四大对象:Execcutor,StatementHandler,ParameterHandler和ResultSetHandler来完成数据库操作和结果返回。这四大对象都是接口,都会有默认的实现类为我们工作。

  • Executor代表执行器,由它(默认使用SimpleExecutor实现类)来调度StatementHandler,ParameterHandler,ResultSetHandler来执行1对应的SQL
  • StatementHandler的作用是使用数据库的Statement(默认使用PreparedStatement实现类)执行操作,它起到承上启下的作用
  • ParmeterHandler用于SQL对参数的处理,我们使用的是DeafultParameterHandler这个实现类
  • ResultSetHandler是进行最后数据集(ResultSet)的封装返回结果的,使用的是DeafultResultSetHandler这个实现类

执行器

执行器(Executor)起到了至关重要的作用。他是一个真正执行Java和数据库交互的东西。

我们还可以看看Configuration类中的 newExecutor 方法是如何执行的

可以看到如果没有在配置文件settings设置defaultExecutorType,那么默认就是SimpleExecutor对象,接着看,因为cacheEnable属性默认为true(可以在settings中配置),所以再用 CachingExecutor 对象包一下。最后有一个interceptorChain.pluginAll  这将通过拦截器栈使用一层层代理对象使用插件,这里先混个眼熟。

在Mybatis中存在三种执行器,我们可以在Mybatis的配置文件中进行选择,使用的是settings元素的defaultExecutorType属性

  • SIMPLE,简易执行器,不配置它就是默认执行器
  • RESULT,是一种执行器重用预处理语句
  • BATCH,执行批处理的执行器

以默认的SIMPLE执行器simpleExecutor的查询方法为例

 

Mybatis根据Configuration来构建StatementHandler,然后使用prepareStatement方法对SQL进行预编译并对参数进行初始化。

我们再看这个prepareStatement方法,可以看到它调用StatementHandler的prepare()进行了预编译和基础设置,然后通过StatementHandler的parameterize()来设置参数

最后使用resultHandler组装结果并返回给调用者完成一次查询

数据库会话器

数据库会话器(StatementHandler)是专门处理数据库会话的我们来看使用Configuration对象的newStatementHandler方法是如何创建StatementHandler的

 

看到没有,又是interceptorChain.pluginAll,再混个眼熟。

我们还可以看到一个RoutingStatementHandler对象,它实现了StatementHandler接口,这个对象其实并不是真正为我们服务的对象,它会通过适配模式找到对应的StatementHandler来执行。也就是根据配置来决定(在构造方法中进行选择)SimpleStatemnetHandler(默认),PreparedStatementHandler,CallableStatementHandler这三种的哪一种。

 

我们这里以默认的PreparedStatementHandler为例,来看看它的三个主要的方法:prepare,parameterize和query(用于查询),在看Executor的时候知道了调用PreparedStatementHandler的方法过程,prepare->parameterize->query查询/update添加更新删除。我们可以在PreparedStatementHandler的父类BaseStatementHandler中看到这三个方法的实现:

prepare方法中又调用instentiateStatement()方法对SQL进行预编译,然后设置一些基础配置,比如设置超时,设置最大行数等。

然后我们再来看parameterize()方法设置参数

 

最后我们来看使用query()方法完成查询

 

因为之前已经将SQL预编译并设置了参数,所以这里已经很简单了,我们直接调用JDBC中熟悉的PreparedStatement对象的execute()方法执行SQL即可,然后使用ResultSetHandler封装结果并返回。查询方法如此,增删改调用的update也是如此。

参数处理器

再来看看Configuration的newParameterHandler方法

又是有调用 interceptorChain.pluginAll 方法使用拦截器链使用插件

Mybatis通过类型处理器(ParameterHandler)对预编译语句进行参数设置的。它的作用很明显,那就是完成对预编译参数的设置。Mybatis为ParameterHandler提供了一个默认的实现类,DefaultParameterHandler。我们来看这个setParameters()方法,它就是用来设置预编译SQL的参数

还是上面的prepareStatement类parameterize()方法中调用的setParameters()方法,我们点进去看看:

我们可以看到它从parameterObject对象中去取参数,然后使用typeHandler进行参数处理,如果你有自定义typeHandler,那么就会签名注册的typeHandler进行自定义的参数处理

结果处理器

再来看看Configuration的newResultSetHandler方法

哈哈,再混个眼熟

ResultSetHandler用来封装结果集,这也是一个接口,我们一般使用一个默认的DefaultResultSetHandler类,默认就是使用这个类进行处理。因为使用接口所以它会使用JDK动态代理,如果涉及到延迟加载的功能就是用CGLIB,然后通过typeHandler或ObjectFactory进行组装结果,这个结果集封装比较复杂,我们一般也不去修改它。

总结

SqlSession是通过Executor参创建StatementHandler来运行的,而StatementHandler要经过下面三步:

  • prepared预编译SQL
  • parameterize设置参数
  • query/update执行SQL

其中parameterize是调用parameterHandler的方法去设置参数,而参数是根据typeHandler去处理的,query/update语句调用之后还是通过typeHandler进行结果集的封装,如果是update的语句就返回整数;如果是query就使用typeHandler设置结果类型,然后用ObjectFactory提供的规则组装对象返回给调用者。

插件

我们发现在Configuration初始化四大对象的时候都会调用interceptorChain.pluginAll()方法,这个方法就是在使用责任链方式调用插件,我们有机会在四大对象调度的时候插入我们的代码去执行一些特殊的要求以满足特殊的场景需求。来看看这个方法:

再点进入我们Interceptor接口的plugin方法:

自定义插件就必须实现接口interceptor,也看到了这个接口有3个方法:

  • intercept方法:它将直接覆盖你所覆盖对象的原有方法,因此它是插件的核心方法。里面有个Invocation对象,调用它的proceed()方法就是在调用真实被拦截的方法(类似于放行)
  • plugin方法:target是被拦截的对象,它的作用是给被拦截对象生成一个代理对象并返回它。为了方便,Mybatis使用org.apache.ibatis.plugin.Plugin中的wrap静态方法来生成代理对象,其实就是使用JDK动态代理生成代理对象。我们往往使用plugin方法去生成代理对象。
  • setProperties方法:Mybatis允许在plugin元素中配置所需参数,方法在插件初始化的时候就被调用了一次,然后把插件对象存入到配置中,以便后面取出.

插件的初始化

插件的初始化是在Mybatis初始化的时候完成的,这点我们可以在XMLConfigBuilder中的pluginElement方法中可以看到:

如何将插件加入到Configuration?来看看configuration的addInterceptor()方法

这个拦截器链就是将拦截器放入List中,从上面的InterceptorChain类可以知道。

插件的代理和反射设计(Interceptor接口)

插件用的是责任链模式。首先什么是责任链模式?就是一个对象在多个角色中传递,处在传递链上的任何角色都有机会拦截这个对象并处理它。Mybatis的责任链就是使用InterceptorChain定义的。

plugin方法就是生成代理对象的方法。从第一个对象(四大对象中的一个)开始,将对象传递给plugin方法,然后就会返回一个代理对象;如果存在第二个插件,那么就会拿到第一个代理对象传递给plugin方法方法在返回一个代理对象的代理...以此类推,有多少拦截器就会生成多少个代理对象,所以使用太多插件会带来性能问题。

代理对象如果要我们自己去实现工作量较大,所以Mybatis提供了一个常用的工具类用于生成代理对象,它便是Plugin类,使用它的wrap方法获得代理对象(底层使用JDK动态代理)。

代理对象在调用真实方法时就会进入到invoke方法中,在invoke方法中,如果存在签名的拦截方法,就调用intercept方法并返回结果;如果没有签名方法,那么就用method.invoke直接反射我们要调用的方法

所以在这里,interceptor接口的intercept方法被调用,我们自定义接口需要操作的逻辑主要就写在intercept方法。

常用的工具类MetaObject

在Mybatis中,四大对象给我们提供public设置参数的方法很少,我们难于直接设置或获得相关的属性信息,但是因为有了MeataObject这个类就可以进行修改了。

  • forObject方法:  用于包装对象,但是这个方法已经不再使用了,而是用Mybatis为我们提供的SystemMeteObject(Object obj)
  • getValue方法:用于获得对象属性值,支持OGNL
  • setValue方法:   用于设置对象属性值,支持OGNL

签名

我们可以拦截四大对象中的任意一个,但是需要我们进行签名才能运行插件。签名需要一定要素:

1.确定需要拦截的对象

再来回顾一下四大对象都有什么作用。

  • Executor是执行SQL的执行器,包括组装参数,执行SQL和组装结果集返回结果,但是一般我们拦截的不多。
  • StatementHandler是执行SQL的过程,我们可以拦截这个对象对SQL或参数进行修改
  • ParameterHandler用于参数的组装,我们拦截这个对象重写组装参数规则
  • ResultSetHandler用于结果集的组装,我们可以拦截这个对象来重写组装结果规则

 2.拦截方法和参数

当你确定拦截什么对象,接下来就要确定需要拦截什么方法及方法的参数,但是我们需要理解每个四大对象中的所用方法和参数。

@Intercepts(
        {
            @Signature(type=StatementHandler.class,method="parameterize",args=java.sql.Statement.class)
        })
public class MyFirstPlugin implements Interceptor{

其中,@interceptor说明他是一个拦截器。@Signature是注册拦截器签名的地方,只有满足了签名条件的四大对象的才会拦截,type用于指明拦截四大对象中的哪一个(这里拦的是StatementHandler对象),method指明我们需要拦截哪个方法,args则表明该方法的参数,需要根据拦截器方法的参数进行设置。

/**
 * 完成插件签名:
 * 告诉MyBatis当前插件用来拦截哪个对象的哪个方法
 */
@Intercepts(
        {
            @Signature(type=StatementHandler.class,method="parameterize",args=java.sql.Statement.class)
        })
public class MyFirstPlugin implements Interceptor{

    /**
     * intercept:拦截:
     * 拦截目标对象的目标方法的执行;
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // TODO Auto-generated method stub
        System.out.println("MyFirstPlugin...intercept:"+invocation.getMethod());
        //动态的改变一下sql运行的参数:以前1号员工,实际从数据库查询3号员工
        Object target = invocation.getTarget();
        System.out.println("当前拦截到的对象:"+target);
        //拿到:StatementHandler==>ParameterHandler===>parameterObject
        //拿到target的元数据
        MetaObject metaObject = SystemMetaObject.forObject(target);
        Object value = metaObject.getValue("parameterHandler.parameterObject");
        System.out.println("sql语句用的参数是:"+value);
        //修改完sql语句要用的参数
        metaObject.setValue("parameterHandler.parameterObject", 11);
        //执行目标方法
        Object proceed = invocation.proceed();
        //返回执行后的返回值
        return proceed;
    }

    /**
     * plugin:
     *         包装目标对象的:包装:为目标对象创建一个代理对象
     */
    @Override
    public Object plugin(Object target) {
        // TODO Auto-generated method stub
        //我们可以借助Plugin的wrap方法来使用当前Interceptor包装我们目标对象
        System.out.println("MyFirstPlugin...plugin:mybatis将要包装的对象"+target);
        Object wrap = Plugin.wrap(target, this);
        //返回为当前target创建的动态代理
        return wrap;
    }

    /**
     * setProperties:
     *         将插件注册时 的property属性设置进来
     */
    @Override
    public void setProperties(Properties properties) {
        // TODO Auto-generated method stub
        System.out.println("插件配置的信息:"+properties);
    }

}

在Mybatis初始化的时候就会初始化插件,而插件初始化的时候就会调用setProperties方法,用于初始化参数,所以 intercept(插件) 中方法的调用顺序是:setproperties->plugin->intercept

最后,我们在Mybatis配置文件的plugin元素中配置插件并设置一个参数

    <!--plugins:注册插件  -->
    <plugins>
        <plugin interceptor="cn.lynu.dao.MyFirstPlugin">
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </plugin>
    </plugins>

总结

  • 能少用插件就少用,因为它通过层层代理对象的责任链去反射方法,性能不高,所以少用可以提高系统性能
  • 自定义插件,需要先确定拦截哪个四大对象,然后明确这个对象的哪个方法,参数是什么。也只有这样我们才能签名拦截器
  • 当有多个拦截器存在时,理清楚调用顺序
  • 尽量少动Mybatis底层的东西底层的设计,以减少错误的发生

 

以上是关于Mybatis内部原理与插件原理的主要内容,如果未能解决你的问题,请参考以下文章

MyBatis源码分析插件实现原理

《深入浅出MyBatis技术原理与实战》——7. 插件

Mybatis-PageHelper分页插件的使用与相关原理分析

Mybatis插件原理和整合Spring

深入浅出MyBatis:MyBatis插件及开发过程

mybatis插件机制原理