当前位置 : 主页 > 编程语言 > java >

MyBatis自定义插件机制分析(源码级剖析)

来源:互联网 收集:自由互联 发布时间:2022-10-14
** 问题:什么是Mybatis插件?有什么作用?** 一般开源框架都会提供扩展点,让开发者自行扩展,从而完成逻辑的增强。 基于插件机制可以实现了很多有用的功能,比如说分页,字段加

image.png

** 问题:什么是Mybatis插件?有什么作用?**

一般开源框架都会提供扩展点,让开发者自行扩展,从而完成逻辑的增强。

基于插件机制可以实现了很多有用的功能,比如说分页,字段加密,监控等功能,这种通用的功能,就如同AOP一样,横切在数据操作上

而通过Mybatis插件可以实现对框架的扩展,来实现自定义功能,并且对于用户是无感知的。

2 Mybatis插件介绍

Mybatis插件本质上来说就是一个拦截器,它体现了JDK动态代理和责任链设计模式的综合运用

Mybatis中针对四大组件提供了扩展机制,这四个组件分别是: image.png

Mybatis中所允许拦截的方法如下:

  • Executor 【SQL执行器】【update,query,commit,rollback】
  • StatementHandler 【Sql语法构建器对象】【prepare,parameterize,batch,update,query等】
  • ParameterHandler 【参数处理器】【getParameterObject,setParameters等】
  • ResultSetHandler 【结果集处理器】【handleResultSets,handleOuputParameters等】

能干什么?

  • 分页功能:mybatis的分页默认是基于内存分页的(查出所有,再截取),数据量大的情况下效率较低,不过使用mybatis插件可以改变该行为,只需要拦截StatementHandler类的prepare方法,改变要执行的SQL语句为分页语句即可

  • 性能监控:对于SQL语句执行的性能监控,可以通过拦截Executor类的update, query等方法,用日志记录每个方法执行的时间

如何自定义插件?

在使用之前,我们先来看看Mybatis提供的插件相关的类,过一遍它们分别提供了哪些功能,最后我们自己定义一个插件

用于定义插件的类

前面已经知道Mybatis插件是可以对Mybatis中四大组件对象的方法进行拦截,那拦截器拦截哪个类的哪个方法如何知道,就由下面这个注解提供拦截信息

@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Intercepts { Signature[] value(); }

由于一个拦截器可以同时拦截多个对象的多个方法,所以就使用了Signture数组,该注解定义了拦截的完整信息

@Documented @Retention(RetentionPolicy.RUNTIME) @Target({}) public @interface Signature { // 拦截的类 Class<?> type(); // 拦截的方法 String method(); // 拦截方法的参数 Class<?>[] args(); }

已经知道了该拦截哪些对象的哪些方法,拦截后要干什么就需要实现Intercetor#intercept方法,在这个方法里面实现拦截后的处理逻辑

public interface Interceptor { /** * 真正方法被拦截执行的逻辑 * * @param invocation 主要目的是将多个参数进行封装 */ Object intercept(Invocation invocation) throws Throwable; // 生成目标对象的代理对象 default Object plugin(Object target) { return Plugin.wrap(target, this); } // 可以拦截器设置一些属性 default void setProperties(Properties properties) { // NOP } }

3 自定义插件

需求:把Mybatis所有执行的sql都记录下来

步骤:① 创建Interceptor的实现类,重写方法

​ ② 使用@Intercepts注解完成插件签名 说明插件的拦截四大对象之一的哪一个对象的哪一个方法

​ ③ 将写好的插件注册到全局配置文件中

①.创建Interceptor的实现类

public class MyPlugin implements Interceptor { private final Logger logger = LoggerFactory.getLogger(this.getClass()); // //这里是每次执行操作的时候,都会进行这个拦截器的方法内 Override public Object intercept(Invocation invocation) throws Throwable { //增强逻辑 StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = statementHandler.getBoundSql(); String sql = boundSql.getSql(); logger.info("mybatis intercept sql:{}", sql); 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 ); } }

② 使用@Intercepts注解完成插件签名 说明插件的拦截四大对象之一的哪一个对象的哪一个方法

@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class}) }) public class SQLStatsInterceptor implements Interceptor {

③ 将写好的插件注册到全局配置文件中

<?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.itheima.interceptor.MyPlugin"> <property name="dialect" value="mysql" /> </plugin> </plugins> </configuration>

核心思想:

就是使用JDK动态代理的方式,对这四个对象进行包装增强。具体的做法是,创建一个类实现Mybatis的拦截器接口,并且加入到拦截器链中,在创建核心对象的时候,不直接返回,而是遍历拦截器链,把每一个拦截器都作用于核心对象中。这么一来,Mybatis创建的核心对象其实都是代理对象,都是被包装过的。

image.png

4 源码分析-插件

  • 插件的初始化:插件对象是如何实例化的? 插件的实例对象如何添加到拦截器链中的? 组件对象的代理对象是如何产生的?
  • 拦截逻辑的执行
插件配置信息的加载

我们定义好了一个拦截器,那我们怎么告诉Mybatis呢?Mybatis所有的配置都定义在XXx.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.itheima.interceptor.MyPlugin"> <property name="dialect" value="mysql" /> </plugin> </plugins> </configuration>

对应的解析代码如下(XMLConfigBuilder#pluginElement):

private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { // 获取拦截器 String interceptor = child.getStringAttribute("interceptor"); // 获取配置的Properties属性 Properties properties = child.getChildrenAsProperties(); // 根据配置文件中配置的插件类的全限定名 进行反射初始化 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance(); // 将属性添加到Intercepetor对象 interceptorInstance.setProperties(properties); // 添加到配置类的InterceptorChain属性,InterceptorChain类维护了一个List<Interceptor> configuration.addInterceptor(interceptorInstance); } } }

主要做了以下工作:

  • 遍历解析plugins标签下每个plugin标签
  • 根据解析的类信息创建Interceptor对象
  • 调用setProperties方法设置属性
  • 将拦截器添加到Configuration类的IntercrptorChain拦截器链中
  • 对应时序图如下:

    image.png

    代理对象的生成

    Executor代理对象(Configuration#newExecutor)

    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); } if (cacheEnabled) { executor = new CachingExecutor(executor); } // 生成Executor代理对象逻辑 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }

    ParameterHandler代理对象(Configuration#newParameterHandler)

    public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); // 生成ParameterHandler代理对象逻辑 parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; }

    ResultSetHandler代理对象(Configuration#newResultSetHandler)

    public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); // 生成ResultSetHandler代理对象逻辑 resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; }

    StatementHandler代理对象(Configuration#newStatementHandler)

    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; }

    通过查看源码会发现,所有代理对象的生成都是通过InterceptorChain#pluginAll方法来创建的,进一步查看pluginAll方法

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

    InterceptorChain#pluginAll内部通过遍历Interceptor#plugin方法来创建代理对象,并将生成的代理对象又赋值给target,如果存在多个拦截器的话,生成的代理对象会被另一个代理对象所代理,从而形成一个代理链,执行的时候,依次执行所有拦截器的拦截逻辑代码,我们再跟进去

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

    Interceptor#plugin方法最终将目标对象和当前的拦截器交给Plugin.wrap方法来创建代理对象。该方法是默认方法,是Mybatis框架提供的一个典型plugin方法的实现。让我们看看在Plugin#wrap方法中是如何实现代理对象的

    public static Object wrap(Object target, Interceptor interceptor) { // 1.解析该拦截器所拦截的所有接口及对应拦截接口的方法 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); // 2.获取目标对象实现的所有被拦截的接口 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); // 3.目标对象有实现被拦截的接口,生成代理对象并返回 if (interfaces.length > 0) { // 通过JDK动态代理的方式实现 return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } // 目标对象没有实现被拦截的接口,直接返回原对象 return target; }

    最终我们看到其实是通过JDK提供的Proxy.newProxyInstance方法来生成代理对象

    以上代理对象生成过程的时序图如下:

    拦截逻辑的执行

    通过上面的分析,我们知道Mybatis框架中执行Executor、ParameterHandler、ResultSetHandler和StatementHandler中的方法时真正执行的是代理对象对应的方法。而且该代理对象是通过JDK动态代理生成的,所以执行方法时实际上是调用InvocationHandler#invoke方法(Plugin类实现InvocationHandler接口),下面是Plugin#invoke方法

    @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { 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) { throw ExceptionUtil.unwrapThrowable(e); } }

    注:一个对象被代理很多次

    问题:同一个组件对象的同一个方法是否可以被多个拦截器进行拦截?

    答案是肯定的,所以我们配置在最前面的拦截器最先被代理,但是执行的时候却是最外层的先执行。

    具体点:

    假如依次定义了三个插件:插件1,插件2 和 插件3。

    那么List中就会按顺序存储:插件1,插件2 和 插件3。

    而解析的时候是遍历list,所以解析的时候也是按照:插件1,插件2,插件3的顺序。

    但是执行的时候就要反过来了,执行的时候是按照:插件3,插件2和插件1的顺序进行执行。

    <img src=".\img\image-20210903145021790.png" alt="image-20210903145021790" style="zoom:67%;" />

    当 Executor 的某个方法被调用的时候,插件逻辑会先行执行。执行顺序由外而内,比如上图的执行顺序为 plugin3 → plugin2 → Plugin1 → Executor。

    网友评论