springboot框架项目实践应用二十(扩展mybatis插件及原理解析)

1.引言

说起mybatis框架的插件,大家都非常熟悉分页插件PageHelper,当我们持久层框架选型mybatis后,实现分页是非常容易的事情,只需要3个小步骤

  • 导入插件依赖
  • 增加插件配置
  • 代码使用(只需要一行代码)

比如说,我在springboot项目中,需要PageHelper插件,这么实现一下就可以了。

导入依赖:

xml 复制代码
<!--pagehelper-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.3</version>
</dependency>

增加配置:

yaml 复制代码
pagehelper:
  helper-dialect: mysql
  reasonable: true
  support-methods-arguments: true
  params: count=countSql

示例代码:

java 复制代码
/**
* 分页查询账户列表
* @param pageNum 页码
* @param pageSize 页大小
* @return
*/
@Override
public List<Account> findAll(Integer pageNum, Integer pageSize) {
    // 设置分页
    PageHelper.startPage(pageNum,pageSize);
    // 查询数据
    return accountMapper.findAll();
}

查询第一页,页面大小2:http://127.0.0.1:8080/account/list?pageNum=1&pageSize=2

通过案例,看到PageHelper分页插件,真是太方便!

那么,今天我们就来探讨一下,mybatis框架的插件机制原理,以及自定义一个统计sql语句执行耗时的插件。继续一文搞懂系列,来吧。

2.案例

2.1.需求分析

假设在项目中,框架选型springboot + mybatis,我们现在有一个需求,要统计sql语句的执行耗时,这个需求到了你的手里,你该如何实现?

你可能想到的方案

  • 通过过滤器Servlet Filter来实现,粒度太粗,Filter拦截的是请求级范围,做不到精准统计sql语句的执行时间,这个方案不合适
  • 通过spring拦截器来实现,虽然比起Filter拦截请求,Interceptor可以拦截到controller方法级别,还是不能实现sql语句执行耗时的精准统计,这个方案不合适
  • 还有一个实现方案:spring的aop,这个方案可行

但是,我们仔细想想,还有更合适的方案吗?既然是统计sql语句的执行耗时,是数据库层面的事情,放到应用层框架中,那就是持久层框架的事情。

那么mybatis框架有没有提供这样的能力呢?答案是有的,插件就可以解决我们的问题。

于是,你决定编写一个mybatis框架的插件,来实现这个需求,很完美!

2.2.代码实现

下面的案例,方便你理解整个mybatis框架插件的使用方式,我在一个只有mybatis框架的环境中做演示。

当然,最后我会告诉你,springboot整合mybatis框架的环境中,如何使用插件,事实上,整合环境中,比独立使用mybatis框架要更容易!

2.2.1.扩展插件Interceptor

mybatis框架,专门为插件提供了一个接口:Interceptor,我们只要实现该接口即可。

java 复制代码
/**
 * 统计sql执行耗时
 *
 * @author yanghouhua
 * @since 1.0.0
 */
@Slf4j
@Intercepts(
        {
                @Signature(type=StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
                @Signature(type=StatementHandler.class, method = "update", args = {Statement.class}),
                @Signature(type=StatementHandler.class, method = "batch", args = {Statement.class})
        }
)
public class CountSqlTimeInterceptor  implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 开始时间
        long start = System.currentTimeMillis();

        // 获取Jdbc封装的StatementHandler
        Object target = invocation.getTarget();
        StatementHandler statementHandler = (StatementHandler)target;

        // 执行
        try {
            return invocation.proceed();
        } finally {
            long end = System.currentTimeMillis();
            BoundSql boundSql = statementHandler.getBoundSql();
            String sql = boundSql.getSql();
            log.info("执行SQL:{},共耗时:{}毫秒.", sql, (end - start));
        }
    }

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

    @Override
    public void setProperties(Properties properties) {
            log.info("插件配置信息{}", properties);
    }
}

2.2.2.配置插件

在mybatis主配置文件中,增加插件配置,我的案例是:sqlMapConfig.xml

xml 复制代码
<plugins>
    <plugin interceptor="com.quanbu.plugin.CountSqlTimeInterceptor">
        <property name="xxxProperty" value="xxxValue"/>
    </plugin>
</plugins>

2.2.3.执行效果

下面是一段查询账户列表数据代码,非常简单!

java 复制代码
 @Test
    public void findAllTest() throws  Exception{
        // 初始化环境,拿到sqlSession
        InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 拿到接口代理对象
        AccountMapper mapper = sqlSession.getMapper(AccountMapper.class);
        List<Account> list = mapper.findAll();
        list.forEach(o ->{
            System.out.println(o);
        });

        // 释放资源
        sqlSession.close();

    }

执行结果:

从执行结果,我们看到通过自定义插件CountSqlTimeInterceptor,实现sql语句执行耗时的统计。有没有一点小激动?

代码实现很简单,关键是要搞清楚原理机制,下一小节再做原理分析,在这之前,我先告诉你,如果是springboot整合mybatis环境中,如何使用插件,两个小步骤即可

  • 编写自定义插件,就是上面的CountSqlTimeInterceptor
  • 将CountSqlTimeInterceptor交给spring容器管理,即在类上面加上注解:@Component,像这样
java 复制代码
@Component
@Slf4j
@Intercepts(
        {
                @Signature(type=StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
                @Signature(type=StatementHandler.class, method = "update", args = {Statement.class}),
                @Signature(type=StatementHandler.class, method = "batch", args = {Statement.class})
        }
)
public class CountSqlTimeInterceptor implements Interceptor {
    ....省略代码.....
}

是不是更简单、方便、直接!

2.3.原理分析

2.3.1.分析思路

插件的使用,相信你都知道了,毕竟是很容易的一个事情。那么我们来做原理分析吧。就从插件代码CountSqlTimeInterceptor开始。

在CountSqlTimeInterceptor代码上,需要重点关注的地方

  • 类上面的注解:@Intercepts
  • 方法:Intercept
  • 方法:plugin

mybatis框架的插件机制,应用了责任链设计模式,这也是为什么在文章开篇,我会引出过滤器Filter,拦截器Interceptor的原因。它们都是责任链模式的应用,且它们都需要在应用中告诉拦截什么,在什么地方拦截

  • 过滤器Filter,拦截请求
  • 拦截器Interceptor,拦截controller方法

那么mybatis框架的插件,通过@Interceptor注解,告诉插件需要在哪里拦截,比如案例代码

java 复制代码
@Intercepts(
        {
                @Signature(type=StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
                @Signature(type=StatementHandler.class, method = "update", args = {Statement.class}),
                @Signature(type=StatementHandler.class, method = "batch", args = {Statement.class})
        }
)

通过@Intercepts注解,@Signature注解,指明该插件需要拦截StatementHandler对象中的query、update、batch方法。

那么StatementHandler是什么呢?这就要回到mybatis框架的使用上来,在mybatis框架中,提供应用层编程接口SqlSession,通过SqlSession在应用层完成数据库的操作。

但是它的底层,是要委托给Executor组件来执行,Executor组件具体再委托三个组件

  • StatementHandler,负责执行sql语句,它就是Jdbc中Statement的封装
  • ParameterHandler,负责参数处理
  • ResultSetHandler,负责结果集处理,它就是Jdbc中ResultSet的封装

看一个图,你就明白了

到这里,你应该能明白插件类上注解声明的作用了。剩下

  • 方法plugin,用于创建目标对象,即StatementHandler的代理对象,mybatis框架插件还应用了动态代理
  • 方法Intercept,用于实现插件业务逻辑处理

2.3.2.关键源码

mybatis框架插件相关的关键源码,我们需要关注的有

下面我只贴与插件相关的关键源码,如果你感兴趣,可以自行翻阅完整源码,当然,我建议你这么去做,这是技术人应该要有的最纯粹的追求!

2.3.2.1.Interceptor
java 复制代码
public interface Interceptor {
    Object intercept(Invocation var1) throws Throwable;

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

    default void setProperties(Properties properties) {
    }
}
2.3.2.2.InterceptorChain
java 复制代码
// 插件集合列表
private final List<Interceptor> interceptors = new ArrayList();

// 责任链模式应用,依次应用每个插件
public Object pluginAll(Object target) {
        Interceptor interceptor;
        for(Iterator var2 = this.interceptors.iterator(); var2.hasNext(); target = interceptor.plugin(target)) {
            interceptor = (Interceptor)var2.next();
        }

        return target;
    }
2.3.2.3.Plugin
java 复制代码
// jdk动态代理InvocationHandler的实现
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);
         // jdk动态代理,创建目标对象的代理对象
        return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
    }
}
2.3.2.4.XMLConfigBuilder
java 复制代码
// 解析插件元素
private void pluginElement(XNode parent) throws Exception {
        if (parent != null) {
            Iterator var2 = parent.getChildren().iterator();

            while(var2.hasNext()) {
                XNode child = (XNode)var2.next();
                String interceptor = child.getStringAttribute("interceptor");
                Properties properties = child.getChildrenAsProperties();
                // 创建插件对象
                Interceptor interceptorInstance = (Interceptor)this.resolveClass(interceptor).getDeclaredConstructor().newInstance();
                interceptorInstance.setProperties(properties);
                // 将插件对象注册到Configuration中,底层注册到了InterceptorChain中
                this.configuration.addInterceptor(interceptorInstance);
            }
        }

    }
2.3.2.5.Configuration
java 复制代码
// 创建ParameterHandler
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
        parameterHandler = (ParameterHandler)this.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 = (ResultSetHandler)this.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 = (StatementHandler)this.interceptorChain.pluginAll(statementHandler);
        return statementHandler;
}
相关推荐
喵手7 分钟前
如何利用Java的Stream API提高代码的简洁度和效率?
java·后端·java ee
掘金码甲哥14 分钟前
全网最全的跨域资源共享CORS方案分析
后端
m0_4805026421 分钟前
Rust 入门 生命周期-next2 (十九)
开发语言·后端·rust
张醒言27 分钟前
Protocol Buffers 中 optional 关键字的发展史
后端·rpc·protobuf
鹿鹿的布丁44 分钟前
通过Lua脚本多个网关循环外呼
后端
墨子白44 分钟前
application.yml 文件必须配置哇
后端
xcya1 小时前
Java ReentrantLock 核心用法
后端
用户466537015051 小时前
如何在 IntelliJ IDEA 中可视化压缩提交到生产分支
后端·github
小楓12011 小时前
MySQL數據庫開發教學(一) 基本架構
数据库·后端·mysql
天天摸鱼的java工程师1 小时前
Java 解析 JSON 文件:八年老开发的实战总结(从业务到代码)
java·后端·面试