深入理解Spring AOP:面向切面编程在业务逻辑与系统服务中的应用
Java 中的 SpringAOP 相关内容,以及代理模式和常用的 AspectJ 注解都将进行详细讲解。 具体来说,会分别对 SpringAOP 进行阐释,同时也会深入探讨代理模式,并且详细介绍常用的 AspectJ 注解。

2023 年 9 月 8 日 10 时 16 分 01 秒进行了更新,作者是洞之蝉。
这篇文章主要介绍了 Java 中的一些内容。其中包括 SpringAOP 以及代理模式。还详细讲解了常用的 AspectJ 注解。Spring 为面向切面编程提供了丰富的支持。它允许通过将应用的业务逻辑与系统级服务分离,比如审计和事务管理,来进行内聚性的开发。有需要的朋友可以参考一下。
目录
一、AOP简述
回到主题,什么是 AOP 呢?AOP 指的是面向切面编程。Spring 为面向切面编程提供了丰富的支持,它允许将应用的业务逻辑与系统级服务进行分离,比如审计和事务管理等,从而实现内聚性的开发。
应用对象只实现它们应该做的——完成业务逻辑——仅此而已。
它们不负责其他的系统级关注点,比如日志支持,也不负责事务支持,甚至都不涉及意识层面的相关事宜。
如下图,可以很直接明了的展示整个AOP的过程:
1.1 一些基本概念
通知(Adivce)
通知有5种类型:
我们或许会询问,那通知在系统中的代码究竟是一个方法呢,还是一个对象呢,亦或是一个类,又或者是一个接口之类的呢?
我想说一点,其实并非如此。你可以这样理解,通知就相当于我们日常生活中所说的通知,例如“某某人,你 2019 年 9 月 1 号来学校报个到”。通知主要体现的是告知我们何时执行,规定一个时间,是在系统运行中的某个时间点,比如抛异常的时候,或者方法执行之前等,它并非对应代码中的方法,也并非对应代码中的方法。并非对应代码中的方法!
连接点、切面、引入和织入。二、代理模式。
AOP 思想的实现通常基于代理模式。在 JAVA 中,一般会采用 JDK 动态代理模式。然而我们都清楚,JDK 动态代理模式仅仅能够代理接口,要是想代理类的话就无法实现了。
Spring AOP 会以这样的方式进行切换。因为 Spring AOP 同时支持 CGLIB、ASPECTJ、JDK 动态代理这几种方式。如果你的真实对象实现了接口,Spring AOP 就会默认使用 JDK 动态代理;如果真实对象没有实现接口,Spring AOP 就会采用 cglib 代理。
这里简单说说代理模式,代理模式的UML类图如下:
2.1 静态代理
//接口类:
interface Person {
void speak();
}
//真实实体类:
演员这一类别属于人。 演员这一类别具备人的属性。 演员这一类别与人类相关。 演员这一类别在人的范畴内。 演员这一类别和人有着紧密联系。 演员这一类别是人的一种表现形式。 演员这一类别归属于人这一群体。 演员这一类别与人类的特征相关联。 演员这一类别在人的体系中。 演员这一类别和人类的范畴相契合。
private String content;
这个类的构造函数 Actor 接收一个字符串类型的参数 content 。
this.content = content;
}
@Override
public void speak() {
输出这个内容。
}
}
//代理类:
Agent 可以作为 Person 被使用。
private Actor actor;
private String before;
private String after;
这个类的构造函数接收一个 Actor 类型的 actor、一个 String 类型的 before 和一个 String 类型的 after 作为参数。
this.actor = actor;
this.before = before;
this.after = after;
}
@Override
public void speak() {
//before speak
System.out.println("在演员说话之前,特工说:" + before);
//real speak
this.actor.speak();
//after speak
System.out.println("演员讲话之后,特工说:" + after);
}
}
//测试方法:
public class StaticProxy {
主函数 public static void main 带有字符串数组参数 String[] args 。
创建了一个名为 actor 的 Actor 对象,其构造函数的参数为 "I am a famous actor!"
Agent 被创建,其参数为 actor,同时包含字符串"Hello I am an agent."以及"That's all!"。
agent.speak();
}
}
2.2 动态代理
在讲关于 JDK 的动态代理方法之前,我们可以先思考一下,如果要你来构建一个能够代理任意类的任意方法的代理类,你会如何去实现呢?有一种比较简单的做法,那就是通过反射来获取 Class 和 Method,然后调用该方法,同时实现一些代理相关的方法。我对此进行了尝试,很快就找到了问题的关键所在。所以,还是采用 JDK 的动态代理接口吧。
JDK自带方法
首先介绍一下最核心的一个接口和一个方法:
首先是位于 java.lang.reflect 包中的 InvocationHandler 接口:
公共接口 InvocationHandler 。
定义了一个公共的方法 invoke,该方法接收三个参数,分别是代理对象 proxy、方法 method 和参数数组 args,用于在代理对象上调用指定的方法并传递相应的参数。
throws Throwable;
}
我们对被代理的类进行操作时,这些操作都会由该接口中的 invoke 方法来实现。这个 invoke 方法中的参数,其含义分别是:
使用方法首先是要实现该接口,接着我们能够在 invoke 方法里调用被代理类的方法并获取返回值,自然也可以在调用该方法的前后去进行一些额外的操作,以此来实现动态代理,下面的例子会详细进行阐述。
另外一个很重要的静态方法是在 java.lang.reflect 包里面的 Proxy 类的 newProxyInstance 方法。
类加载器 loader 和接口数组 interfaces 用于创建代理实例,通过调用 Proxy 的 newProxyInstance 方法来实现,该方法接收类加载器、接口数组以及实现 InvocationHandler 接口的调用处理器作为参数,最终返回一个代理对象。
Class>[] interfaces,
InvocationHandler h)
会导致 IllegalArgumentException 的抛出。
其中的参数含义如下:
该方法会给出一个经过修改的类的实例,这样就能随意地调用这个实例的方法。以下是一个实际的例子。
Fruit接口:
public interface Fruit {
public void show();
}
Apple实现Fruit接口:
公共类 Apple 实现了 Fruit 接口。
@Override
public void show() {
System.out.println("<<<);
}
}
代理类Agent.java:
public class DynamicAgent {
//实现InvocationHandler接口,并且可以初始化被代理类的对象
static class MyHandler implements InvocationHandler {
private Object proxy;
public MyHandler(Object proxy) {
this.proxy = proxy;
}
//自定义invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(">>>>before invoking");
//真正调用方法的地方
调用 method 并传入 this.proxy 和 args 作为参数,得到的结果赋值给 Object 类型的 ret 。
System.out.println 输出了“>>>after invoking”。
return ret;
}
}
//返回一个被修改过的对象
公共静态的对象进行代理操作(接口类 clazz,对象 proxy)。
创建一个代理对象,该代理对象通过 Proxy.newProxyInstance 方法创建,其类加载器为 interfaceClazz 的类加载器,实现的接口为 interfaceClazz 数组,同时指定了一个实现 InvocationHandler 接口的匿名内部类,用于处理代理对象的方法调用。
new MyHandler(proxy));
}
}
测试类:
public class ReflectTest {
公共静态的 void 方法 main,其参数为 String 类型的数组 args,会抛出 InvocationTargetException 和 IllegalAccessException 异常。
//注意一定要返回接口,不能返回实现类否则会报错
Fruit fruit 等于 (Fruit) 动态代理.代理(Fruit 类, 新的苹果());
fruit.show();
}
}
结果:
可以看到,对于不同的实现类而言,能够用同一个动态代理类去进行代理,从而实现了“一次编写到处代理”的这种效果。
但是这种方法存在一个缺点,即被代理的类必须要实现某个接口。这在很大程度上对本方法的使用场景进行了限制。下面还有另外一个方法,该方法使用了 CGlib 增强库。
2.3 CGLIB库的方法
CGlib 是一个字节码增强库,它为 AOP 等方面提供了底层的支持。接下来让我们来看一看它是怎样实现动态代理的。
引入 net.sf.cglib.proxy 包中的 Enhancer 类。
导入 net.sf.cglib.proxy 包中的 MethodInterceptor 类。
引入 net.sf.cglib.proxy 中的 MethodProxy 类。
引入 Java 语言的反射机制中的 Method 类。
一个名为 CGlibAgent 的类实现了 MethodInterceptor 接口。
private Object proxy;
获取实例的操作是针对给定的代理对象进行的。具体来说,就是获取与该代理对象相关联的实例。
this.proxy = proxy;
创建了一个名为 Enhancer 的对象,该对象通过 new Enhancer()来实例化。
enhancer 将其超类设置为 this.proxy 的类。
// 回调方法
enhancer.setCallback(this);
// 创建代理对象
return enhancer.create();
}
//回调方法
@Override
公共的对象会拦截一个对象、一个方法、一个对象数组以及一个方法代理。这个拦截操作会抛出可抛出的异常。
System.out.println("在调用之前");
//真正调用
Object 会返回 methodProxy.invokeSuper(o, objects) 的结果。
System.out.println("在调用之后");
return ret;
}
public static void main(String[] args) {
CGlibAgent 被创建了一个实例,这个实例就是 cGlibAgent,它通过 new CGlibAgent() 来实现。
Apple apple 等于 (Apple) cGlibAgent 获得实例(新的 Apple);
apple.show();
}
}
Spring 中的 AOP 方面,涉及到 @AspectJ3.1 和 @AspectJ 的由来。
AspectJ 是一个 AOP 框架。它能够在编译期对 java 代码进行 AOP 编译。这样能让 java 代码具备 AspectJ 的 AOP 功能,不过这需要特殊的编译器。可以说 AspectJ 是目前实现 AOP 框架中最成熟且功能最丰富的语言。更幸运的是,AspectJ 与 java 程序完全兼容,几乎是无缝关联的。所以对于有 java 编程基础的工程师来说,上手和使用都非常容易。
AspectJ 本身就是一门语言,它需要特定的编译器(ajc 编译器)。Spring AOP 的目的和 ApectJ 一样,都是统一处理横切业务。不过,Spring AOP 与 ApectJ 不同,它不尝试提供完整的 AOP 功能,尽管它完全有能力做到。Spring AOP 更注重与 Spring IOC 容器的结合,借助这一优势来解决横切业务问题。所以在 AOP 的功能完善方面,AspectJ 具有更大的优势。同时,Spring 注意到 ApectJ 在 AOP 的实现方式上依赖于特殊编译器(ajc 编译器),于是 Spring 很巧妙地回避了这一点,转而采用动态代理技术的实现原理来构建 Spring AOP 的内部机制(动态织入),这是它与 ApectJ(静态织入)最根本的区别。AspectJ 1.5 之后,引入了@Aspect 形式的注解风格进行开发。Spring 很快也跟上了这种方式。所以在 Spring 2.0 之后,就使用了和 AspectJ 一样的注解。请注意,Spring 使用了与 AspectJ 5 相同的注解。然而,Spring 仍然没有使用 AspectJ 的编译器。Spring 的底层是通过动态代理技术来实现的,所以它并不依赖于 AspectJ 的编译器。
Spring AOP 使用了 AspectJ 的那一套注解。其实,它实现 AOP 的底层是通过动态代理来实现的,这种动态代理包括 JDK 动态代理或者 CGLib 动态代理,并且是动态植入的。
3.2 举个栗子
小狗类,会说话:
public class Dog {
private String name;
public void say(){
System.out.println(name + "在发出汪汪的叫声!");
}
public String getName() {
return name;
}
将名字进行设置,这个名字是一个字符串类型的参数,参数名为 name 。
this.name = name;
}
}
切面类:
@Aspect //声明自己是一个切面类
public class MyAspect {
/**
* 前置通知
*/
//@Before是增强中的方位
// @Before括号中的就是切入点了
//before()就是所谓的增强(建言),简单来说,就是要去做某件事。
在执行(* com.zdy..*(..))之前
public void before(){
System 输出了一行内容,内容为“前置通知....”。
}
}
这个类是重点。首先,使用@Aspect 来声明自己是切面类。接着,通过 before() 进行增强。并且,@Before(方位)加上切入点能够具体定位到特定某个类的特定某个方法的方位。Spring 配置文件:
//开启AspectJ功能.
然后Main方法:
创建一个名为 ac 的 ApplicationContext 对象,通过 new ClassPathXmlApplicationContext("applicationContext.xml") 的方式进行实例化。
Dog dog 等于通过 ac 获取名为“dog”的 Bean 所生成的对象。
输出狗的类。
dog.say();
输出结果:
com.zdy.Dog 的增强类由 SpringCGLIB 生成,类名为 com.zdy.Dog$$EnhancerBySpringCGLIB$$80a9ee5f
前置通知....
null在汪汪叫!...
简单来说,就是将切面类放入容器中,启动一个 AdpectJ 的功能。这样一来,Spring AOP 就能依据切面类中的(@Before + 切入点),准确地找到特定类的某个方法(这里指的是 com.zdy 包下的所有类的所有方法),接着把增强 before() 切入到该方法中。
3.3 举个Spring Boot中的栗子
这个栗子很实用,它涉及到用 Aop 来做切面,以实现对 Web 请求的日志进行统一处理。
@Aspect
@Component
public class WebLogAspect {
Logger 类的 getLogger 方法获取到当前类的日志记录器,然后创建了一个私有日志记录器 logger 。
@Pointcut 表示一个切入点,它指定了一个执行公共方法的规则,这些公共方法位于 com.didispace.web 包及其子包中,方法名可以是任意的,并且可以有任意数量的参数。
public void webLog(){}
@Before("webLog()")
公共方法用于在特定操作之前执行一些准备工作。它接收一个 JoinPoint 对象作为参数,并可能抛出 Throwable 异常。这个方法的主要目的是在执行后续的业务逻辑之前,进行一些前置的处理或准备工作,以确保整个流程的正确性和稳定性。它可以根据具体的业务需求,进行各种不同的操作,如记录日志、获取上下文信息、进行权限验证等。
// 接收到请求,记录请求内容
ServletRequestAttributes 这个变量 attributes 等于 (ServletRequestAttributes) 类型的 RequestContextHolder.getRequestAttributes() 的返回值。
从 attributes 中获取到的请求为 HttpServletRequest 类型的 request 。
// 记录下请求内容
logger 输出信息,内容为“URL : ”加上 request 的请求 URL 转换为字符串后的结果
logger 输出信息,内容为“HTTP_METHOD : ”以及 request 的获取方法。
logger 输出信息,内容为“IP : ”以及 request 获取到的远程地址(request.getRemoteAddr())。
logger 输出信息,内容为:"ARGS : " 加上 joinPoint 获取的参数数组转换为字符串后的结果,即 Arrays.toString(joinPoint.getArgs())
}
在返回之后(返回值为“ret”,切入点为“webLog()”)
公共的方法用于在返回之后进行操作(Object ret 为返回的对象),会抛出 Throwable 异常。
// 处理完请求,返回内容
logger 输出信息,内容为 "RESPONSE : " 与 ret 进行拼接后的结果。
}
}
通过 @Pointcut 定义的切入点是 com.didispace.web 包下的所有函数,也就是对 web 层的所有请求处理进行切入点定义。接着通过 @Before 来实现对请求内容的日志记录,这里只是说明过程,可根据需要调整内容。最后通过 @AfterReturning 来记录请求返回的对象。
运行程序并访问“//localhost:8080/hello?name=didi”,就能够得到下面的日志输出。