沃梦达 / IT编程 / 数据库 / 正文

使用SpringAop动态获取mapper执行的SQL,并保存SQL到Log表中

使用Spring AOP动态获取mapper执行的SQL并保存到Log表中,可以方便我们在程序调试和优化时快速定位问题,本攻略分为以下步骤:

使用Spring AOP动态获取mapper执行的SQL并保存到Log表中,可以方便我们在程序调试和优化时快速定位问题,本攻略分为以下步骤:

步骤一:添加依赖

首先,在项目的pom.xml中添加以下依赖:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.2</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.2</version>
</dependency>

这里我们使用了AspectJ框架动态代理实现了AOP功能。

步骤二:定义日志实体类

在log表中要保存的信息比如SQL语句、方法名、执行时间等可以定义为一个Log实体类。如下:

public class Log {
    //id
    private Long id;
    //操作时间
    private Date operTime;
    //操作人
    private String operUser;
    //执行的方法名
    private String methodName;
    //执行的SQL语句
    private String sql;
    //执行时间
    private Long time;

    // 省略 getter 和 setter
}

步骤三:定义AOP切面

AOP切面主要是通过@Aspect注解来标记切面类,并且在类中定义对mapper接口中的每一个方法进行拦截的Advice。

@Component
@Aspect
public class LogAspect {
    @Autowired
    private LogDao logDao;

    /**
     * 切入点
     */
    @Pointcut("execution(* com.example.mapper.*.*(..))")
    public void logPointCut() {
    }

    /**
     * 前置通知
     *
     * @param joinPoint 切入点
     */
    @Before("logPointCut()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        // 获取方法名
        String methodName=joinPoint.getSignature().getName();
        // 获取目标类名
        String targetName=joinPoint.getTarget().getClass().getSimpleName();

        // 获取目标方法参数
        Object[] params=joinPoint.getArgs();

        // 反射获取mapper接口对应的MapperProxy类对象
        MapperProxy<?> mapperProxy=(MapperProxy<?>) ((DefaultSqlSession) sqlSession).getMapperProxy();

        // 反射获取MapperProxy类的h字段,即使用了JDK动态代理的接口实现对象
        Field hField=Proxy.class.getDeclaredField("h");
        hField.setAccessible(true);
        Object obj=hField.get(mapperProxy);

        // 反射获取委托类对象
        Field mapperInterfaceField=obj.getClass().getDeclaredField("mapperInterface");
        mapperInterfaceField.setAccessible(true);
        Class<?> mapperInterface=(Class<?>) mapperInterfaceField.get(obj);
        String mapperClassName=mapperInterface.getName();

        Method mapperMethod=getMapperMethod(mapperInterface, methodName,params);// 利用反射获取Mapper接口中的方法

        // 获取操作的SQL语句
        MappedStatement mappedStatement=getMappedStatement(sqlSession, mapperInterface, mapperMethod);
        Object parameterObject=getParameterObject(mappedStatement, params);
        BoundSql boundSql=mappedStatement.getBoundSql(parameterObject);
        String sql=boundSql.getSql();

        String operUser="";//这里可以根据实际情况获取操作人
        Log log=new Log();
        log.setOperTime(new Date());
        log.setOperUser(operUser);
        log.setMethodName(mapperClassName + "." + methodName);
        log.setSql(sql);
        log.setTime(0L);//这里可以记录查询时间

        logDao.save(log);// 保存日志到log表
    }

    private Method getMapperMethod(Class<?> mapperInterface, String methodName,Object[] params) {
        Method method=null;
        for (Method m : mapperInterface.getMethods()) {
            // 找到方法名和参数个数都匹配的方法
            if (m.getName().equals(methodName)&& m.getParameterCount() == params.length) {
                method=m;
                break;
            }
        }
        return method;
    }

    /**
     * 获取MappedStatement对象
     *
     * @param sqlSession
     * @param mapperInterface
     * @param mapperMethod
     * @return
     */
    private MappedStatement getMappedStatement(SqlSession sqlSession,Class<?> mapperInterface, Method mapperMethod) {
        String id=mapperInterface.getName() + "." + mapperMethod.getName();
        Configuration configuration=sqlSession.getConfiguration();
        MappedStatement mappedStatement=configuration.getMappedStatement(id);
        return mappedStatement;
    }

    /**
     * 获取MappedStatement的参数对象
     *
     * @param mappedStatement
     * @param params
     * @return
     */
    private Object getParameterObject(MappedStatement mappedStatement,Object[] params) {
        Object parameterObject=null;
        if (mappedStatement.getParameterMap() != null) {
            parameterObject=mappedStatement.getConfiguration().newParameterObject(mappedStatement.getParameterMap(), params);
        } else if (params != null && params.length > 1) {
            StringBuilder sb=new StringBuilder();
            for (Object obj : params) {
                sb.append(obj);
            }
            parameterObject=sb.toString();
        } else if (params != null && params.length == 1) {
            parameterObject=params[0];
        }
        return parameterObject;
    }
}

在上述代码中,注入了LogDao实例到当前类中。在doBefore方法中获取方法名、目标类名、代理对象等信息,通过反射获取对应的MapperProxy对象,最后获取到执行的SQL语句,并保存到log表中。

步骤四:配置Logger

我们可以使用log4j或logback等日志框架将SQL语句输出到日志文件中。例如在logback.xml中加如下配置:

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

<logger name="com.example.mapper" additivity="false" level="DEBUG">
    <appender-ref ref="STDOUT" />
</logger>

这样就可以在程序控制台或其他日志格式文件中看到每次执行SQL操作时的日志信息了。

示例

这里给出两个例子,测试数据基于Spring Boot和MyBatis环境:

示例一

@SpringBootTest
@RunWith(SpringRunner.class)
public class TestUserService {
@Autowired
private UserService userService;

@Test
public void test() {
    List<User> userList=userService.getUserList(2L, "Tom");

    User user=userService.getUserById(2L);

    System.out.println(user);
}
}

执行上述代码后,会在控制台和日志文件中打印出以下信息:

2021-08-31 11:12:13.889 [main] DEBUG c.e.m.UserMapper.selectByParam - ==>  Preparing: SELECT * FROM t_user WHERE id = ?
2021-08-31 11:12:13.896 [main] DEBUG c.e.m.UserMapper.selectByParam - ==> Parameters: 2(Long)
2021-08-31 11:12:13.898 [main] DEBUG c.e.m.UserMapper.selectByParam - <==      Total: 1
User(id=2, name=Tom, age=20, sex=male, createDate=Tue Aug 24 10:52:17 CST 2021, updateDate=)

示例二

@SpringBootTest
@RunWith(SpringRunner.class)
public class TestOrderService {
@Autowired
private OrderService orderService;

@Test
public void test() {
    List<Order> orderList=orderService.getOrderList(2L, "2021-08-30");

    Order order=orderService.getOrderById(2L);

    System.out.println(order);
}
}

执行上述代码后,会在控制台和日志文件中打印出以下信息:

2021-08-31 11:16:47.166 [main] DEBUG c.e.m.OrderMapper.queryOrderList - ==>  Preparing: SELECT a.id, a.user_id, a.price, a.create_time, a.update_time, b.name, b.age, b.sex FROM t_order a, t_user b WHERE a.user_id = b.id AND a.id = ?
2021-08-31 11:16:47.169 [main] DEBUG c.e.m.OrderMapper.queryOrderList - ==> Parameters: 2(Long)
2021-08-31 11:16:47.178 [main] DEBUG com.example.mapper.LogAspect - 保存日志到log表:Log(id=1, operTime=Tue Aug 31 11:16:47 CST 2021, operUser=, methodName=com.example.mapper.OrderMapper.queryOrderList, sql=SELECT a.id, a.user_id, a.price, a.create_time, a.update_time, b.name, b.age, b.sex FROM t_order a, t_user b WHERE a.user_id = b.id AND a.id = ?, time=0)
Order(id=2, userId=2, price=30.0, createTime=Mon Aug 30 10:52:17 CST 2021, updateTime=)

本文标题为:使用SpringAop动态获取mapper执行的SQL,并保存SQL到Log表中