Spring AOP 和 MyBatis插件 优化指数统计,代替MySQL触发器

时间:2024-11-08 20:52:04

Spring AOP 和 MyBatis插件 优化指数统计,代替MySQL触发器

  • 操作原理:在Spring AOP的环绕通知中调用MyBatis插件获取到具体的SQL和参数,在执行SQL之后,利用这些参数来新增“统计指数”。

    以下是具体实现步骤和示例代码:

实现步骤

  1. 定义MyBatis拦截器:首先使用MyBatis插件来拦截SQL的执行,获取到SQL语句和参数。
  2. 定义Spring AOP的环绕通知:在AOP切面中,使用环绕通知拦截特定的Mapper方法。
  3. 在环绕通知中调用MyBatis拦截器:在SQL执行前通过MyBatis拦截器获取SQL和参数,在SQL执行成功后执行额外的业务逻辑。

实现代码

1. 定义MyBatis拦截器获取SQL和参数

首先定义一个MyBatis拦截器,拦截并缓存最近执行的SQL和参数,这样可以在AOP通知中访问这些信息。

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.RowBounds;
import java.sql.Connection;
import java.util.Properties;

@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SqlInterceptor implements Interceptor {
    
    // 定义一个线程安全的变量用于存储最近的SQL和参数
    private static final ThreadLocal<String> currentSql = new ThreadLocal<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取StatementHandler并解析SQL语句
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        String sql = statementHandler.getBoundSql().getSql();
        
        // 缓存SQL
        currentSql.set(sql);

        // 执行原方法
        return invocation.proceed();
    }

    public static String getCurrentSql() {
        return currentSql.get();
    }
    
    // 移除数据,防止内存溢出
    public static void removeSql() {
        currentSql.remove();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {}
}

2. 配置拦截器

将拦截器添加到MyBatis配置中,使其生效。

<plugins>
    <plugin interceptor="com.example.interceptor.SqlInterceptor"/>
</plugins>

3. 定义Spring AOP的环绕通知

在Spring AOP切面中,使用环绕通知来拦截Mapper方法,获取SQL语句并在SQL执行后调用额外的业务逻辑。

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class SqlExecutionAopInterceptor {

    @Autowired
    private SomeOtherService someOtherService; // 依赖注入业务服务

    // 环绕通知拦截特定的Mapper方法,比如吊销铅笔
    @Around("execution(* com.example.mapper.SteelMapper.revokeSteel(..))")
    public Object aroundExecution(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result;

        try {
            System.out.println("开始执行SQL操作...");
            
            // 在SQL执行之前,通过拦截器获取SQL语句
            String sql = SqlInterceptor.getCurrentSql();
            SqlInterceptor.removeSql();
            System.out.println("即将执行的SQL语句:" + sql);

            // 执行Mapper方法,获取SQL执行结果
            result = joinPoint.proceed();

            // 检查SQL执行结果,执行后续业务逻辑
            if (result != null && result instanceof Integer && (Integer) result > 0) {
                System.out.println("SQL执行成功,执行额外操作...");
                
                // 调用业务方法,传入所需的SQL参数(根据需求从方法参数中获取)
                someOtherService.addRevokeCount(sql);
            } else {
                System.out.println("SQL未成功执行,未执行额外操作");
            }

        } catch (Exception e) {
            System.out.println("SQL执行过程中出现异常:" + e.getMessage());
            throw e; // 抛出异常,确保事务一致性
        }

        return result;
    }
}

4. 定义业务服务类并实现“新增统计指数”

在业务服务SomeOtherService中,实现“新增统计指数”的逻辑。你可以利用SQL中包含的参数来执行额外的更新逻辑。

import org.springframework.stereotype.Service;

@Service
public class SomeOtherService {

    public void addRevokeCount(String sql) {
        // 解析SQL获取必要的参数(根据实际业务情况解析SQL中的参数)
        // 例如提取铅笔ID等数据
        System.out.println("新增统计指数,SQL信息:" + sql);
        
        // 在这里调用Mapper方法,执行更新操作
    }
}

总结

  • MyBatis拦截器用于捕获即将执行的SQL,将SQL语句暂存至ThreadLocal中。
  • 环绕通知在SQL执行前后分别获取SQL和执行结果,确保在SQL执行成功后执行附加业务逻辑。
  • 业务方法调用中可以使用SQL语句的内容来执行特定的操作。

这样可以确保每次SQL执行前后在Spring AOP和MyBatis拦截器的配合下获取并使用SQL语句,完成业务需求。

SqlInterceptor 执行 invocation.proceed();然后 SqlExecutionAopInterceptor 又执行joinPoint.proceed(),会冲突吗,也就是导致SQL反复执行吗?

在这个实现方案中,SqlInterceptorSqlExecutionAopInterceptorproceed() 方法的调用不会导致 SQL 反复执行,因为它们在不同的层次和时间点生效,并且具有不同的职责。

具体说明

  1. SqlInterceptor(MyBatis 拦截器)

    • 它是 MyBatis 插件,拦截 MyBatis 在生成并准备执行 SQL 语句时的行为。
    • 调用 invocation.proceed() 是为了让 MyBatis 继续执行原本的逻辑,否则拦截器会阻止原方法的执行,导致 SQL 无法被正常执行。
    • 此拦截器仅用于捕获 SQL 语句及其参数,不会实际改变 SQL 执行流程或执行 SQL。
  2. SqlExecutionAopInterceptor(Spring AOP 拦截器)

    • 它是 Spring AOP 切面,用于拦截特定的 Java 方法调用,例如 DAO 层的方法。
    • 调用 joinPoint.proceed() 是为了继续执行被拦截的方法,如果不调用 proceed(),原方法逻辑将不会被执行。
    • 它负责在方法执行前后添加逻辑,比如在执行后根据执行结果进行附加操作。

是否会导致 SQL 反复执行?

不会。SqlInterceptorSqlExecutionAopInterceptor 都是拦截器,但它们作用在不同层次上:

  • SqlInterceptor 仅拦截 MyBatis 内部的 SQL 执行操作,不涉及具体的 Java 方法调用。
  • SqlExecutionAopInterceptor 仅拦截由 Spring 管理的 DAO 层方法调用,与 MyBatis 拦截器是独立的,执行 joinPoint.proceed() 是为了确保 DAO 方法继续执行。

两者结合使用时:

  • SqlExecutionAopInterceptor 拦截方法时会调用 joinPoint.proceed() 执行 DAO 方法,这会触发 MyBatis 执行 SQL。
  • SqlInterceptor 在 MyBatis 执行 SQL 时捕获 SQL 语句并保存。
  • 最终,SQL 只会被执行一次,不会重复执行。

总结

SqlInterceptorSqlExecutionAopInterceptorproceed() 方法调用不会冲突,也不会导致 SQL 反复执行,因为它们在不同的层次和作用域中运行。如果实现中没有其他逻辑错误,整个执行链条会按照预期进行。