头图

background

When Mybatis executes SQL queries and updates, it cannot know the specific SQL execution time and whether there are problems such as slow queries. Need to be able to monitor Sql when executing Sql, and locate the location where the slow query problem occurs

environment

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

Program

Implement the Interceptor interface to implement your own business logic.

Total technical realization points

  1. Implement a custom interceptor
  2. Implement the loading of a custom interceptor
  3. Implement the injection of custom interceptors

Implement a custom interceptor

  1. The interceptor that implements Mybatis requires a custom class to implement the Interceptor interface. And implement the SqlLogInterceptor#intercept method of the interface.
  2. Add the annotation Intercepts implementation class SqlLogInterceptor specify the location where the interceptor takes effect. Specify the signature of the class method through the Signature Only methods that meet the signature will be intercepted and executed.
  3. Now you want to block Sql monitor the execution time, you need to specify Signature the type to StatementHandler.class , only StatementHandler.class effect. The method is query and update , which means that the query and update methods are valid.
  4. Component annotation on the interceptor so that it can be registered into the container by 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();
    }
}

Implement the loading of a custom interceptor

Now that the main logic of Mybatis's Sql interception monitoring has been realized. And we need to load the interceptor into Mybatis. But for applications that use SpringBoot, the application uses MybatisAutoConfiguration to initialize Mybatis, which uses ObjectProvider to provide entry for custom loading of the interceptor.
It does not support common configuration methods.

Implementation of ObjectProvider needs to be loaded by Spring as a Bean, and all Interceptor are injected into the custom ObjectProvider ( SqlLogInterceptorProvider ApplicationContext is injected during the instantiation process, through which the bean instance array of Interceptor


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;
    }
}

Through the above code, it can be realized that when the ObjectProvider mechanism gets the instance, all the Bean instances of the custom Interceptor are obtained.

Implement the injection of custom interceptors

Now that the logic of the above custom interceptor has been implemented, the loading mechanism of the custom interceptor has also been opened up. The rest is how to Interceptor instantiated 0611caffbc4e32 instance into Mybatis. The second step is to use the method provided by Mybatis-Springboot, using MybatisAutoConfiguration .

In MybatisAutoConfiguration , there is a parameter ObjectProvider<Interceptor[]> that is injected into the interceptor when MybatisAutoConfiguration instantiated through Spring's injection mechanism.

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();
    }
}

This configuration class not only injects custom interceptors. Such as custom typehandler, DatabaseId, etc. can be injected through the mechanism of ObjectProvider
Because it is the Configuration class, Bean will be automatically executed, so the initialization of SqlSessionTemplate the initialization of SqlSessionFactory

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();
    }
}

In the getObject method, the initialization of Mybatis will be executed, and finally the SqlSessionFactory instance will be generated

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();
    }
}

Mainly in the buildSqlSessionFactory method, the interceptor of Interceptor sqlSessionFactory , and the Configuration.newStatementHandler will implement the interception of the execution of Sql through the interceptor proxy target method.

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;
    }
}

Because StatementHandler is stateful, each call of Mapper different, and the parameters are also different. Then each call needs to generate a new StatementHandler object, and there may be multiple interceptors to implement multiple proxy for this object.
The question here is that generates a new multi-level proxy object of StatementHandler every time. Can performance be guaranteed?


疯狂小兵
193 声望9 粉丝

专注做后端,用java和go做工具,编写世界