一 概念
Joint Point
JointPoint是程序运行过程中可识别的点,这个点可以用来作为AOP切入点。JointPoint对象则包含了和切入相关的很多信息。比如切入点的对象,方法,属性等。我们可以通过反射的方式获取这些点的状态和信息,用于追踪tracing和记录logging应用信息。
Pointcut
pointcut 是一种程序结构和规则,它用于选取join point并收集这些point的上下文信息。
pointcut通常包含了一系列的Joint Point,我们可以通过pointcut来同时操作jointpoint。单从概念上,可以把Pointcut当做jointpoint的集合。
JointPoint和ProceedingJoinPoint区别
JointPoint
通过JpointPoint对象可以获取到下面信息
# 返回目标对象,即被代理的对象
Object getTarget();
# 返回切入点的参数内容
Object[] getArgs();
# 返回切入点的Signature
Signature getSignature();
# 返回切入的类型,比如method-call,field-get等等,感觉不重要
String getKind();
ProceedingJoinPoint
Proceedingjoinpoint 继承了 JoinPoint。是在JoinPoint的基础上暴露出 proceed 这个方法。proceed很重要,这个是aop代理链执行的方法。
环绕通知=前置+目标方法执行+后置通知,proceed方法就是用于启动目标方法执行的
暴露出这个方法,就能支持 aop:around 这种切面(而其他的几种切面只需要用到JoinPoint,,这也是环绕通知和前置、后置通知方法的一个最大区别。这跟切面类型有关), 能决定是否走代理链还是走自己拦截的其他逻辑。建议看一下 JdkDynamicAopProxy的invoke方法,了解一下代理链的执行原理。
JointPoint使用详解
这里详细介绍JointPoint的方法,这部分很重要是coding核心参考部分。开始之前我们思考一下,我们到底需要获取切入点的那些信息。我的思考如下
切入点的方法名字及其参数
切入点方法标注的注解对象(通过该对象可以获取注解信息)
切入点目标对象(可以通过反射获取对象的类名,属性和方法名)
注:有一点非常重要,Spring的AOP只能支持到方法级别的切入。换句话说,切入点只能是某个方法。
针对以上的需求JDK提供了如下API
1 获取切入点所在目标对象
Object targetObj =joinPoint.getTarget();
# 可以发挥反射的功能获取关于类的任何信息,例如获取类名如下
String className = joinPoint.getTarget().getClass().getName();
2.获取切入点方法的名字
getSignature());是获取到这样的信息 :修饰符+ 包名+组件名(类名) +方法名
这里我只需要方法名
String methodName = joinPoint.getSignature().getName()
3. 获取方法上的注解
方法1:xxxxxx是注解名字
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null)
{
xxxxxx annoObj= method.getAnnotation(xxxxxx.class);
}
return null;
方法2:上面我们已经知道了方法名和类的对象,通过反射可以获取类的内部任何信息。
// 切面所在类
Object target = joinPoint.getTarget();
String methodName = joinPoint.getSignature().getName();
Method method = null;
for (Method m : target.getClass().getMethods()) {
if (m.getName().equals(methodName)) {
method = m;
// xxxxxx annoObj= method.getAnnotation(xxxxxx.class);同上
break;
}
}
4. 获取方法的参数
这里返回的是切入点方法的参数列表
Object[] args = joinPoint.getArgs();
实战
logParameter
/**
* 参数日志记录
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface LogParameter {
/**
* 是否启用
* @return
*/
boolean enable() default true;
/**
* 启用结果输出
* @return
*/
boolean enableResult() default true;
/**
* 忽略参数名
* @return
*/
String[] ignoreName() default {};
/**
* 忽略参数类型
* @return
*/
Class[] ignoreType() default {};
/**
* 输出登录用户信息
* @return
*/
boolean loginUserInfo() default true;
/**
* 记录 http 信息
* 当前记录 uri
*/
boolean recordHttp() default false;
}
LogParameterAspect
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.springframework.util.StopWatch;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.function.Supplier;
/**
* 日志记录(log文件)
*/
@Aspect
public class LogParameterAspect {
private Logger LOG_PARAM = LoggerTool.getLogParam();
/**
* 获取登录用户信息的函数
*/
private Supplier loginUserGetFunc;
/**
* 获取Http用户信息的函数
*/
private Supplier httpInfoGetFunc;
private static final String SW_ID = "param";
private static final String SW_ASPECT = "aspect";
private static final String SW_ASPECT_AFTER = "aspectAfter";
private static final String SW_PROCEED = "proceed";
public LogParameterAspect() {
}
public LogParameterAspect(Supplier loginUserGetFunc) {
this.loginUserGetFunc = loginUserGetFunc;
}
public LogParameterAspect(Supplier httpInfoGetFunc, Object emtpy) {
this.httpInfoGetFunc = httpInfoGetFunc;
}
public LogParameterAspect(Logger logger) {
if (logger == null) {
return;
}
LOG_PARAM = logger;
}
/**
* 记录日志 - 方法级别
* 如果类上也有,以类的为准
* @param point
* @param logParamMethod
* @return
* @throws Throwable
*/
@Around("@annotation(logParamMethod)")
public Object recordParamOfMethod(ProceedingJoinPoint point, LogParameter logParamMethod) throws Throwable {
return this.handleLogRecord(point, logParamMethod);
}
/**
* 记录日志 - 类级别
* @param point
* @param logParamClass
*/
@Around("@within(logParamClass)")
public Object recordPram(ProceedingJoinPoint point, LogParameter logParamClass) throws Throwable {
LogParameter logParamMethod = this.getLogAnnOnMethod(point, point.getTarget().getClass());
// 如果防范上有此注解,优先使用方法上的
if (logParamMethod != null) {
return point.proceed();
}
return this.handleLogRecord(point, logParamClass);
}
/**
* 日志记录处理
* @param point
* @param logParameter
* @return
* @throws Throwable
*/
private Object handleLogRecord(ProceedingJoinPoint point, LogParameter logParameter) throws Throwable {
StopWatch stopWatch = new StopWatch(SW_ID);
stopWatch.start(SW_ASPECT);
if (!logParameter.enable()) {
// 关闭参数日志输出
return point.proceed();
}
MethodSignature methodSignature = (MethodSignature) point.getSignature();
List<ParameterInfoVo> paramList = ParameterInfoVo.buildParameter(methodSignature, point.getArgs());
final String specChar = CharSeparator.COMMA;
StringBuffer paramStr = new StringBuffer();
try {
paramList.parallelStream().forEach(p -> {
if (this.ignore(p, logParameter)) {
return;
}
String value = ObjectTool.objectToJsonIgnoreBaseType(p.value);
paramStr.append(specChar).append(p.name).append(CharSeparator.EQUALS_SIGN).append(value);
});
paramStr.delete(0, specChar.length());
} catch (Exception e) {
e.printStackTrace();
}
// 将登录用户的信息拼接到末尾
this.joinLoginUserInfoToParam(paramStr, logParameter);
// 将httpUri信息拼接到末尾
this.joinHttpInfoToParam(paramStr, logParameter);
String methodName = StringUtils.join(methodSignature.getDeclaringTypeName(), CharSeparator.WELL, methodSignature.getName());
Object result = null;
String exMsg = null;
stopWatch.stop();
stopWatch.start(SW_PROCEED);
try {
result = point.proceed();
return result;
} catch (Throwable throwable) {
exMsg = throwable.getMessage();
throw throwable;
} finally {
stopWatch.stop();
logResult(stopWatch, logParameter, paramStr, methodName, result, exMsg);
}
}
/**
* 结果输出到log日志
* @param stopWatch 计时器
* @param logParameter log注解对象
* @param paramStr 入参信息
* @param targetMethodName 目标方法
* @param result 处理结果
* @param exMsg 异常消息
*/
private void logResult(StopWatch stopWatch, LogParameter logParameter, StringBuffer paramStr, String targetMethodName, Object result, String exMsg) {
if (!stopWatch.isRunning()) {
// 适应异步方法的计时,可获取到等待时长
stopWatch.start(SW_ASPECT_AFTER);
}
String execResult = null;
if (logParameter.enableResult() && ValidUtil.isNotBlank(result)) {
if (result instanceof Future) {
// 对异步调用的支持,异步获取结果
new Thread(() -> {
Object callResult = null;
String callExMsg = null;
try {
callResult = ((Future) result).get();
} catch (NullPointerException e) {
callExMsg = e.toString();
} catch (Exception e) {
callExMsg = e.getLocalizedMessage();
} finally {
this.logResult(stopWatch, logParameter, paramStr, targetMethodName, callResult, callExMsg);
}
}).start();
return;
}
execResult = ObjectTool.objectToJsonString(result);
}
stopWatch.stop();
String swInfo = stopWatch.toString();
if (ValidUtil.isNotBlank(exMsg)) {
this.LOG_PARAM.warn("{}->param=={}\t---->ex=={}\t---->costTime=={}", targetMethodName, paramStr, exMsg, swInfo);
} else if (logParameter.enableResult()) {
this.LOG_PARAM.info("{}->param=={}\t---->result=={}\t---->costTime=={}", targetMethodName, paramStr, execResult, swInfo);
} else {
this.LOG_PARAM.info("{}->param=={}\t---->costTime=={}", targetMethodName, paramStr, swInfo);
}
}
/**
* 获取方法上的日志注解
* @param point
* @param targetClass
* @return
* @throws NoSuchMethodException
*/
private LogParameter getLogAnnOnMethod(ProceedingJoinPoint point, Class<?> targetClass) throws NoSuchMethodException {
Signature signature = point.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = targetClass.getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
return targetMethod.getAnnotation(LogParameter.class);
}
/**
* 判断该参数是否需要过滤
* @param paramInfo
* @param logParam
* @return
*/
private boolean ignore(ParameterInfoVo paramInfo, LogParameter logParam) {
if (paramInfo == null) {
return true;
}
if (logParam.ignoreName().length > 0) {
for (String in : logParam.ignoreName()) {
if (paramInfo.name.equals(in)) {
return true;
}
}
}
if (logParam.ignoreType().length > 0) {
for (Class ic : logParam.ignoreType()) {
if (paramInfo.type.equals(ic)) {
return true;
}
}
}
return false;
}
/**
* 将登录用户的信息拼接到入参信息中
* @param param
* @param logParam
*/
private void joinLoginUserInfoToParam(StringBuffer param, LogParameter logParam) {
if (loginUserGetFunc != null && logParam.loginUserInfo()) {
Object loginUserInfo = loginUserGetFunc.get();
if (ValidUtil.isBlank(loginUserInfo)) {
return;
}
String info = ObjectTool.objectToJsonString(loginUserInfo);
param.append(".--loginUserInfo:" + info);
}
}
/**
* 将http信息拼接到末尾
* @param param
* @param logParam
*/
private void joinHttpInfoToParam(StringBuffer param, LogParameter logParam) {
if (httpInfoGetFunc != null && logParam.recordHttp()) {
Object obj = httpInfoGetFunc.get();
if (obj == null) {
return;
}
Map<String, Object> maps = (Map) obj;
StringBuffer sb = new StringBuffer();
maps.forEach((String key, Object value) -> {
sb.append(key);
sb.append("=");
sb.append(value);
sb.append(",");
});
param.append(".--httpInfo:[" + sb.toString() + "]");
}
}
/**
* 方法参数信息
*/
private static class ParameterInfoVo {
private String name;
private Class type;
private Object value;
static List<ParameterInfoVo> buildParameter(MethodSignature methodSignature, Object[] values) {
String[] names = methodSignature.getParameterNames();
Class[] types = methodSignature.getParameterTypes();
List<ParameterInfoVo> paramMap = new ArrayList<>(names.length);
for (int i = 0; i < names.length; i++) {
Class type = null;
Object value = null;
if (types.length > i) {
type = types[i];
}
if (values.length > i) {
value = values[i];
}
ParameterInfoVo param = new ParameterInfoVo();
param.name = names[i];
param.type = type;
param.value = value;
paramMap.add(param);
}
return paramMap;
}
}
public LogParameterAspect setLoginUserGetFunc(Supplier loginUserGetFunc) {
this.loginUserGetFunc = loginUserGetFunc;
return this;
}
public LogParameterAspect setHttpInfoGetFunc(Supplier httpInfoGetFunc) {
this.httpInfoGetFunc = httpInfoGetFunc;
return this;
}
}