目录
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。下图就是AOP的核心概念和学习路线图,掌握此图是关键:
Spring中有个非常重要的知识点——AOP,即面向切面编程,spring中提供的一些非常牛逼的功能都是通过aop实现的,比如
- spring事务管理:@Transactional
- spring异步处理:@EnableAsync
- spring缓存技术的使用:@EnableCaching
- spring中各种拦截器:@EnableAspectJAutoProxy
spring中的aop功能主要是通过2种代理来实现的:1、jdk动态代理,2、cglib代理。
一、方案举例
想象一下下,写了一个功能代码(比如SayHello()),想要在函数前后都做点什么,最简单的就是去写一段硬编码:
1.写死代码
这是功能的接口
public interface Greeting {
void sayHello(String name);
}
在实现类里边去增加前置方法和后置方法
public class GreetingImpl implements Greeting {
@Override
public void sayHello(String name) {
before();
System.out.println("Hello! " + name);
after();
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
比如我们要统计每个方法的执行时间,以对性能作出评估,那是不是要在每个方法的一头一尾都做点手脚呢?这样写死的方法会累死码农们,于是来一个加强版。
2.静态代理
单独为 GreetingImpl 这个类写一个代理类,接口还是未变,实现类抽取出来放一边,这样就进行了解耦,后置和前置功能的实现放到这个静态代理类中去绑定结合:
//Greeting接口的实现类
public class GreetingImpl implements Greeting {
@Override
public void sayHello(String name) {
System.out.println("Hello! " + name);
}
}
//绑定前置和后置方法的静态代理类
public class GreetingProxy implements Greeting {
private GreetingImpl greetingImpl;
public GreetingProxy(GreetingImpl greetingImpl) {
this.greetingImpl = greetingImpl;
}
@Override
public void sayHello(String name) {
before();
greetingImpl.sayHello(name);
after();
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
用这个 GreetingProxy 去代理 GreetingImpl,下面看看客户端如何来调用:
public class Client {
public static void main(String[] args) {
Greeting greetingProxy = new GreetingProxy(new GreetingImpl());
greetingProxy.sayHello("Jack");
}
}
这样写没错,但是有个问题,每增强一个功能接口的实现类都要去实现一遍这个实现类的Proxy代理方法,最后的结果就会导致XxxProxy 这样的类会越来越多,如何才能将这些代理类尽可能减少呢?最好只有一个代理类。这时我们就需要使用 JDK 提供的动态代理了。
3.JDK代理
(1)使用方式
第一种简介的创建动态代理的方式,通过重写InvocationHandler#invoke方法
@Test
public void m2() throws Exception {
// 1. 创建代理类的处理器
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("InvocationHandler被调用的方法是:" + method.getName());
return null;
}
};
// 2. 创建代理实例
IService proxyService = (IService) Proxy.newProxyInstance(IService.class.getClassLoader(),
new Class[]{IService.class}, invocationHandler);
// 3. 调用代理的方法
proxyService.m1();
proxyService.m2();
proxyService.m3();
}
第二种可以是通过实现InvocationHandler接口,如:
public class CostTimeInvocationHandler implements InvocationHandler {
private Object target;
public CostTimeInvocationHandler(Object target){
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long starTime = System.nanoTime();
Object result = method.invoke(this.target, args);
long endTime = System.nanoTime();
System.out.println(this.target.getClass() +
".m1()方法耗时(纳秒):" + (endTime - starTime));
return result;
}
/**
* 用来创建targetInterface接口的代理对象
* @param target 需要被代理的对象
* @param targetInterface 被代理的接口
* @param <T>
* @return
*/
public static <T> T createProxy(Object target, Class<T> targetInterface) {
if (!targetInterface.isInterface()) {
throw new IllegalStateException("targetInterface必须是接口类型!");
} else if (!targetInterface.isAssignableFrom(target.getClass())) {
throw new IllegalStateException("target必须是targetInterface接口的实现类!");
}
return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), new CostTimeInvocationHandler(target));
}
}
(2)流程总结和原理
Java中的开发流程一般如下:
1、首先必须要通过实现 InvocationHandler 接口创建自己的调用处理器;这个接口中只定义了一个方法:
Interface InvocationHandler(Object proxy, Method method, Object[] args)
2、创建被代理的接口和类;
3、通过Proxy的静态方法
newProxyInstance(ClassLoaderloader, Class[] interfaces, InvocationHandler h)
创建一个代理;4、通过代理调用方法;
JDK 动态代理具体的原理主要也是在InvocationHandler类和Proxy类中实现:
1、Invocation负责对方法进行包装增强。
动态代理就是要生成一个包装类对象,由于代理的对象是动态的,所以叫动态代理。这个包装类需要我们来实现,但是jdk给出了约束,它必须实现InvocationHandler,该InvocationHandler包含被代理对象,并负责分发请求给被代理对象,分发前后均可以做增强。2、Proxy负责生成具体的动态代理类
动态代理生成的类型是$Proxy+数字
的“新的类型”,继承自Proxy,且实现了被代理的接口(所以JDK的代理需要有接口)。代理类持有InvocationHandler(继承自Proxy),继承了被代理的接口(能拿到所有的方法信息),在执行每个接口的方法直接会去调用Invocation中增强过的方法。
public final class $Proxy0 extends Proxy implements 被代理的Interface
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
4.cglib动态代理
(1)使用流程
public class CglibTest {
@Test
public void test1() {
//使用Enhancer来给某个类创建代理类,步骤
//1.创建Enhancer对象
Enhancer enhancer = new Enhancer();
//2.通过setSuperclass来设置父类型,即需要给哪个类创建代理类
enhancer.setSuperclass(Service1.class);
/*3.设置回调,需实现org.springframework.cglib.proxy.Callback接口,
此处我们使用的是org.springframework.cglib.proxy.MethodInterceptor,
也是一个接口,实现了Callback接口,
当调用代理对象的任何方法的时候,都会被MethodInterceptor接口的invoke方法处理*/
enhancer.setCallback(new MethodInterceptor() {
/**
* 代理对象方法拦截器
* @param o 代理对象
* @param method 被代理的类的方法,即Service1中的方法
* @param objects 调用方法传递的参数
* @param methodProxy 方法代理对象
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
System.out.println("调用方法:" + method);
//可以调用MethodProxy的invokeSuper调用被代理类的方法
Object result = methodProxy.invokeSuper(o, objects);
return result;
}
});
//4.获取代理对象,调用enhancer.create方法获取代理对象,
//这个方法返回的是Object类型的,所以需要强转一下
Service1 proxy = (Service1) enhancer.create();
//5.调用代理对象的方法
proxy.m1();
proxy.m2();
}
}
(2)CGLIB原理
CGLIB动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。主要工作流程分为4个步骤:
首先生成代理对象。【创建增强类enhancer,设置代理类的父类,设置回调拦截方法,返回创建的代理对象】
调用代理类中的方法。【这里调用的代理类中的方法实际上是重写的父类的拦截。重写的方法中会去调用intercept方法
- 调用intercept,方法中会对调用代理方法中的invokeSuper方法。我们去调用该方法的时候,在代理类中会先判断是否实现了方法拦截的接口,没实现的话直接调用目标类的方法;如果实现了那就会被方法拦截器拦截,在方法拦截器中会对目标类中所有的方法建立索引,其实大概就是将每个方法的引用保存在数组中,我们就可以根据数组的下标直接调用方法,而不是用反射;索引建立完成之后,方法拦截器内部就会调用invoke方法(这个方法在生成的FastClass中实现),在invoke方法内就是调用CGLIB这种方法,也就是调用对应的目标类的方法一
调用代理类中的代理方法,代理方法中通过super.method来实际真正的调用要执行的方法,CGLIB创建代理的过程,相当于创建了一个新的类,可以通过CGLIB来配置这个新的类需要实现的接口,以及需要继承的父类
CGLIB可以为类创建代理,但是这个类不能是final类型的,CGLIB为类创建代理的过程,实际上为通过继承来实现的,相当于给需要被代理的类创建了一个子类,然后会重写父类中的方法,来进行增强,继承的特性大家应该都知道,final修饰的类是不能被继承的,final修饰的方法不能被重写,static修饰的方法也不能被重写,private修饰的方法也不能被子类重写,而其他类型的方法都可以被子类重写,被重写的这些方法可以通过CGLIB进行拦截增强
5.JDK和CGLIB的区别
Java动态代理只能够对接口进行代理,不能对普通的类进行代理(因为所有生成的代理类的父类为Proxy,Java类继承机制不允许多重继承);CGLIB非常强大,不管是接口还是类,都可以来创建代理。
Java动态代理使用Java原生的反射API进行操作,在生成类上比较高效;CGLIB使用ASM框架直接对字节码进行操作,在类的执行过程中比较高效
生成类上,JDK动态代理只能对实现了接口的类生成代理,而不能针对类。CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法
二、AOP的概念
切面(Aspect):其实就是共有功能的实现。如日志切面、权限切面、事务切面等。在实际应用中通常是一个存放共有功能实现的普通Java类,之所以能被AOP容器识别成切面,是在配置中指定的。
增强(Advice):是切面的具体实现。以目标方法为参照点,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)5种。在实际应用中通常是切面类中的一个方法,具体属于哪类通知,同样是在配置中指定的。
连接点(Joinpoint):就是程序在运行过程中能够插入切面的地点。例如,方法调用、异常抛出或字段修改等,但spring只支持方法级的连接点。
切入点(Pointcut):用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点的正则表达式来定义的。
目标对象(Target):就是那些即将切入切面的对象,也就是那些被通知的对象。这些对象中已经只剩下干干净净的核心业务逻辑代码了,所有的共有功能代码等待AOP容器的切入。
代理对象(Proxy):将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象的功能等于目标对象的核心业务逻辑功能加上共有功能。代理对象对于使用者而言是透明的,是程序运行过程中的产物。
织入(Weaving):将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译期、类装载期及运行期,当然不同的发生点有着不同的前提条件。譬如发生在编译期的话,就要求有一个支持这种AOP实现的特殊编译器;发生在类装载期,就要求有一个支持AOP实现的特殊类装载器;只有发生在运行期,则可直接通过Java语言的反射机制与动态代理机制来动态实现。还有引入(Introduction),用以区分类和方法的拦截。
1.项目aop的实现
项目中aop的实现基于两种模式或者综合,Spring AspectJ+(execution拦截表达式、基于@Annotation的注解拦截)
(1).Spring + AspectJ(基于注解:通过 AspectJ execution 表达式拦截方法)
通过表达式去匹配各个符合条件的切入点,如下就实现了aop.demo.GreetingImpl类下的任意方法拦截匹配,那么匹配后的切入点都会织入代理中。
@Aspect
@Component
public class GreetingAspect {
@Around("execution(* aop.demo.GreetingImpl.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
before();
Object result = pjp.proceed();
after();
return result;
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
类上面标注的 @Aspect 注解,这表明该类是一个 Aspect(其实就是 Advisor)。该类无需实现任何的接口,只需定义一个方法(方法叫什么名字都无所谓),只需在方法上标注 @Around 注解,在注解中使用了 AspectJ 切点表达式。方法的参数中包括一个 ProceedingJoinPoint 对象,它在 AOP 中称为 Joinpoint(连接点),可以通过该对象获取方法的任何信息,例如:方法名、参数等。
虽然有了execution拦截表达式,但是有时候我们只想满足精确的某些方法,靠表达式难免会有缺漏或者是带入了一些不想带入的切入点进来,这可是很考验写拦截表达式的人,我们可以采用点对点的更精确的基于注解的方法。
(2).Spring + AspectJ(基于注解:通过 AspectJ @annotation 表达式拦截方法)
为了拦截指定的注解的方法,我们首先需要来自定义一个注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Tag {
}
以上定义了一个 @Tag 注解,此注解可标注在方法上,在运行时生效。
只需将前面的 Aspect 类的切点表达式稍作改动:
@Aspect
@Component
public class GreetingAspect {
@Around("@annotation(aop.demo.Tag)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
...
}
...
}
这次使用了 @annotation() 表达式,只需在括号内定义需要拦截的注解名称即可。
直接将 @Tag 注解定义在您想要拦截的方法上,就这么简单:
@Component
public class GreetingImpl implements Greeting {
@Tag
@Override
public void sayHello(String name) {
System.out.println("Hello! " + name);
}
}
2.项目AOP实现
项目中可能就会同时采用Spring AspectJ的拦截表达式和注解拦截方法,假设要做一个操作日志的功能,需要对某些关键操作进行记录,将要记录操作日志的方法保存为枚举类。比如常见的增删改操作都要记录其操作:
public enum Type {
DEFAULT("",""),
ADD("1","新增"),
UPDATE("2","修改"),
DELETE("3","删除")
;
private String id;
private String operationType;
Type(String id,String operationType){
this.id=id;
this.operationType=operationType;
}
public String getId() {return id;}
public void setId(String id) {this.id = id;}
public String getOperationType() {return operationType;}
public void setOperationType(String operationType) {
this.operationType = operationType;}
}
写一个注解类,该注解联合枚举就可以作为一个标记,每一个需要记录加一个对应注解,相应的操作就会留下操作类型信息,方便切面拦截下来调用处理。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface LogAnno {
//操作类型
public Type operationType() default Type.DEFAULT;
//操作备注
public String remark() default "";
}
切面类的编写,在这里我们采用了Spring AspectJ +两种拦截表达式,拦截特定类(
execution(* main.com.*.*(..)
)下的加了特定注解(@annotation(main.com.LogAnno)
)的操作,可以进行后置加强方法操作和后置返回方法操作,一个是切下了切入点的输入信息,一个是切下了切入点返回值的信息,具体按照实际情景来各取所需吧。在拦下了输入参数信息的同时,我们可以采用反射来获取输入参数的属性和值,操作日志就相当于留下了现场的快照,我们只需要在写一个操作日志的写入数据库方法就可以保存下这些信息了。
/**
* 切面类,含有多个通知
*/
@Aspect
@Component
public class MyAspect {
//声明公共切入点
@Pointcut("execution(* main.com.UserServiceImpl.*(..))")
private void PointCut1(){}
//声明指定包内的注解切入点
@Pointcut("@annotation(main.com.LogAnno) && execution(* main.com.*.*(..)) ")
private void PointCutofAnno(){}
//拦截返回值
@AfterReturning(value="PointCut1()" ,returning="ret")
public void myAfterReturning(JoinPoint joinPoint, Object ret){
System.out.println("后置通知 : " + joinPoint.getSignature().getName() + " , -->" + ret);
}
//拦截@注解的方法
@AfterReturning(value="PointCutofAnno()",returning = "ret")
public void myAfterReturningofAnno (JoinPoint joinPoint,Object ret){
//获取参数
Object[] objs=joinPoint.getArgs();
//获取返回值
Object obj=objs[0];
Map<String ,Object> inMap= getParameter(obj);
Map<String ,Object> outMap= getParameter(ret);
System.out.println(inMap);
System.out.println(outMap);
}
@Before(value="PointCutofAnno()")
public void myBeforeReturningofAnno (JoinPoint joinPoint){
//获取参数
Object[] objs=joinPoint.getArgs();
//获取返回值
Object obj=objs[0];
Map<String ,Object> inMap= getParameter(obj);
System.out.println(inMap);
}
//拓展日志的功能,对拦截的入参进行反射获取信息
private Map<String, Object> getParameter(Object obj) {
try {
//反射对象中的属性
Class clazz=obj.getClass();
Field[] fields= clazz.getDeclaredFields();
Map<String,Object> resultMap=new java.util.HashMap<>();
//遍历并返回
for(Field field:fields){
String fieldName=field.getName();
PropertyDescriptor pd=new PropertyDescriptor(fieldName,clazz);
Method readMethod = pd.getReadMethod();
Object resultObj= readMethod.invoke(obj);
resultMap.put(fieldName,resultObj);
}
return resultMap;
}
catch (Exception e){
e.printStackTrace();
}
return null;
}
}
运行test代码,最后执行结果如下图所示: