mybatis插件原理及使用

mybatis插件原理及使用

文章目录

  • mybatis插件原理及使用
      • 一、简介
      • 二、插件应用场景
      • 三、插件编写方式
        • 3.1 标识拦截方法
          • 3.1.1 Intercepts注解
          • 3.1.2 Signature注解
        • 3.2 可拦截方法
          • 3.2.1 Executor接口
          • 3.2.2 ParameterHandler接口
          • 3.2.3 StatementHandler接口
          • 3.2.4 ResultSetHandler接口
        • 3.3 实现拦截逻辑Interceptor
      • 四、插件实现原理
        • 4.1 解析配置xml文件中的插件(拦截器)配置
        • 4.2 拦截器添加到拦截器链中
        • 4.3 生成拦截对象进行请求处理
        • 4.4 拦截对象链执行
        • 4.5 具体拦截对象执行
        • 4.6 拦截代理生成及执行
      • 五、插件使用示例
        • 5.1 添加maven依赖
        • 5.2 添加实体类Stu.java
        • 5.3 添加分页类Page.java
        • 5.4 添加Mapper接口StuMapper.java
        • 5.5 添加插件类PagePlugin.java
        • 5.6 添加mysql配置文件db.properties和mybatis配置文件mybatis-config.xml
        • 5.6 添加主类MybatisPluginStudy.java
      • 六、备注

一、简介

这里介绍mybatis中插件的原理及使用。将结合源码及示例进行说明。

二、插件应用场景

插件是mybatis的扩展方式,可用于添加默认值、日志记录、黑白名单等。

三、插件编写方式

mybatis插件是通过拦截器实现的,编写插件需要标识拦截方法和实现拦截逻辑。

3.1 标识拦截方法

标识拦截拦截方法是通过注解org.apache.ibatis.plugin.Intercepts和注解org.apache.ibatis.plugin.Signature实现的。

3.1.1 Intercepts注解

Intercepts注解内是一个Signature注解(每个Signature注解标识了一个需要拦截的方法)列表。Intercepts注解定义如下:

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

Signature注解位于Intercepts注解内,每个Signature注解标识了一个需要拦截的方法。Signature注解定义如下:

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

3.2 可拦截方法

mybatis插件可以对Executor、ParameterHandler、ResultSetHandler和StatementHandler接口的实现类进行拦截。
mybatis对上述接口的处理流程如下:

request Executor StatementHandler ParameterHandler ResultSetHandler start dispose start pre parameter(prepare/parameterize method) dispose end pre parameter dispose start dispose start dispose end disppose dispose start dispose end disppose end disppose end disppose request Executor StatementHandler ParameterHandler ResultSetHandler 插件请求处理流程图
3.2.1 Executor接口

Executor接收请求处理,负责请求处理的全过程。

public interface Executor {
     

  ResultHandler NO_RESULT_HANDLER = null;

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

}
3.2.2 ParameterHandler接口

ParameterHandler是参数的处理,通过插件可以对参数进行修改设置。

public interface ParameterHandler {
     

  Object getParameterObject();

  void setParameters(PreparedStatement ps)throws SQLException;
}
3.2.3 StatementHandler接口

StatementHandler是执行sql的过程,通过插件可以重写sql。

public interface StatementHandler {
     

  Statement prepare(Connection connection, Integer transactionTimeout)
      throws SQLException;

  void parameterize(Statement statement)
      throws SQLException;

  void batch(Statement statement)
      throws SQLException;

  int update(Statement statement)
      throws SQLException;

  <E> List<E> query(Statement statement, ResultHandler resultHandler)
      throws SQLException;

  <E> Cursor<E> queryCursor(Statement statement)
      throws SQLException;

  BoundSql getBoundSql();

  ParameterHandler getParameterHandler();

}
3.2.4 ResultSetHandler接口

ResultSetHandler是结果的处理,通过插件可以对结果进行修改。

public interface ResultSetHandler {
     

  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  void handleOutputParameters(CallableStatement cs) throws SQLException;
}

3.3 实现拦截逻辑Interceptor

mybatis插件需要实现org.apache.ibatis.plugin.Interceptor接口,在方法intercept(Invocation invocation)方法中实现自定义的插件逻辑。Interceptor接口完整定义如下:

public interface Interceptor {
     
  //实现自定义插件逻辑
  Object intercept(Invocation invocation) throws Throwable;
  //创建代理
  default Object plugin(Object target) {
     
    return Plugin.wrap(target, this);
  }
  //参数传递  
  default void setProperties(Properties properties) {
     
    // NOP
  }
}

四、插件实现原理

mybatis插件实现运用了代理模式和责任链模式。

插件的实现流程如下:

解析配置xml文件中的插件配置-XMLConfigBuilder.pluginElement
拦截器添加到拦截器链中-Configuration.addInterceptor
生成拦截对象进行请求处理-如Configuration.newExecutor
拦截对象链执行-InterceptorChain.pluginAll
具体拦截对象执行-Interceptor.plugin
拦截代理生成及执行-Plugin.wrap和Plugin.invoke

4.1 解析配置xml文件中的插件(拦截器)配置

XMLConfigBuilder.pluginElement(XNode parent)方法解析mybatis-config.xml的节点。关键源码如下:

public class XMLConfigBuilder extends BaseBuilder {
     
    //解析plugin节点
    private void pluginElement(XNode parent) throws Exception {
     
        if (parent != null) {
     
          for (XNode child : parent.getChildren()) {
     
            String interceptor = child.getStringAttribute("interceptor");
            Properties properties = child.getChildrenAsProperties();
            Interceptor interceptorInstance = (Interceptor) 			      resolveClass(interceptor).newInstance();
            interceptorInstance.setProperties(properties);
            configuration.addInterceptor(interceptorInstance);
          }
        }
      }
}

4.2 拦截器添加到拦截器链中

Configuration.addInterceptor(Interceptor interceptor)将拦截器添加到拦截器链(位于Configuration类中)中。关键源码如下:

public class Configuration {
     
	//内部实现是List
    protected final InterceptorChain interceptorChain = new InterceptorChain();
    //添加拦截器
    public void addInterceptor(Interceptor interceptor) {
     
        interceptorChain.addInterceptor(interceptor);
    }
}

4.3 生成拦截对象进行请求处理

通过Configuration.new*系列方法(如: newExecutor,newParameterHandler,newStatementHandler, newResultSetHandler)获取代理对象进行拦截处理。以newExecutor方法为例,关键源码如下:

public class 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) interceptorChain.pluginAll(executor);
        return executor;
      }
}

4.4 拦截对象链执行

InterceptorChain.pluginAll(Object target)方法进行拦截对象链执行。关键源码如下:

public class InterceptorChain {
     
  private final List<Interceptor> interceptors = new ArrayList<>();
  public Object pluginAll(Object target) {
     
    for (Interceptor interceptor : interceptors) {
     
      target = interceptor.plugin(target);
    }
    return target;
  }
}

4.5 具体拦截对象执行

Interceptor.plugin(Object target)执行,关键源码如下:

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

4.6 拦截代理生成及执行

Plugin.wrap(Object target, Interceptor interceptor)生成拦截对象代理及最终实现Plugin.invoke(Object proxy, Method method, Object[] args),关键源码如下:

public class Plugin implements InvocationHandler {
     
 //生成拦截对象代理
  public static Object wrap(Object target, Interceptor interceptor) {
     
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
     
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }
 //对象代理执行
 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);
    }
  }
}

五、插件使用示例

这里以分页插件为示例。

5.1 添加maven依赖

<dependency>
    <groupId>org.mybatisgroupId>
    <artifactId>mybatisartifactId>
    <version>3.5.2version>
dependency>
<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
    <version>8.0.17version>
dependency>
<dependency>
    <groupId>org.apache.commonsgroupId>
    <artifactId>commons-lang3artifactId>
    <version>3.6version>
dependency>

5.2 添加实体类Stu.java

package com.study.plugin.bean;
import org.apache.commons.lang3.builder.ToStringBuilder;
public class Stu {
     
    private Long id;
    private String name;
    //....省略get和set方法
    @Override
    public String toString() {
     
        return ToStringBuilder.reflectionToString(this);
    }
}

5.3 添加分页类Page.java

package com.study.plugin.bean;
import org.apache.commons.lang3.builder.ToStringBuilder;
import java.util.Collections;
import java.util.List;
public class Page<T> {
     
    private Long pageNo;
    private Long pageSize;
    private Long total;
    public Page() {
     
    }
    public Page(Long pageNo, Long pageSize) {
     
        this.pageNo = pageNo;
        this.pageSize = pageSize;
    }
    //....省略get和set方法
    @Override
    public String toString() {
     
        return ToStringBuilder.reflectionToString(this);
    }
}

5.4 添加Mapper接口StuMapper.java

package com.study.plugin.mapper;
import com.study.plugin.bean.Stu;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
public interface StuMapper {
     
    @Select("select * from stu")
    List<Stu> selectStuPage(Page page);
}

5.5 添加插件类PagePlugin.java

package com.study.plugin.plugin;
import com.study.plugin.bean.Page;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import static com.sun.jmx.mbeanserver.Util.cast;

@Intercepts({
     
        @Signature(type = StatementHandler.class, method = "prepare", args = {
     Connection.class, Integer.class})
})
public class PagePlugin implements Interceptor {
     
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
     
        Object target = invocation.getTarget();
        if (target instanceof StatementHandler) {
     
            StatementHandler statementHandler = cast(target);
            Object[] args = invocation.getArgs();
            Connection connection = (Connection) args[0];
            MetaObject metaObject = SystemMetaObject.forObject(statementHandler);

            BoundSql boundSql = statementHandler.getBoundSql();
            //获取请求参数
            Object obj = boundSql.getParameterObject();
            Page page = getPage(obj);
            if (page == null) {
     
                return invocation.proceed();
            }

            String sql = boundSql.getSql();
            //重写sql
            String newSql = sql + " limit " + (page.getPageNo() - 1) * page.getPageSize() + "," + page.getPageSize();
            metaObject.setValue("delegate.boundSql.sql", newSql);

            //获取总数
            long total = getTotal(connection, sql);
            page.setTotal(total);
            return invocation.proceed();
        }

        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
     
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
     

    }
	
    //获取分页参数
    Page getPage(Object obj) {
     
        if (obj instanceof Page) {
     
            return cast(obj);
        }

        if (obj instanceof Map) {
     
            Optional pageOpt = ((Map) obj).values().stream().filter(t -> t instanceof Page).findFirst();
            return (Page) pageOpt.get();
        }
        return null;
    }

    //获取总数
    long getTotal(Connection connection, String sql) {
     
        try {
     
            String countSql = "select count(*) " + sql.substring(sql.indexOf("from"));
            PreparedStatement preStat = connection.prepareStatement(countSql);
            ResultSet rs = preStat.executeQuery();
            if (rs.next()) {
     
                return rs.getLong(1);
            }
        } catch (Exception e) {
     
            System.out.println(e);
        }
        return 0L;
    }
}

5.6 添加mysql配置文件db.properties和mybatis配置文件mybatis-config.xml

db.properties

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/school
jdbc.username=xx
jdbc.password=xx

mybatis配置文件mybatis-config.xml



<configuration>
    <properties resource="db.properties"/>

    <plugins>
        <plugin interceptor="com.study.plugin.plugin.PagePlugin"/>
    plugins>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            dataSource>
        environment>
    environments>

    <mappers>
        <package name="com.study.plugin.mapper"/>
    mappers>
configuration>

5.6 添加主类MybatisPluginStudy.java

package com.study.plugin;
import com.study.plugin.bean.Page;
import com.study.plugin.bean.Stu;
import com.study.plugin.mapper.StuMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.InputStream;
import java.util.List;

public class MybatisPluginStudy {
     
    public static void main(String[] args) throws Exception {
     
        //获取mybatis配置文件
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory factory = builder.build(inputStream);


        //获取sqlSession
        SqlSession session = factory.openSession();
        StuMapper stuMapper = session.getMapper(StuMapper.class);

        //方法执行
        Page<Stu> page = new Page(2L,2L);
        List<Stu> stuList = stuMapper.selectStuPage(page);
        System.out.println(page);
        stuList.forEach(System.out::println);
        session.close();
    }
}

输出:
com.study.plugin.bean.Page@543295b0[pageNo=2,pageSize=2,total=7]
com.study.plugin.bean.Stu@202b0582[id=4,name=n4]
com.study.plugin.bean.Stu@235ecd9f[id=5,name=n5]

六、备注

由于工作时间原因,这里对mybatis插件进行了大体介绍,或有疏漏,后期有再进行完善。

你可能感兴趣的:(mybatis使用,mybatis原理)