开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情 Spring框架的两大核心之一:AOP,有时候面试会问到AOP的原理是什么。可能很多人会回答是代理,但是再往深的理解可能就了解的不太多了。这节内容就来具体分析一下到底有哪些方式可以实现AOP。
Aspectj
上面提到了AOP的原理可以理解为代理方式实现的,这里文章之初就先把这个观点给否定掉。因为,不一定需要代理才可以实现AOP增强,创建如下例子。
@Service
public class MyService {
public void foo(){
System.out.println("foo执行");
}
}
@Aspect
public class MyAspect {
@Before("execution(* com.a07.MyService.foo())")
public void before(){
System.out.println("before执行");
}
}
@SpringBootApplication
public class Application07 {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application07.class, args);
MyService bean = context.getBean(MyService.class);
bean.foo();
context.close();
}
}
该例子是一个简单的AOP增强逻辑,如果细心观察可以发现切面类没有添加相应的注解来使其加入Srping容器之中,按理是无法执行增强逻辑的。这里直接说结论,是可以的,因为在这个程序中引入了aspectj的一个插件如下。
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.14.0</version>
<configuration>
<complianceLevel>1.8</complianceLevel>
<source>8</source>
<target>8</target>
<showWeaveInfo>true</showWeaveInfo>
<verbose>true</verbose>
<Xlint>ignore</Xlint>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
通过该插件编译,可以直接将增强代码编译到原始类中,不再依靠代理的方式。这种方式,是在编译期对类进行修改,平时基本使用不到。因为直接在编译期修改类本身,那么这种方式也就可以用来增强static修饰的静态方法。正因如此,该方式可以脱离Spring本身来使用,直接new出原始类进行调用,也会被增强。
Agent
agent称为java探针技术,直译就是代理,是一种可以动态修改java字节码的技术,具体细节可以查看相关文档了解。通过这个特性,我们也可以用来代替spring实现aop的功能增强,使用的jar包就是aspectjweaver提供的jar包,我们在VM执行参数上添加如下代码。
-javaagent:D:\maven\repository\org\aspectj\aspectjweaver\1.9.4\aspectjweaver-1.9.4.jar
这样在类加载的过程中就会根据相应的逻辑来进行字节码的修改。这种方式和上面类似,都是修改文件本身,都没有产生新的代理类来实现。不过这种方式使用也是更为少见,而且aspectjweaver包对新版本的jdk支持也比较欠缺。
Proxy
以上两种方式,做一定的了解便可,只是为了否定掉AOP的实现只能用代理的说法。我们重点关注的还是spring本身在使用的,也是最为重要的Proxy方式。该方式分两种,一种是JDK代理,一种是Cglib代理。下面我们从这两个角度来进行分析。
JDK代理
应该很多人知道jdk代理是对接口的一种代理方式,我们创建一个例子如下。
public class JdkProxyDemo {
interface Foo {
void foo();
}
static class Target implements Foo {
@Override
public void foo() {
System.out.println("foo执行");
}
}
public static void main(String[] args) {
ClassLoader classLoader = JdkProxyDemo.class.getClassLoader();
Foo fooProxy = (Foo) Proxy.newProxyInstance(classLoader, new Class[]{Foo.class}, (proxy, method, args1) -> {
System.out.println("before-----");
Object invoke = method.invoke(new Target(), args1);
System.out.println("after------");
return invoke;
});
fooProxy.foo();
}
}
这里只是一个jdk代理的简单案例,我们分析一下代码构成,既然JDK代理是对接口的代理方式,我们就创建一个接口,并创建一个实现类。代理的实现依托于Proxy类,使用该类的newProxyInstance可以创建代理对象,里面需要三个参数,按顺序来分析其中含义如下:
- ClassLoader:类加载器,因为需要创建代理类,新创建出来的类需要类加载器进行加载,这里和当前类使用同一种类加载器即可。
- Class<?>[]:一个Class的集合,正是需要被代理的接口集合。
- InvocationHandler:调用的处理逻辑就是写在这里,是一个接口,使用匿名内部类来实现。
针对InvocationHandler的invoke方法三个参数做进一步的解析如下:
- proxy:代理对象。
- method:被调用的方法,可以使用反射来调用方法本身。
- args:被调用方法的参数。
了解了各个参数的含义,再回看上面的示例代码,我们使用method参数反射调用方法本身,并传入调用类和参数,最后将方法返回值进行返回,我们写的示例代码foo方法没有返回值,但是代码本身不知道哪些方法有返回值哪些方法没有返回值,这里做一个统一的Object类型返回。在调用的上下我们可以添加增强逻辑的代码。
Cglib代理
Cjlib是通过继承来进行代理,我们再创建一个例子如下。
public class CglibProxyDemo {
static class Target {
public void foo() {
System.out.println("foo执行");
}
}
public static void main(String[] args) {
Target targetProxy = (Target) Enhancer.create(Target.class, (MethodInterceptor) (o, method, objects, methodProxy) -> {
System.out.println("before-----");
Object invoke = method.invoke(new Target(), objects);
System.out.println("after------");
return invoke;
});
targetProxy.foo();
}
}
这里代码和JDK代理的代码有很多相似的地方,最大的区别是创建代理的方式,我们这里使用Enhancer的create方法来创建代理对象,这里我们继续分析方法参数的含义:
- Class:需要被代理的类,也就是代理对象的父类。
- MethodInterceptor:类似上面的InvocationHandler,只是这里多了一个参数methodProxy,后面再介绍。
这里没有添加类加载器,阅读源码后会发现,源码中是通过被代理类来获取被代理类的类加载器进行使用,也就是Target的类加载器。
其他的基本和JDK代理方式的调用是一致的,也是通过方法的反射来进行调用。这里重点关注一下多出来的第四个参数:methodProxy。直译为方法代理,我们直接使用。
Object invoke = methodProxy.invoke(o,objects);
使用方式和methodProxy一模一样,但是有很大的区别,区别就是,该方式没有使用反射,具体的原因待后面对Cglib的底层原理分析来进行解答。不过还有另外一种用法如下代码所示。
Object invoke = methodProxy.invokeSuper(o,objects);
这里的o是上面MethodInterceptor中的代理类对象。这里也没有用反射,区别就是,这里不需要目标对象,只需要代理对象即可,所以也不需要去new一个目标对象了。这里可以说一下,Spring使用的是上面反射的方式,并没有使用methodProxy参数调用的方式。
这里特殊强调一点Cglib由于使用继承的方式来实现代理,所以一些不允许继承的情况就需要避免掉,比如在Target类添加final关键字,是会直接报错。也不能在foo方法上添加final关键字,虽然不会报错,但是由于代理子类无法继承该方法,无法达到增强效果。