Mybatis插件

132 阅读5分钟

插件简介

⼀般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者⾃⾏拓展。这样的好处是显⽽易⻅ 的,⼀是增加了框架的灵活性。⼆是开发者可以结合实际需求,对框架进⾏拓展,使其能够更好的⼯ 作。以MyBatis为例,我们可基于MyBatis插件机制实现分⻚、分表,监控等功能。由于插件和业务 ⽆ 关,业务也⽆法感知插件的存在。因此可以⽆感植⼊插件,在⽆形中增强功能

Mybatis插件介绍

Mybati s作为⼀个应⽤⼴泛的优秀的ORM开源框架,这个框架具有强⼤的灵活性,在四⼤组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler)处提供了简单易⽤的插 件扩 展机制。Mybatis对持久层的操作就是借助于四⼤核⼼对象。MyBatis⽀持⽤插件对四⼤核⼼对象进 ⾏ 拦截,对mybatis来说插件就是拦截器,⽤来增强核⼼对象的功能,增强功能本质上是借助于底层的 动 态代理实现的,换句话说,MyBatis中的四⼤对象都是代理对象 image.png

MyBatis所允许拦截的⽅法如下:

  • 执⾏器Executor (update、query、commit、rollback等⽅法);
  • SQL语法构建器StatementHandler (prepare、parameterize、batch、updates query等⽅ 法);
  • 参数处理器ParameterHandler (getParameterObject、setParameters⽅法);
  • 结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等⽅法);

Mybatis插件原理

在四⼤对象创建的时候

  • 1、每个创建出来的对象不是直接返回的,⽽是interceptorChain.pluginAll(parameterHandler);
  • 2、获取到所有的Interceptor (拦截器)(插件需要实现的接⼝);调⽤ interceptor.plugin(target);返回 target 包装后的对象
  • 3、插件机制,我们可以使⽤插件为⽬标对象创建⼀个代理对象;AOP (⾯向切⾯)我们的插件可 以为四⼤对象创建出代理对象,代理对象就可以拦截到四⼤对象的每⼀个执⾏;

拦截

插件具体是如何拦截并附加额外的功能的呢?以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;
}
public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
        target = interceptor.plugin(target);
    }
    return target;
}

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

如果我们想要拦截Executor的query⽅法,那么可以这样定义插件:

@Intercepts({
        @Signature(
                type = Executor.class,
                method = "query",
                args=
                        {MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}
        )
})
public class ExeunplePlugin implements Interceptor {
    //省略逻辑
}

除此之外,我们还需将插件配置到sqlMapConfig.xm l中。

<plugins>
    <plugin interceptor="com.lagou.plugin.ExamplePlugin">
    </plugin>
</plugins>

这样MyBatis在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链) 中。 待准备⼯作做完后,MyBatis处于就绪状态。我们在执⾏SQL时,需要先通过DefaultSqlSessionFactory 创建 SqlSession。Executor 实例会在创建 SqlSession 的过程中被创建, Executor实例创建完毕后, MyBatis会通过JDK动态代理为实例⽣成代理类。这样,插件逻辑即可在 Executor相关⽅法被调⽤前执⾏。

以上就是MyBatis插件机制的基本原理

⾃定义插件

插件接⼝

Mybatis 插件接⼝-Interceptor

  • Intercept⽅法,插件的核⼼⽅法
  • plugin⽅法,⽣成target的代理对象
  • setProperties⽅法,传递插件所需参数
Intercepts ({//注意看这个⼤花括号,也就这说这⾥可以定义多个@Signature对多个地⽅拦截,都⽤
    这个拦截器
    @Signature (type = StatementHandler .class , //这是指拦截哪个接⼝
            method = "prepare"//这个接⼝内的哪个⽅法名,不要拼错了
            args = { Connection.class, Integer .class}),//// 这是拦截的⽅法的⼊参,按
    顺序写到这,不要多也不要少,如果⽅法重载,可是要通过⽅法名和⼊参来确定唯⼀的
})
public class MyPlugin implements Interceptor {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    // //这⾥是每次执⾏操作的时候,都会进⾏这个拦截器的⽅法内

    Override
    public Object intercept(Invocation invocation) throws Throwable {
        //增强逻辑
        System.out.println("对⽅法进⾏了增强....");
        return invocation.proceed(); //执⾏原⽅法
    }

    /**
     * //主要是为了把这个拦截器⽣成⼀个代理放到拦截器链中
     * ^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 );
    }
}

sqlMapConfig.xml

<plugins>
     <plugin interceptor="com.lagou.plugin.MySqlPagingPlugin">
    <!--配置参数-->
    <property name="name" value="Bob"/>
    </plugin>
</plugins>

mapper接⼝

public interface UserMapper {
    List<User> selectUser();
}

mapper.xml

<mapper namespace="com.lagou.mapper.UserMapper">
 <select id="selectUser" resultType="com.lagou.pojo.User">
 SELECT
 id,username
 FROM
 user
 </select>
</mapper>

测试类

public class PluginTest {
    @Test
    public void test() throws IOException {
        InputStream resourceAsStream =
                Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new
                SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        List<User> byPaging = userMapper.selectUser();
        for (User user : byPaging) {
            System.out.println(user);
        }
    }
}

源码分析

执⾏插件逻辑

Plugin实现了 InvocationHandler接⼝,因此它的invoke⽅法会拦截所有的⽅法调⽤。invoke⽅法会 对 所拦截的⽅法进⾏检测,以决定是否执⾏插件逻辑。该⽅法的逻辑如下:

// -Plugin
public Object invoke(Object proxy, Method method, Object[] args) throws
        Throwable {
    try {
     /*
     *获取被拦截⽅法列表,⽐如:
     * signatureMap.get(Executor.class), 可能返回 [query, update,
    commit]
     */
        Set<Method> methods =
                signatureMap.get(method.getDeclaringClass());
        //检测⽅法列表是否包含被拦截的⽅法
        if (methods != null && methods.contains(method)) {
            //执⾏插件逻辑
            return interceptor.intercept(new Invocation(target, method,
                    args));
            //执⾏被拦截的⽅法
            return method.invoke(target, args);
        } catch(Exception e){
        }
    }

}

invoke⽅法的代码⽐较少,逻辑不难理解。⾸先,invoke⽅法会检测被拦截⽅法是否配置在插件的

@Signature注解中,若是,则执⾏插件逻辑,否则执⾏被拦截⽅法。插件逻辑封装在intercept中,该 ⽅法的参数类型为Invocationo Invocation主要⽤于存储⽬标类,⽅法以及⽅法参数列表。下⾯简单看 ⼀下该类的定义

public class Invocation {
    private final Object target;
    private final Method method;
    private final Object[] args;
    public Invocation(Object targetf Method method, Object[] args) {
        this.target = target;
        this.method = method;
//省略部分代码
        public Object proceed() throws InvocationTargetException,
                IllegalAccessException { //调⽤被拦截的⽅法
>> —

关于插件的执⾏逻辑就分析结束