背景
Mybatis在执行Sql查问和更新时,无奈晓得具体的sql执行工夫,是否存在慢查问等问题。须要在执行Sql时能对Sql进行监控,并定位到慢查问的问题产生的地位
环境
- Mybatis
- Spring
- SpringBoot
- SpringMVC
计划
实现Interceptor接口来实现本人的业务逻辑。
技术实现总共分
- 实现自定义的拦截器
- 实现自定义拦截器的加载
- 实现自定义拦截器的注入
实现自定义的拦截器
- 实现Mybatis的拦截器须要自定义类实现
Interceptor
接口。并实现接口的SqlLogInterceptor#intercept
办法。 - 在实现类
SqlLogInterceptor
减少注解Intercepts
,指定该拦截器失效的地位。通过Signature
注解指定类办法的签名。合乎签名的办法才会被拦挡执行。 - 当初要拦挡Sql监控执行工夫,则须要指定
Signature
的type为StatementHandler.class
,仅对StatementHandler.class
失效。method为query
和update
,示意对query和update办法都失效。 - 在拦截器上标注
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的多级代理对象,性能上能有保障吗