在寫接口時,為了方便后續(xù)問題排查踊赠,需要記錄接口的入?yún)⒑统鰠?結(jié)果)呵扛。
常用的方法是使用slf4j的.info方法打日志。
如:
public class TestAopLogServiceImpl implements TestAopLogService {
private static final Logger log = LoggerFactory.getLogger(TestAopLogServiceImpl.class);
@Override
public TestAopLogRespDTO testAopLog(TestAopLogReqDTO reqDTO) {
// 記錄入?yún)? log.info("methodName = {}, parameters = {}", "testAopLog", JSON.toJSONString(reqDTO));
// 此處開始調(diào)用服務(wù)獲取響應(yīng)DTO
// mock一個
TestAopLogRespDTO respDTO = new TestAopLogRespDTO();
respDTO.setRespCode(200);
respDTO.setRespMsg("success");
respDTO.setRespData("this is data");
// 記錄結(jié)果
log.info("methodName = {}, result = {}", "testAopLog", JSON.toJSONString(respDTO));
return respDTO;
}
}
方法邏輯很清晰筐带,包含記錄入?yún)⒔翊@取結(jié)果,記錄結(jié)果伦籍,返回蓝晒。
日志記錄就不再展示了腮出。
然而,每個接口都需要記錄入?yún)⒑徒Y(jié)果芝薇,略微有點小麻煩利诺。
于是,使用AOP搞定剩燥。
注:AOP可以搞定這個問題慢逾,本次試驗不保證也不認為這是一個完美的方案,只能說是一種思路灭红。
@Around("execution(* com.XXX.service.impl..*.*(..))")
public Object aopLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 反射調(diào)用方法所在類
Class clz = joinPoint.getTarget().getClass();
String methodName = joinPoint.getSignature().getName();
Object[] paramValues = joinPoint.getArgs();
log.info("className = {}, methodName = {}, parameters = {}", clz.getSimpleName(), methodName, JSON.toJSONString(paramValues));
// 獲取方法結(jié)果
Object result = joinPoint.proceed();
log.info("className = {}, methodName = {}, parameters = {}", clz.getSimpleName(), methodName, JSON.toJSONString(result));
// 返回
return result;
}
關(guān)于AOP的內(nèi)容略過侣滩。
一頓操作猛如虎。該方法通過joinPoint獲取對應(yīng)的類变擒,方法名君珠,入?yún)⒌龋⒄{(diào)用其proceed方法計算出結(jié)果娇斑。
加入此AOP之后策添,service.impl下所有包下的所有類的所有方法都會被代理。
接口的代碼量減少毫缆。
public TestAopLogRespDTO testAopLog(TestAopLogReqDTO reqDTO) {
// 此處開始調(diào)用服務(wù)獲取響應(yīng)DTO
// mock一個
TestAopLogRespDTO respDTO = new TestAopLogRespDTO();
respDTO.setRespCode(200);
respDTO.setRespMsg("success");
respDTO.setRespData(((TestAopLogService) AopContext.currentProxy()).test());
return respDTO;
}
然而唯竹,這樣的操作會讓之后的維護者以為是沒打日志,存在重復加日志的可能性苦丁。
此外浸颓,并不是所有接口都需要打日志。有些QPS超高的接口旺拉,打了日志反而消耗存儲产上。
如果在方法上加需要打日志的注解,既可以表示打了日志蛾狗,也可以做到選擇性打日志晋涣。
新建兩個注解,對應(yīng)需要記錄入?yún)⒑陀涗浗Y(jié)果沉桌。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AddParamLog {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AddResultLog {
}
此時谢鹊,AOP修改為:
@Around("execution(* com.XXX.service.impl..*.*(..))")
public Object aopLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 反射調(diào)用方法所在類
Class clz = joinPoint.getTarget().getClass();
// 反射出被攔截的方法,以判斷是否包含注解
String methodName = joinPoint.getSignature().getName();
Class[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
Method method = clz.getMethod(methodName, parameterTypes);
//
if (method.getAnnotation(AddParamLog.class) != null) {
// 獲取當前方法入?yún)?打日志用
Object[] paramValues = joinPoint.getArgs();
log.info("className = {}, methodName = {}, parameters = {}", clz.getSimpleName(), methodName, JSON.toJSONString(paramValues));
}
// 獲取方法結(jié)果
Object result = joinPoint.proceed();
if (method.getAnnotation(AddResultLog.class) != null) {
log.info("className = {}, methodName = {}, parameters = {}", clz.getSimpleName(), methodName, JSON.toJSONString(result));
}
// 返回
return result;
}
該方法反射出對應(yīng)方法,判斷其是否包含注解蒲牧,以決定是否需要記錄日志撇贺。
此外,可以使用更簡單的注解方式冰抢,直接在@Around注解中指定只有使用了AddParamLog注解的方法才會被代理松嘶。
@Around("@annotation(AddParamLog)")
也可在注解中增加屬性,進一步擴展功能挎扰。如在AddParamLog注解中增加time屬性翠订,表示只在某時間段內(nèi)才打日志巢音。
用AOP打日志就基本搞定了。
疑問:
如果在接口中調(diào)用另一個會被代理的接口尽超,兩個接口的日志都會被記錄么官撼?
答案是否定的,因為AOP生成的是代理類似谁,在代理類中調(diào)用test()或者this.test()方法傲绣,指向的都是原類,不會記錄日志巩踏。
如下:
@AddParamLog
@Override
public TestAopLogRespDTO testAopLog(TestAopLogReqDTO reqDTO) {
// 此處開始調(diào)用服務(wù)獲取響應(yīng)DTO
// mock一個
TestAopLogRespDTO respDTO = new TestAopLogRespDTO();
respDTO.setRespCode(200);
respDTO.setRespMsg("success");
// 此處調(diào)用另一個理論上會被代理的接口
respDTO.setRespData(test());
return respDTO;
}
@AddParamLog
@Override
public String test() {
return "ceshi";
}
testAopLog方法在調(diào)用test時候秃诵,test不會記錄日志,而直接調(diào)用test方法時塞琼,會記錄菠净。
那么如何解決呢,有兩個方法彪杉。主要操作是拿到代理的引用毅往,再進行test方法調(diào)用。
方法一:
更新xml配置為
<aop:aspectj-autoproxy expose-proxy="true" />
更新調(diào)用方法為
((TestAopLogService) AopContext.currentProxy()).test()
方法二:
注入本身
public class TestAopLogServiceImpl implements TestAopLogService {
@Autowirde
private TestAopLogService self;
@AddParamLog
@Override
public TestAopLogRespDTO testAopLog(TestAopLogReqDTO reqDTO) {
// 此處開始調(diào)用服務(wù)獲取響應(yīng)DTO
// mock一個
TestAopLogRespDTO respDTO = new TestAopLogRespDTO();
respDTO.setRespCode(200);
respDTO.setRespMsg("success");
// 此處調(diào)用另一個理論上會被代理的接口
respDTO.setRespData(self.test());
return respDTO;
}
@AddParamLog
@Override
public String test() {
return "ceshi";
}
}
當然派近,這兩種操作都是不優(yōu)雅的攀唯。
最棒的方法是避免在接口中調(diào)用另外接口的方法。
AOP注解的value配置參考构哺,即切面Pointcut配置:
使用最頻繁的是execution和@annotation革答。
舉例:
任意公共方法的執(zhí)行:
execution(public * *(..))
任何一個以“set”開始的方法的執(zhí)行:
execution(* set*(..))
AccountService 接口的任意方法的執(zhí)行:
execution(* com.xyz.service.AccountService.*(..))
定義在service包里的任意類的任意方法的執(zhí)行:
execution(* com.xyz.service.*.*(..))
定義在service包和所有子包里的任意類的任意方法的執(zhí)行:
execution(* com.xyz.service..*.*(..))
定義在pointcutexp包和所有子包里的JoinPointObjP2類的任意方法的執(zhí)行:
execution(* com.test.spring.aop.pointcutexp..JoinPointObjP2.*(..))")@annotation(org.springframework.transaction.annotation.Transactional)
此外切點表達式支持與或非運算战坤,很強大曙强。
參考:
- Spring AOP中pointcut expression表達式解析 http://blog.csdn.net/kkdelta/article/details/7441829
- AOP實現(xiàn)攔截對象以及獲取切入目標方法和注解 http://blog.csdn.net/guan_shijie/article/details/52573189
- 在同一個類中調(diào)用另一個方法沒有觸發(fā) Spring AOP 的問題 https://segmentfault.com/a/1190000008379179
- Spring AOP無法攔截內(nèi)部方法調(diào)用 http://www.reibang.com/p/6534945eb3b5