在开发和维护大型应用时,监控方法的执行时间和记录日志是非常重要的任务。通过这些信息,开发者可以了解系统的性能瓶颈,追踪错误,并优化代码。Spring AOP(Aspect-Oriented Programming)提供了一种非侵入式的方式来实现这些功能。本文将详细介绍如何通过自定义注解和Spring AOP实现方法执行时间的监控和日志记录。
Spring AOP 是一种通过动态代理实现的面向切面编程框架。它允许开发者定义切面(Aspect),并在特定的点(Pointcut)织入增强处理(Advice)。常见的应用场景包括日志记录、事务管理、权限控制、性能监控等。
@TakeTime
为了方便地标记需要监控的方法,我们可以创建一个自定义注解 @TakeTime
。
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TakeTime {
String methodName() default "";
}
@Documented
:标记该注解应该被作为被标注的程序成员的公共API,可以被如 javadoc
等工具文档化。@Target(ElementType.METHOD)
:指定该注解只能用于方法。@Retention(RetentionPolicy.RUNTIME)
:指定注解在运行时保留,可以通过反射获取。通过创建一个基于Spring AOP的切面,可以在方法执行前后记录时间和日志信息。
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Component
@Aspect
public class TakeTimeAspect {
private static final Logger log = LoggerFactory.getLogger(TakeTimeAspect.class);
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
// 统一线程本地变量
ThreadLocal startTime = new ThreadLocal<>();
ThreadLocal endTime = new ThreadLocal<>();
/**
* 定义切点:带有@TakeTime注解的方法
*/
@Pointcut("@annotation(com.example.demo.annotation.TakeTime)")
public void takeTime() {
}
/**
* 方法执行前
*
* @param joinPoint 连接点
*/
@Before("takeTime()")
public void doBefore(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] params = joinPoint.getArgs();
// 记录开始时间
startTime.set(System.currentTimeMillis());
String startDateTime = sdf.format(new Date(startTime.get()));
log.info("【方法开始】==> 方法名称: {}, 开始时间: {}, 参数: {}",
methodName, startDateTime, JSON.toJSONString(params));
// 记录请求信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String requestId = UUID.randomUUID().toString();
log.info("【请求信息】==> 请求ID: {}, URL: {}, 方法: {}, 参数: {}",
requestId, request.getRequestURL().toString(),
request.getMethod(), JSON.toJSONString(params));
}
/**
* 方法执行后
*
* @param joinPoint 连接点
* @param ret 返回值
*/
@AfterReturning(pointcut = "takeTime()", returning = "ret")
public void doAfterReturning(JoinPoint joinPoint, Object ret) {
// 记录结束时间
endTime.set(System.currentTimeMillis());
String endDateTime = sdf.format(new Date(endDateTime));
log.info("【方法结束】==> 方法名称: {}, 结束时间: {}, 执行时长: {}ms, 返回值: {}",
joinPoint.getSignature().getName(),
endDateTime,
endTime.get() - startTime.get(),
JSON.toJSONString(ret));
// 清除线程本地变量
startTime.remove();
endTime.remove();
}
/**
* 方法执行异常
*
* @param joinPoint 连接点
* @param ex 异常
*/
@AfterThrowing(pointcut = "takeTime()", throwing = "ex")
public void doAfterThrowing(JoinPoint joinPoint, Throwable ex) {
endTime.set(System.currentTimeMillis());
String endDateTime = sdf.format(new Date(endTime.get()));
log.error("【方法异常】==> 方法名称: {}, 异常时间: {}, 异常信息: {}, 执行时长: {}ms",
joinPoint.getSignature().getName(),
endDateTime,
ex.getMessage(),
endTime.get() - startTime.get());
// 清除线程本地变量
startTime.remove();
endTime.remove();
}
/**
* 格式化日期
*
* @param date 日期对象
* @return 格式化后的日期字符串
*/
private String formatDateTime(Date date) {
return sdf.format(date);
}
}
@TakeTime
注解@Service
public class UserService {
@TakeTime
public User getUserById(Long id) {
// 方法实现
return userRepository.findById(id).orElse(null);
}
}
当调用 getUserById
方法时,会输出以下日志信息:
方法开始
【方法开始】==> 方法名称: getUserById, 开始时间: 2023年12月25日 12:34:56, 参数: [1]
【请求信息】==> 请求ID: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx, URL: http://localhost:8080/api/user/1, 方法: GET, 参数: [1]
方法结束
【方法结束】==> 方法名称: getUserById, 结束时间: 2023年12月25日 12:34:56, 执行时长: 100ms, 返回值: {"id":1,"username":"admin","email":"[email protected]"}
通过自定义注解和Spring AOP,可以实现对方法执行时间的监控和详细日志的记录。这不仅有助于性能优化和问题排查,还能提升开发效率和系统可维护性。希望本文可以帮助你在实际项目中更好地利用Spring AOP进行方法执行时间监控和日志记录!