AOP

AOP

AOP的实现有AspectJ、JDK动态代理、CGLIB动态代理,SpringAOP不是一种新的AOP实现,其底层采用的是JDK或CGLIB动态代理。

AOP是一种思想,Spring AOP是实现AOP的一种技术实现

AspectJ是一个基于Java语言的AOP框架。从Spring2.0以后引入了AspectJ的支持。对于目前的Spring框架,建议开发者使用AspectJ实现Spring AOP。使用AspectJ实现SpringAOP的方式有两种,一种是基于XML配置开发AspectJ,二是基于注解开发AspectJ。

spring 结合 AspectJ实现AOP

参考文档

1. AOP定义

AOP (Aspect Orient Programming),面向切面编程,AOP 是一种编程思想,是面向对象编程的一种补充。传统的OOP开发中的代码逻辑是至上而下的,在这些至上而下的过程中会产生一些横切性的问题(核心业务中总掺杂着一些不相关联的特殊业务),这些横切性的问题和我们的主业务逻辑关系不大,会散落在代码的各个地方,造成难以维护。AOP的编程思想就是把业务逻辑和横切的问题进行分离,从而达到解耦的目的,使代码的重用性和开发效率高。

如下图所示:

 

 

 AOP可以拦截指定的方法并且对方法增强,而且无需侵入到业务代码中,使业务与非业务处理逻辑分离,比如Spring的事务,通过事务的注解配置,Spring会自动在业务方法中开启、提交业务,并且在业务处理失败时,执行相应的回滚策略。

2. AOP的作用

AOP 采取横向抽取机制(动态代理),取代了传统纵向继承机制的重复性代码,其应用主要体现在事务处理、日志管理、权限控制、异常处理等方面。主要作用是分离功能性需求和非功能性需求,使开发人员可以集中处理某一个关注点或者横切逻辑,减少对业务代码的侵入,增强代码的可读性和可维护性。简单的说,AOP 的作用就是保证开发者在不修改源代码的前提下,为系统中的业务组件添加某种通用功能。

3. AOP的应用场景

比如典型的AOP的应用场景:

 

AOP可以拦截指定的方法,并且对方法增强,比如:事务、日志、权限、性能监测等增强,而且无需侵入到业务代码中,使业务与非业务处理逻辑分离。

 4. AOP的术语

AOP核心概念

 

连接点是目标对象(被切入的类----业务逻辑)中的方法

切入点是关联多个被拦截的连接点

植入(织入)把增强代码应用到目标对象上,生成代理对象的过程

通知:两部分组成(内容、位置)1.增强的代码 ;2.增强代码放入的位置(哪些切入点的前、后);通知相当于拦截器,切入点相当于拦截的规则

Spring AOP采用的就是基于运行时增强的代理技术(动态织入),这点后面会分析,这里主要重点分析一下静态织入,ApectJ采用的就是静态织入的方式(静态织入)。ApectJ主要采用的是编译期织入,在这个期间使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。

Spring AOP 通知分类

 

Spring AOP 织入时期

SpringAop的底层技术

  • JDK动态代理
  • CGLIB代理

Spring AOP默认使用标准JDK动态代理实现AOP代理器,这保证任何接口都可以被代理。如果被代理的不是接口类,则会切换到CGLIB代理。

JDK动态代理是基于反射实现,且基于接口实现的,不是继承:因为java是单继承,底层源码中代理类已经自动继承了Proxy类,只能实现目标对象的接口了。

 

CGLIB是基于继承实现的:接口——被代理类——代理类

 

 

 

 在使用jdk动态代理生成代理对象是一个继承了Proxy类并且实现了Service接口。如果在配置类上加上@EnableAspectJAutoProxy(proxyTargetClass = true),表示修改AOP的实现方式为CGLIB代理,CGLIB代理使用的为继承,所有产生的代理对象等同于目标对象

编译时期的织入还是运行时期的织入?

  • 两者都是在运行时期织入(SpringAop是运行期织入)

初始化时期织入还是获取对象时期织入?

  • 通过源码分析,可以知道是在初始化时期织入

5. Spring AOP三种使用方式

AOP编程其实是很简单的事情,纵观AOP编程,程序员只需要参与三个部分:

  • 定义普通业务组件
  • 定义切入点,一个切入点可能横切多个业务组件
  • 定义增强处理,增强处理就是在AOP框架为普通业务组件织入的处理动作

 

所以进行AOP编程的关键就是定义切入点和定义增强处理(通知),一旦定义了合适的切入点和增强处理,AOP框架将自动生成AOP代理,即:代理对象的方法=增强处理+被代理对象的方法

注意,使用Spring AOP 的开启aspectJ支持功能时,需要使用以下代码启动aspect的注解功能支持:


<!-- 启动@aspectj的自动代理支持,使用JDK动态代理-->
<aop:aspectj-autoproxy proxy-target-class="false"/>

 

6. Spring AOP注解 (@AspectJ注释风格

一旦您确定了一个方面是实现给定需求的最佳方法,那么如何在使用Spring AOP或AspectJ以及在方面语言(代码)风格、@AspectJ注释风格Spring XML风格之间做出选择呢?这些决定受到许多因素的影响,包括应用程序需求、开发工具和团队对AOP的熟悉程度。

@AspectJ样式支持额外的实例化模型和更丰富的切入点组合。它的优点是将方面保持为模块化单元。它还有一个优势,即@AspectJ方面可以被Spring AOP和AspectJ理解(并因此消费)。因此,如果以后决定需要AspectJ的功能来实现其他需求,可以轻松迁移到经典的AspectJ设置。总的来说,除了企业服务的简单配置之外,Spring团队更喜欢使用@AspectJ风格来定制方面。

@Aspect

在类上个使用,定义一个切面,应用切面后,动态生成的代理类对象bean会交给IOC容器管理

  • singleton即切面只会有一个实例
  • perthis每个切入点表达式匹配的连接点对应的AOP对象(代理对象)都会创建一个新切面实例
  • pertarget每个切入点表达式匹配的连接点对应的目标对象都会创建一个新的切面实例

默认是singleton实例化模型,Schema风格只支持singleton实例化模型,而@AspectJ风格支持这三种实例化模型。

如果一个对象实现了一个接口,在使用AOP对其进行增强时,将会产生一个新的代理对象,但是这个代理对象不会等于目标对象,因为在代理中,Spring默认使用jdk动态代理的方式去增强,也就是说在对目标对象织入时,会有jdk生成一个代理对象,该代理对象执行了原始对象中的逻辑,并在此基础上加入增强逻辑。

@Pointcut

定义在切面方法上,定义一个切入点

切入点指示符:为了方法通知应用到相应过滤的目标对象方法上,SpringAOP提供了匹配表达式,这些表达式也叫切入点指示符。

通配符

在定义匹配表达式时,通配符几乎随处可见,如*、.. 、+ ,它们的含义如下:

  1. .. :匹配方法定义中的任意数量的参数,此外还匹配类定义中的任意数量包
  2. + :匹配给定类的任意子类
  3. * :匹配任意数量的字符

类型签名表达式

为了方便类型(如接口、类名、包名)过滤方法,Spring AOP 提供了within关键字。其语法格式如下:

within(<type name>)

type name 则使用包名或者类名替换即可:

复制代码
//匹配com.zejian.dao包及其子包中所有类中的所有方法
@Pointcut("within(com.zejian.dao..*)")

//匹配UserDaoImpl类中所有方法
@Pointcut("within(com.zejian.dao.UserDaoImpl)")

//匹配UserDaoImpl类及其子类中所有方法
@Pointcut("within(com.zejian.dao.UserDaoImpl+)")

//匹配所有实现UserDao接口的类的所有方法
@Pointcut("within(com.zejian.dao.UserDao+)")
复制代码

方法签名表达式

如果想根据方法签名进行过滤,关键字execution可以帮到我们,语法表达式如下

//scope :方法作用域,如public,private,protect
//returnt-type:方法返回值类型
//fully-qualified-class-name:方法所在类的完全限定名称
//parameters 方法参数
execution(<scope> <return-type> <fully-qualified-class-name>.*(parameters))

对于给定的作用域、返回值类型、完全限定类名以及参数匹配的方法将会应用切点函数指定的通知

复制代码
//匹配UserDaoImpl类中的所有方法
@Pointcut("execution(* com.zejian.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl类中的所有公共的方法
@Pointcut("execution(public * com.zejian.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl类中的所有公共方法并且返回值为int类型
@Pointcut("execution(public int com.zejian.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl类中第一个参数为int类型的所有公共的方法
@Pointcut("execution(public * com.zejian.dao.UserDaoImpl.*(int , ..))")
复制代码

其他指示符

  • bean:Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法;

    //匹配名称中带有后缀Service的Bean。
    @Pointcut("bean(*Service)")
    private void myPointcut1(){
  • this :用于匹配当前AOP代理对象类型的执行方法;请注意是AOP代理对象的类型匹配,这样就可能包括引入接口类型匹配

    //匹配了任意实现了UserDao接口的代理对象的方法进行过滤
    @Pointcut("this(com.zejian.spring.springAop.dao.UserDao)")
    private void myPointcut2(){}
  • target :用于匹配当前目标对象类型的执行方法;

    //匹配了任意实现了UserDao接口的目标对象的方法进行过滤
    @Pointcut("target(com.zejian.spring.springAop.dao.UserDao)")
    private void myPointcut3(){}

     

  • @within:用于匹配所以持有指定注解类型内的方法;请注意与within是有区别的, within是用于匹配指定类型内的方法执行;

    //匹配使用了MarkerAnnotation注解的类(注意是类)
    @Pointcut("@within(com.zejian.spring.annotation.MarkerAnnotation)")
    private void myPointcut4(){}
  • @annotation(com.zejian.spring.MarkerMethodAnnotation) : 根据所应用的注解进行方法过滤

    //匹配使用了MarkerAnnotation注解的方法(注意是方法)
    @Pointcut("@annotation(com.zejian.spring.annotation.MarkerAnnotation)")
    private void myPointcut5(){}

     

通知注解

  1. Before:在目标方法被调用之前做增强处理,@Before只需要指定切入点表达式即可

  2. AfterReturning:在目标方法正常完成后做增强,@AfterReturning除了指定切入点表达式后,还可以指定一个返回值形参名returning,代表目标方法的返回值

  3. AfterThrowing:主要用来处理程序中未处理的异常,@AfterThrowing除了指定切入点表达式后,还可以指定一个throwing的返回值形参名,可以通过该形参名来访问目标方法中所抛出的异常对象

  4. After:在目标方法完成之后做增强,无论目标方法时候成功完成。@After可以指定一个切入点表达式

  5. Around:环绕通知,在目标方法完成前,后做增强处理,@Around环绕通知是最重要的通知类型,像事务,日志等都是环绕通知,注意编程中核心是一个ProceedingJoinPoint,通过该对象的proceed()方法来执行目标函数,proceed()的返回值就是环绕通知的返回值

@Around环绕通知

ProceedingJoinPoint继承JoinPoint,JoinPoint的getThis()方法可以获得目标对象,所以ProceedingJoinPoint.proceed()可以执行目标对象的方法(连接点)

 

 

案例:

复制代码
 1 @Component
 2 @Aspect
 3 public class Operator {
 4 
 5     @Pointcut("execution(* ...service.*)")
 6     public void pointCut(){
 7         //配置切点
 8     }
 9     
10     @Before("pointCut()")
11     public void doBefore(JoinPoint joinPoint){
12         //通知-在目标方法被调用之前做增强处理
13     }
14     
15     @After("pointCut()")
16     public void doAfter(JoinPoint joinPoint){
17         //通知-在目标方法完成之后做增强
18     }
19     
20     @AfterReturning(pointcut="pointCut()",returning="returnVal")
21     public void afterReturn(JoinPoint joinPoint,Object returnVal){
22         //通知-在目标方法正常完成后做增强
23     }
24     
25     @AfterThrowing(pointcut="pointCut()",throwing="error")
26     public void afterThrowing(JoinPoint joinPoint,Throwable error){
27         //通知-主要用来处理程序中未处理的异常
28     }
29     
30     @Around("pointCut()")
31     public void around(ProceedingJoinPoint pjp){
32         //通知-环绕通知,在目标方法完成前后做增强处理,环绕通知是最重要的通知类型,像事务,日志等都是环绕通知
33         System.out.println("AOP Aronud before...");
34         pjp.proceed();//执行目标方法
35         System.out.println("AOP Aronud after...");
36     }
37 }
View Code
复制代码

Aspect优先级

在同一个切面中,如果有多个通知需要在同一个切点函数指定的过滤目标方法上执行,那些在目标方法前执行(”进入”)的通知函数,最高优先级的通知将会先执行在执行在目标方法后执行(“退出”)的通知函数,最高优先级会最后执行。而对于在同一个切面定义的通知函数将会根据在类中的声明顺序(作为优先级)执行。

如果在不同的切面中定义多个通知响应同一个切点,进入时则优先级高的切面类中的通知函数优先执行,退出时则最后执行,定义AspectOne类和AspectTwo类并实org.springframework.core.Ordered 接口,该接口用于控制切面类的优先级,同时重写getOrder方法,定制返回值,返回值(int 类型)越小优先级越大。其中AspectOne返回值为0,AspectTwo的返回值为3,显然AspectOne优先级高于AspectTwo。

 

@DeclareParents

@DeclareParents注解也是Aspectj提供的,在使用基于Aspectj注解的Spring AOP时,我们可以在切面中通过@DeclareParents指定满足指定表达式的类将自动实现某些接口。这个只是在运行时会将生成的代理类实现指定的接口。有接口就会有实现,对应的实现类也需要我们在@DeclareParents声明自动实现的接口时声明。现假设我们有一个接口叫CommonParent,其实现类叫CommonParentImpl,代码如下。

复制代码
 1 public interface CommonParent {
 2 
 3     public void doSomething();
 4 
 5 }
 6 public class CommonParentImpl implements CommonParent {
 7 
 8     public void doSomething() {
 9         System.out.println("-----------do something------------");
10     }
11 
12 }
View Code
复制代码

然后我们希望我们的所有的Service实现类都可以在运行时自动实现CommonParent接口,即所有的Service实现类在运行时都可以被当做CommonParent来使用。那我们可以定义如下这样一个切面类和对应的Advice。

复制代码
@Component
@Aspect
public class DeclareParentsAspect {

    @DeclareParents(value="com.elim.spring.aop.service..*", defaultImpl=CommonParentImpl.class)
    private CommonParent commonParent;

    @Before("bean(userService) && this(commonParent)")
    public void beforeUserService(CommonParent commonParent) {
        commonParent.doSomething();
    }

}
View Code
复制代码

如上,我们先在切面类中声明了一个CommonParent类型的属性,然后在上面使用了@DeclareParents注解表示我们需要把CommonParent声明为某些指定类型的父接口,然后通过@DeclareParents的value属性指定需要作用的类的形式,其语法和Pointcut表达式类似。通过defaultImpl属性指定默认CommonParent接口的实现类是CommonParentImpl。然后我们声明了一个before类型的Advice,在其中直接把我们的bean当做CommonParent类型的对象使用。

com.elim.spring.aop.service..*所有类会实现CommonParent 接口,并且用CommonParentImpl类中重写的方法作为com.elim.spring.aop.service..*中所有类的重写方法(方法复制操作)
    @DeclareParents(value="com.elim.spring.aop.service..*", defaultImpl=CommonParentImpl.class)
    private CommonParent commonParent;

 

 

 

 

 

 

posted @   堤苏白  阅读(470)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示