Spring AOP & AspectJ之基础应用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012152619/article/details/78756674

1、概念解析

AOP(Aspect OrientedProgramming),即面向切面编程,可以说是OOP(Object OrientedProgramming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即"切面"。所谓切面,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

使用横切技术,AOP把软件系统分为两个部分:核心关注点横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

首先让我们从一些重要的AOP概念和术语开始:

1.1 切面(Aspect)

一个关注点的模块化,这个关注点可能会横切多个对象。类是对物体特征的抽象,切面就是对横切关注点的抽象,事务管理是J2EE应用中一个关于横切关注点的很好的例子。


1.2 连接点(Joinpoint)

在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器


1.3 通知(Advice)

在切面的某个特定的连接点上执行的动作。许多AOP框架(包括Spring)都是以拦截器(Interceptor)做通知模型,并维护一个以连接点为中心的拦截器链。


1.4 切入点(Pointcut)

匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行,例如,当执行某个特定名称的方法时。切入点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法


1.5 引入(Introduction)

用来给一个类型声明额外的方法或属性(也被称为连接类型声明(inter-type declaration))。Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段


1.6 目标对象(TargetObject)

被一个或者多个切面所通知的对象。也被称做被通知对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个被代理对象


1.7 AOP代理(AOP Proxy)

AOP框架创建的对象,用来实现切面契约(例如通知方法执行等等)。Spring默认使用JDK动态代理,在需要代理类而不是代理接口的时候,Spring会自动切换为使用CGLIB代理,不过现在的项目都是面向接口编程,所以JDK动态代理相对来说用的还是多一些


1.8 织入(weave)

把切面连接到其它的应用程序类型或者对象上,并创建一个被通知对象。根据不同的实现技术, AOP织入有三种方式:

(1)编译器织入, 需要有特殊的Java编译器

(2)类装载期织入, 需要有特殊的类装载器

(3)动态代理织入, 在运行期为目标类添加通知生成子类的方式

Spring 采用动态代理织入, 而AspectJ采用编译器织入和类装载期织入。


1.9 举个栗子

在 Spring AOP 中,所有的方法执行都是Joinpoint。 而 Pointcut 是一个描述信息,,它修饰的是Joinpoint,,通过Joinpoint,我们可以确定哪些Joinpoint可以被织入 Advice。

Advice是在Joinpoint上执行的,而Pointcut规定了哪些Advice可以在哪些Jointpoint上执行

以上概念比较抽象,不易理解,下面用一个生活中的例子描述Joinpoint、Pointcut、Advice、Aspect的作用:

杭州市区发生一起交通事故,肇事者驾车逃逸,据目击者称,肇事车辆是一辆车牌尾号为“110”的黑色轿车,现在警方已在高速公路的各个收费站设卡,拦截可疑车辆。

如果将该事件当做一个AOP处理流程,角色解释如下:

Joinpoint:所有通过收费站的车辆。Joinpoint是所有可能被织入Advice的候选点,在 SpringAOP中,则可以认为所有方法执行点都是Joinpoint。在上例中,所有经过收费站的都可能是嫌疑车辆。

Pointcut:车牌尾号为“110”、车身颜色为黑色。所有的Joinpoint都可以织入 Advice,但是我们并不希望在所有方法上都织入 Advice,而Pointcut的作用就是提供一组规则来匹配Joinpoint,将满足规则的Joinpoint织入Advice。在上例中,警方不需要拦截所有车辆,只需要拦截符合特征的部分车辆

Advice:拦截嫌疑车辆,审问司机。Advice是一个动作,即一段 Java 代码,这段 Java 代码是作用于Pointcut所限定的那些Joinpoint上的。在上例中,“拦截并审问”这个动作只会针对那些“车牌尾号为110、车身颜色为黑色”的嫌疑车辆而执行。

Aspect:Pointcut 与 Advice 的组合。因此在这里我们就可以类比:凡是发现车牌尾号为“110”、车身颜色为黑色的车辆都要拦截并审问,这一系列的整体动作可以看做是一个Aspect。

2、AspectJ引入

@AspectJJava 语言的一个AOP 实现,定义了AOP 编程中的语法规范,并提供特殊的代码编译器、调试工具等来支持AspectJ语法。

Spring中,通过java注解或XML配置都可以接入AspectJ

注解方式支持@AspectJ

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

XML方式支持@AspectJ:

<aop:aspectj-autoproxy/>

下面用@AspectJ来模拟客人去饭店吃饭的一个场景。

作为食客,他们应该只关注自己的事情,比如,怎么点菜、怎么吃饭。而点菜后怎么通知后厨,吃完后怎么收拾桌子,不是食客关注的重点,这些动作应该有另外一个角色——服务员来完成。在这个场景中,以AOP的视角来看,食客吃饭应该被看作核心关注点,而服务员提供的各种服务应该被看作横切关注点

食客实现类:

@Service("restaurantCustomer")
public class RestaurantCustomer{
 
      //食客吃饭
      public void eat(){
             System.out.println("客人开始吃饭");
             try {
                    Thread.sleep(2000);
             } catch (InterruptedException e) {
                    e.printStackTrace();
             }
             System.out.println("客人结束吃饭");
      }
 
}

服务员切面实现类:

@Component
@Aspect
public class WaiterAspect {
     
      @Pointcut(value = "execution( * clf.learning.winner.springbase.aspect.RestaurantCustomer.eat())")
      public void eatPointcut() {  //该方法是一个pointcut标志:表示应用程序执行到CustomerImpl.eat()方法时织入advice
            
      }
 
      @Before(value = "eatPointcut()")
      public void beforeAdvice() {  //该方法是一个前置通知,在RestaurantCustomer.eat()之前执行
             System.out.println("---服务员引导客人入座、点菜");
      }
     
      @AfterReturning (value ="eatPointcut()")
      public void afterAdvice() {  //该方法是一个后置通知,在RestaurantCustomer.eat()之后执行
             System.out.println("---服务员收拾餐桌");
      }
     
}

@Aspect 注解用于标明该类是一个切面实现类。

需要注意的是,被@Aspect 标注的类就不能作为其他切面的目标对象, 因为使用 @Aspect 后,这个类就会被排除在auto-proxying机制之外。

RestaurantCustomer.eat()执行时,会得到以下的打印结果:



3、Aspect语法

3.1 类型匹配

所谓类型匹配就是匹配符合某些要求的类或者接口。语法规则为:

注解? 类的全限定名字

  • 注解:可选,代表类上持有的注解,如@Deprecated
  • 类的全限定名:必填,可以是任何类全限定名。

类型匹配可以使用的通配符:

  • *——匹配任何数量字符;
  • ..——匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。
  • +——匹配指定类型的子类型;仅能作为后缀放在类型模式后边。

例子:

java.lang.String    匹配String类型;

java.*.String       匹配java包下的任何一级子包下的String类型;如匹配java.lang.String,但不匹配java.lang.ss.String

java..*            匹配java包及任何子包下的任何类型;                           如匹配java.lang.String、java.lang.annotation.Annotation

java.lang.*ing      匹配任何java.lang包下的以ing结尾的类型;

java.lang.Number+  匹配java.lang包下的任何Number的子类型;如匹配java.lang.Integer,也匹配java.math.BigInteger


3.2 方法匹配

语法规则为:

注解? 修饰符? 返回值类型 类型声明? 方法名(参数列表) 异常列表?

  • 注解:可选,方法上持有的注解,如@Deprecated;
  • 修饰符:可选,如public、protected;
  • 返回值类型:必填,可以是任何类型模式;“*”表示所有类型;
  • 类型声明:可选,可以是任何类型模式;
  • 方法名:必填,可以使用“*”进行模式匹配;
  • 参数列表:“()”表示方法没有任何参数;“(..)”表示匹配接受任意个参数的方法,“(..,java.lang.String)”表示匹配接受java.lang.String类型的参数结束,且其前边可以接受有任意个参数的方法;“(java.lang.String,..)”表示匹配接受java.lang.String类型的参数开始,且其后边可以接受任意个参数的方法;“(*,java.lang.String)”表示匹配接受java.lang.String类型的参数结束,且其前边接受有一个任意类型参数的方法;
  • 异常列表:可选,以“throws 异常全限定名列表”声明,异常全限定名列表如有多个以“,”分割,如throws java.lang.IllegalArgumentException, java.lang.ArrayIndexOutOfBoundsException

3.3 组合表达式

AspectJ使用 且(&&)、或(||)、非(!)来组合切入点表达式。

在Schema风格下,由于在XML中使用“&&”需要使用转义字符“&amp;&amp;”来代替之,很不方便,因此SpringAOP 提供了and、or、not来代替&&、||、!


4、Pointcut声明

一个 Pointcut 声明由两部分组成:

  • 一个方法签名,包括方法名和相关参数
  • 一个切入点表达式,用来指定哪些方法执行是我们感兴趣的,即在哪里可以织入Advice

在@AspectJ 风格的 AOP 中,我们使用一个方法来描述 Pointcut,如上例中:

@Pointcut(value= "execution( *clf.learning.winner.springbase.aspect.RestaurantCustomer.eat() )")
public void eatPointcut() { 
            
}

@Pointcut注解标明,该方式是一个切入点,该方法的返回类型必须为void

execution( *clf.learning.winner.springbase.aspect.RestaurantCustomer.eat() )是一个切入点表达式,切入点表达式由标志符和操作参数组成。”execution”就是一个标志符,而括号里的”* clf.learning.winner.springbase.aspect.RestaurantCustomer.eat()”就是操作参数

切点表达式也可以在Advice声明中直接使用    

@Pointcut(value = "execution( *clf.learning.winner.springbase.aspect.RestaurantCustomer.eat() )")
public void eatPointcut() {  //该方法是一个pointcut标志:表示应用程序执行到CustomerImpl.eat()方法时织入advice
            
}
 
@Before(value = "eatPointcut()")
public void beforeAdvice() { 
      System.out.println("---服务员引导客人入座、点菜");
}

等同于:

@Before(value = " execution( *clf.learning.winner.springbase.aspect.RestaurantCustomer.eat() )")
public void beforeAdvice() { 
      System.out.println("---服务员引导客人入座、点菜");
}

Spring AOP支持的切入点指示符有以下几种:

4.1 execution

匹配特定的方法执行,如:

任意公共方法的执行:

execution(public * *(..))


任意公共的,只有一个参数且类型为java.util.Date的方法执行:

execution(public * *(java.util.Date))


任何一个名字以”set”开始的方法的执行:

execution(* set*(..))


AccountService接口定义的任意方法的执行:

execution(*com.xyz.service.AccountService.*(..))


在service包中定义的任意方法的执行:

execution(*com.xyz.service.*.*(..))


在service包或其子包中定义的任意方法的执行:

execution(*com.xyz.service..*.*(..))


4.2 within

匹配特定包下的方法执行,如:

在service包中的任意方法执行:

within(com.xyz.service.*)


在service包或其子包中的任意方法执行:

within(com.xyz.service..*)


4.3 this

匹配特定类或接口的代理对象的方法执行,不支持通配符,如:

实现了AccountService接口的代理对象的任意方法执行:

this(com.xyz.service.AccountService)


 该指示符还可以将代理对象传入到Advice方法当中,如:

@Before("before() && this(proxy)")
public void beforeAdvide(JoinPoint point, Object proxy){
              
}

4.4 target

匹配特定类或接口的方法执行,不支持通配符,如:

实现AccountService接口的目标对象的任意方法执行:

target(com.xyz.service.AccountService)


 该指示符还可以将目标对象传入到Advice方法当中,如:

@Before("before() && target(target)")
public void beforeAdvide(JoinPoint point, Object target){
 
}

4.5 args

匹配参数是指定类型的方法执行,如

任何一个只接受一个参数,并且运行时所传入的参数是Serializable 接口的方法执行:

args(java.io.Serializable)


 该指示符还可以将目标方法的入参传入到Advice方法当中,如:

匹配只有一个参数且类型为String的方法执行

@Before(value =" args(name)")
public void doSomething(String name) {

}

匹配第一个参数为String类型的方法执行:

@Before(value =" args(name, ..)")
public void doSomething(String name) {

}
匹配第二个参数为String类型的方法执行:

@Before(value =" args(*, name, ..)")
public void doSomething(String name) {

}
注意,该标识符是匹配运行时传入的参数类型,不是匹配方法签名的参数类型;参数类型列表中的参数必须是类型全限定名, 不支持通配符;args属于动态切入点,这种切入点开销比较大,非特殊情况最好不要使用


4.6 @target

匹配持有特定注解(类级别)的类的方法执行,与@within的功能类似,但必须要指定注解接口的保留策略为RUNTIME。不支持通配符。如:

目标对象中有一个@Transactional 注解的任意方法执行:

@target(org.springframework.transaction.annotation.Transactional)


4.7 @within

与@target类似


4.8 @args

匹配运行时入参持有特定注解的方法执行,不支持通配符,如:

任何一个只接受一个参数,并且运行时所传入的参数类型具有@Classified 注解的方法执行:

@args(com.xyz.security.Classified)


4.9 @annotation

匹配持有特定注解(方法级别)的方法执行,不支持通配符,如:

任何一个被@Transactional 注解的方法执行:

@annotation(org.springframework.transaction.annotation.Transactional)


4.10 bean

Spring AOP扩展的,在AspectJ中无相应概念

匹配bean名字满足特定要求的方法执行,如

匹配以 “Service” 或 “ServiceImpl”结尾的 bean:

bean(*Service || *ServiceImpl)


5、Advice声明

Advice 是和一个Pointcut 表达式关联在一起的,并且在切入点匹配的方法执行之前或者之后或者前后运行。Pointcut 表达式可以是简单的一个Pointcut名字的引用,也可以是完整的Pointcut表达式。

Spring AOP支持五中通知模型:前置通知(Before advice)、后置通知(After returning advice)、异常通知(After throwing advice)、最终通知(After (finally) advice)、环绕通知(Around Advice)。

5.1 前置通知

在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。

使用 @Before 注解声明前置通知:

//将 pointcut 和 advice 同时定义
@Before("within(com.xys.service..*)")
public void doSomethingk(){
   
}

5.2 后置通知

在某连接点正常完成后执行的通知,例如,一个方法没有抛出任何异常,正常返回

使用 @AfterReturning 注解来声明:

@AfterReturning(pointcut ="within(com.xys.service..*)", returning="retVal")
public void doSomethingk(Object retVal) {
   
}

5.3 异常通知

抛出异常通知在一个方法抛出异常后执行。

使用@AfterThrowing注解来声明:

@AfterThrowing(pointcut="com.xyz.myapp.dataAccessOperation()",throwing="ex")
public void doRecoveryActions(DataAccessExceptionex) {
    // ...
}

throwing属性可以限制匹配的异常类型


5.4 最终通知

当某连接点退出的时候执行的通知,即finally代码块执行后,不论是正常返回还是异常退出)执行的通知

@After("com.xyz.myapp.dataAccessOperation()")
public void doReleaseLock() {
    // ...
}


5.5 环绕通知

包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。它使得通知有机会在一个方法执行之前和执行之后运行,而且它可以决定这个方法在什么时候执行,如何执行,甚至是否执行。

环绕通知使用@Around注解来声明。通知的第一个参数必须是 ProceedingJoinPoint类型。在通知体内,调用ProceedingJoinPoint的proceed()方法使得连接点方法执行。如果不调用proceed()方法,连接点方法则不会执行。

    @Around("com.xys..dataAccessOperation()")
    public ObjectdoAroundAccessCheck(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        // 开始
        Object retVal = pjp.proceed();
        stopWatch.stop();
        // 结束
        System.out.println("invoke method:" + pjp.getSignature().getName() + ", elapsed time: " +stopWatch.getTotalTimeMillis());
        return retVal;
    }

5.6 通知参数的获取

有很多场景下,我们需要获取连接点的方法参数,传递到Advice中以作判断、处理等。

通过切入点表达式可以将相应的参数自动传递给通知方法,例如上文中将返回值和异常传递给通知方法,以及通过切入点标识符的”arg”属性传递运行时参数。

需要注意的是,在Spring AOP中,execution和bean指示符不支持自动传递参数。不过,可以利用组合表达式达到目的:

@Before(value="execution(*test(*)) && args(param)", argNames="param") 
public voidbefore1(String param) { 
    System.out.println("param:" +param); 
}

首先execution(*test(*))匹配任何方法名为test,且有一个任何类型的参数;然后args(param)将查找通知方法上同名的参数,并在方法执行时(运行时)匹配传入的参数是使用该同名参数类型,即java.lang.String;如果匹配将把该被通知参数传递给通知方法上同名参数。

“argNames”用于指定参数名称,避免参数绑定的二义性,如:

@Before("args(param) && target(bean) && @annotation(secure)", argNames="jp,param,bean,secure") 
public voidbefore(JoinPoint jp, String param, IPointcutService pointcutService, Secure secure) { 
    //…… 
} 

如果第一个参数类型是JoinPoint、ProceedingJoinPoint或JoinPoint.StaticPart类型,应该从”argNames”属性省略掉该参数名(可选,写上也对),这些类型对象会自动传入的,但必须作为第一个参数:

@Before(value="args(param)", argNames="param") 
public void before(JoinPoint jp, String param) { 
    System.out.println("param:" +param); 
}

此外,Spring AOP提供使用org.aspectj.lang.JoinPoint类型获取连接点数据,任何通知方法的第一个参数都可以是JoinPoint(环绕通知是ProceedingJoinPoint,JoinPoint子类),当然第一个参数位置也可以是JoinPoint.StaticPart类型,这个只返回连接点的静态部分。

  • JoinPoint:提供访问当前被通知方法的目标对象、代理对象、方法参数等数据
  • ProceedingJoinPoint:用于环绕通知,使用proceed()方法来执行目标方法
  • JoinPoint.StaticPart:提供访问连接点的静态部分,如被通知方法签名、连接点类型等

    @Before("execution(*com.abc.service.*.many*(..))")
    public void permissionCheck(JoinPointpoint) {
        System.out.println("@Before:模拟权限检查...");
        System.out.println("@Before:目标方法为:"+
               point.getSignature().getDeclaringTypeName() +
                "." + point.getSignature().getName());
        System.out.println("@Before:参数为:" +Arrays.toString(point.getArgs()));
        System.out.println("@Before:被织入的目标对象为:"+ point.getTarget());
    }
 
 
    @Around("execution(*com.abc.service.*.many*(..))")
    public Object process(ProceedingJoinPointpoint) throws Throwable {
        System.out.println("@Around:执行目标方法之前...");
        //访问目标方法的参数:
        Object[] args = point.getArgs();
        if (args != null && args.length> 0 && args[0].getClass() == String.class) {
           args[0] = "改变后的参数1";
        }
        //用改变后的参数执行目标方法
        Object returnValue =point.proceed(args);
        System.out.println("@Around:执行目标方法之后...");
        System.out.println("@Around:被织入的目标对象为:" + point.getTarget());
        return "原返回值:""+returnValue + ",这是返回结果的后缀";
    }




猜你喜欢

转载自blog.csdn.net/u012152619/article/details/78756674