AOP(Aspect Orient Programming),我们一般称为面向切面编程,作为面向对象的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务、日志、缓存、分布式锁等等。AOP实现的关键在于AOP框架自动创建的AOP代理,AOP代理主要分为静态代理和动态代理,静态代理的代表为AspectJ;而动态代理则以Spring AOP为代表。Spring的主要动态代理有CGLib和JDK自动代理。
使用AspectJ的编译时增强实现AOP
AspectJ是静态代理的增强,所谓的静态代理就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强。
编译成字节码.class比原来的.java会
多了一些代码,这就是AspectJ的静态代理,它会在编译阶段将Aspect织入Java字节码中, 运行的时候就是经过增强之后的AOP对象。
使用Spring AOP
与AspectJ的静态代理不同,Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler
接口和Proxy
类。
如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final
,那么它是无法使用CGLIB做动态代理的。
现在我们做一个测试:
首先定义一个接口:
package cn.chinotan.service;
/**
* @program: test
* @description: 动物
* @author: xingcheng
**/
public interface Animal {
/**
* 跑
* @param where 在什么地方跑
* @return
*/
String run (String where);
}
其实现类:
package cn.chinotan.service.impl;
import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
/**
* @program: test
* @description: 狗
* @author: xingcheng
**/
@Service
public class Dog implements Animal {
@Action
@Override
public String run(String where) {
System.out.println("狗往" + where + "跑");
return "地点是:" + where;
}
}
其中@Action为自定义的注解,用来指定aop代理的切入点
package cn.chinotan.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @program: test
* @description: 动作
* @author: xingcheng
**/
@Target(ElementType.METHOD)
public @interface Action {
}
定义Aspect:
package cn.chinotan.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* @program: test
* @description: 动作aop实现
* @author: xingcheng
**/
@Aspect
@Component
public class ActionAspect {
@Pointcut("@annotation(cn.chinotan.aop.Action)")
void actionPointCut() {
}
@Before("actionPointCut()")
void beforeAction() {
System.out.println("热身运动");
}
}
其中有几种aop的通知注解:
- @Before: 前置通知, 在方法执行之前执行
- @After: 后置通知, 在方法执行之后执行
- @AfterRunning:返回通知, 在方法成功执行返回结果之后执行
- @AfterThrowing: 异常通知, 在方法抛出异常之后
- @Around: 环绕通知,围绕着方法执行
@Pointcut是切入点的注解:
这里使用了@annotation 可以在使用了自定义注解的配置方法上实现切入
也可以使用execution(* *(..))的形式:
声明切入点
第一个*表示 方法 返回值(例如public int)
第二个* 表示方法的全限定名(即包名+类名)
perform表示目标方法参数括号两个.表示任意类型参数
方法表达式以“*”号开始,表明了我们不关心方法返回值的类型。然后,我们指定了全限定类名和方法名。对于方法参数列表,
我们使用两个点号(..)表明切点要选择任意的perform()方法,无论该方法的入参是什么
execution表示执行的时候触发
在启动的application.yml配置文件中加入
spring.aop.proxy-target-class: false
这个是控制aop的具体实现方式,为true 的话使用cglib,为false的话使用java的Proxy,默认是false
之后运行controller:
package cn.chinotan.controller;
import cn.chinotan.service.Animal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @program: test
* @description: test类
* @author: xingcheng
**/
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
Animal animal;
@GetMapping("/aopRun")
public String aopRun() {
animal.run("狗窝");
System.out.println("dog代理为:" + animal.getClass());
return "ok";
}
}
打印日志:
可以看到类型是com.sun.proxy.$Proxy71
,也就是前面提到的Proxy类,因此这里Spring AOP使用了JDK的动态代理。
再来看看不实现接口的情况,修改Dog类:
配置proxy-target-class: false依旧
package cn.chinotan.service.impl;
import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
/**
* @program: test
* @description: 狗
* @author: xingcheng
**/
@Service
public class Dog {
@Action
public String run(String where) {
System.out.println("狗往" + where + "跑");
return "地点是:" + where;
}
}
打印日志:
可以看到类被CGLIB增强了,也就是动态代理。这里的CGLIB代理就是Spring AOP的代理,这个类也就是所谓的AOP代理,AOP代理类在切点动态地织入了增强处理。
可以看到:
AspectJ在编译时就增强了目标对象,Spring AOP的动态代理则是在每次运行时动态的增强,生成AOP代理对象,区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。
java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP,如果目标对象实现了接口,可以强制使用CGLIB实现AOP,如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换
误区注意:
在平时开发中,我们通常在Service中定义了一个方法并且切入之后,从Controller里面调用该方法可以实现切入,但是当在同一个Service中实现另一方法并调用改方法时却无法切入
类似于:
package cn.chinotan.service.impl;
import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
/**
* @program: test
* @description: 狗
* @author: xingcheng
* @create: 2018-10-27 16:00
**/
@Service
public class Dog implements Animal {
@Action
@Override
public String run(String where) {
System.out.println("狗往" + where + "跑");
return "地点是:" + where;
}
@Override
public void runToEat(String food) {
run("狗窝");
System.out.println("狗在吃" + food);
}
}
package cn.chinotan.service.impl;
import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
/**
* @program: test
* @description: 狗
* @author: xingcheng
* @create: 2018-10-27 16:00
**/
@Service
public class Dog implements Animal {
@Action
@Override
public String run(String where) {
System.out.println("狗往" + where + "跑");
return "地点是:" + where;
}
@Override
public void runToEat(String food) {
run("狗窝");
System.out.println("狗在吃" + food);
}
}
我们在执行runToEat方法时,调用了自己类中的另一个方法,结果为:
可以看到run()的切面方法并没有执行,以上结果的出现与Spring AOP的实现原理息息相关,由于Spring AOP采用了动态代理实现AOP,在Spring容器中的bean(也就是目标对象)会被代理对象代替,代理对象里加入了我们需要的增强逻辑,当调用代理对象的方法时,目标对象的方法就会被拦截,
通过调用代理对象的action方法,在其内部会经过切面增强,然后方法被发射到目标对象,在目标对象上执行原有逻辑,如果在原有逻辑中嵌套调用了work方法,则此时work方法并没有被进行切面增强,因为此时它已经在目标对象内部。而解决方案很好地说明了,将嵌套方法发射到代理对象,这样就完成了切面增强。可以看下源码:
在代码3处,如果配置了exposeProxy开关,则会将代理对象暴露在当前线程中,以供其它需要的地方使用,通过使用静态的全局ThreadLocal变量就解决了问题。
spring提供了一个这样的类:
可以看到他可以获取到当前的aop代理,但是在获取之前,得开启exposeProxy开关
@EnableAspectJAutoProxy(proxyTargetClass = false, exposeProxy = true)
这样就可以进行代理了,打印日志为:
既然这样可以,那是不是直接applicationContext.getBean()也可以呢?实验过后得到的结果是可行,而且配置中的expose-proxy也不用设置成true,那试一下:
package cn.chinotan.service.impl;
import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
/**
* @program: test
* @description: 狗
* @author: xingcheng
* @create: 2018-10-27 16:00
**/
@Service
public class Dog implements Animal {
@Autowired
ApplicationContext applicationContext;
@Action
@Override
public String run(String where) {
System.out.println("狗往" + where + "跑");
return "地点是:" + where;
}
@Override
public void runToEat(String food) {
// Dog dog = (Dog) AopContext.currentProxy();
Dog dog = (Dog) applicationContext.getBean("dog");
dog.run("狗窝");
System.out.println("狗在吃" + food);
}
}
打印日志为:
可见同样可以