目录
- AspectJ实现AOP
- Java Agent实现AOP
- Proxy(代理)模式实现AOP
- JDK代理
- CGLIB代理
- AOP的底层实现
- 切点
- Aspect与Advisor切面
- AOP底层的实现演示
- Spring中的代理选择
在Java中,AOP(面向切面编程)的实现可以通过以下几种方法:
Spring AOP
:Spring框架提供了强大的AOP支持。Spring AOP主要基于代理模式实现,可以应用于方法执行的前后,异常处理等场景。Spring AOP可以通过XML配置或注解的方式实现。Spring AOP默认使用JDK动态代理,如果目标对象实现了接口,则使用JDK动态代理,否则使用CGLIB代理。AspectJ
:AspectJ是一个基于Java语言的AOP框架,它扩展了Java语言,提供了更丰富的AOP特性。AspectJ使用编译时织入,而不是运行时织入,因此它可以应用于任何Java应用程序,而不仅仅是Spring应用程序。AspectJ提供了更强大的功能,如环绕通知(around advice)、前置通知(before advice)、后置通知(after advice)和异常通知(after throwing advice)等。Java Agent
:它是一种特殊的工具,可以在 JVM 启动时或运行时加载,以修改类的字节码,从而实现 AOP 效果。
AspectJ实现AOP
一般我们说Spring的AOP都是代理增强,但是一种是利用插件AspectJ
直接修改class文件来实现增强。
准备 service
package com.cys.spring.chapter08;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class MyService {
private static final Logger log = LoggerFactory.getLogger(MyService.class);
public static void foo() {
log.debug("foo()");
}
}
这里要注意的是,这个aspectj类是没有交给Spring容器进行管理的
创建切面类
package com.cys.spring.chapter08;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Aspect // 注意此切面并未被 Spring 管理
public class MyAspect {
private static final Logger log = LoggerFactory.getLogger(MyAspect.class);
@Before("execution(* com.itheima.service.MyService.foo())")
public void before() {
log.debug("before()");
}
}
测试类:
package com.cys.spring.chapter08;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class TestAspectj {
private static final Logger log = LoggerFactory.getLogger(TestAspectj.class);
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(TestAspectj.class, args);
MyService service = context.getBean(MyService.class);
log.debug("service class: {}", service.getClass());
service.foo();
context.close();
}
}
运行结果发现,我们的service class并不是代理类,但是却实现了增强,打印了before()
。
这是因为我们使用的是aspectj插件进行的实现,因此无需容器也可以进行增强。
aspectj插件进行的实现,可以直接修改的是.class文件的字节码,因此静态类或不用容器直接调用对象的方法也可以执行。
Java Agent实现AOP
准备工作
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class MyService {
private static final Logger log = LoggerFactory.getLogger(MyService.class);
final public void foo() {
log.debug("foo()");
this.bar();
}
public void bar() {
log.debug("bar()");
}
}
切面类
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Aspect // 注意此切面并未被 Spring 管理
public class MyAspect {
private static final Logger log = LoggerFactory.getLogger(MyAspect.class);
//MyService类中的所有方法都被增强
@Before("execution(* com.itheima.service.MyService.*())")
public void before() {
log.debug("before()");
}
}
测试
import com.itheima.service.MyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class A10 {
private static final Logger log = LoggerFactory.getLogger(A10.class);
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A10.class, args);
MyService service = context.getBean(MyService.class);
log.debug("service class: {}", service.getClass());
service.foo();
}
}
运行得到结果,并没有得到增强。
因为我们 第一没有用aop的增强 第二没有加入插件 aspectj的插件 所以没有进行增强。
下面使用agent增强 :
运行时需要在 VM options 里加入 -javaagent:C:/Users/manyh/.m2/repository/org/aspectj/aspectjweaver/1.9.7/aspectjweaver-1.9.7.jar
把其中 C:/Users/manyh/.m2/repository 改为你自己 maven 仓库起始地址
Proxy(代理)模式实现AOP
这也是最重要的实现方式。上面两种了解即可。
JDK代理
JDK代理是JDK自带的技术,他只能对接口进行代理。在生成代理类时,JDK实际上是在运行时动态地创建字节码。这个过程是透明的,你不需要直接处理字节码。
以下是JDK动态代理的基本步骤:
- 定义接口:首先,我们需要定义一个接口,该接口将声明目标对象需要实现的方法。
- 实现目标对象:接着,我们需要实现这个接口,定义目标对象的具体行为。
- 实现InvocationHandler:我们需要创建一个类来实现
InvocationHandler
接口,并重写其invoke
方法。这个方法会在代理对象上的方法被调用时被调用,我们可以在这里添加额外的逻辑,可以使用Lambda表达式。 - 创建代理对象:最后,我们使用
Proxy
类的静态方法newProxyInstance
来创建一个代理对象。这个方法需要三个参数:类加载器、代理接口数组和InvocationHandler
对象。
示例:
package com.cys.spring.chapter08;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class JdkProxyDemo {
interface Foo {
void foo();
}
static class Target implements Foo {
@Override
public void foo() {
System.out.println("target foo");
}
}
public static void main(String[] args) {
// 原对象
Target target = new Target();
// 获取classLoader,用来加载在运行期间动态生成字节码
ClassLoader classLoader = JdkProxyDemo.class.getClassLoader();
Foo proxyInstance = (Foo) Proxy.newProxyInstance(classLoader, new Class[] {Foo.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before...");
Object res = method.invoke(target, args);
System.out.println("after...");
return res;
}
});
proxyInstance.foo();
}
}
运行结果如下:
before...
target foo
after...
这样,我们就成功地在运行时动态地为目标对象的方法前后都添加了额外的逻辑。
特点:
- 代理对象和原对象是兄弟关系,他们都实现了接口Foo
- 目标对象可以是final的,后面讲的CGLIB代理就不可以
CGLIB代理
CGLIB(Code Generation Library)是一个第三方库,它扩展了JDK的动态代理,允许对没有实现接口的类进行代理。CGLIB通过继承目标类来创建代理对象,而不是像JDK动态代理那样实现接口。因此,CGLIB代理的对象必须是可继承的。
CGLIB代理的工作原理大致如下:
- 生成子类:CGLIB在运行时动态生成一个目标类的子类。这个子类继承了目标类,并覆盖了目标类中的方法。
- 方法拦截:CGLIB通过修改字节码来在子类中插入额外的逻辑。具体来说,它会修改方法调用指令,使得当这些方法在代理对象上被调用时,它们会先调用一个拦截器(Interceptor)对象的方法。
- 拦截器实现:你需要提供一个实现了
MethodInterceptor
接口的类作为拦截器。这个类需要实现intercept
方法,该方法会在代理对象的方法被调用时被调用。你可以在这个方法中添加额外的逻辑,并决定是否调用目标对象的方法。 - 创建代理对象:使用
Enhancer
类来创建代理对象。Enhancer
是CGLIB库中的一个核心类,它提供了创建代理对象的方法。你需要设置拦截器,并指定要代理的目标类。 - 方法调用:当通过代理对象调用一个方法时,CGLIB会先调用拦截器的
intercept
方法。在这个方法中,你可以添加任何你想要的额外逻辑,然后决定是否调用目标对象的方法。如果决定调用目标对象的方法,你可以使用MethodProxy
对象来调用它。 - 字节码操作:为了实现上述功能,CGLIB使用ASM库来在运行时动态地修改字节码。ASM库允许你读取、修改和写入Java类的字节码。CGLIB通过ASM库修改目标类的字节码,以在方法调用前插入拦截器的逻辑。
示例:
package com.cys.spring.chapter08;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.InvocationHandler;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* @author Ethan
* @date 2024/3/6
* @description
*/
public class CglibProxyDemo {
static class Target {
public void foo() {
System.out.println("target foo");
}
}
public static void main(String[] args) {
Target target = new Target();
Target proxy = (Target) Enhancer.create(Target.class, new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("before...");
Object res = method.invoke(target, args);
System.out.println("after...");
return res;
}
});
proxy.foo();
}
}
运行结果一样。
另外我们发现,上面的invoke
方法还有一个参数MethodProxy
,使用他也可以完成代理,并且避免使用反射,他有两种方式使用:
// 1. 使用MethodProxy。不再使用反射
Object res = methodProxy.invoke(target, args);
// 2. 连原对象target都不需要了,需要代理对象
Object res = methodProxy.invokeSuper(o, args);
特点:
- 原对象与代理对象是父子关系,即代理对象继承了原对象;
- 因此原对象和原对象的方法都不能是final修饰。
AOP的底层实现
切点
在Spring中其提供了PointCut接口来表示切点,其源码如下:
package org.springframework.aop;
public interface Pointcut {
/**
* 返回类过滤器
*/
ClassFilter getClassFilter();
/**
* 返回方法过滤器
*/
MethodMatcher getMethodMatcher();
/**
* Canonical Pointcut instance that always matches.
*/
Pointcut TRUE = TruePointcut.INSTANCE;
}
其中最重要的两个方法是获取类过滤器和方法过滤器的方法。
在AspectJ中,有一个实现类AspectJExpressionPointcut
,其源码如下:
使用 AspectJExpressionPointcut
,可以定义复杂的切入点表达式,包括基于方法名、参数类型、返回类型、执行者(target)、参数值等的匹配规则。
Aspect与Advisor切面
在Sping底层,有两个切面的概念,一个是aspect
,一个是advisor
。
Aspect(切面)
Aspect
是一个模块化的组件,它封装了横切关注点(cross-cutting concerns)。横切关注点通常指的是那些跨越多个应用程序模块或对象的关注点,如日志记录、事务管理、安全检查等。一个切面可以包含一个或多个通知(advice),这些通知定义了何时以及如何执行横切逻辑。
在Spring AOP中,切面通常使用AspectJ的注解(如@Aspect
、@Before
、@After
、@Around
等)来定义。一个切面可能包含多个通知,每个通知都对应一个特定的连接点(join point),如方法执行、异常处理等。
Advisor(通知器)
Advisor
是Spring AOP中的另一个核心概念。它封装了一个通知(advice)和一个切入点表达式(pointcut expression),用于确定何时应用通知。切入点表达式定义了哪些连接点(如方法调用)将触发通知的执行。
在Spring AOP中,Advisor
通常以编程方式创建,而不是通过AspectJ注解。Advisor
的一个常见实现是DefaultPointcutAdvisor
,它接受一个Pointcut
和一个Advice
作为参数。
区别与联系
- 区别:
Aspect
是一个更高级的概念,它封装了多个横切关注点,并通常使用AspectJ注解来定义。Advisor
是一个更底层的概念,它封装了一个通知和一个切入点表达式,用于确定何时应用通知。
- 联系:
- 在Spring AOP中,
Aspect
可以被自动注册为Advisor
。当Spring容器检测到带有AspectJ注解的类时,它会将这些类注册为Aspect
,并将它们转换为Advisor
。 - 你可以手动创建和配置
Advisor
,但这通常不是必需的,因为Spring可以自动为你处理Aspect
到Advisor
的转换。
- 在Spring AOP中,
AOP底层的实现演示
为了更方便我们演示底层细节,这里我们选择AspectJExpressionPointcut
切点和Advisor
切面。
看如下案例:
package com.cys.spring.chapter08;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
/**
* @author Ethan
* @date 2024/3/6
* @description
*/
public class TestSpringProxy {
public static void main(String[] args) {
// 1.创建切点
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
// 添加切点表达式
pointcut.setExpression("execution(* foo())");
// 2.创建通知
MethodInterceptor advice = invocation -> {
System.out.println("before...");
Object res = invocation.proceed(); // 调用目标
System.out.println("after...");
return res;
};
// 3.创建切面
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
// 创建代理,使用Spring提供的工厂方法
Target1 target1 = new Target1();
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target1);
proxyFactory.addAdvisor(advisor);
I1 proxy = (I1) proxyFactory.getProxy();
System.out.println(proxy.getClass());
proxy.foo();
proxy.bar();
}
interface I1 {
void foo();
void bar();
}
static class Target1 implements I1 {
@Override
public void foo() {
System.out.println("target1 foo");
}
@Override
public void bar() {
System.out.println("target1 bar");
}
}
static class Target2 implements I1 {
@Override
public void foo() {
System.out.println("target2 foo");
}
@Override
public void bar() {
System.out.println("target2 bar");
}
}
}
上面代码中,我们创建两个类,分别都实现了接口,然后我们在main方法中自己创建了切点、切面、通知,以及使用代理工厂创建了代理,然后分别调用foo方法和bar方法。
运行结果如下:
class com.cys.spring.chapter08.TestSpringProxy$Target1$$EnhancerBySpringCGLIB$$8416bf5d
before...
target1 foo
after...
target1 bar
注意,如果发生以下错误:
Caused by: java.lang.ClassNotFoundException: org.aspectj.weaver.tools.PointcutDesignatorHandler
则是缺少依赖包:<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>
通过上面结果发现以下两点:
- 我们的代理对象类型为
$$EnhancerBySpringCGLIB$$8416bf5d
,可以看到他使用了CGLIB代理 - foo方法符合切点表达式,被增强了,bar方法没有
下面再看下哪些情况会使用JDK代理。
Spring中的代理选择
在Spring中,也是选择代码来实现AOP功能,而且上面说的JDK代理和CGLIB代理他都会使用。
那在Spring中什么时候使用JDK代理和什么时候使用CGLIB代理呢?
先说结论:
- 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现
- 如果目标对象实现了接口,可以强制使用CGLIB实现AOP(proxyTargetClass=true)
- 如果目标对象没有实现了接口,必须采用CGLIB库
在上面的案例中,我们的Target1已经实现了接口,为什么还是使用的CGLIB呢?那是因为Spring自己并不知道Target1到底实没实现接口,需要我们明确告诉他,代码如下:
proxyFactory.setInterfaces(target1.getClass().getInterfaces());
再次测试下:
package com.cys.spring.chapter08;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
public class TestSpringProxy {
public static void main(String[] args) {
// 1.创建切点
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
// 添加切点表达式
pointcut.setExpression("execution(* foo())");
// 2.创建通知
MethodInterceptor advice = invocation -> {
System.out.println("before...");
Object res = invocation.proceed(); // 调用目标
System.out.println("after...");
return res;
};
// 3.创建切面
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
// 创建代理,使用Spring提供的工厂方法
Target1 target1 = new Target1();
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target1);
proxyFactory.addAdvisor(advisor);
proxyFactory.setInterfaces(target1.getClass().getInterfaces());
I1 proxy = (I1) proxyFactory.getProxy();
System.out.println(proxy.getClass());
proxy.foo();
proxy.bar();
}
interface I1 {
void foo();
void bar();
}
static class Target1 implements I1 {
@Override
public void foo() {
System.out.println("target1 foo");
}
@Override
public void bar() {
System.out.println("target1 bar");
}
}
static class Target2 implements I1 {
@Override
public void foo() {
System.out.println("target2 foo");
}
@Override
public void bar() {
System.out.println("target2 bar");
}
}
}
运行结果如下:
class com.cys.spring.chapter08.$Proxy0
before...
target1 foo
after...
target1 bar
可以看到,这时候代理类就变成JDK代理实现的了。
但是当我们设置proxyTargetClass=true
时,总是使用CGLIB代理实现
package com.cys.spring.chapter08;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
public class TestSpringProxy {
public static void main(String[] args) {
// 1.创建切点
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
// 添加切点表达式
pointcut.setExpression("execution(* foo())");
// 2.创建通知
MethodInterceptor advice = invocation -> {
System.out.println("before...");
Object res = invocation.proceed(); // 调用目标
System.out.println("after...");
return res;
};
// 3.创建切面
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
// 创建代理,使用Spring提供的工厂方法
Target1 target1 = new Target1();
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target1);
proxyFactory.addAdvisor(advisor);
proxyFactory.setInterfaces(target1.getClass().getInterfaces());
proxyFactory.setProxyTargetClass(true);
I1 proxy = (I1) proxyFactory.getProxy();
System.out.println(proxy.getClass());
proxy.foo();
proxy.bar();
}
interface I1 {
void foo();
void bar();
}
static class Target1 implements I1 {
@Override
public void foo() {
System.out.println("target1 foo");
}
@Override
public void bar() {
System.out.println("target1 bar");
}
}
static class Target2 implements I1 {
@Override
public void foo() {
System.out.println("target2 foo");
}
@Override
public void bar() {
System.out.println("target2 bar");
}
}
}
运行后如下:
class com.cys.spring.chapter08.TestSpringProxy$Target1$$EnhancerBySpringCGLIB$$f9f7352c
before...
target1 foo
after...
target1 bar