本文正在参与「金石方案」。本文已同步于同名公众号《宁在春》

感兴趣的小伙伴,能够点个关注吗~~ hh

在焦虑不安的时刻内,我只能祈祷我平安无事。

前言

最近在参与金石方案,在考虑写什么的时,想到自己在项目中运用过的mybatis的插件,就想趁这个机遇聊一聊咱们触摸频繁的Mybatis.

假如是运用过Mybatis的小伙伴,那么咱们触摸过的第一个Mybatis的插件天然便是分页插件(Mybatis-PageHelper)啦。

你有了解过它是如何完成的吗?你有没有自己编写 Mybatis 插件去完成一些自定义需求呢?

插件是一种常见的扩展办法,大多数开源框架也都支持用户经过增加自定义插件的办法来扩展或改动框架原有的功用。

Mybatis 中也供给了插件的功用,虽然叫插件,可是实践上是经过阻拦器( Interceptor )完成的,经过阻拦某些办法的调用,在履行方针逻辑之前刺进咱们自己的逻辑完成。另外在 MyBatis 的插件模块中还触及责任链形式和 JDK 动态署理~

文章纲要:

学会自己编写Mybatis插件(拦截器)实现自定义需求

一、运用场景

  1. 一些字段的主动填充
  2. SQL句子监控、打印、数据权限等
  3. 数据加解密操作、数据脱敏操作
  4. 分页插件
  5. 参数、成果集的类型转化

这些都是一些能够运用Mybatis插件完成的场景,当然也能够运用其他的办法来完成,只不过阻拦的当地不一样算了,有早有晚。

二、Mybatis完成自定义阻拦器

咱们用自定义阻拦器完成一个相对简略的需求,在大多数表设计中,都会有create_time和update_time等字段,在创立或更新时需求更新相关字段。

假如是运用过MybatisPlus的小伙伴,或许知道在MybatisPlus中有一个主动填充功用,经过完成MetaObjectHandler接口中的办法来进行完成(首要的完成代码在com.baomidou.mybatisplus.core.MybatisParameterHandler中).

但运用Mybatis,并没有相关的办法或 API 能够直接来完成。所以咱们这次就用以此处作为切入点,自定义阻拦器来完成相似的主动填充功用。

编写步骤

  1. 编写一个阻拦器类完成 Interceptor 接口
  2. 增加阻拦注解 @Intercepts
  3. 在xml文件中装备阻拦器或许增加到Configuration中

根底的环境我就不再贴出来啦哈,直接上三个步骤的代码

2.1、编写阻拦器

package com.nzc.interceptor;
​
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.springframework.beans.factory.annotation.Value;
​
import java.lang.reflect.Field;
import java.util.*;
​
/**
 * @author 宁在春
 * @version 1.0
 * @description: 经过完成阻拦器来完成部分字段的主动填充功用
 * @date 2023/4/6 21:49
 */
@Intercepts({
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Slf4j
public class MybatisMetaInterceptor implements Interceptor {
​
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
    String sqlId = mappedStatement.getId();
    log.info("------sqlId------" + sqlId);
    SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
    Object parameter = invocation.getArgs()[1];
    log.info("------sqlCommandType------" + sqlCommandType);
    log.info("阻拦查询请求 Executor#update 办法" + invocation.getMethod());
    if (parameter == null) {
      return invocation.proceed();
     }
    if (SqlCommandType.INSERT == sqlCommandType) {
​
      Field[] fields = getAllFields(parameter);
      for (Field field : fields) {
        log.info("------field.name------" + field.getName());
        try {
          // 注入创立时刻
          if ("createTime".equals(field.getName())) {
            field.setAccessible(true);
            Object local_createDate = field.get(parameter);
            field.setAccessible(false);
            if (local_createDate == null || local_createDate.equals("")) {
              field.setAccessible(true);
              field.set(parameter, new Date());
              field.setAccessible(false);
             }
           }
         } catch (Exception e) {
         }
       }
     }
    if (SqlCommandType.UPDATE == sqlCommandType) {
      Field[] fields = getAllFields(parameter);
      for (Field field : fields) {
        log.info("------field.name------" + field.getName());
        try {
          if ("updateTime".equals(field.getName())) {
            field.setAccessible(true);
            field.set(parameter, new Date());
            field.setAccessible(false);
           }
         } catch (Exception e) {
          e.printStackTrace();
         }
       }
     }
    return invocation.proceed();
   }
​
  @Override
  public Object plugin(Object target) {
    return Interceptor.super.plugin(target);
   }
   
  // 稍后会议开说的
  @Override
  public void setProperties(Properties properties) {
    System.out.println("=======begin");
    System.out.println(properties.getProperty("param1"));
    System.out.println(properties.getProperty("param2"));
    Interceptor.super.setProperties(properties);
    System.out.println("=======end");
   }
​
  /**
   * 获取类的一切特点,包括父类
   *
   * @param object
   * @return
   */
  public static Field[] getAllFields(Object object) {
    Class<?> clazz = object.getClass();
    List<Field> fieldList = new ArrayList<>();
    while (clazz != null) {
      fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
      clazz = clazz.getSuperclass();
     }
    Field[] fields = new Field[fieldList.size()];
    fieldList.toArray(fields);
    return fields;
   }
}
​

2.2、增加到Mybatis装备

我这儿运用的JavaConfig的办法

package com.nzc.config;
​
import com.nzc.interceptor.*;
import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
@Configuration
public class MyBatisConfig {
​
  @Bean
  public ConfigurationCustomizer configurationCustomizer() {
    return new ConfigurationCustomizer() {
      @Override
      public void customize(org.apache.ibatis.session.Configuration configuration) {
        // 敞开驼峰命名映射
        configuration.setMapUnderscoreToCamelCase(true);
        MybatisMetaInterceptor mybatisMetaInterceptor = new MybatisMetaInterceptor();
        Properties properties = new Properties();
        properties.setProperty("param1","javaconfig-value1");
        properties.setProperty("param2","javaconfig-value2");
        mybatisMetaInterceptor.setProperties(properties);
        configuration.addInterceptor(mybatisMetaInterceptor);
       }
     };
   }
​
​
}

假如是xml装备的话,则是如下: property 是设置 阻拦器中需求用到的参数

<configuration>
  <plugins>
    <plugin interceptor="com.nzc.interceptor.MybatisMetaInterceptor"> 
      <property name="param1" value="value1"/>
      <property name="param2" value="value2"/>
    </plugin>
  </plugins>
</configuration>    

2.3、测验

测验代码:完成了一个SysMapper的增删改查

package com.nzc.mapper;
​
​
import com.nzc.entity.SysUser;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
​
import java.util.List;
​
/**
 * @author 宁在春
 * @description 针对表【sys_user】的数据库操作Mapper
 */
@Mapper
public interface SysUserMapper {
​
  @Select("SELECT * FROM tb_sys_user")
  List<SysUser> list();
​
  @Insert("insert into tb_sys_user(id,username,realname,create_time,update_time) values (#{id}, #{username}, #{realname}, #{createTime}, #{updateTime})")
  Boolean insert(SysUser sysUser);
​
  @Update("update tb_sys_user set  username=#{username} , realname=#{realname},update_time=#{updateTime}  where id=#{id}")
  boolean update(SysUser sysUser);
}
/**
 * @author 宁在春
 * @version 1.0
 * @description: TODO
 * @date 2023/4/6 21:38
 */
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class SysUserMapperTest {
​
  @Autowired
  private SysUserMapper sysUserMapper;
​
​
  @Test
  public void test1(){
    System.out.println(sysUserMapper.list());
   }
​
  @Test
  public void test2(){
    SysUser sysUser = new SysUser();
    sysUser.setId("1235");
    sysUser.setUsername("nzc5");
    sysUser.setRealname("nzc5");
    System.out.println(sysUserMapper.insert(sysUser));
   }
​
  @Test
  public void test3(){
    SysUser sysUser = new SysUser();
    sysUser.setId("1235");
    sysUser.setUsername("nzc7");
    sysUser.setRealname("nzc5");
    System.out.println(sysUserMapper.update(sysUser));
   }
}
​

当然要点不在这儿,而是在咱们打印的日志上,一起来看看作用吧

学会自己编写Mybatis插件(拦截器)实现自定义需求

此处相关日志对应Interceptor中的日志打印,想要了解的更为具体的能够debug检查一番。

2.4、小结

经过这个小小的事例,我想大伙对于Mybatis中的阻拦器应当是没有那般陌生了吧,接下来再来仔细聊聊吧

假如你运用过MybatisPlus的话,在读完这篇博文后,能够考虑考虑下面这个问题,或去看一看源码,将常识串联起来,假如能够的话,记得把答案贴到评论区啦~~~

考虑:还记得这一末节开端咱们聊到的MybatisPlus完成的主动填充功用吗?它是怎么完成的呢?

三、阻拦器接口介绍

MyBatis 插件能够用来完成阻拦器接口 Interceptor ,在完成类中对阻拦方针和办法进行处理

public interface Interceptor {
 // 履行阻拦逻辑的办法
 Object intercept(Invocation invocation) throws Throwable;
​
  //这个办法的参数 target 便是阻拦器要阻拦的方针,该办法会在创立被阻拦的接口完成类时被调用。
  //该办法的完成很简略 ,只需求调用 MyBatis 供给的 Plug 类的 wrap 静态办法就能够经过 Java 动态署理阻拦方针方针。
 default Object plugin(Object target) {
  return Plugin.wrap(target, this);
  }
​
 //这个办法用来传递插件的参数,能够经过参数来改动插件的行为
 default void setProperties(Properties properties) {
  // NOP
  }
​
}

有点懵没啥事,一个一个展开说:

intercept 办法

Object intercept(Invocation invocation) throws Throwable;

简略说便是履行阻拦逻辑的办法,但不得不说这句话是个高度归纳~

首先咱们要理解参数Invocation是个什么东东:

public class Invocation {
​
 private final Object target; // 阻拦的方针信息
 private final Method method; // 阻拦的办法信息
 private final Object[] args; // 阻拦的方针办法中的参数public Invocation(Object target, Method method, Object[] args) {
  this.target = target;
  this.method = method;
  this.args = args;
  }
​
 // get...
 // 运用反射来履行阻拦方针的办法
 public Object proceed() throws InvocationTargetException, IllegalAccessException {
  return method.invoke(target, args);
  }
​
}

联络咱们之前完成的自定义阻拦器上的注解:

@Intercepts({
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
  1. target对应咱们阻拦的Executor方针
  2. method对应Executor#update办法
  3. args对应Executor#update#args参数

plugin办法

这个办法其实也很好说:

那便是Mybatis在创立阻拦器署理时分会判别一次,当时这个类 Interceptor 究竟需不需求生成一个署理进行阻拦,假如需求阻拦,就生成一个署理方针,这个署理便是一个 {@link Plugin},它完成了jdk的动态署理接口 {@link InvocationHandler},假如不需求署理,则直接回来方针方针本身 加载机遇:该办法在 mybatis 加载核心装备文件时被调用

 default Object plugin(Object target) {
  return Plugin.wrap(target, this);
  }
public class Plugin implements InvocationHandler {
​
​
  //  运用反射,获取这个阻拦器 MyInterceptor 的注解 Intercepts和Signature,然后解析里边的值,
  //  1 先是判别要阻拦的方针是哪一个
  //  2 然后依据办法名称和参数判别要对哪一个办法进行阻拦
  //  3 依据成果做出决议,是回来一个方针呢还是署理方针
  public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    // 这边便是判别当时的interceptor是否包括在
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
     }
    //假如不需求署理,则直接回来方针方针本身
    return target;
   }
​
  //....
​
}

setProperties办法

在阻拦器中或许需求运用到一些变量参数,并且这个参数是可装备的,这个时分咱们就能够运用这个办法啦,加载机遇:该办法在 mybatis 加载核心装备文件时被调用

 default void setProperties(Properties properties) {
  // NOP
  }

关于如何运用:

javaConfig办法设置:

@Bean
public ConfigurationCustomizer configurationCustomizer() {
  return new ConfigurationCustomizer() {
    @Override
    public void customize(org.apache.ibatis.session.Configuration configuration) {
      // 敞开驼峰命名映射
      configuration.setMapUnderscoreToCamelCase(true);
      MybatisMetaInterceptor mybatisMetaInterceptor = new MybatisMetaInterceptor();
      Properties properties = new Properties();
      properties.setProperty("param1","javaconfig-value1");
      properties.setProperty("param2","javaconfig-value2");
      mybatisMetaInterceptor.setProperties(properties);
      configuration.addInterceptor(mybatisMetaInterceptor);
     }
   };
}

经过mybatis-config.xml文件进行装备

<configuration>
  <plugins>
    <plugin interceptor="com.nzc.interceptor.MybatisMetaInterceptor">
      <property name="param1" value="value1"/>
      <property name="param2" value="value2"/>
    </plugin>
  </plugins>
</configuration>    

测验作用便是测验事例上那般,经过了解阻拦器接口的信息,对于之前的事例不再是那般含糊啦

接下来再接着聊一聊阻拦器上面那一坨注解信息是用来干嘛的吧,

留意

当装备多个阻拦器时, MyBatis 会遍历一切阻拦器,按次序履行阻拦器的 plugin 口办法, 被阻拦的方针就会被层层署理。

在履行阻拦方针的办法时,会一层层地调用阻拦器,阻拦器通 invocation proceed()调用基层的办法,直到真实的办法被履行。

办法履行的成果 从最里边开端向外 层层回来,所以假如存在按次序装备的三个签名相同的阻拦器, MyBaits 会依照 C>B>A>target.proceed()>A>B>C 的次序履行。假如签名不同, 就会依照 MyBatis 阻拦方针的逻辑履行.

这也是咱们最开端谈到的Mybatis插件模块所运用的设计形式-责任链形式。

四、阻拦器注解介绍

上一个章节,咱们只阐明如何完成Interceptor接口来完成阻拦,却没有阐明要阻拦的方针是谁,在什么时分进行阻拦.就关系到咱们之前编写的注解信息啦.

@Intercepts({
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})

这两个注解用来装备阻拦器要阻拦的接口的办法。

@Intercepts({})注解中是一个@Signature()数组,能够在一个阻拦器中一起阻拦不同的接口和办法。

MyBatis 答应在己映射句子履行过程中的某一点进行阻拦调用。默认情况下, MyBatis 答应运用插件来阻拦的接口包括以下几个。

  • Executor
  • ParameterHandler
  • ResultSetHandler
  • StatementHandler

@Signature 注解包括以下三个特点。

  1. type 设置阻拦接口,可选值是前面提到的4个接口
  2. method 设置阻拦接口中的办法名 可选值是前面4个接口中所对应的办法,需求和接口匹配
  3. args 设置阻拦办法的参数类型数组 经过办法名和参数类型能够确定唯一一个办法

Executor 接口

下面便是Executor接口的类信息

public interface Executor {
​
 int update(MappedStatement ms, Object parameter) throws SQLException;
​
 <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
​
 <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
​
 <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
​
 List<BatchResult> flushStatements() throws SQLException;
​
 void commit(boolean required) throws SQLException;
​
 void rollback(boolean required) throws SQLException;
​
 CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
​
 boolean isCached(MappedStatement ms, CacheKey key);
​
 void clearLocalCache();
​
 void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
​
 Transaction getTransaction();
​
 void close(boolean forceRollback);
​
 boolean isClosed();
​
 void setExecutorWrapper(Executor executor);
​
}

我只会简略说一些最常用的~

1、update

int update(MappedStatement ms, Object parameter) throws SQLException;

该办法会在一切的 INSERT、UPDATE、DELETE 履行时被调用,因而假如想要阻拦这类操作,能够阻拦该办法。接口办法对应的签名如下。

@Intercepts({
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})

2、query

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
​
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

该办法会在一切 SELECT 查询办法履行时被调用 经过这个接口参数能够获取许多有用的信息,这也是最常被阻拦的办法。

@Intercepts({@Signature(
  type = Executor.class,
  method = "query",
  args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
  type = Executor.class,
  method = "query",
  args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})

3、queryCursor:

 <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

该办法只要在查询 的回来值类型为 Cursor 时被调用 。接口办法对应的签名相似于之前的。

//该办法只在经过 SqlSession 办法调用 commit 办法时才被调用 
void commit(boolean required) throws SQLException; 
//该办法只在经过 SqlSessio口办法调用 rollback 办法时才被调用
void rollback(boolean required) throws SQLException;
//该办法只在经过 SqlSession 办法获取数据库衔接时才被调用,
Transaction getTransaction();
//该办法只在推迟加载获取新的 Executor 后才会被履行
void close(boolean forceRollback);
//该办法只在推迟加载履行查询办法前被履行
boolean isClosed();

注解的编写办法都是相似的。

ParameterHandler 接口

public interface ParameterHandler {
​
  //该办法只在履行存储过程处理出参的时分被调用
  Object getParameterObject();
  //该办法在一切数据库办法设置 SQL 参数时被调用。
  void setParameters(PreparedStatement ps) throws SQLException;
}
​

我都写一块啦,假如要阻拦某一个的话只写一个即可

@Intercepts({
    @Signature(type = ParameterHandler.class, method = "getParameterObject", args = {}),
    @Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class})
})

ResultSetHandler 接口

public interface ResultSetHandler {
  //该办法会在除存储过程及回来值类型为 Cursor 以外的查询办法中被调用。
  <E> List<E> handleResultSets(Statement stmt) throws SQLException;
  //只会在回来值类型为 ursor 查询办法中被调用  
  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
  //只在运用存储过程处理出参时被调用 ,
  void handleOutputParameters(CallableStatement cs) throws SQLException;
}
@Intercepts({
    @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}),
    @Signature(type = ResultSetHandler.class, method = "handleCursorResultSets", args = {Statement.class}),
    @Signature(type = ResultSetHandler.class, method = "handleOutputParameters", args = {CallableStatement.class})
})

StatementHandler 接口

public interface StatementHandler {
  //该办法会在数据库履行前被调用 优先于当时接口中的其他办法而被履行
  Statement prepare(Connection connection, Integer transactionTimeout)
    throws SQLException;
  //该办法在 prepare 办法之后履行,用于处理参数信息 
  void parameterize(Statement statement)
    throws SQLException;
  //在大局设置装备 defaultExecutorType BATCH 时,履行数据操作才会调用该办法
  void batch(Statement statement)
    throws SQLException;
  //履行UPDATE、DELETE、INSERT办法时履行
  int update(Statement statement)
    throws SQLException;
  //履行 SELECT 办法时调用,接口办法对应的签名如下。
  <E> List<E> query(Statement statement, ResultHandler resultHandler)
    throws SQLException;
​
  <E> Cursor<E> queryCursor(Statement statement)
    throws SQLException;
​
  //获取实践的SQL字符串
  BoundSql getBoundSql();
​
  ParameterHandler getParameterHandler();
​
}
@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,Integer.class}),
    @Signature(type = StatementHandler.class, method = "parameterize", args = {Statement.class}),
    @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class}),
    @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
    @Signature(type = StatementHandler.class, method = "query", args = {Statement.class,ResultHandler.class}),
    @Signature(type = StatementHandler.class, method = "queryCursor", args = {Statement.class}),
    @Signature(type = StatementHandler.class, method = "getBoundSql", args = {}),
    @Signature(type = StatementHandler.class, method = "getParameterHandler", args = {})
}

假如有时刻的话,我会愈加建议看了的小伙伴,自己去完成接口做个测验,验证一番,也能了解的更完全些。看会了,许多时分常识的回忆还是浅的。

五、进一步考虑

看完这篇文章后,不知道你有没有什么收成。

再次看看这张文章纲要的图吧

学会自己编写Mybatis插件(拦截器)实现自定义需求

试着考虑考虑下面几个问题:

  • Mybatis插件适用于哪些场景?回忆一下你做过的项目,是否有能够运用Mybatis插件来完成的呢?
  • 你能够编写一个Mybatis插件了吗?
  • 感兴趣的话,你能够试着去了解一下Mybatis分页插件的完成办法。

最后留下一个遇到的问题,也是下一篇文章或许会写的吧,一起也运用到了今天所谈到了的阻拦器。

在项目中,你们都是如何针对表中某些字段进行加解密的呢?

后语

其实也是由于上面留下的那个问题,才让我去拜读了《Mybatis技能内幕》和《Mybatis从入门到通晓》两本书,收成还是有不少的。

其中《Mybatis技能内幕》内容相对更进阶一些,假如是已经会运用Mybatis的小伙伴,能够看一看,经过书籍再去看源码,能够温习到之前许多不太清楚的当地。

下次再会吧。


求职Java方向:

base:广州、深圳、杭州

vx:nzc_wyh

写于2023年4月8号凌晨,作者:宁在春