想起以前有个需求,controller的一个方法需要传入起始时间和结束时间查询某段时间内的数据,其中结束时间限制在距当前时间一个月以前。但是经常容易忘记这个规定,使输入的结束时间查过了限制,查出了多余的数据,所以考虑通过增加切面,在方法被执行前获取请求参数,校验参数中的结束时间,如果超出时间限制则加以校正。最近在看《Spring实战》,正好复习一下。
AOP(Aspect Oriented Programming)是Spring的两大核心概念之一,在Spring编译期加入或者运行时将功能逻辑动态添加到目标对象的方法中,实现功能复用和业务流程的分离。

Spring AOP功能示意简图
先来看看AOP的原理。
它基于接口的JDK动态代理或者面向类的CGLIB实现。这里主要讲JDK动态代理的一个简单实现。
目标类实现了接口。代理类自身有一个Object的引用,通过构造函数指向目标对象(后面对Object对象的操作实际上是对目标对象的操作)。在程序运行期间,通过JDK的Proxy的newProxyInstance(省略参数)方法,重写invoke方法,在Object对象的方法执行前后,插入日志等非业务逻辑。可以看到,这里运用了反射来进行一系列操作
目标对象的行为已得到了增强,在客户端内通过传入已实现接口的目标对象,获取代理对象,操作代理对象就能看到增强后的效果了。AOP的应用有日志、事务管理等。

JDK动态代理UML图
下面来看一个AOP的实例
UserDao没有动态代理之前,调用say()方法的控制台输出:

使用代理模式的方法后:
代理类DynamicProxy
@Slf4j
public class DynamicProxy {
private Object object;
public DynamicProxy(Object object) {
this.object = object;
}
public Object getProxyObject() {
return Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("开始say");
Object value = method.invoke(object, args);
log.info("结束say");
return value;
}
});
}
}
客户端ProxyInstance
public class ProxyInstance {
public static void main(String[] args) {
UserDao userDao = new UserDaoImpl();
DynamicProxy dynamicProxy = new DynamicProxy(userDao);
UserDao proxy = (UserDao) dynamicProxy.getProxyObject();
proxy.say();
}
}
运行ProxyInstance的控制台输出:

笔者在这里实现了动态添加日志功能,者看起来可能跟在方法执行前后添加逻辑差不多,但还是不一样的。代理模式不需要修改被代理类的内部实现,通过代理类动态添加功能,将业务调用和其他逻辑分离,使业务开发人员更专注于业务开发。
再来看AOP的实现,有基于JDK的Spring AOP和依赖外部实现的AspectJ 它们之间的区别如下:
| 类型 | Spring AOP | AspectJ |
|---|---|---|
| 依赖 | JDK | 外部依赖 |
| 连接点支持时期 | 方法执行期 | 所有时期均可支持连接点 |
| 织入时期 | 运行时 | 编译器和类加载时 |
总结:Spring AOP比AspectJ构建方便因此使用更加便捷,但是Aspect在编译期将切面织入到代码中,效率更高。
再回到AOP, 它有以下几个概念:
通知(Advice):添加到切点的逻辑
切面(Aspect):切点和通知共同构成了切面
切点(Cut Point):通知发生在何处
织入(Weaving):将切面应用到目标对象并创建代理,切面在指定的连接点织入目标对象
连接点:切点表示一个区域,连接点表示这个区域能够插入切面的一个点
环绕:通知方法将目标方法环绕起来
通过前面对AOP的总结,我们选择AspectJ进行面向切面编程。
业务方
Performance.java
@Component
public class Performance {
public void perform() {
System.out.println("perform action");
}
}
定义一个切面
Audience.java
@Aspect
@Component
public class Audience {
@Pointcut("execution(* com.xingren.aspect.Performance.perform(..))")
public void performance() {
}
@Before("performance()")
public void silenceCellPhones() {
System.out.println("silencing cell phones");
}
@Before("performance()")
public void takeSeats() {
System.out.println("Taking seats");
}
@AfterReturning("performance()")
public void applause() {
System.out.println("applause");
}
@AfterThrowing("performance()")
public void demandRefund() {
System.out.println("demand refund");
}
重点在于@Pointcut,execution(* com.xingren.aspect.Performance.perform(..)定义了通知发生的地点,也可以传入参数,比如@Pointcut("execution(* com.xingren.aspect.ArgumentPerformance.perform(int)) && args(trackNumber)"),那么连接点上的注解也需要包含该参数。
测试类
AOPTest.java
public class AOPTest {
@Autowired
private Performance performance;
@Test
public void testPerform() {
performance.perform();
}
}
运行结果:

可以看到在业务方执行前后的连接点均得到了通知。
也可以使用环绕通知,它集成了前置通知和后置通知,更加灵活,但是需要额外额外的参数ProceedingJoinPoint。
下面是一个例子。
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint joinPoint) {
System.out.println("silencing cell phones");
System.out.println("Taking seats");
try {
joinPoint.proceed();
System.out.println("clap clap");
} catch (Throwable throwable) {
System.out.println("demand refund");
}
}
运行测试类得到结果:

可以看到环绕通知实现了和前置、后置通知相似的功能,并且通过ProceedingJoinPoint可以增加更加丰富的功能,比如获取目标方法的注解等,这里就不详细实现了。
文中可能存在不正确的地方,欢迎指正。
总结:本文主要讲了Spring AOP的JDK动态代理实现,Spring AOP和AspectJ的区别以及AspectJ的简单实用实例。实际上要实现增强对象的功能,还有一种选择,JDK静态代理,它和AspectJ一样也是在编译期生成字节码文件的,但静态代理只能为一个目标对象服务,如果目标对象过多,则会产生很多代理类。三种方法,可以根据自己的业务情况合理选择。我个人选择:如何目标对象较少,则选择JDK静态代理,否则AspectJ。
参考文章:
《Spring In Action Fourth Edition》 第四章面向切面的Spring