关于java:Mybatis技术内幕Mybatis的日志拦截器及原理

39次阅读

共计 10623 个字符,预计需要花费 27 分钟才能阅读完成。

背景

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 上午
 */
@Component
public 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 的多级代理对象,性能上能有保障吗

正文完
 0