背景

Mybatis在执行Sql查问和更新时,无奈晓得具体的sql执行工夫,是否存在慢查问等问题。须要在执行Sql时能对Sql进行监控,并定位到慢查问的问题产生的地位

环境

  1. Mybatis
  2. Spring
  3. SpringBoot
  4. SpringMVC

计划

实现Interceptor接口来实现本人的业务逻辑。

技术实现总共分

  1. 实现自定义的拦截器
  2. 实现自定义拦截器的加载
  3. 实现自定义拦截器的注入

实现自定义的拦截器

  1. 实现Mybatis的拦截器须要自定义类实现Interceptor接口。并实现接口的SqlLogInterceptor#intercept办法。
  2. 在实现类SqlLogInterceptor减少注解Intercepts,指定该拦截器失效的地位。通过Signature注解指定类办法的签名。合乎签名的办法才会被拦挡执行。
  3. 当初要拦挡Sql监控执行工夫,则须要指定Signature的type为StatementHandler.class,仅对StatementHandler.class失效。method为queryupdate,示意对query和update办法都失效。
  4. 在拦截器上标注Component注解,使其能够被Spring注册进容器中。
import com.alibaba.fastjson.JSON;import org.apache.ibatis.executor.statement.StatementHandler;import org.apache.ibatis.mapping.MappedStatement;import org.apache.ibatis.plugin.Interceptor;import org.apache.ibatis.plugin.Intercepts;import org.apache.ibatis.plugin.Invocation;import org.apache.ibatis.plugin.Signature;import org.apache.ibatis.session.ResultHandler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;import org.springframework.util.ReflectionUtils;import java.lang.reflect.Field;import java.sql.Statement;import java.util.Map;import java.util.Optional;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.TimeUnit;/** * @author followtry * @since 2021/8/12 10:42 上午 *///增加Spring的注解,容许加载为Spring的@Component@Intercepts(        value = {                @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),                @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),        })public class SqlLogInterceptor implements Interceptor {    private static final Logger logger = LoggerFactory.getLogger(SqlLogInterceptor.class);    public static final Long SLOW_SQL = TimeUnit.MILLISECONDS.toMillis(100);    private static Map<String,String> sqlSignMap = new ConcurrentHashMap<>();    @Override    public Object intercept(Invocation invocation) throws Throwable {        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();        Object parameterObject = statementHandler.getBoundSql().getParameterObject();        String sql = statementHandler.getBoundSql().getSql();        //将所有的换行都        sql = sql.replaceAll("\n", " ");        String param = JSON.toJSONString(parameterObject);        long startTime = System.currentTimeMillis();        long endTime = System.currentTimeMillis();        boolean resSuc = true;        Object proceed;        try {            proceed = invocation.proceed();            endTime = System.currentTimeMillis();        } catch (Exception e) {            resSuc = false;            endTime = System.currentTimeMillis();            throw e;        } finally {            long cost = endTime - startTime;            boolean isSlowSql = false;            String signature = null;            if (SLOW_SQL < cost) {                isSlowSql = true;                signature = genSqlSignature(invocation, sql);            }            LogUtils.logSql(sql, param, cost, resSuc, isSlowSql, signature);        }        return proceed;    }    private String genSqlSignature(Invocation invocation, String sql) {        Optional<String> signatureOpt = Optional.ofNullable(sqlSignMap.get(sql));        if (!signatureOpt.isPresent()) {            try {                StatementHandler statementHandler = (StatementHandler) invocation.getTarget();                Field delegate = statementHandler.getClass().getDeclaredField("delegate");                ReflectionUtils.makeAccessible(delegate);                StatementHandler statementHandlerV2 = (StatementHandler) delegate.get(statementHandler);                Field mappedStatementField = statementHandlerV2.getClass().getSuperclass().getDeclaredField("mappedStatement");                ReflectionUtils.makeAccessible(mappedStatementField);                MappedStatement mappedStatement = (MappedStatement) mappedStatementField.get(statementHandlerV2);                sqlSignMap.put(sql,mappedStatement.getId());                return mappedStatement.getId();            } catch (NoSuchFieldException | IllegalAccessException e) {                //ignore                return null;            }        }        return signatureOpt.get();    }}

实现自定义拦截器的加载

既然曾经实现了Mybatis的Sql拦挡监控的次要逻辑。而咱们要将该拦截器加载进Mybatis。然而对于应用SpringBoot的利用来说,利用应用MybatisAutoConfiguration来初始化Mybatis,其应用ObjectProvider为拦截器的自定义加载提供入口,
而不反对常见的配置的形式。

实现ObjectProvider须要被Spring加载为Bean,并将所有的Interceptor的bean实例都注入到自定义的ObjectProvider(SqlLogInterceptorProvider)中,并在实例化过程中注入ApplicationContext,通过其能够获取到Interceptor的bean实例数组。

import com.alibaba.fastjson.JSON;import org.apache.ibatis.plugin.Interceptor;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.BeansException;import org.springframework.beans.factory.ObjectProvider;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationContextAware;import org.springframework.stereotype.Component;import java.util.Map;/** * @author followtry * @since 2021/8/12 11:17 上午 */@Componentpublic class SqlLogInterceptorProvider implements ObjectProvider<Interceptor[]>, ApplicationContextAware {    private static final Logger log = LoggerFactory.getLogger(SqlLogInterceptorProvider.class);    private Interceptor[] interceptors;    private ApplicationContext applicationContext;    @Override    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {        this.applicationContext = applicationContext;        setInterceptors();    }    private void setInterceptors() {        Map<String, Interceptor> beansOfType = this.applicationContext.getBeansOfType(Interceptor.class);        this.interceptors = beansOfType.values().toArray(new Interceptor[0]);        log.info("inject interceptors, {}", JSON.toJSONString(interceptors));    }    @Override    public Interceptor[] getObject() throws BeansException {        return this.interceptors;    }    @Override    public Interceptor[] getObject(Object... args) throws BeansException {        return this.interceptors;    }    @Override    public Interceptor[] getIfAvailable() throws BeansException {        return this.interceptors;    }    @Override    public Interceptor[] getIfUnique() throws BeansException {        return this.interceptors;    }}

通过如上的代码,就能够实现ObjectProvider机制获取实例时获取到的都是自定义的Interceptor的Bean实例。

实现自定义拦截器的注入

既然如上的自定义拦截器的逻辑曾经实现,自定义拦截器的加载机制也曾经买通。剩下的就是怎么将曾经实例化后的Interceptor实例注入到Mybatis中了。次步骤是应用的Mybatis-Springboot提供的形式,应用MybatisAutoConfiguration

MybatisAutoConfiguration的构造方法中,有个参数ObjectProvider<Interceptor[]> 就是通过Spring的注入机制在MybatisAutoConfiguration实例化时将拦截器注入进去的。

public class MybatisAutoConfiguration implements InitializingBean {    public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider,                                    ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider,                                    ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider,                                    ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {        this.properties = properties;        //此处将自定义的拦截器注入进配置类中        this.interceptors = interceptorsProvider.getIfAvailable();        this.typeHandlers = typeHandlersProvider.getIfAvailable();        this.languageDrivers = languageDriversProvider.getIfAvailable();        this.resourceLoader = resourceLoader;        this.databaseIdProvider = databaseIdProvider.getIfAvailable();        this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();    }}

该配置类不仅只注入自定义的拦截器。如自定义的typehandler,DatabaseId等都能够通过ObjectProvider的机制注入进来。
因为其为Configuration类,对于其中的Bean注解的办法都会主动执行,所以会持续实现SqlSessionFactory的初始化和SqlSessionTemplate的初始化。

public class MybatisAutoConfiguration implements InitializingBean {    @Bean    @ConditionalOnMissingBean    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {        //SqlSessionFactoryBean为实现了FactoryBean接口的类,也是用的Spring的机制,通过调用其getObject办法获取SqlSessionFactory实例        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();        factory.setDataSource(dataSource);        factory.setVfs(SpringBootVFS.class);        if (StringUtils.hasText(this.properties.getConfigLocation())) {            factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));        }        applyConfiguration(factory);        if (this.properties.getConfigurationProperties() != null) {            factory.setConfigurationProperties(this.properties.getConfigurationProperties());        }        //此处判断interceptors不为空则将其作为插件参数设置进去,此处只是设置参数,还未执行解析等动作        if (!ObjectUtils.isEmpty(this.interceptors)) {            factory.setPlugins(this.interceptors);        }        if (this.databaseIdProvider != null) {            factory.setDatabaseIdProvider(this.databaseIdProvider);        }        //设置类型的别名的包,该包门路下的类都会设置别名        if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {            factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());        }        if (this.properties.getTypeAliasesSuperType() != null) {            factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());        }        //设置TypeHandler所在的包门路        if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {            factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());        }        //设置通过ObjectProvider形式注入进来的TypeHandler        if (!ObjectUtils.isEmpty(this.typeHandlers)) {            factory.setTypeHandlers(this.typeHandlers);        }        //设置mapper的映射地址        if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {            factory.setMapperLocations(this.properties.resolveMapperLocations());        }        Set<String> factoryPropertyNames = Stream                .of(new BeanWrapperImpl(SqlSessionFactoryBean.class).getPropertyDescriptors()).map(PropertyDescriptor::getName)                .collect(Collectors.toSet());        Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();        if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {            // Need to mybatis-spring 2.0.2+            factory.setScriptingLanguageDrivers(this.languageDrivers);            if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {                defaultLanguageDriver = this.languageDrivers[0].getClass();            }        }        if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {            // Need to mybatis-spring 2.0.2+            factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);        }        //该步骤是应用的FactoryBean机制,通过getObject获取具体对象的实例。        return factory.getObject();    }}

getObject办法时,会执行Mybatis的初始化,并最终生成SqlSessionFactory实例

public class SqlSessionFactoryBean        implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {    public SqlSessionFactory getObject() throws Exception {        if (this.sqlSessionFactory == null) {            afterPropertiesSet();        }        return this.sqlSessionFactory;    }    public void afterPropertiesSet() throws Exception {        notNull(dataSource, "Property 'dataSource' is required");        notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");        state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),                "Property 'configuration' and 'configLocation' can not specified with together");        //具体执行SqlSessionFactory实例化的中央        this.sqlSessionFactory = buildSqlSessionFactory();    }}

次要在buildSqlSessionFactory办法中将Interceptor的拦截器注入进sqlSessionFactory,在Configuration.newStatementHandler办法会通过拦截器代理指标办法实现对执行Sql的拦挡。

public class Configuration {    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;    }}

因为StatementHandler的各个子对象都是有状态的,每次调用的Mapper不同,参数也不同。那每次调用都须要新生成StatementHandler对象,而对于该对象又可能存在多个拦截器实现屡次代理。
此处的疑难就是每次都新生成StatementHandler的多级代理对象,性能上能有保障吗