使用 AOP 记录日志

几天前,本教主接到mentor布置的任务:使用 AOP 来记日志,替代掉重复日志的代码。

本教主自号聪明机智寡言侠义小郎君,此事自然是小Case。

躬身亲做AOP

为了使用 AOP,首先我们得认识到 AOP 是什么对吧,术语原理什么的请翻以前的博文,此番写一下 AOP 的种类。

  1. 总所周知,spring 是个强大的容器,本身实现spring AOP。
  2. 除了spring AOP,业内还有个叫 AspectJ 的玩意,它比 spring AOP 强大了很多很多,它有多厉害?它强大到spring AOP只实现了它部分的功能,spring AOP 只能拦截普通方法,AspectJ 却能拦截构造方法和字段。

所以总结地说,下面将会有2种 AOP,一种是 spring AOP,一种是 AspectJ。

一、spring AOP

实现 spring AOP 有两种做法,一种是使用 xml 来定义切面、切点和通知,另一种是使用@Aspect注解(这种做法实际上是前一种的简化版)

使用 xml 来定义切面

此种方法稍微累赘(spring曾因为大量使用xml配置而被诟病,后来慢慢转向使用注解)。

1.首先要写一个切面(aspect):

1
2
3
4
5
6
7
8
9
10
/**
* Created by qiuyongchen on 2016-09-01 17:54:32.
*/
public class OtaOrderLogAspect {

public String OtaTaskLog(){
doSomething()...
}

}

这个切面里的 OtaTaskLog就是一个 advice 通知,还记得我之前说过的话吗?

一个切面里,有切入点,有通知,如果切入点匹配到了被代理的函数,就启动通知。

上边代码里的OtaOrderLogAspect类就是个切面,里边的OtaTaskLog方法相当于通知,若是好运,匹配到目标方法,就将其拦截,调用通知后再执行目标方法。

2.其次需要将OtaOrderLogAspect托管给 spring。

1
2
<bean id = "logAspect"
class="com.xxx. OtaOrderLogAspect"/>

到这里为止,spring 就知道该切面的存在了,那么,该怎么做,才能让 spring 在恰当的代码处启动通知,执行额外的动作呢?答案是使用 spring AOP。

3.使用 spring AOP

使用 spring AOP 特有的 xml 配置,逐个定义 aspect/pointcut/advice,如下:

1
2
3
4
5
<aop:config>
<aop:aspect ref="logAspect">
<aop:before pointcut="execution(* *(..))" method="OtaTaskLog" />
</aop:aspect>
</aop:config>

上面的配置中,定义了切面(就是OtaOrderLogAspect类),定义了一个 before通知,该通知对应的切入点是任意类里的任意方法(execution( (..))是一种 AspectJ 切入点表达式语言写出来的,该语言可以用来筛选我们想要拦截的方法,具体语法可浏览:http://www.cnblogs.com/javaee6/p/3779826.html),也定义了一个advice 的核心方法(advice分有 before/after/around 等几种),也就是OtaTaskLog,拦截到目标方法后,先执行OtaTaskLog方法,再执行目标方法。

使用@Aspect注解来创建切面

看了上面的那种写法,您是不是觉得有点繁琐呢?每次定义一个切点或切面都要额外写一个 xml 配置,神仙都觉得麻烦。
于是呢,在众多程序员的吐槽中,springAOP 增加了注解的用法,使用注解,完全不需要配置 xml。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

@Service("otaOrderLogAspect")
@Aspect
@Slf4j
public class OtaOrderLogAspect {

private static final int LOG_MIN = 0;

private static final int LOG_MAX = 3820;

@Resource
private IOtaOrderLogService otaOrderLogService;

/**
* 供应商主动推送的接口处的切点
*/
@Pointcut("execution(* *(int, int)) && @annotation(OtaAopLog))")
public void OtaPushLogPointCut(){}

@Around("OtaPushLogPointCut() && @annotation(otaAopLog)")
public Response OtaPushLog(ProceedingJoinPoint proceedingJoinPoint, OtaAopLog otaAopLog) {
OtaOrderLog orderLog = new OtaOrderLog();

// 获取注解中的参数
int actionType = 0;
actionType = otaAopLog.actionType();
orderLog.setActionType((byte)actionType);

// 记录入参
Object[] args = proceedingJoinPoint.getArgs();
if (args.length == 2) {
orderLog.setOrderId((Integer) args[0]);
orderLog.setParam(String.valueOf(args[1]));
}

// 记录出参
Response response = new Response();
try {
response = (Response) proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}

orderLog.setResponse(JSONObject.toJSONString(response));

// 本地测试时需睡眠主线程,否则异步存数据会失败
// Thread.sleep(10000);

otaOrderLogService.saveLogAsyn(orderLog);
log.info("本次AOP记录的内容主要为:{}", orderLog);
return response;
}
}

使用@Aspect表明该类是切面,使用@Pointcut来过滤目标方法,使用@Around来创建通知。
除了代码,我们需开启 spring 的自动代理:

1
<aop:aspectj-autoproxy/>

做了以上两点,在任何一个拥有两个参数的方法前,加上一个@OtaAopLog注解,AOP 就能自动帮您记日志了。

二、AspectJ

AspectJ 的功能比 springAOP 强大多了,比如,AspectJ 可以在类执行构造方法时进行拦截。
为了使用AspectJ,需要声明独特的类型 aspect:

1
2
3
4
5
6
7
public aspect LogAspect {
pointcut logPointCut() : execute(* *(int, int));

after() returning() : log() {
doSomething()...
}
}

如果要将 bean 注入到LogAspect中,需要特殊的 xml 声明:

1
2
3
<bean class="com.xxx.LogAspect" factory-method="aspectOf">
<property name="xxxName" ref="xxx"/>
</bean>

之所以要这么声明,是因为 AspectJ的切面在 AspectJ 启动时就创建了,spring 无法干预,自然也就没法注入。
而使用切面提供的 aspectOf() 方法,就能获取由 AspectJ 创建的切面的实例,进而 spring 可以注入。