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;
}
相关推荐
子燕若水8 分钟前
Flask 调试的时候进入main函数两次
后端·python·flask
程序员爱钓鱼10 分钟前
跳转语句:break、continue、goto -《Go语言实战指南》
开发语言·后端·golang·go1.19
Persistence___1 小时前
SpringBoot中的拦截器
java·spring boot·后端
嘵奇1 小时前
Spring Boot 跨域问题全解:原理、解决方案与最佳实践
java·spring boot·后端
堕落年代2 小时前
SpringBoot的单体和分布式的任务架构
spring boot·分布式·架构
码农飞哥2 小时前
互联网大厂Java求职面试实战:Spring Boot与微服务场景深度解析
java·数据库·spring boot·安全·微服务·消息队列·互联网医疗
意倾城3 小时前
浅说MyBatis-Plus 的 saveBatch 方法
java·mybatis
景天科技苑3 小时前
【Rust泛型】Rust泛型使用详解与应用场景
开发语言·后端·rust·泛型·rust泛型
I_itaiit3 小时前
Spring Boot之Web服务器的启动流程分析
spring boot·nettywebserver·httphandler·webhandler
老李不敲代码4 小时前
榕壹云搭子系统技术解析:基于Spring Boot+MySQL+UniApp的同城社交平台开发实践
spring boot·mysql·微信小程序·uni-app·软件需求