AOP(面向切面编程)是一种编程思想,其作用在于在不改变其原始设计的基础上进行功能增强。这也是Spring的开发理念:无侵入式编程。其实,这是一种代理思想,事实上,SpringAOP是动态代理的一种形式。
在进行AOP的学习前,我们先来了解一下代理:代理的作用是增强程序的功能,其遵循的也是无侵入式编程的思想,即我们的原始程序(委托方)只负责我们自己的功能即可,将一些额外的功能交给代理程序(代理方)去完成。
代理可分为两种,一种是静态代理,另一种是动态代理,其中静态代理可采用继承与组合方法来实现。
1.静态代理
1.1通过继承实现静态代理
通过继承被代理对象,重写被代理方法,可以对其进行代理。
优点:被代理类无需实现接口
缺点:只能代理这个类,要想代理其他类,要想代理其他类需要写新的代理方法。
public class Tank{public void move() {System.out.println("Tank moving cla....");}public static void main(String[] args) {new ProxyTank().move();}
}
class ProxyTank extends Tank{@Overridepublic void move() {System.out.println("方法执行前...");super.move();System.out.println("方法执行后...");}
}
1.2使用组合方法实现静态代理
定义一个 Movable 接口被代理类需要和代理类都需要实现该接口。(接口在这里的目的就是起一个规范作用保证被代理类和代理类都实现了move()方法)。代理类需要将该接口作为属性,实例化时需要传入该接口的对象,这样该代理类就可以实现代理所有实现Movable的类了。
优点:可以代理所有实现接口的类。
缺点:被代理的类必须实现接口。
public class Tank implements Movable{@Overridepublic void move() {System.out.println("Tank moving cla....");}public static void main(String[] args) {Tank tank = new Tank();new LogProxy(tank).move();}
}
class LogProxy implements Movable{private Movable movable;public LogProxy(Movable movable) {this.movable = movable;}@Overridepublic void move() {System.out.println("方法执行前....");movable.move();System.out.println("方法执行后....");}
}
interface Movable {void move();
}
动态代理之SpringAOP
我们首先来了解一下关于SpringAOP中的一些基本概念:
我们通过一个例子来了解上面的基础概念:
AOP流程
上面的流程事实上是为了告诉我们一件事,如果切入点与通知匹配成功,那么此时我们使用的原始对象就不是原本的Bean了,而是通过代理创建的代理对象,如果没有成功,那么依旧使用原始对象,这样也就不会发生报错、异常等问题了。
切入点表达式
关于切入点表达式 的书写,可以采用通配符的形式,快速描述。
AOP通知类型
AOP通知类型可分为五种,分别是前置通知
(@Before)
、后置通知(@After)
、环绕通知(@Around)
、返回后通知(执行成功通知,@AfterReturning
)以及抛出异常后通知(
@AfterThrowing
)。其中环绕通知是最常用的,也是最重要的。
对于ProceedingJoinPoint这个形参,如果不使用这个形参调用pjp.proceed()方法,那么则会跳过原始方法的执行,即隔离原始方法,我们可以通过这个特性进行权限控制,比如我们在执行某个操作时进行身份校验,如果身份符合,那么我们就执行原始方法,否则就不执行。
小案例
那么,我们来实现一个小案例,计算出一个方法的万次执行时间:
首先,我们需要在Spring容器中扫描AOP包,并告诉Spring容器要以注解形式使用AOP。
package configs;
import org.springframework.context.annotation.*;
@Configuration
@ComponentScan({"dao","service","aop"})
@PropertySource({"classpath:db.properties"})
@Import({JdbcConfig.class, MybatisConfig.class})
@EnableAspectJAutoProxy//告诉Spring我是以注解形式开发的AOP,去启动Aspect
public class SpringConfig {
}
随后我们定义通知,我们选择环绕通知来实现,其步骤为,首先需要定义一个切入点,这个切入点的表达式为@Pointcut("execution(* service.*.*(..))")//1.定义切入点
,代表我们对service层内容都进行切入。
随后我们需要绑定关系,即定义切面:使用注解形式@Around("pt()")
需要注意的是,在使用环绕通知时我们为了对原始操作进行调用,因此需要传入一个ProceedingJoinPoint
对象,其调用proceed
方法代表执行原始操作。
并且我们需要将该通知转换为Bean,因为Spring是管理Bean的。即在开头加上@Component注解
同时还要告诉Spring在读到我后将我作为AOP处理。
最后完整代码如下:
package aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Component//3要让Spring可控制,将其变成Bean
@Aspect//读到我后把我当AOP处理
public class MyAdvice {@Pointcut("execution(* service.*.*(..))")//1.定义切入点private void pt(){}//2.绑定关系即切面
// @Around("pt()")
// public void around(ProceedingJoinPoint pjd) throws Throwable {
// System.out.print("before..\n");
// //表示对原始操作的调用,需要传入一个ProceedingJoinPoint对象,其调用proceed方法代表执行原始操作
// pjd.proceed();
// System.out.print("\nafter..\n");
// }@Around("pt()")public Object around(ProceedingJoinPoint pjd) throws Throwable {long start=System.currentTimeMillis();//表示对原始操作的调用,需要传入一个ProceedingJoinPoint对象,其调用proceed方法代表执行原始操作for(int i=0;i<10000;i++){Object rs=pjd.proceed();}long end=System.currentTimeMillis();System.out.print("执行万次时间为: "+(end-start)+"ms");return 100;}
}
此外,需要注意的是,上面并没有给出具体是哪个方法的执行效率,这不方便我们进行有针对性的优化,我们可以通过Signature signature=pjd.getSignature()来获取对象信息,完整代码如下:
@Around("pt()")public Object around(ProceedingJoinPoint pjd) throws Throwable {Signature signature=pjd.getSignature();long start=System.currentTimeMillis();//表示对原始操作的调用,需要传入一个ProceedingJoinPoint对象,其调用proceed方法代表执行原始操作for(int i=0;i<10000;i++){Object rs=pjd.proceed();}long end=System.currentTimeMillis();System.out.print(signature.getDeclaringTypeName()+"的"+signature.getName()+"方法执行万次时间为: "+(end-start)+"ms");return rs;}
此外,我们还需要注意的是,通知方法是需要有返回值的,即原始对象的返回值我们需要返回一下(此时的原始对象已经是通过代理生成的),同时我们可以在该模块对返回结果进行封装修改,比如查询结果加上分页功能。
AOP获取数据
在某些情况,我们需要根据不同的原始对象做出不同的处理,此时我们就需要通过获取原始对象中的数据来做出区分,从而做出对应的处理,在这里,数据可分为参数、返回值以及异常
。
那么该如何获取呢?很简单,我们只需要两个对象即可:JoinPoint
与ProceedingJoinPoint
,其中后者是前者的子类,我们多用后者获取环绕通知的数据,而前者获取其他通知的数据。
那么,这个数据获取了有什么用呢?事实上,我们可以对获取的参数进行处理,比如我们所需要的数据编码格式为utf8类型,这里我们就可以对参数转换一下。
@Around("pt()")public Object around(ProceedingJoinPoint pjd) throws Throwable {Object[] args=pjd.getArgs();//表示对原始操作的调用,需要传入一个ProceedingJoinPoint对象,其调用proceed方法代表执行原始操作for(int i=0;i<10000;i++){Object rs=pjd.proceed(args);}return rs;}
同样的,我们也可以使用JoinPoint来获取参数。在环绕通知中,使用ProceedingJoinPoint可以获取参数以及结果,那么对于其他通知类型呢?我们可以在参数列表中给定一个Object对象来接收,同时还要在@AfterReturning的注解中指定接收值是rs这个对象,即:(value = “pt()”,returning = “rs”)
@AfterReturning(value = "pt()",returning = "rs")//这里指定的rs一定要与下面形参中的rs保持一致。public Object aftermethod(Object rs){System.out.print(rs+"\n");return rs;}
此外,需要注意的是,如果形参中有JoinPoint对象,那么该对象一定要在Object这个接收对象之前,即
@AfterReturning(value = "pt()",returning = "rs")public Object aftermethod(JoinPoint jp,Object rs){System.out.print(rs+"\n");return rs;}
那么对于异常该如何接收呢,很简单,和上面的写法类似:
@AfterReturning(value = "pt()",returning = "t")public Object afterthrow(Throwable t){System.out.print(t+"\n");return t;}
总结一下,AOP是一种编程范式,称为面向切面编程。可以用于功能增强。通常我们都对接口进行描述,如果是对实现类则会导致耦合度太高。