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
- Mybatis
- Spring
- SpringBoot
- SpringMVC
Program
Implement the Interceptor interface to implement your own business logic.
Total technical realization points
- Implement a custom interceptor
- Implement the loading of a custom interceptor
- Implement the injection of custom interceptors
Implement a custom interceptor
- The interceptor that implements Mybatis requires a custom class to implement the
Interceptor
interface. And implement theSqlLogInterceptor#intercept
method of the interface. - Add the annotation
Intercepts
implementation classSqlLogInterceptor
specify the location where the interceptor takes effect. Specify the signature of the class method through theSignature
Only methods that meet the signature will be intercepted and executed. - Now you want to block Sql monitor the execution time, you need to specify
Signature
the type toStatementHandler.class
, onlyStatementHandler.class
effect. The method isquery
andupdate
, which means that the query and update methods are valid. 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?
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。