目录
在新项目中有个需求,需要记录下增删改的SQL并落表。一般考虑到的是需要利用Spring的AOP,抽取日志记录的功能,在持久层操作的时候进行统一增强。项目用到的持久层框架是MyBatis,那么直接想到的就是利用MyBatis的插件机制对执行的SQL进行记录。那么具体如何实现呢?原理又是什么?读了这篇文章的小伙伴就可以搞清楚啦。
工程源码在最后~
MyBatis大家都比较熟悉,实际使用也非常广泛,其内部提供了插件扩展机制来拦截SQL的执行。
其执行原理如下图所示:
SQL执行依赖于sqlSession,是MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能,sqlSession又依赖Executor执行器,执行器负责SQL语句的生成和查询缓存的维护,既然是封装了JDBC,SQL构建也是必不可少的,MyBatis封装了StatementHandler,对于参数处理和结果集处理用到了ParameterHandler和ResultSetHandler这两个处理器。对于这四大组件的说明如下:
组件
说明
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所允许拦截的方法如下:
- 执行器Executor (update、query、commit、rollback等方法);
- SQL语法构建器StatementHandler (prepare、parameterize、batch、updates query等方 法);
- 参数处理器ParameterHandler (getParameterObject、setParameters方法);
- 结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等方法);
在四大对象创建的时候
以_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应用必须要标注@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(按需加载)等。
我们了解了MyBatis插件原理和SpringBoot的自动装配原理之后,接下来的操作就很简单了。想要记录SQL只需要拦截Executor执行器就可以了,再结合SpringBoot的自动装配实现可插拔。
至于项目所需的依赖就不多赘述,把这个功能单独抽取为一个模块,在需要的时候通过pom引入即可。
我们要拦截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的封装和字符串的处理。
日志记录这种功能是通用的,我们除了单独抽取成独立模块之外,对于记录的日志信息和记录的操作也应该抽取出来方便后期的扩展。
日志信息包括用户的信息及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);}
我们想使用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
标签: 无
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
最新评论