面向方面编程 (AOP) 通过提供另一种思考程序结构的方式来补充面向对象编程 (OOP)。 OOP 中模块化的关键单元是类,而 AOP 中模块化的单元是方面。方面支持跨多种类型和对象的关注点(例如事务管理)的模块化。 (此类关注点在 AOP 文献中通常被称为“横切”关注点。)
Aspect: 切面 跨多个类的关注点的模块化。事务管理是企业 Java 应用程序中横切关注点的一个很好的例子。在 Spring AOP 中,方面是通过使用常规类(基于模式的方法)或使用 @Aspect 注释的常规类(@AspectJ 样式)来实现的。
Join Point: 连接点 程序执行过程中的点,例如方法的执行或异常的处理。在 Spring AOP 中,连接点始终代表方法执行。
Advice:通知:某个方面在特定连接点采取的操作。不同类型的建议包括“周围”、“之前”和“之后”通知。 (通知类型稍后讨论。)许多 AOP 框架(包括 Spring)将建议建模为拦截器,并在连接点周围维护一系列拦截器。
Pointcut: 切入点:匹配连接点的谓词。建议与切入点表达式关联,并在与切入点匹配的任何连接点运行(例如,执行具有特定名称的方法)。与切入点表达式匹配的连接点的概念是 AOP 的核心,Spring 默认使用 AspectJ 切入点表达式语言。
Target object: 目标对象:由一个或多个方面建议的对象。也称为“建议对象”。由于 Spring AOP 是使用运行时代理实现的,因此该对象始终是代理对象。
Aop Proxy: AOP 代理:由 AOP 框架创建的对象,用于实现方面契约(建议方法执行等)。在Spring框架中,AOP代理是JDK动态代理或CGLIB代理。
Weaving: 编织:将方面与其他应用程序类型或对象链接起来以创建建议的对象。这可以在编译时(例如,使用 AspectJ 编译器)、加载时或运行时完成。 Spring AOP 与其他纯 Java AOP 框架一样,在运行时执行编织。
环绕通知是最常用的通知,但是我们还是建议实现所需功能的最弱的通知类。例如,只想输出参数列表,那么最好使用before去处理,当然环绕通知也可以实现。以最优解完成所需功能,出错的可能性也会相对更小。例:不需要使用环绕通知的 JoinPoint 上调用proceed() 方法。
SpringAop
默认使用标准的JDK
动态代理作为Aop
代理。这使用任何接口都可以被代理。
因为JDK
动态代理只支持接口,所以关于类的代理,SpringAop
交给了CGLIB
官方说明:如果要代理的目标对象至少实现一个接口,则使用JDK动态代理。目标类型实现的所有接口都被代理。如果目标对象未实现任何接口,则会创建 CGLIB 代理。
如果需要指定CGLIB来进行代理的话
注解方式
@EnableAspectJAutoProxy(proxyTargetClass = true)
xml方式
<aop:aspectj-autoproxy proxy-target-class="true"/>
SpringAop的支持是在IOC之上的,也就是说 SpringAop是依赖IOC的,只有当我们的对象是Bean才能支持Aop的功能
注解方式:@EnableAspectJAutoProxy**
标注在Bean对象上
xml形式:
在Bean对象上标注@Aspect
使得成为切面。
@Aspect
@Component
public class LogPointcut {}
在切面类中定义一个void
方法,并使用@Pointcut
注解来指示切入点表达式
* com.mfyuan.aopdemo.model.ExecutionPoint.*()
*
代表修饰符。包+类+方法名
这三个都可以为*
,也可以为 User*
这样通配的形式参数列表
,
()
表示没有参数(..)
表示可以有任意参数(零个或多个)(*)
只能有一个参数(+)
表示至少有一个参数(String,..)
表示第一个必须是String,并且允许有其他任何参数(..,int,..)
表示参数列表中必须拥有一个int参数,并且允许有其他任何参数within(com.mfyuan.aopdemo.model.*)
表示com.mfyuan.aopdemo.model包下的所有类的方法都被定义为切点。args(String,int)
表示匹配方法中第一个是String,第二个是int的方法目标对象类型
的切点表达式。它通常用于匹配某个接口或类的所有实现类的方法。
target(com.mfyuan.aopdemo.service.UserService)
表示UserService的实现类或子类的所有方法都是接入点。注解
的**方法
**
@annotation(com.mfyuan.aopdemo.annotation.SwitchDataSource)
只有标注了SwitchDataSource这个注解的方法才会定义为切入点。bean(userDestroyServiceImpl)
beanname 为userDestroyServiceImpl
为切入点*Service
来处理官网:由于 Spring AOP 框架基于代理的性质,根据定义,目标对象内的调用不会被拦截。对于JDK代理,只能拦截代理上的公共接口方法调用。使用 CGLIB,代理上的公共和受保护方法调用将被拦截(如果需要,甚至包可见的方法)。但是,通过代理进行的常见交互应始终通过公共签名来设计。
简单说明:内部调用的方式,即使是切入点也不会触发通知。
多个切点可以使用 ||
&&
!
来进行组合
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
@Before
前置通知在目前方法前执行
在 ExecutionPoint
类中包含int参数的方法前调用 beforeLog
方法
@Pointcut("execution(* com.mfyuan.aopdemo.model.ExecutionPoint.*(..,int,..))")
private void executionPoint(){}
@Before(value = "executionPoint()")
public void beforeLog(JoinPoint joinPoint){
log.info("{} before",joinPoint.getSignature().getName());
}
当匹配的方法执行正常返回时运行(执行异常时不能执行)
所有方法中带有(String,int)
两个参数的方法返回成功后调用returnLog
@Pointcut("args(String,int)")
private void argPoint(){}
@AfterReturning(value = "argPoint()")
public void returnLog(JoinPoint joinPoint){
log.info("{} return",joinPoint.getSignature().getName());
}
如果需要获取到原方法执行的返回值,可以通过returning
属性来帮忙绑定一个代理方法的参数,需名称对应
@AfterReturning(value = "argPoint()",returning = "returning")
public void returnLog(JoinPoint joinPoint,Object returning){
log.info("{} return val {}\t return",joinPoint.getSignature().getName(),returning);
}
当匹配的方法执行通过引发异常退出时运行
model包下的所有类都如果抛出异常则执行throwingLog
@Pointcut("within(com.mfyuan.aopdemo.model.*)")
private void withinPoint(){}
@AfterThrowing(value = "withinPoint()")
public void throwingLog(JoinPoint joinPoint){
log.info("{} throwing ",joinPoint.getSignature().getName());
}
如果你希望只有抛出指定类型的异常时才执行的话,可以通过throwing
来绑定方法的参数类型,默认是Throwalbe
。抛出属性中使用的名称必须与通知方法中的参数名称相对应
只有当匹配的方法出现算术异常时才执行。
@AfterThrowing(value = "withinPoint()",throwing = "ex")
public void throwingLog(JoinPoint joinPoint,ArithmeticException ex){
log.info("{} throwing ",joinPoint.getSignature().getName());
}
在匹配的方法执行退出时允许。无论他是否抛出异常都将执行,类型try{}catch{}finally{}
中的finally
通常用于释放资源。
model包下的中所有的方法退出后,无论他是否产生异常。都执行afterLog
方法
@After(value = "withinPoint()")
public void afterLog(JoinPoint joinPoint){
log.info("{} after",joinPoint.getSignature().getName());
}
与@AfterRuturn不同的是@AfterRuturn只在正常退出的方法之后调用,而@After则是所有
他可以在方法之前,之后,返回后,或者抛出异常时,执行。这里的之前,之后等,都是指调用point.proceed()
时。
环绕通知的一个参数必须是ProceedingJoinPoint
类型的
@Around(value = "executionPoint()")
public Object around(ProceedingJoinPoint joinPoint){
log.info("{} around before ",joinPoint.getSignature().getName());
Object proceed = null;
try {
// 不传参数则是默认传入的参数,也可以根据默认参数进行修改后再传入
proceed = joinPoint.proceed();
log.info("{} around returning ",joinPoint.getSignature().getName());
} catch (Throwable e) {
log.info("{} around throwing ",joinPoint.getSignature().getName());
throw new RuntimeException(e);
}finally {
log.info("{} around finally ",joinPoint.getSignature().getName());
}
return proceed;
}
getArgs()
: 返回方法参数。getThis()
: 返回代理对象。也就是通过jdk动态代理。或者cglib代理出来的对象。getTarget()
: 返回目标对象。getSignature()
:返回建议的方法的说明。toString()
:打印建议的方法的有用说明可以通过args
来绑定参数。
匹配切点的第一个参数与通知方法的参数a进行绑定execution是绑定类型,而args可以绑定参数名称
@Pointcut(value = "execution(* com.mfyuan.aopdemo.model.ExecutionPoint.*(..,int,..)) && args(a,..)")
private void executionPoint(String a){}
官方样例,支持泛型,但不支持泛型集合。
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
@annotation 与注解绑定,获取注解的值,
// 切点
@Pointcut("@annotation(com.mfyuan.aopdemo.annotation.SwitchDataSource) && @annotation(name)")
private void annotationPoint(SwitchDataSource name){
}
// 通知
@Before(value = "annotationPoint(name)")
public void switchDateLog(JoinPoint joinPoint,SwitchDataSource name){
log.info("{} switchDateSource before",joinPoint.getSignature().getName());
log.info("SwitchDataSource name {}",name.name());
}
定义了多个同样的通知他的顺序是无序的,按照Ordered.getOrder()
越小的越先执行为标准。如果需要让某一个通知比较先执行,可以通过标注@Order
注解来实现该需求
public static void main(String[] args) {
// 代理了Pojo这个对象
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
// 获取这个代理对象
Pojo pojo = (Pojo) factory.getProxy();
// 这里是代理对象调用的.foo()
pojo.foo();
}
public class SimplePojo implements Pojo {
public void foo() {
// 一旦进入 对象本来,则是对象本来进行调用,而不是代理对象来进行调用。
this.bar();
}
public void bar() {
// some logic...
}
}
通常我们使用代理对象去调用方式的时候,我们是对代理对象进行调用,但是一旦到达目标对象本身,也就是自己调用自己的内部方法时,则是通过
this
对象来进行调用的。这很关键,这会导致我们一些需要的代理在内部方法调用中失效。
官方给了解决方案,但是不推荐
public class SimplePojo implements Pojo {
public void foo() {
// 通过在代理对象内部进行获取当前的代理对象,再通过代理对象去调用内部的其他方法
((Pojo) AopContext.currentProxy()).bar();
}
public void bar() {
// some logic...
}
}
class MyHttpClient{
String doGet(String url);
String doPost(String url);
String doPut(String url);
String doDelete(String url);
}
@Aspect
@Commpent
class PointcutAdrive{
@Pointcut(value = "execution(* *.MyHttpClient.do*(String,..)) && args(url)")
private void executeTimePoint(String url);
@Around(value = "executeTimePoint(url)"")
public Object executeTimeAround(ProceedingJoinPoint joinPoint,String url){
log.info("{} around before ",joinPoint.getSignature().getName());
Object proceed = null;
try {
log.info("{} execute url {} ",joinPoint.getSignature().getName(),url);
// 不传参数则是默认传入的参数,也可以根据默认参数进行修改后再传入
proceed = joinPoint.proceed();
log.info("{} around returning ",joinPoint.getSignature().getName());
} catch (Throwable e) {
log.info("{} around throwing ",joinPoint.getSignature().getName());
throw new RuntimeException(e);
}finally {
log.info("{} around finally ",joinPoint.getSignature().getName());
}
long end = System.currentTimeMillis();
log.info("execute time {}ms",end - start);
return proceed;
}
}