mybatis框架的插件机制

团队介绍

光大科技有限公司智能云计算部运维服务团队光大e信项目组,致力于为全集团员工提供信息共享、工作协同的一站式移动办公服务平台,助力集团进行战略数字化转型。团队拥有经验丰富的互联网技术研发人员,在自主研发创新项目中不断总结技术经验,不定期进行原创技术文章和实践经验分享,共同去探索如何更好地去进行大型金控集团数字化战略转型IT信息建设。

01

什么是mybatis插件机制

mybatis框架通过提供拦截器(interceptor)的方式,支持用户扩展或者改变原有框架的功能,就是mybatis框架中的插件机制。

02

支持拦截的方法

Executor(update、query、commit、rollback等方法):Executor为SQL执行器。

StatementHandler(prepare、parameterize、batch、update、query等方法):StatementHandler负责操作 Statement 对象与数据库进行交流,调用 ParameterHandler 和 ResultSetHandler对参数进行映射,对结果进行实体类的绑定。

ParameterHandler(getParameterObject、setParameters方法):ParameterHandler用于处理SQL语句中的参数。

ResultSetHandler(handleResultSets、handleOutputParameters等方法):ResultSetHandler用于处理SQL执行完成以后返回的结果集映射。

03

插件机制应用场景

性能监控

对SQL语句执行的性能监控,可以通过拦截Executor类的update、query等方法,用日志记录每个方法执行的时间。真实生产环境可以设置性能监控开关,以免性能监控拖慢正常业务响应速度。

黑白名单功能

有些业务系统,生产环境单表数据量巨大,有些SQL语句是不允许在生产环境执行的。可以通过拦截Executor类的update、 query等方法,对SQL语句进行拦截与黑白名单中的SQL语句或者关键词进行对比,从而决定是否继续执行SQL语句。

公共字段统一赋值

一般业务系统都会有创建者、创建时间、修改者、修改时间四个字段,对于这四个字段的赋值,实际上可以在DAO层统一拦截处理。可以用mybatis插件拦截Executor类的update方法,对相关参数进行统一赋值即可。

其它

mybatis扩展性还是很强的,基于插件机制,基本上可以控制SQL执行的各个阶段,如执行阶段、参数处理阶段、语法构建阶段、结果集处理阶段,具体可以根据项目来灵活运用。

04

SQL执行时长统计demo

首先,自定义SQLProcessTimeInterceptor,在Executor层面进行拦截,用于统计SQL执行时长。用户自定义Interceptor除了继承Interceptor接口外,还需要使用@Intercepts和@Signature两个注解进行标识。@Intercepts注解指定了一个@Signature注解列表,每个@Signature注解中都标识了需要拦截的方法信息,其中@Signature注解中的type属性用于指定需要拦截的类型,method属性用于指定需要拦截的方法,args属性指定了被拦截方法的参数列表。由于java有重载的概念,通过type、method、args三个属性可以标识出唯一的方法。

import lombok.extern.slf4j.Slf4j;import org.apache.ibatis.executor.Executor;import org.apache.ibatis.mapping.MappedStatement;import org.apache.ibatis.plugin.*;import org.apache.ibatis.session.ResultHandler;import org.apache.ibatis.session.RowBounds;@Slf4j@Intercepts({        @Signature(type = Executor.class, method = \\\"update\\\", args = {MappedStatement.class, Object.class}),        @Signature(type = Executor.class, method = \\\"query\\\", args = {MappedStatement.class, Object.class,                RowBounds.class, ResultHandler.class})})public class SqlProcessTimeInterceptor implements Interceptor {    @Override    public Object intercept(Invocation invocation) throws Throwable {        long start = System.currentTimeMillis();        log.info(\\\"start time :\\\" + System.currentTimeMillis());
Object result = invocation.proceed(); long end = System.currentTimeMillis(); log.info(\\\"end time :\\\" + end); log.info(\\\"process time is :\\\" + (end - start) + \\\"ms\\\"); return result; }
@Override public Object plugin(Object target) { return Plugin.wrap(target, this); }
@Override public void setProperties(Properties properties) {
}}

Object intercept(Invocation invocation)是实现拦截逻辑的地方,内部要通过invocation.proceed()显式地推进责任链前进,也就是调用下一个拦截器拦截目标方法。
Object plugin(Objecttarget)就是用当前这个拦截器生成对目标target的代理,实际是通过Plugin.wrap(target,this)来完成的,把目标target和拦截器this传给了包装函数。

setProperties(Properties properties)用于设置额外的参数,参数配置在拦截器的Properties节点里,也可以通过代码的方式配置properties属性。

然后,将自定义SQLProcessTimeInterceptor加入到配置中,供mybatis框架初始化的时候拉取。

import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;
@Configurationpublic class MybatisConfiguration { @Bean ConfigurationCustomizer mybatisConfigurationCustomizer() { return new ConfigurationCustomizer() { @Override public void customize(org.apache.ibatis.session.Configuration configuration) { configuration.addInterceptor(new SqlProcessTimeInterceptor()); } }; }}

最后,执行单元测试查看是否拦截成功。

//单元测试@Testpublic void selectUserById() {   User user = userMapper.selectUserById(1L);   Assert.assertNotNull(user);}
//运行结果start time :1611127784286end time :1611127784703process time is :417ms

05

mybatis插件机制原理

由于以上demo拦截的是Executor,以下原理分析基于Executor拦截。ParameterHandler、ResultHandler、ResultSetHandler、StatementHandler拦截过程原理类似。

1、将自定义的Interceptor配置进mybaits框架中,以便mybatis框架在初始化的时候将自定义Interceptor加入到Configuration.interceptorChain中。

配置分为两种方式:

一种为xml配置文件的方式:Mybatis初始化的时候,会通过XMLConfigBuilder.pluginElement(XNode parent)对xml进行解析,然后将Interceptor加入到interceptorChain中。配置方式如下:

<!-- mybatis-config.xml --><plugins>  <plugin interceptor=\\\"自定义Interceptor类的全路径\\\">    <property name=\\\"propertyKey\\\" value=\\\"propertyValue\\\"/>  </plugin></plugins>

另外一种方式是用@Configuration注解定义配置类:可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器。本文demo采取的配置方式就是这种。

两种方式最终都会调用到InterceptorChain.addInterceptor(Interceptor interceptor)方法。

//Configuration类 public void addInterceptor(Interceptor interceptor) {    interceptorChain.addInterceptor(interceptor); }
 //InterceptorChain类  public void addInterceptor(Interceptor interceptor) {    interceptors.add(interceptor);  }

2、下图为mybatis框架执行SQL操作的请求流转过程图。executor执行器会把SQL操作委托给statementHandler处理。statementHandler会调用ParamenterHander进行对SQL参数的一些处理,然后再调用statement执行SQL。执行完成以后会返回ResultSet结果集,然后ResultSetHandler会对结果集合进行处理(例如:返回结果和实体类的映射)。

3、Mybatis框架中会通过Configuration.newExecutor()生成executor对象,生成过程中会通过pluinAll()方法生成Executor的代理对象,以达到将Interceptor中自定义功能织入到Executor中的目的。参见代码:

//Configuration类  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;  }

进入pluginAll方法会发现该方法会遍历InterceptorChain的interceptor集合,并调用interceptor的plugin方法。注意生成代理对象重新赋值给target,如果有多个拦截器的话,生成的代理对象会被另一个代理对象代理,从而形成一个代理链条。所以插件不宜定义过多,以免嵌套层级太多影响程序性能。

//InterceptorChain类  public Object pluginAll(Object target) {  //遍历interceptor集合    for (Interceptor interceptor : interceptors) {    //调用自定义Interceptor的plugin方法      target = interceptor.plugin(target);    }    return target;  }

自定义插件的interceptor.plugin方法一般考虑调用mybatis提供的工具方法:Plugin.wrap(),该类实现了InvocationHandler接口。当代理executor对象被调用时,会触发plugin.invoke()方法,该方法是真正的Interceptor.intercept()被执行的地方。

//Plugin类 public static Object wrap(Object target, Interceptor interceptor) { //获取自定义Interceptor的@Signature注解信息    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;  }

getSignatureMap方法:首先会拿到拦截器这个类的 @Interceptors注解,然后拿到这个注解的属性 @Signature注解集合,然后遍历这个集合,遍历的时候拿出 @Signature注解的type属性(Class类型),然后根据这个type得到带有method属性和args属性的Method。由于 @Interceptors注解的 @Signature属性是一个属性,所以最终会返回一个以type为key,value为Set的Map结构。

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);    // issue #251    if (interceptsAnnotation == null) {      throw new PluginException(\\\"No @Intercepts annotation was found in interceptor \\\" + interceptor.getClass().getName());          }    Signature[] sigs = interceptsAnnotation.value();    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();    for (Signature sig : sigs) {      Set<Method> methods = signatureMap.get(sig.type());      if (methods == null) {        methods = new HashSet<Method>();        signatureMap.put(sig.type(), methods);      }      try {        Method method = sig.type().getMethod(sig.method(), sig.args());        methods.add(method);      } catch (NoSuchMethodException e) {        throw new PluginException(\\\"Could not find method on \\\" + sig.type() + \\\" named \\\" + sig.method() + \\\". Cause: \\\" + e, e);      }    }    return signatureMap;  }

Plugin.invoke()方法是真正自定义插件逻辑被调起的地方。

//Plugin类  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {    try {      Set<Method> methods = signatureMap.get(method.getDeclaringClass());      //判断当前method方法是否在自定义类@Intercepts注解的@Signature列表中,如果当前方法需要被拦截,则执行Interceptor.intercept方法      if (methods != null && methods.contains(method)) {      //自定义Interceptor中附加功能被执行的地方        return interceptor.intercept(new Invocation(target, method, args));      }      //如果当前方法不需要被拦截,则直接被调用并返回      return method.invoke(target, args);    } catch (Exception e) {      throw ExceptionUtil.unwrapThrowable(e);    }  }

interceptor.intercept(new Invocation(target, method, args))中的参数Invocation对象对目标类、目标方法和方法参数进行了封装。需要注意的是,自定义Interceptor中的intercept方法中不要忘记执行invocation.process()方法,否则整个责任链会中断掉。

public class Invocation {
private final Object target; private final Method method; private final Object[] args; ...... . . ...... //processed方法,触发被代理类目标方法的执行 public Object proceed() throws InvocationTargetException, IllegalAccessException { return method.invoke(target, args); }
}

至此mybatis插件机制的使用方式和运行机理介绍完成。

06

总结

本篇文章介绍了mybatis框架插件机制的应用场景和运行原理。插件模块的实现思想以设计模式中的责任链模式和代理模式为主,降低了对象之间的耦合,增强了系统的可扩展性,满足了开放封闭原则。可将插件思想灵活运用于日常研发工作中。

往期推荐 ●●

// 1

|玩转Selenium——Web页面自动化实战

// 2

|自定义控制器开发(一)

// 3

|初识分布式锁及其实现与使用

// 4

|基于Prometheus的分布式监控平台落地与实践

欢迎关注EBCloud!

作者 | 柳梦阳

1

原创文章,作者:EBCloud,如若转载,请注明出处:https://www.sudun.com/ask/32576.html

Like (0)
EBCloud的头像EBCloud
Previous 2024年4月2日 下午3:28
Next 2024年4月2日 下午3:28

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注