项目开发中我们需要记录各个服务的调用日志,作为审计记录或者供debug查看,或者性能以及使用率分析等等。通过记录日志和异常,我们能找出,哪些功能在哪个时间段被哪些模块调用,入参都有哪些,反应时间多长,这样我们就能比较快的找出项目问题所在或者优化项目。那么如何实现这种功能,Spring AOP给我们提供了现成的方法。
当然实现的方法有很多,最直接的莫过于在每个调用的进入和对出都记录一天日志(logger),然后通过通过日志的分析,但是每个方法都需要写日志记录的代码,显得很臃肿重复,因为记录日志的代码都是类似的,就是记录参数和退出时间。 而使用AOP我们只需要在一个记录编写记录日志大代码,其他地方加上注解就可以了,方便快捷,便于修改。
第一步,添加依赖
主要是在你的pom文件中添加如下依赖
<properties>
<org.aspectj-version>1.7.4</org.aspectj-version>
<cglib.version>3.1</cglib.version>
</properties>
....
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>${cglib.version}</version>
</dependency>
第二步,创建注解
我们在每个注解的方法上加上描述这样就更清楚知道这个方法是干什么的了。
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemLog {
String description() default "";
}
第三步,实现注解
需要注意的,本例只是为了说明如何使用AOP实现日志记录,所以我们只是在控制台打印日志,并没有记录到数据库或者文件。实际项目中我们一般都是记录到数据库或者日志文件。
在展示具体实现前,简要介绍一下几种通知的作用。
1.前置通知
*(before advice, 也就是代码中的 @Before(“serviceAspect()”)):在连接点前面执行,对连接点不会造成影响(前置通知有异常的话,会对后续操作有影响)2.正常返回通知
(after returning advice, 也就是代码中的 @AfterReturning(pointcut = “serviceAspect()”)):在连接点正确执行之后执行,如果连接点抛异常,则不执行3.异常返回通知
(after throw Advice, 也就是代码中的@AfterThrowing(pointcut = “serviceAspect()”, throwing = “e”)):在连接点抛异常的时候执行4.返回通知
(after, 也就是代码中的@After(“serviceAspect()”)):无论连接点是正确执行还是抛异常,都会执行。
具体示例代码在这里,欢迎加星,fork, 谢谢!
package com.yq.exceptiondemo.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Method;
/**
* Simple to Introduction
* className: SystemLogAspect
*
* @author EricYang
* @version 2018/6/9 19:43
*/
@Aspect
@Component
public class SystemLogAspect {
//本地异常日志记录对象
private static final Logger logger = LoggerFactory.getLogger(SystemLogAspect. class);
@Pointcut("@annotation(com.yq.exceptiondemo.config.SystemLog)")
public void serviceAspect() {
System.out.println("我是一个切入点");
}
/**
* 前置通知 用于拦截记录用户的操作
*
* @param joinPoint 切点
*/
@Before("serviceAspect()")
public void before(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpSession session = request.getSession();
//读取session中的用户 等其他和业务相关的信息,比如当前用户所在应用,以及其他信息, 例如ip
String ip = request.getRemoteAddr();
try {
System.out.println("doBefore enter。 任何时候进入连接点都调用");
System.out.println("method requested:" + (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"));
System.out.println("method description:" + getServiceMethodDescription(joinPoint));
System.out.println("remote ip:" + ip);
//日志存入数据库
System.out.println("doBefore end");
} catch (Exception e) {
logger.error("doBefore exception");
logger.error("exceptionMsg={}", e.getMessage());
}
}
/**
* 后通知(After advice) :当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
* @param joinPoint
*/
@After("serviceAspect()")
public void after(JoinPoint joinPoint) {
System.out.println("after executed. 无论连接点正常退出还是异常退出都调用");
}
/**
* 后通知(After advice) :当某连接点退出的时候执行的通知。
* @param joinPoint
*/
@AfterReturning(pointcut = "serviceAspect()")
public void AfterReturnning(JoinPoint joinPoint)
{
System.out.println("AfterReturning executed。只有当连接点正常退出时才调用");
Object[] objs = joinPoint.getArgs();
}
/**
* 异常通知 用于拦截层记录异常日志
* @param joinPoint
* @param e
*/
@AfterThrowing(pointcut = "serviceAspect()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Throwable e) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpSession session = request.getSession();
String ip = request.getRemoteAddr();
String params = "";
if (joinPoint.getArgs() != null && joinPoint.getArgs().length > 0) {
for ( int i = 0; i < joinPoint.getArgs().length; i++) {
params += (joinPoint.getArgs()[i]) + "; ";
}
}
try {
System.out.println("doAfterThrowing enter。 只有当连接点异常退出时才调用");
System.out.println("exception class:" + e.getClass().getName());
System.out.println("exception msg:" + e.getMessage());
System.out.println("exception method:" + (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"));
System.out.println("method description:" + getServiceMethodDescription(joinPoint));
System.out.println("remote ip:" + ip);
System.out.println("method parameters:" + params);
//日志存入数据库
System.out.println("doAfterThrowing end");
} catch (Exception ex) {
logger.error("doAfterThrowing exception");
logger.error("exception msg={}", ex.getMessage());
}
logger.error("method={}, code={}, msg={}, params={}",
joinPoint.getTarget().getClass().getName() + joinPoint.getSignature().getName(), e.getClass().getName(), e.getMessage(), params);
}
/**
* 获取注解中对方法的描述信息
*
* @param joinPoint 切点
* @return 方法描述
* @throws Exception
*/
public static String getServiceMethodDescription(JoinPoint joinPoint)
throws Exception {
String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class targetClass = Class.forName(targetName);
Method[] methods = targetClass.getMethods();
String description = "";
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length) {
description = method.getAnnotation(SystemLog.class).description();
break;
}
}
}
return description;
}
}
第四步,应用注解
我们在需要记录日志的方法上加上我们的注解.
@SystemLog(description = "helloWorld测试")
@ApiOperation(value = "hello demo", notes = "just for demo")
@GetMapping(value = "/hello", produces = "text/plain;charset=UTF-8")
public String hello() {
ReturnResult ret = new ReturnResult(Constants.QUERY_OK, "Hello World");
return ret.toString();
}
@SystemLog(description = "devide测试")
@ApiOperation(value = "hello exception demo, service method will throw native exception", notes = "just for demo")
@ApiImplicitParams({
@ApiImplicitParam(name = "a", defaultValue = "10", value = "a", required = true, dataType = "int", paramType = "query"),
@ApiImplicitParam(name = "b", defaultValue = "3", value = "b", required = true, dataType = "int", paramType = "query")
})
@GetMapping(value = "/devide", produces = "text/plain;charset=UTF-8")
public String exceptionDemo(@RequestParam("a") int a, @RequestParam("b") int b) {
log.info("Enter exceptionDemo a={} devided by b={}", a, b);
int c= computerSvc.devide(a, b);
ReturnResult ret = new ReturnResult(Constants.QUERY_OK, null);
ret.setObj(Integer.valueOf(c));
log.info("End exceptionDemo ret={}", ret);
return ret.toString();
}
第五步,查看效果
启动程序,然后调用这些方法。 我们的项目使用Swagger,因此我只需要在swagger提供的web页面上调用方法就可以了。 特别注意的是为了展示异常的记录,我们编写a /b的方法,当b等于0就出现异常。
请看效果图
效果图2, 调用的rest产生异常时
具体示例代码在这里,欢迎加星,fork, 谢谢!