精通Mybatis之插件体系(与中间件实现的一些思考)

Posted 木兮君

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了精通Mybatis之插件体系(与中间件实现的一些思考)相关的知识,希望对你有一定的参考价值。

前言

这应该是精通mybatis系列的最后一篇文章了,插件体系讲完后大家有不明白的地方可以一起探讨,在上一篇博客精通Mybatis之Configuration配置体系中讲述拦截器链的时候,小编有说过拦截器主要是针对了插件体系,并且在四个地方做了相应的增强,他还和aop还稍微不一样,更加简洁明了吧,所以今天小编带大家看看mybatis的插件体系。

mybatis插件的四大组件入口

插件机制是为了对MyBatis现有体系进行扩展 而提供的入口。底层通过动态代理实现。可供代理拦截的接口有四个:

  • Executor:执行器
  • StatementHandler:JDBC处理器
  • ParameterHandler:参数处理器
  • ResultSetHandler:结果集处理器

这四个接口已经涵盖从发起接口调用到SQl声明、参数处理、结果集处理的全部流程。接口中任何一个方法都可以进行拦截改变方法原有属性和行为。不过这是一个非常危险的行为,稍不注意就会破坏MyBatis核心逻辑还不自知。所以在在使用插件之前一定要非常清晰MyBatis内部机制。

源码阅读
为什么是以上四个入口,咱们眼见为实:
Configuration中方法:

//Executor 
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);
    
    //这里如果配置了interceptorChain则遍历拦截链然后执行plugin方法
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  
  //ParameterHandler 
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) 
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
  
//ResultSetHandler 
  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) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  
//StatementHandler 
  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) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  

中间件实现的思考

对一个框架做扩展,需要很好的设计思路和设计模式,这个比写业务代码更具有挑战并且如果没有经验的话很难想得详细和全面。这里小编讲解一下自己的思路和总结的经验(小编只是个菜鸟,如果有什么不对的地方不要喷小编):

  1. 易用性:当对原框架或原业务逻辑进行扩展的时候,咱们要保证第三方调用足够的简单,最好是加个注解或者是带个尽可能简单的参数即可实现较为困难的逻辑,将困难逻辑封装而不给调用者添麻烦。还有一点就是不需要修改任何配置。
  2. 对原业务所有场景都支持:不能影响原业务或原代码的逻辑,也就是兼容之前的版本,并可以扩展,这点还是很难的,之前无论是什么场景都需要支持,而不是说只能支持特定的几个场景了。
  3. 对用户友好:这个就是代码的约束和规范让人容易接受,如果可能的话可以引导用户习惯。当中间件出错的时候必须是报错,让三方能够知道是什么错误。
  4. 代码0侵入:这个是非常难做到的,可能只需要修改配置或只是引入三方jar包就神不知鬼不觉的实现了一个强大的功能,比方说分布式调用链的实现。

以上是小编思考的几个点,其实mybatis的插件实现也相当于是中间件的实现,希望大家有所收获。

分页实现

首先小编先对mybatis 的plugin类做个测试:

public class PluginTest 
    @Test
    public void testMyPlugin()
        MyPlugin myPlugin = msg -> msg + " MyPlugin";
        Interceptor interceptor = new MyPluginInterceptor();
        MyPlugin wrap = (MyPlugin)Plugin.wrap(myPlugin, interceptor);
        System.out.println(wrap.wrappedString("hello"));
    

    public interface MyPlugin

        String wrappedString(String msg);
    

    @Intercepts(
            @Signature(type = MyPlugin.class, method = "wrappedString", args = String.class))
    public static  class MyPluginInterceptor implements Interceptor 


        @Override
        public Object intercept(Invocation invocation) throws Throwable 
            System.out.println("intercept");
            return invocation.proceed();
        

        @Override
        public Object plugin(Object target) 
            return null;
        

        @Override
        public void setProperties(Properties properties) 

        
    

测试结果:

intercept
hello MyPlugin

这里先看下plugin的源码:

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) 
  	//解析注解生成map对象
    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;
  
 //实际方法
  @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);
    
  
 //解析  @Intercepts(
 //          @Signature(type = MyPlugin.class, method = "wrappedString", args = String.class))
 //注释
  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,解析成一个map,key为类的类型,values为类中的方法放入set集合中
    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<?>[interfaces.size()]);
  


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
  


看完上面大家其实已经知道Plugin ,Interceptor的作用了吧,我们其实做分页插件就是实现Interceptor接口然后配置进去。

简易版分页插件原理

分页插件原理
首先设定一个Page类,其包含total、size、index 3个属性,在Mapper接口中声明该参数即表示需要执行自动分页逻辑。
总体实现步骤包含3个:

  1. 检测是否满足分页条件
  2. 自动求出当前查询的总行数
  3. 修改原有的SQL语句 ,添加 limit 关键字。

Page类:

public class Page 

    private Integer pageNum;

    private Integer pageSize;

    private Integer total;

    public Integer getPageNum() 
        return pageNum;
    

    public void setPageNum(Integer pageNum) 
        this.pageNum = pageNum;
    

    public Integer getPageSize() 
        return pageSize;
    

    public void setPageSize(Integer pageSize) 
        this.pageSize = pageSize;
    

    public Integer getTotal() 
        return total;
    

    public void setTotal(Integer total) 
        this.total = total;
    

PageInterceptor类

@Intercepts(@Signature(type = StatementHandler.class,
        method = "prepare", args = Connection.class, Integer.class))
public class PageInterceptor implements Interceptor 
    @Override
    @SuppressWarnings("unchecked")
    public Object intercept(Invocation invocation) throws Throwable 
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        Object parameterObject = boundSql.getParameterObject();
        Page page = null;
        if (parameterObject instanceof Page) 
            page = (Page) parameterObject;
         else if (parameterObject instanceof Map) 
            page = (Page) ((Map) parameterObject).values().stream().
                    filter(item -> item instanceof Page).findFirst().orElse(null);
        
        if (page != null) 
            int total = getTotalSize(invocation,statementHandler);
            page.setTotal(total);
            String limitSql = String.format("%s limit %s , %s",boundSql.getSql(),page.getPageNum(),page.getPageSize());
            SystemMetaObject.forObject(boundSql).setValue("sql",limitSql);
        
        return invocation.proceed();
    

    private int getTotalSize(Invocation invocation, StatementHandler statementHandler) throws SQLException 
        int count = 0;
        BoundSql boundSql = statementHandler.getBoundSql();
        String countSql = String.format("select count(*) from (%s) as _page",boundSql.getSql());
        Connection connection = (Connection) invocation.getArgs()[0];
        PreparedStatement preparedStatement = connection.prepareStatement(countSql);
        statementHandler.getParameterHandler().setParameters(preparedStatement);
        ResultSet resultSet = preparedStatement.executeQuery();
        if(resultSet.next())
             count = resultSet.getInt(1);
        
        resultSet.close();
        preparedStatement.close();;

        return count;
    

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

    @Override
    public void setProperties(Properties properties) 

    

测试类:

@Before
    public void init() throws SQLException 
        // 获取构建器
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        // 解析XML 并构造会话工厂
        factory = factoryBuilder.build(ExecutorTest.class.getResourceAsStream("/mybatis-config.xml"));
        factory.getConfiguration().addInterceptor(new PageInterceptor());
        sqlSession = factory.openSession();

    
@Test
    public void selectByPage() 
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        Page page = new Page();
        page.setPageNum(1);
        page.setPageSize(10);
        List<User> users = mapper.selectByPage(page);
        System.out.println("总行数:" + page.getTotal() + ",当前查询条数:" + users.size());
    

然后那个sql其实是select * from users

<select id="selectByPage" resultMap="result_user">
        select * from users
    </select>

结果:

22:39:40,562 DEBUG selectByPage:54 - ==>  Preparing: select count(*) from (select * from users) as _page 
22:39:以上是关于精通Mybatis之插件体系(与中间件实现的一些思考)的主要内容,如果未能解决你的问题,请参考以下文章

精通Mybatis之结果集处理流程与映射体系(联合查询与嵌套映射)

精通Mybatis之结果集处理流程与映射体系(无死角懒加载讲解)

精通Mybatis之结果集处理流程与映射体系(重点mybatis嵌套子查询,循环依赖解决方案)

精通Mybatis之Configuration配置体系

精通Mybatis之Configuration配置体系

mybatis-插件体系之原理