zoukankan      html  css  js  c++  java
  • 建议收藏,mybatis插件原理详解

    上次发文说到了如何集成分页插件,今天我们接着来聊mybatis插件的原理。

    插件原理分析

    mybatis插件涉及到的几个类:

    我将以 Executor 为例,分析 MyBatis 是如何为 Executor 实例植入插件的。Executor 实例是在开启 SqlSession 时被创建的,因此,我们从源头进行分析。先来看一下 SqlSession 开启的过程。

    public SqlSession openSession() {
        return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
    }
    
    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        try {
            // 省略部分逻辑
            
            // 创建 Executor
            final Executor executor = configuration.newExecutor(tx, execType);
            return new DefaultSqlSession(configuration, executor, autoCommit);
        } 
        catch (Exception e) {...} 
        finally {...}
    }
    

    Executor 的创建过程封装在 Configuration 中,我们跟进去看看看。

    // Configuration类中
    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;
        
        // 根据 executorType 创建相应的 Executor 实例
        if (ExecutorType.BATCH == executorType) {...} 
        else if (ExecutorType.REUSE == executorType) {...} 
        else {
            executor = new SimpleExecutor(this, transaction);
        }
        if (cacheEnabled) {
            executor = new CachingExecutor(executor);
        }
        
        // 植入插件
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }
    

    如上,newExecutor 方法在创建好 Executor 实例后,紧接着通过拦截器链 interceptorChain 为 Executor 实例植入代理逻辑。那下面我们看一下 InterceptorChain 的代码是怎样的。

    public class InterceptorChain {
        private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
        public Object pluginAll(Object target) {
            // 遍历拦截器集合
            for (Interceptor interceptor : interceptors) {
                // 调用拦截器的 plugin 方法植入相应的插件逻辑
                target = interceptor.plugin(target);
            }
            return target;
        }
        /** 添加插件实例到 interceptors 集合中 */
        public void addInterceptor(Interceptor interceptor) {
            interceptors.add(interceptor);
        }
        /** 获取插件列表 */
        public List<Interceptor> getInterceptors() {
            return Collections.unmodifiableList(interceptors);
        }
    }
    

    上面的for循环代表了只要是插件,都会以责任链的方式逐一执行(别指望它能跳过某个节点),所谓插件,其实就类似于拦截器。

    这里就用到了责任链设计模式,责任链设计模式就相当于我们在OA系统里发起审批,领导们一层一层进行审批。

    以上是 InterceptorChain 的全部代码,比较简单。它的 pluginAll 方法会调用具体插件的 plugin 方法植入相应的插件逻辑。如果有多个插件,则会多次调用 plugin 方法,最终生成一个层层嵌套的代理类。形如下面:

    当 Executor 的某个方法被调用的时候,插件逻辑会先行执行。执行顺序由外而内,比如上图的执行顺序为 plugin3 → plugin2 → Plugin1 → Executor

    plugin 方法是由具体的插件类实现,不过该方法代码一般比较固定,所以下面找个示例分析一下。

    // TianPlugin类
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    
    //Plugin
    public static Object wrap(Object target, Interceptor interceptor) {
        /*
         * 获取插件类 @Signature 注解内容,并生成相应的映射结构。形如下面:
         * {
         *     Executor.class : [query, update, commit],
         *     ParameterHandler.class : [getParameterObject, setParameters]
         * }
         */
        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;
    }
    

    如上,plugin 方法在内部调用了 Plugin 类的 wrap 方法,用于为目标对象生成代理。Plugin 类实现了 InvocationHandler 接口,因此它可以作为参数传给 Proxy 的 newProxyInstance 方法。

    到这里,关于插件植入的逻辑就分析完了。接下来,我们来看看插件逻辑是怎样执行的。

    执行插件逻辑

    Plugin 实现了 InvocationHandler 接口,因此它的 invoke 方法会拦截所有的方法调用。invoke 方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:

    //在Plugin类中
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            /*
             * 获取被拦截方法列表,比如:
             *    signatureMap.get(Executor.class),可能返回 [query, update, commit]
             */
            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);
        }
    }
    

    invoke 方法的代码比较少,逻辑不难理解。首先,invoke 方法会检测被拦截方法是否配置在插件的 @Signature 注解中,若是,则执行插件逻辑,否则执行被拦截方法。插件逻辑封装在 intercept 中,该方法的参数类型为 Invocation。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;
        }
        // 省略部分代码
        public Object proceed() throws InvocationTargetException, IllegalAccessException {
            //反射调用被拦截的方法
            return method.invoke(target, args);
        }
    }
    

    关于插件的执行逻辑就分析到这,整个过程不难理解,大家简单看看即可。

    自定义插件

    下面为了让大家更好的理解Mybatis的插件机制,我们来模拟一个慢sql监控的插件。

    /**
     * 慢查询sql 插件
     */
    @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
    public class SlowSqlPlugin implements Interceptor {
    
        private long slowTime;
    
        //拦截后需要处理的业务
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            //通过StatementHandler获取执行的sql
            StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
            BoundSql boundSql = statementHandler.getBoundSql();
            String sql = boundSql.getSql();
    
            long start = System.currentTimeMillis();
            //结束拦截
            Object proceed = invocation.proceed();
            long end = System.currentTimeMillis();
            long f = end - start;
            System.out.println(sql);
            System.out.println("耗时=" + f);
            if (f > slowTime) {
                System.out.println("本次数据库操作是慢查询,sql是:");
                System.out.println(sql);
            }
            return proceed;
        }
    
        //获取到拦截的对象,底层也是通过代理实现的,实际上是拿到一个目标代理对象
        @Override
        public Object plugin(Object target) {
            //触发intercept方法
            return Plugin.wrap(target, this);
        }
    
        //设置属性
        @Override
        public void setProperties(Properties properties) {
            //获取我们定义的慢sql的时间阈值slowTime
            this.slowTime = Long.parseLong(properties.getProperty("slowTime"));
        }
    }
    

    然后把这个插件类注入到容器中。

    然后我们来执行查询的方法。

    耗时28秒的,大于我们定义的10毫秒,那这条SQL就是我们认为的慢SQL。

    通过这个插件,我们就能很轻松的理解setProperties()方法是做什么的了。

    回顾分页插件

    也是实现mybatis接口Interceptor。

    @SuppressWarnings({"rawtypes", "unchecked"})
    @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}),
        }
    )
    public class PageInterceptor implements Interceptor {
            @Override
        public Object intercept(Invocation invocation) throws Throwable {
            ...
        }
    

    intercept方法中

    //AbstractHelperDialect类中
    @Override
    public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
            String sql = boundSql.getSql();
            Page page = getLocalPage();
            //支持 order by
            String orderBy = page.getOrderBy();
            if (StringUtil.isNotEmpty(orderBy)) {
                pageKey.update(orderBy);
                sql = OrderByParser.converToOrderBySql(sql, orderBy);
            }
            if (page.isOrderByOnly()) {
                return sql;
            }
            //获取分页sql
            return getPageSql(sql, page, pageKey);
     }
    //模板方法模式中的钩子方法
     public abstract String getPageSql(String sql, Page page, CacheKey pageKey);
    

    AbstractHelperDialect类的实现类有如下(也就是此分页插件支持的数据库就以下几种):

    我们用的是MySQL。这里也有与之对应的。

        @Override
        public String getPageSql(String sql, Page page, CacheKey pageKey) {
            StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
            sqlBuilder.append(sql);
            if (page.getStartRow() == 0) {
                sqlBuilder.append(" LIMIT ? ");
            } else {
                sqlBuilder.append(" LIMIT ?, ? ");
            }
            pageKey.update(page.getPageSize());
            return sqlBuilder.toString();
        }
    

    到这里我们就知道了,它无非就是在我们执行的SQL上再拼接了Limit罢了。同理,Oracle也就是使用rownum来处理分页了。下面是Oracle处理分页

        @Override
        public String getPageSql(String sql, Page page, CacheKey pageKey) {
            StringBuilder sqlBuilder = new StringBuilder(sql.length() + 120);
            if (page.getStartRow() > 0) {
                sqlBuilder.append("SELECT * FROM ( ");
            }
            if (page.getEndRow() > 0) {
                sqlBuilder.append(" SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( ");
            }
            sqlBuilder.append(sql);
            if (page.getEndRow() > 0) {
                sqlBuilder.append(" ) TMP_PAGE WHERE ROWNUM <= ? ");
            }
            if (page.getStartRow() > 0) {
                sqlBuilder.append(" ) WHERE ROW_ID > ? ");
            }
            return sqlBuilder.toString();
        }
    

    其他数据库分页操作类似。关于具体原理分析,这里就没必要赘述了,因为分页插件源代码里注释基本上全是中文。

    Mybatis插件应用场景

    • 水平分表
    • 权限控制
    • 数据的加解密

    总结

    Spring-Boot+Mybatis继承了分页插件,以及使用案例、插件的原理分析、源码分析、如何自定义插件。

    涉及到技术点:JDK动态代理、责任链设计模式、模板方法模式。

    Mybatis插件关键对象总结:

    • Inteceptor接口:自定义拦截必须实现的类。
    • InterceptorChain:存放插件的容器。
    • Plugin:h对象,提供创建代理类的方法。
    • Invocation:对呗代理对象的封装。
  • 相关阅读:
    (Java实现) 洛谷 P1603 斯诺登的密码
    (Java实现) 洛谷 P1036 选数
    (Java实现) 洛谷 P1036 选数
    (Java实现) 洛谷 P1012 拼数
    (Java实现) 洛谷 P1012 拼数
    (Java实现) 洛谷 P1028 数的计算
    (Java实现) 洛谷 P1028 数的计算
    (Java实现) 洛谷 P1553 数字反转(升级版)
    8.4 确定两个日期之间的月份数或年数
    (Java实现) 洛谷 P1553 数字反转(升级版)
  • 原文地址:https://www.cnblogs.com/tianweichang/p/14154366.html
Copyright © 2011-2022 走看看