mybatis 插件原理

1.Mybatis插件原理

Mybatis的插件,是采用责任链机制,通过JDK动态代理来实现的。默认情况下,Mybatis允许使用插件来拦截四个对象:

  1. Executor:执行CURD操作;
  2. StatementHandler:处理sql语句预编译,设置参数等相关工作;
  3. ParameterHandler:设置预编译参数用的;
  4. ResultSetHandler:处理结果集。

这个我们可以从Mybatis的源码中看到,例如下面创建Executor的时候,就是返回了一个代理Executor对象:

开始进入源码环节。

下面是InterceptorChain 类的源码:可以看到内部有一个拦截器链,调用pluginAll方法时,会遍历所有的拦截器的plugin方法

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

  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

查看 Interceptor源码:它是一个接口,其plugin方法又调用了Plugin.wrap方法,继续跟入。

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
  }

}

Plugin源码:进到这里,思路就豁然开朗了,wrap方法里就是通过JDK动态代理创建了一个代理对象。下面我们逐行看下,都经过了哪些步骤。

/**
 *    Copyright 2009-2021 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.plugin;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.ibatis.reflection.ExceptionUtil;

/**
 * @author Clinton Begin
 */
public class Plugin implements InvocationHandler {

  private final Object target;
  private final Interceptor interceptor;
  private final Map<Class<?>, Set<Method>> signatureMap;

  private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }

  public static Object wrap(Object target, Interceptor interceptor) {
    // 得到拦截器上的注解的 classType、 method、parameter参数的Map
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // 获取被代理的Class类型
    Class<?> type = target.getClass();
    // 找到signatureMap中包含的 type 的所有父类接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      // 创建一个代理对象
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // 代理对象执行方法时,会进入这里,先判断该方法是否在被代理的集合中
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      // 当前执行的方法是被代理的话,则去执行对应的拦截器的intercept方法
      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);
    }
  }

  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    // 获取拦截器上的 Intercepts 注解
    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());
    }
    // 得到注解的 classType、 method、parameter参数,放入Map中,返回
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
      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;
  }

  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<>();
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[0]);
  }

}

 1.

Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);

getSignatureMap方法,获取拦截器上的注解内容。得到拦截器上的@Intercepts注解,解析 Intercepts注解上的参数,放入HashMap集合中,格式: {{key=type, value=[method(args...}, ...}

,这些参数用于表明该拦截器用来拦截哪些类的哪些方法。说的有点抽象,上代码,如下:

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

解析的就是Intercepts注解内部Signature里面的type、method、args这些参数。(上面注解的意思是,拦截Executor类的update(MappedStatement mappedStatement, Object obj)方法!)

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    // 获取拦截器上的 Intercepts 注解
    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());
    }
    // 得到注解的 type、 method、parameter参数,放入Map中,返回
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
      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;
  }

2.

// 获取被代理的Class类型
Class<?> type = target.getClass();

这一行肉眼可见,就是用于得到被代理的Class对象。

3.

// 找到type 的所有被拦截的接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);

这里就是遍历出type的接口中,被拦截的接口:

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<>();
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[0]);
  }

4.创建代理对象

if (interfaces.length > 0) {
  // 创建一个代理对象
  return Proxy.newProxyInstance(
      type.getClassLoader(),
      interfaces,
      new Plugin(target, interceptor, signatureMap));
}
return target;

所以当执行target 中的方法时,就会执行Plugin的invoke方法:

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)) {// 当前执行的方法是被代理的话,则去执行对应的拦截器的intercept方法
      return interceptor.intercept(new Invocation(target, method, args));
    }
    return method.invoke(target, args);// 非被拦截器拦截的方法,直接回调执行,返回结果即可
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }
}

至此源码分析完毕!接下来演示实现一个分页插件。

2.自定义Mybatis插件

-1. Mybatis的拦截器,用来拦截SQL请求

分页实体类:

public class PageInfo implements Serializable {
    private int totalNumber; // 当前表中总条目数量
    private int currentPage; // 当前页数
    private int totalPage;   // 总页数
    private int pageSize=3;    // 每页显示条数
    private int startIndex=1;  // 检索的起始位置
    private int totalSelect; // 检索的总数目

    public int getTotalNumber() {
        return totalNumber;
    }

    public void setTotalNumber(int totalNumber) {
        this.totalNumber = totalNumber;
        // 计算
        this.count();
    }

    public int getCurrentPage() {
        return currentPage;
    }

    public void setCurrentPage(int currentPage) {
        this.currentPage = currentPage;
    }

    public int getTotalPage() {
        return totalPage;
    }

    public void setTotalPage(int totalPage) {
        this.totalPage = totalPage;
    }

    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }

    public int getStartIndex() {
        return startIndex;
    }

    public void setStartIndex(int startIndex) {
        this.startIndex = startIndex;
    }

    public int getTotalSelect() {
        return totalSelect;
    }

    public void setTotalSelect(int totalSelect) {
        this.totalSelect = totalSelect;
    }

    public void count(){
        int totalPageTmp = this.totalNumber / this.pageSize;
        // 总条数不能被页面大小整除时,总页数 + 1
        int plus = (this.totalNumber % this.pageSize) == 0? 0:1;
        totalPageTmp += plus;
        if (totalPageTmp<=0){
            totalPageTmp = 1;
        }
        this.totalPage = totalPageTmp;  // 计算得到总页数
        if (this.totalPage<this.currentPage){
            this.currentPage = this.totalPage;
        }
        if (this.currentPage<1){
            this.currentPage = 1;
        }
        this.startIndex = (this.currentPage - 1) * this.pageSize;   // 起始位置等于之前所有页面数乘以页面大小
        this.totalSelect = this.pageSize; // 检索数量等于页面大小
    }
}

插件类:

@Intercepts(@Signature(type=StatementHandler.class, method="prepare", args={Connection.class, Integer.class}))
public class SimbaPagePlugin implements Interceptor {
    private Log log = LogFactory.getLog(this.getClass());

    private String type;

    // 插件的核心业务
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        /**
         * 1. 拿到原始的SQL语句: select * from user;
         * 2. 修改原始SQL,加上分页: select * from user limit 0,3;
         * 3. 执行jdbc方法查询总数: select count(1) from (select * from user) a;
         */
        // 从 invocation 中获取 StatementHandler 对象
        StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
        // 拿到原始 SQL
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        log.info("$$$$$$$$$$$$$$$$$$$$$$$$ 原始sql:  " + sql);

        // 拿到分页参数
        Object paramObj = boundSql.getParameterObject();

        // 将 statementHandler 对象转成 metaObject
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);

        // 从metaObject对象中取得 MappedStatement 对象
        MappedStatement mappedStatement = (MappedStatement)metaObject.getValue("delegate.mappedStatement");

        // 获取 mapper 接口中的方法名称, 如: selectUserByPage
        String mapperMethodName = mappedStatement.getId();
        log.info("$$$$$$$$$$$$$$$$$$$$$$$$ mapperMethodName:  " + mapperMethodName);
        // 匹配到以 "ByPage"结尾的方法
        if (mapperMethodName.matches(".*ByPage$")){
            Map<String, Object> params = (Map<String, Object>) paramObj;
            PageInfo pageInfo = (PageInfo)params.get("page");

            // 查询总条数
            String countSql = "select count(0) from ("+ sql + ") a";
            // 执行 jdbc 操作
            Connection conn = (Connection)invocation.getArgs()[0];
            PreparedStatement ps = conn.prepareStatement(countSql);
            ParameterHandler parameterHandler = (ParameterHandler)metaObject.getValue("delegate.parameterHandler");
            parameterHandler.setParameters(ps);
            ResultSet rs = ps.executeQuery();
            if (rs.next()){
                pageInfo.setTotalNumber(rs.getInt(1));
            }
            rs.close();
            ps.close();

            // 改造原始 sql,加上 limit 分页
            String pageSql = this.generatePageSql(sql, pageInfo);
            log.info("$$$$$$$$$$$$$$$$$$$$$$$$ 分页sql:  " + pageSql);
            // 更新执行流程中的 sql
            metaObject.setValue("delegate.boundSql.sql", pageSql);

        }

        // 把执行流程交给mybatis
        return invocation.proceed();
    }


    private String generatePageSql(String sql, PageInfo pageInfo) {
        StringBuilder sb = new StringBuilder();
        sb.append(sql);
        if ("mysql".equalsIgnoreCase(type)){
            sb.append(" limit " + pageInfo.getStartIndex() + " , " + pageInfo.getTotalSelect());
        }else if ("oracle".equalsIgnoreCase(type)){
            //TODO
        }
        return sb.toString();
    }

    // 把自定义的插件加入到mybatis中去执行
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    // 设置属性
    @Override
    public void setProperties(Properties properties) {
        type = properties.getProperty("type");   // 可以根据这里的得到的属性,做适配:mysql、Oracle、Mongodb
        log.info("$$$$$$$$$$$$$$$$$$$$$$$$ type:  " + type);
    }
}

-2. 在配置文件中添加拦截器配置

<?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>
    <!-- Mybatis 插件 -->
    <plugins>
        <plugin interceptor="com.simba.self.plugin.SimbaPagePlugin">
            <property name="type" value="mysql"/>
        </plugin>
    </plugins>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC" />
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/simba?serverTimezone=UTC"/>
                <property name="username" value="root"/>
                <property name="password" value="qwer@123"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>
</configuration>

-3.测试

public class Test {
    public static void testJdbc() throws Exception {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        // 打开会话
        try (SqlSession session = sqlSessionFactory.openSession()){
            UserMapper userMapper = session.getMapper(UserMapper.class);
            PageInfo pageInfo = new PageInfo();
            pageInfo.setPageSize(4);
            Map<String,Object> param = new HashMap<>();
            param.put("page", pageInfo);
            List<User> pageData = userMapper.selectByPage(param);
            for (User u: pageData){
                System.out.println("pageData: " + u);
            }
        }
    }

    public static void main(String[] args) throws Exception {
        testJdbc();
    }

}

-4.测试结果

 

 

 

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值