AOP实战:一个面向切面的实践项目,方法级别的简单监控
背景
在开发过程中,我们经常会需要对方法进行一些简单的监控,例如监控某个方法的执行时间,必要的时候打印入参和返回值,对抛出的异常进行记录。这样的一些监控点虽然很小,但是这些重复的代码散落在各处而且侵入到业务逻辑当中让业务代码显得非常杂乱。因此,将这个切面抽离出来变得非常有意义,所以有了本项目。(完整代码请查看 simplemonitor)
- 如果你也为重复代码感到不舒服,可以试试这个模块。
- 如果你刚开始学习SpringAOP,这个监控程序可谓SpringAOP界的HelloWorld,可以参考一下自己实现一个。
原理
使用SpringAOP,对加了指定注解的类或方法进行环切(@Around),记录执行时间,并通过连接点(ProceedingJoinPoint)获取所需的信息
实现
注解
这里,我们定义一个注解 @TimeConsuming,使用这个注解来标记切点。并且提供了一些可以自定义的选项参数,参数的含义可以查看代码注释及下文 参数配置 一节
package com.github.dadiyang.simplemonitor;
import java.lang.annotation.*;
/**
* 监控方法的耗时信息
* @author dadiyang
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.TYPE})
public @interface TimeConsuming {
/**
* 是否打印完整的方法名、方法参数和返回值
* @return 是否打印完整的方法名、方法参数和返回值
*/
boolean fullMsg() default false;
/**
* 是否打印完整的参数
* @return 是否打印完整的参数
*/
boolean fullArg() default false;
/**
* 是否打印完整的返回值
* @return 是否打印完整的返回值
*/
boolean fullReturnVal() default false;
/**
* 是否打印完整的方法名称(包括全类名和方法的全类名)
* @return 是否打印完整的方法名称
*/
boolean fullMethodName() default false;
/**
* 是否监控方法抛出的异常,打印异常信息
* 注意:监控方法抛出的异常只会以logLevel指定的日志级别打印方法相关信息和 e.getMessage(),最后把异常重新抛出
* @return 是否监控方法抛出的异常,默认 true
*/
boolean monitorException() default true;
/**
* 日志级别(0: TRACE, 1: DEBUG, 2: INFO, 3: WARN, 4: ERROR),默认 2,即INFO级别
* @return 日志级别
*/
int logLevel() default 2;
/**
* 是否使用被注解方法所属的类对应的日志类进行日志输出
* 即 LoggerFactory.getLogger(方法所属类);
* <p>
* 默认 false
* @return 是否使用被注解方法所属的类对应的日志类进行日志输出
*/
boolean useSourceClassLog() default false;
}
监控类
package com.github.dadiyang.simplemonitor;
import com.alibaba.fastjson.JSON;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* 使用切面记录每个接口调用的耗时
* @author dadiyang
*/
@Aspect
@Component
public class TimeConsumingMonitor {
private static final Logger log = LoggerFactory.getLogger(TimeConsumingMonitor.class);
private static final int MAX_STRING_LENGTH = 128;
/**
* 拦截类上的 TimeConsuming 注解
*/
@Around(value = "@within(timeConsuming)")
public Object cutClazz(ProceedingJoinPoint joinPoint, TimeConsuming timeConsuming) throws Throwable {
return logging(joinPoint, timeConsuming);
}
/**
* 拦截方法上的 TimeConsuming 注解
*/
@Around(value = "@annotation(timeConsuming)")
public Object cutMethod(ProceedingJoinPoint joinPoint, TimeConsuming timeConsuming) throws Throwable {
if (joinPoint.getSignature().getDeclaringType().isAnnotationPresent(timeConsuming.getClass())) {
// 此方法仅拦截方法上的 TimeConsuming 注解
return joinPoint.proceed(joinPoint.getArgs());
}
return logging(joinPoint, timeConsuming);
}
/**
* 记录接口耗时和方法参数简单摘要
*/
private Object logging(ProceedingJoinPoint joinPoint, TimeConsuming timeConsuming) throws Throwable {
Logger logger = getLogger(joinPoint, timeConsuming);
try {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(joinPoint.getArgs());
// 如果获取不到注解或者设置的日志级别与当前logger的级别不匹配,则直接返回结果
if (timeConsuming == null || !needPrint(timeConsuming.logLevel(), logger)) {
return result;
}
long time = System.currentTimeMillis() - start;
String argsString = "[]";
if (joinPoint.getArgs().length > 0) {
argsString = summary(joinPoint.getArgs(), timeConsuming.fullMsg() || timeConsuming.fullArg());
}
String resultString = "";
if (!(result instanceof Void)) {
resultString = summary(result, timeConsuming.fullMsg() || timeConsuming.fullReturnVal());
}
String methodName = getMethodName(joinPoint, timeConsuming);
printLog(timeConsuming.logLevel(), logger, "调用方法{}, 参数: {}, 结果: {}, 执行耗时: {}", methodName, argsString, resultString, time);
return result;
} catch (Throwable throwable) {
// 如果需要监控异常信息,才打印异常日志
if (timeConsuming.monitorException() && needPrint(timeConsuming.logLevel(), logger)) {
String methodName = getMethodName(joinPoint, timeConsuming);
String argString = summary(joinPoint.getArgs(), timeConsuming.fullMsg() || timeConsuming.fullArg());
printLog(timeConsuming.logLevel(), logger, "调用方法{}, 参数:{}, 抛出异常:{}", methodName, argString, throwable.getMessage());
}
// 把异常抛出
throw throwable;
}
}
/**
* 根据条件获取日志实例
* @param joinPoint 连接点
* @param timeConsuming 注解
* @return 日志实例
*/
private Logger getLogger(ProceedingJoinPoint joinPoint, TimeConsuming timeConsuming) {
return timeConsuming != null && timeConsuming.useSourceClassLog() ? LoggerFactory.getLogger(joinPoint.getSignature().getDeclaringType()) : log;
}
/**
* 根据给你写的日志级别和日志类实例打印日志
* @param level 日志级别
* @param logger 日志类
* @param format 格式
* @param args 日志格式使用的参数
*/
private void printLog(int level, Logger logger, String format, Object... args) {
switch (level) {
case 0:
logger.trace(format, args);
break;
case 1:
logger.debug(format, args);
break;
case 2:
logger.info(format, args);
break;
case 3:
logger.warn(format, args);
break;
case 4:
logger.error(format, args);
break;
default:
logger.info(format, args);
}
}
/**
* 设置的日志级别与给定的logger级别是否一致
* @param level 注解中设置的日志级别
* @param logger 日志类
* @return 是否需要打印日志
*/
private boolean needPrint(int level, Logger logger) {
return (level == 0 && logger.isTraceEnabled())
|| (level == 1 && logger.isDebugEnabled())
|| (level == 2 && logger.isInfoEnabled())
|| (level == 3 && logger.isWarnEnabled())
|| (level == 4 && logger.isErrorEnabled());
}
/**
* @param joinPoint 连接点
* @param timeConsuming 注解
* @return 方法名
*/
private String getMethodName(ProceedingJoinPoint joinPoint, TimeConsuming timeConsuming) {
Signature signature = joinPoint.getSignature();
return timeConsuming.fullMsg() || timeConsuming.fullMethodName() ? signature.toLongString() : signature.toShortString();
}
/**
* 将对象序列化后取摘要
* @param obj 需要被摘要的类
* @param fullMsg 是否使用全信息
* @return 摘要
*/
private String summary(Object obj, boolean fullMsg) {
String argsString = JSON.toJSONString(obj);
if (fullMsg) {
return argsString;
}
if (argsString.length() > MAX_STRING_LENGTH) {
// 参数的简单摘要
argsString = argsString.substring(0, MAX_STRING_LENGTH) + "...";
}
return argsString;
}
}
参数配置
通常情况下,默认的配置是足够使用了,但是如果你有更加个性化的需求,可以对以下同几个参数进行配置
-
fullMsg 是否打印完整的方法名、方法参数和返回值,默认 false
-
fullArg 是否打印完整的参数,默认 false
-
fullReturnVal 是否打印完整的返回值,默认 false
-
fullMethodName 是否打印完整的方法名称(包括全类名和方法的全类名),默认 false
-
monitorException 是否监控方法抛出的异常,默认 true
注意:监控方法抛出的异常只会以logLevel指定的日志级别打印方法相关信息和 e.getMessage(),最后把异常重新抛出 -
logLevel 日志级别(0: TRACE, 1: DEBUG, 2: INFO, 3: WARN, 4: ERROR),默认 2,即INFO级别
-
useSourceClassLog 是否使用被注解方法所属的类对应的日志类进行日志输出
即LoggerFactory.getLogger
(方法所属类);
完整项目代码
完整的代码及测试用例我已经上传到GitHub上,请点击查看 simplemonitor