美文网首页
JAVA基础之切面

JAVA基础之切面

作者: 冰河winner | 来源:发表于2020-06-15 22:52 被阅读0次

    1、概念解析

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

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

    1.png

    使用横切技术,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 目标对象(Target Object)

    被一个或者多个切面所通知的对象。也被称做被通知对象。 既然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是在Joinpoin上执行的,而Pointcut规定了哪些Advice可以在哪些Jointpoint上执。

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

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

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

    • Joinpoint:所有通过收费站的车辆。Joinpoint是所有可能被织入Advice的候选点,在 Spring AOP中,则可以认为所有方法执行点都是Joinpoint。在上例中,所有经过收费站的都可能是嫌疑车辆。
    • Pointcut:车牌尾号为“110”、车身颜色为黑色。所有的Joinpoint都可以织入 Advice,但是我们并不希望在所有方法上都织入 Advice,而 Pointcut的作用就是提供一组规则来匹配Joinpoint,将满足规则的 Joinpoint织入 Advice。在上例中,警方不需要拦截所有车辆,只需要拦截符合特征的部分车辆
    • Advice:拦截嫌疑车辆,审问司机。Advice 是一个动作,即一段 Java 代码, 这段 Java 代码是作用于Pointcut所限定的那些Joinpoint上的。在上例中,“拦截并审问”这个动作只会针对那些“车牌尾号为110、车身颜色为黑色”的嫌疑车辆而执行。
    • Aspect:Pointcut 与 Advice 的组合。因此在这里我们就可以类比:凡是发现车牌尾号为“110”、车身颜色为黑色的车辆都要拦截并审问,这一系列的整体动作可以看做是一个Aspect。

    2、AspectJ 引入

    @AspectJ 是 Java语言的一个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() )" )
    
      /* 该方法是一个pointcut标志:表示应用程序执行到CustomerImpl.eat()方法时织入advice */
        public void eatPointcut() {
        }
    
        @Before( value = "eatPointcut()" )
      /* 该方法是一个前置通知,在RestaurantCustomer.eat()之前执行 */
        public void beforeAdvice() {
            System.out.println( "---服务员引导客人入座、点菜" );
        }
    
        @AfterReturning( value = "eatPointcut()" )
      /* 该方法是一个后置通知,在RestaurantCustomer.eat()之后执行 */
        public void afterAdvice() {
            System.out.println( "---服务员收拾餐桌" );
        }
    }
    

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

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

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

    2.png

    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中使用“&&”需要使用转义字符“&&”来代替之,很不方便,因此Spring AOP 提供了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(DataAccessException ex) {
      // ...
     } 
    

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

    5.4 最终通知

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

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

    5.5 环绕通知

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

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

    @Around("com.xys..dataAccessOperation()")
    public Object doAroundAccessCheck(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 void before1(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 void before(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( JoinPoint point )
    {
       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( ProceedingJoinPoint point ) 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 + ",这是返回结果的后缀");
    }
    

    相关文章

      网友评论

          本文标题:JAVA基础之切面

          本文链接:https://www.haomeiwen.com/subject/wwskxktx.html