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