以下转载和参考自:装配AOP - 廖雪峰的官方网站。
1、代理模式和AOP
前面说过Proxy(代理)模式,比如下面的Foo类中的func1和func2在执行前后需要验证权限和记录日志,那么实际上适合将这些动作提取到一个代理类中去,因为验证权限、日志记录这些功能实际上并不属于Foo类主逻辑功能,这样能降低代码耦合度。
public class Foo {
public void func1() {
securityCheck(); //权限检查
......
log("log");//日志记录
}
public void func2() {
securityCheck(); //权限检查
......
log("log");//日志记录
}
}
public class FooProxy implements Foo{
private final Foo target;
public FooProxy(Foo target) {
this.target = target;
}
public void func1() {
securityCheck(); //权限检查
target.func1();
log("log");//日志记录
}
public void func2() {
securityCheck(); //权限检查
target.func2();
log("log");//日志记录
}
}
在Spring中,可以通过AOP(Aspect Oriented Programming),即面向切面编程来实现代码插入的功能。在AOP中,上面的Foo类中的func1、func2称为“核心逻辑”,权限验证、记录日志这些称为“切面逻辑”,AOP要实现的就是把切面插入到核心逻辑中,通过Proxy模式来实现。
2、实现AOP
如下通过AOP实现了调用Foo中任何方法之前会先执行MyAspect 类中的doAccessCheck()方法,调用Foo中任何方法之后会执行MyAspect 类中的doLogging()方法,调用Bar中任何方法之前之后会执行MyAspect中的beforeBarFun()、afterBarFun()方法。实现AOP的方法是在切面类中添加@Component和@Aspect注解,在@Configuration类(Ioc配置类)加上一个@EnableAspectJAutoProxy注解:
@Component
public class Foo {
public void fun(){
System.out.println("Foo func()");
}
}
@Component
public class Bar {
public void fun(){
System.out.println("Bar func()"); //err.print会以红色显示输出
}
}
@Aspect
@Component
public class MyAspect {
// 在执行Foo的每个方法前执行doAccessCheck()
@Before("execution(public * xsl.Foo.*(..))")
public void doAccessCheck() {}
// 在执行Foo的每个方法后执行doLogging()
@After("execution(public * xsl.Foo.*(..))")
public void doLogging() {}
// 在执行Bar的每个方法前执行beforeBarFun()、每个方法后执行afterBarFun
@Around("execution(public * xsl.Bar.*(..))")
public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {
beforeBarFun();
Object retVal = pjp.proceed();
afterBarFun();
return retVal;
}
private void beforeBarFun(){}
private void afterBarFun(){}
}
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class App {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(App.class);
Bar b = context.getBean(Bar.class);
b.fun();
Foo f = context.getBean(Foo.class);
f.fun();
}
}
AOP的相关依赖:
<properties>
<project.build.srouceEncoding>UTF-8</project.build.srouceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<java.version>11</java.version>
<spring.version>5.2.1.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
AOP中的拦截器有以下类型:
-
@Before:这种拦截器先执行拦截代码,再执行目标代码。如果拦截器抛异常,那么目标代码就不执行了;
-
@After:这种拦截器先执行目标代码,再执行拦截器代码。无论目标代码是否抛异常,拦截器代码都会执行;
-
@AfterReturning:和@After不同的是,只有当目标代码正常返回时,才执行拦截器代码;
-
@AfterThrowing:和@After不同的是,只有当目标代码抛出了异常时,才执行拦截器代码;
-
@Around:能完全控制目标代码是否执行,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能。
拦截器中的注解值使用AspectJ注入语法,其它的示例还有:
@Before("execution(public * xsl.Foo.fun1(..))") //只有Foo类的fun1()方法才会被插入
@Before("execution(public * xsl.*.*(..))") //xsl包下的所有方法都会被插入
@Around("execution(public * update*(..))") //update开头的方法都会被插入
可以只对使用了指定注解的方法才进行Before、Around等插入操作。比如下面@Around的注解值表示只有在使用了@MetricTime注解的方法调用的时候才会插入执行metric()中代码:
@Target(METHOD)
@Retention(RUNTIME)
public @interface MetricTime { //自定义的注解
String value();
}
@Component
public class Foo {
@MetricTime("TimeCost") //使用@MetricTime注解
public void fun(){
System.out.println("Foo func()");
}
}
@Aspect
@Component
public class LoggingAspect {
@Around("@annotation(metricTime)")
public Object metric(ProceedingJoinPoint joinPoint, MetricTime metricTime) throws Throwable {
String name = metricTime.value(); //注解的value值"TimeCost"
long start = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
long t = System.currentTimeMillis() - start;
System.out.println(name + ": " + t + "ms");
}
}
}
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class App {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(App.class);
Foo f = context.getBean(Foo.class);
f.fun();
}
}
3、AOP原理
AOP的原理其实就是编写核心逻辑类的子类,并持有原始实例的引用,如下所示。使用AOP,实际上就是让Spring自动为我们创建一个Proxy,使得调用方能无感知地调用指定方法,但运行期却动态“织入”了其他逻辑,因此,AOP本质上就是一个代理模式。
public FooProxy extends Foo {
private Foo target;
private LoggingAspect aspect;
public FooProxy(Foo target, LoggingAspect aspect) {
this.target = target;
this.aspect = aspect;
}
public void fun() {
// 先执行Aspect的代码:
aspect.doAccessCheck();
// 再执行UserService的逻辑:
return target.fun();
}
}
在Java平台上,对于AOP的织入,有3种方式:
- 编译期:在编译时,由编译器把切面调用编译进字节码,这种方式需要定义新的关键字并扩展编译器,AspectJ就扩展了Java编译器,使用关键字aspect来实现织入;
- 类加载器:在目标类被装载到JVM时,通过一个特殊的类加载器,对目标类的字节码重新“增强”;
- 运行期:目标对象和切面都是普通Java类,通过JVM的动态代理功能或者第三方库实现运行期动态织入。
最简单的方式是第三种,Spring的AOP实现就是基于JVM的动态代理。关于动态代理,可以参见JAVA之反射_milanleon的博客-CSDN博客_类的反射 中相关的内容。JVM的动态代理要求必须实现接口,如果一个普通类没有业务接口,就需要通过CGLIB或者Javassist这些第三方库实现。Spring中也是支持AspectJ,但仅仅是支持AspectJ的切点解析和匹配,即使用@AspectJ注解来做AOP,但并没有使用AspectJ其中的实现AOP的方法,Spring具体的AOP实现还是JDK动态代理与CGLIB(Spring对接口类型使用JDK动态代理,对普通类使用CGLIB创建子类。如果一个Bean的class是final,Spring将无法为其创建子类)。
4、AOP避坑
①、访问使用AOP的Bean时,总是调用方法而非直接访问成员。
②、如果Bean有可能使用AOP的话,不要编写public final成员方法。
如下所示的Foo类,在使用了AOP后,从ApplicationContext中获取的Foo实例实际上是代理类FooProxy对象,而代理类的构造方法中并没有调用super()来执行父类Foo的构造方法,所以成员zoneId实际上并没有被初始化。虽然Java语言规定,任何类的构造方法,第一行必须调用super(),如果没有,编译器会自动加上,但Spring使用CGLIB构造的FooProxy类是直接生成字节码文件,绕过了编译器编译字节码这个步骤。所以直接引用Foo类成员获得的是null。调用final成员方法获得的zoneId成员也为null,因为FooProxy类无法覆写final方法,调用该方法就相当于是直接访问ZoneId成员。
@Component
public class Foo {
public ZoneId zoneId = ZoneId.systemDefault(); //该初始化实际上是在Foo类的构造方法中执行
public final ZoneId getFinalZoneId() { return zoneId; }
public ZoneId getZoneId() { return zoneId; }
}
@Aspect
@Component
public class LoggingAspect {
@Before("execution(public * xsl.Foo.*(..))") //对Foo类使用AOP:执行其方法前先调用metric()方法
public void metric() {
System.out.println("metric func()");
}
}
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class App {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(App.class);
Foo f = context.getBean(Foo.class);
ZoneId zoneId = f.zoneId; // zoneId为null
zoneId = f.getFinalZoneId(); // zoneId为null
zoneId = f.getZoneId(); //zoneId不为null
}
}
5、总结
AOP称为“面向切面编程”,使用AOP可以实现在指定类的方法执行前或执行后插入其它操作,比如想要在Foo的func()方法执行前进行一些其它操作的话,那么可以定义一个切面类,在切面类中定义要插入的动作。