基于MyBatis插件开发SQL日志记录功能_mybatis sql log_小飞飞的技术笔记的博客-CSDN博客

MyBatis logo

目录

一、MyBatis插件介绍

二、MyBatis插件原理

三、基于SpringBoot自动装配实现可插拔日志拦截器

3.1 SpringBoot自动装配原理

3.2 Loghelper日志拦截器实现

3.2.1 插件的实现

3.2.2 可扩展服务

3.2.3 自动装配

    • *

在新项目中有个需求,需要记录下增删改的SQL并落表。一般考虑到的是需要利用Spring的AOP,抽取日志记录的功能,在持久层操作的时候进行统一增强。项目用到的持久层框架是MyBatis,那么直接想到的就是利用MyBatis的插件机制对执行的SQL进行记录。那么具体如何实现呢?原理又是什么?读了这篇文章的小伙伴就可以搞清楚啦。

工程源码在最后~

一、MyBatis插件介绍

MyBatis大家都比较熟悉,实际使用也非常广泛,其内部提供了插件扩展机制来拦截SQL的执行。

其执行原理如下图所示:

mybatis.png

SQL执行依赖于sqlSession,是MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能,sqlSession又依赖Executor执行器,执行器负责SQL语句的生成和查询缓存的维护,既然是封装了JDBC,SQL构建也是必不可少的,MyBatis封装了StatementHandler,对于参数处理和结果集处理用到了ParameterHandlerResultSetHandler这两个处理器。对于这四大组件的说明如下:

组件

说明

Executor

MyBatis执行器,是MyBatis调度的核心,负责SQL语句的生成和查询缓存的维护

StatementHandler

封装了JDBC Statement操作,负责对JDBC statement的操作,如设置参 数、将Statement结果集转换成List集合。

ParameterHandler

负责对用户传递的参数转换成JDBC Statement所需要的参数

ResultSetHandler

负责将JDBC返回的ResultSet结果集对象转换成List类型的集合

当然MyBatis的核心组件不只有这些,还包括TypeHandler(java和mysql类型转换)等,但是我们的插件主要拦截的是这四大组件。对MyBatis来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的动态代理实现的,换句话说,MyBatis中的四大对象都是代理对象。

MyBatis所允许拦截的方法如下:

  1. 执行器Executor (update、query、commit、rollback等方法);
  2. SQL语法构建器StatementHandler (prepare、parameterize、batch、updates query等方 法);
  3. 参数处理器ParameterHandler (getParameterObject、setParameters方法);
  4. 结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等方法); 

二、MyBatis插件原理

在四大对象创建的时候

  • 1、每个创建出来的对象不是直接返回的,而是interceptorChain.pluginAll(parameterHandler);
  • 2、获取到所有的Interceptor (拦截器)(插件需要实现的接口);调用 interceptor.plugin(target);返回target包装后的对象
  • 3、插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP (面向切面)我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个方法

 以_ParameterHandler_为例,我们看下源码分析一下:

public ParameterHandler newParameterHandler(MappedStatement mappedStatement,     Object object, BoundSql sql, InterceptorChain interceptorChain) {    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);    parameterHandler = (ParameterHandler)    interceptorChain.pluginAll(parameterHandler);    return parameterHandler;} public Object pluginAll(Object target) {    for (Interceptor interceptor : interceptors) {        target = interceptor.plugin(target);    }    return target;}

interceptorChain保存了所有的拦截器(interceptors,Interceptor接口的所有实现类),是MyBatis初始化的时候创建的。调用拦截器链中的拦截器依次的对目标进行拦截或增强。interceptor.plugin(target)中的target就可以理解为MyBatis中的四大对象。返回的target是被层层代理后的对象。

三、基于SpringBoot自动装配实现可插拔日志拦截器

3.1 SpringBoot自动装配原理

我们知道一个SpringBoot应用必须要标注@SpringBootApplication注解,而这个注解包含了@EnableAutoConfiguration从而启用自动装配。

@Target({ElementType.TYPE}) //注解的适用范围,Type表示注解可以描述在类、接口、注解或枚举中@Retention(RetentionPolicy.RUNTIME) //表示注解的生命周期,Runtime运行时@Documented //表示注解可以记录在javadoc中@Inherited //表示可以被子类继承该注解@SpringBootConfiguration // 标明该类为配置类@EnableAutoConfiguration // 启动自动配置功能@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes =AutoConfigurationExcludeFilter.class) })public @interface SpringBootApplication {    // 根据class来排除特定的类,使其不能加入spring容器,传入参数value类型是class类型。    @AliasFor(annotation = EnableAutoConfiguration.class)    Class<?>[] exclude() default {};    // 根据classname 来排除特定的类,使其不能加入spring容器,传入参数value类型是class的全类名字符串数组。    @AliasFor(annotation = EnableAutoConfiguration.class)    String[] excludeName() default {};    // 指定扫描包,参数是包名的字符串数组。    @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")    String[] scanBasePackages() default {};    // 扫描特定的包,参数类似是Class类型数组。    @AliasFor(annotation = ComponentScan.class, attribute =    "basePackageClasses")    Class<?>[] scanBasePackageClasses() default {};}

@EnableAutoConfiguration这个注解又把AutoConfigurationImportSelector这个类引入到了Spring容器中。

@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import({AutoConfigurationImportSelector.class})public @interface EnableAutoConfiguration {    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";     Class<?>[] exclude() default {};     String[] excludeName() default {};}

AutoConfigurationImportSelector会遍历整个ClassLoader中所有jar包下的_META/INF/spring.factories_文件。 spring.factories里面保存着springboot的默认提供的自动配置类。

# Initializersorg.springframework.context.ApplicationContextInitializer=\org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener # Application Listenersorg.springframework.context.ApplicationListener=\org.springframework.boot.autoconfigure.BackgroundPreinitializer # Auto Configuration Import Listenersorg.springframework.boot.autoconfigure.AutoConfigurationImportListener=\org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener # Auto Configuration Import Filtersorg.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\org.springframework.boot.autoconfigure.condition.OnBeanCondition,\org.springframework.boot.autoconfigure.condition.OnClassCondition,\org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition # Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\

 SpringApplication.run()被调用的时候就会读取到主类上的注解,从而加载自动配置类。

这属于SPI机制的实现,SPI全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF文件夹查找文件,自动加载文件里所定义的类。除了SpringBoot,在很多框架中都有使用,比如Tomcat、Dubbo(按需加载)等。

3.2 Loghelper日志拦截器实现

我们了解了MyBatis插件原理和SpringBoot的自动装配原理之后,接下来的操作就很简单了。想要记录SQL只需要拦截Executor执行器就可以了,再结合SpringBoot的自动装配实现可插拔。

至于项目所需的依赖就不多赘述,把这个功能单独抽取为一个模块,在需要的时候通过pom引入即可。

3.2.1 插件的实现

我们要拦截SQL只需要在Executor执行器运行的时候进行拦截即可。其中可以拦截的包括update、query、commit、rollback等方法,对于增删改的情况只需要拦截update()方法。拦截器需要实现Interceptor接口,具体实现如下:

/** * SQL日志拦截器 * @Author asong * @Date 2021/8/25 */@Intercepts({ @Signature(type = Executor.class,        method = "update",        args = { MappedStatement.class, Object.class }) })public class SqloperationInterceptor implements Interceptor {     @Override    public Object intercept(Invocation invocation) throws Throwable {        logger.debug(" 用户操作记录开始...");        // 获取拦截参数        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];        BoundSql boundSql = mappedStatement.getBoundSql(invocation.getArgs()[1]);        Configuration configuration = mappedStatement.getConfiguration(); ...

在这里使用@Intercepts注解指明要拦截的Executor组件,指明拦截的update()方法,在intercept()方法中可以获取到MappedStatement 相关的参数,封装了增删改查等SQL内容。至于SQL的解析过程这里就不多说了,涉及到MyBatis对SQL的封装和字符串的处理。

3.2.2 可扩展服务

日志记录这种功能是通用的,我们除了单独抽取成独立模块之外,对于记录的日志信息和记录的操作也应该抽取出来方便后期的扩展。

日志信息包括用户的信息及SQL的信息。用户的信息包括用户ID(唯一)、用户IP地址,SQL的信息包括SQL语句、操作类型(增删改)、操作的表。

CREATE TABLE `sql_operation_log` (  `log_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',  `user_id` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户唯一标识',  `ip_addr` varchar(30) COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'ip地址',  `log_type` tinyint(1) DEFAULT NULL COMMENT '日志类型  0 Insert 1 Update 2 Delete',  `sql_log` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'SQL',  `table_name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '表名',  `oper_time` datetime DEFAULT NULL COMMENT '操作时间',  PRIMARY KEY (`log_id`)) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

 所以在拦截的时候首先获取用户的信息,然后封装Log,最后调用日志服务进行保存。

...SqlOperationLog sqlLog = new SqlOperationLog();// 获取当前用户OperUser operUser = operUserService.getOperUser();sqlLog.setUserId(operUser.getUserId());sqlLog.setIpAddr(operUser.getIpAddr());...if(isRecorded(tableName)) {     logger.info("  插入SQL日志 ---> {}", sqlLog);      // 调用日志服务     logService.save(sqlLog);}

对于用户服务(OperUserService)日志服务(LogService)抽取为接口,框架提供默认实现,需要后期开发者可以自行实现。用户服务即获取用户相关信息,日志服务即如何保存Log日志(默认写入MySQL,如果写MQ则需要开发者实现接口)。

/** * 用户服务接口,需要自行实现 * @Author asong * @Date 2021/9/14 */public interface OperUserService {     OperUser getOperUser(); }
/** * 日志服务接口 * 用于查询和保存SQL操作日志 * 默认使用MySQL * * @Author asong * @Date 2021/9/14 */public interface LogService {     void save(SqlOperationLog operationLog);     List findByCondition(SqlOperationLog operationLog);}

3.2.3 自动装配

我们想使用SpringBoot去自动装配的话就需要在 META/INF/spring.factories 这个文件写入自动配置类。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.csdn.loghelper.config.LoghelperAutoConfiguration

 LoghelperAutoConfiguration自动配置的目的也很简单,注入所需要的Bean即可。从开发者角度讲,这个装配是可以通过注解或者yml配置进行选择启用的,所以这里我们再引入一个注解:@EnableLoghelper。使用时只需要在启动类加上这个注解就可以了。

那么框架我们应该如何设计呢?

先创建注解,这个注解的作用就是引入LoghelperMarker作为开启的标志。

/** * 标记该注解则启用 * @Author asong * @Date 2021/9/14 */@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)// 引入标记类@Import({LoghelperMarker.class})@ComponentScan("com.csdn.loghelper")public @interface EnableLoghelper {}

之后是我们的自动装配类。只有在使用了@EnableLoghelper注解之后才会引入LoghelperMarker这个标记类,从而使拦截器生效。

/** * 自动装配类 * @Author asong * @Date 2021/9/14 */@Configuration// 根据条件配置 引入LoghelperMarker则生效@ConditionalOnBean(LoghelperMarker.class)public class LoghelperAutoConfiguration {     @Autowired    private DataSource dataSource;     @Autowired    private OperUserService operUserService;     @Autowired    private LogService logService;     @Autowired    private LoghelperConfig loghelperConfig;     @Bean    public DefaultOperUserServiceImpl operUserService() {        return new DefaultOperUserServiceImpl();    }     @Bean    public DefaultLogServiceImpl logService() {        return new DefaultLogServiceImpl(dataSource);    }     @Bean    public LoghelperConfig loghelperConfig() {        return new LoghelperConfig();    }     @Bean    public SqloperationInterceptor sqloperationInterceptor() {        return new SqloperationInterceptor(operUserService, logService, loghelperConfig);    }}

 至此,整个SQL日志记录的功能已经完成并且实现了可插拔。在使用的时候引入pom坐标和@EnableLoghelper注解即可开启。对于日志的记录,上层开发者根据业务需要可以实现接口并注入。(自行开发需要加@Primary注解,替换默认Bean)

工程源码已经提交在码云:点此查看


原网址: 访问
创建于: 2023-06-14 18:10:46
目录: default
标签: 无

请先后发表评论
  • 最新评论
  • 总共0条评论