更新时间: 2020-03-31 14:08:48       分类: 学习笔记


简介

面向切面编程是对面向对象编程思想的一个有力补充,传统的面向对象主要通过继承委托来实现代码的复用,但在某些场合下显得有些心有余而力不足。

举个例子,比如需要对一系列的服务进行几乎相同的日志记录操作,如果只采用面向对象的方式,有以下两种解决方式:

  1. 将日志记录委托给一个单独的服务去实现,然后在其他服务中注入日志服务并调用。如果日志服务对于业务逻辑考察的比较详细(比如要统计每个函数的调用次数),可能会对原本的业务代码造成极大的侵入。

  2. 制定一个基类,内嵌日志记录逻辑,然后改造所有的服务继承该基类从而获得日志记录的能力。但这样的继承体系并不稳定。

显然,使用这两种方式都会对原有的代码逻辑进行不小的改动,同时可扩展性也不强。

上述这种涉及多个模块的功能被称为交叉关注点,面对此类场景时,AOP的作用就是将这些交叉关注点与业务逻辑剥离,从而提供一种复用强、同时不侵入业务代码的实现方案,整体的思路如下图所示:

基本术语和概念

在正式开始AOP编程之前,需要先了解一下基础的概念

  1. 通知 (Advice)

    通知负责定义切面的what 和 when,即定义了切面工作的时间点,以及要解决什么问题(行为)。

    - Before 前置,在调用目标方法前调用通知定义的服务 - After 后置,目标方法执行结束后调用通知定义的服务(无视结果) - After Returning 后置,目标方法正常执行结束后,调用通知定义的服务 - After Throwing 后置,目标方法执行过程中若抛出异常,调用通知定义的服务 - Around 环绕,在目标方法执行和执行后,都调用通知定义的服务

  2. 织入点 (Join Point)

    在应用代码中可以插入通知的地方。织入点规范了通知的插入方式和位置。

  3. 切点 (Point Cuts)

    切点定义了切面的where。切面不需要覆盖应用中所有的织入点,因此就需要通过切点来缩小切面接入应用时的范围。

  4. 切面 (Aspect)

    通知和切点组合起来,就定义了一个完整的切面(When, Where, What)

  5. Introduction

    类似装饰器模式,目的是在不改变现有类的基础上为其添加属性和方法。

  6. 编织 (Weaving)

    编织是一个动作,用于描述切面应用于模板对象来创建代理类的过程。编织可能发生在以下几个生命周期中:

    - Compile Time 编译过程中进行编织,AspectJ的实现方式 - Class Load Time 目标类加载到JVM时进行编织 - Run Time 应用执行过程中动态的进行编织,Spring AOP使用的即为此种方式

上述概念之间的联系如图所示:

Spring AOP实例

首先要说明的是,Spring AOP并不是AOP编程的唯一解决方案,AspectJ对于AOP有更好的支持,同时也有更高的性能。但对于日常开发而言,Spring提供的基于动态代理的AOP编程已经足够使用。

下面给出基于注解的Spring AOP编程实例,场景是,定义演出作为业务代码,观众作为切面。

  1. 编写业务代码

    和普通的服务(Service)一样,一般我们会提供一个接口定义和一个实现:

     package com.yuanyan.demos.aop;
    
     public interface Performance {
         /**
          * 抽象的切面,对演出的定义
          */
         void perform(String performer);
     }
    
     package com.yuanyan.demos.aop;
    
     import org.springframework.stereotype.Component;
    	
     @Component("performance")
     public class PerformanceImpl implements Performance {
         @Override
         public void perform(String performer) {
             System.out.println("The " + performer + " is showing !");
         }
     }
    
  2. 编写切面

    我们以“观众”的行为作为切面,定义代码如下:

    	
     @Aspect
     public class Audience {
         /**
          * 定义一个切点,增加复用性
          */
         @Pointcut("execution(* com.yuanyan.demos.aop.Performance.perform(..))")
         public void perform() {
         }
    	
         @Before("perform()")
         public void silencePhoneCalls() {
             System.out.println("silencing phone calls");
         }
    	
         @Before("perform()")
         public void takeSeats() {
             System.out.println("taking seats");
         }
    	
         @AfterReturning("perform()")
         public void clap() {
             System.out.println("CLAP! CLAP! CLAP!");
         }
    	
         @AfterThrowing("perform()")
         public void demandRefund() {
             System.out.println("demand a refund");
         }
    }
    

    首先通过@Pointcut()定义一个切点,其中使用了AspectJ表达式定义了一个切点,第一个*表示不关心函数的返回类型;接下来需要列出完整的类签名和方法名;对于函数参数列表,使用".."表示不关心函数的参数列表,如下:

    对于详细的AspectJ切点表达式,可以参考AspectJ inAction, Second Edition,在此不做赘述。

    接着通过注解定义了一系列的通知方法,他们会在切点对应方法执行的生命周期中被触发,不妨编写一个单元测试来看看实际的效果:

     @RunWith(SpringJUnit4ClassRunner.class)
     @ComponentScan("com.yuanyan.demos.aop")
     @ContextConfiguration(classes = ConcertConfig.class)
     public class PerformanceTest {
         @Resource
         Performance performance;
    	
         @Test
         public void test() {
             performance.perform("lumin");
         }
     }
    

    运行结果如下:

     silencing phone calls
     taking seats
     The lumin is showing !
     CLAP! CLAP! CLAP!
    
  3. 环绕通知和参数注入

    除了进行简单的前置、后置通知,Spring AOP还提供函数参数的注入和环绕通知方式,让我们把上面的代码改造一下,如下:

     @Aspect
     public class Audience {
     		@Pointcut("execution(* com.yuanyan.demos.aop.Performance.perform(..)) && args(performer)")
     		public void perform(String performer) {
     		}
    
     		@Around("perform(performer)")
     		public void watchPerformance(ProceedingJoinPoint proceedingJoinPoint, String performer) {
     			try {
     				System.out.println("silence phone calls");
     				System.out.println("taking seats");
     				proceedingJoinPoint.proceed();
     				System.out.println("CLAP! CLAP! CLAP! for " + performer + "'s wonderful show!");
     			}catch (Throwable t) {
     				System.out.println("demand a refund");
     			}
     		}
     }
    

    首先我们改造切点的定义表达式,将performer参数注入到对应的函数形参上去,接下来声明一个环绕通知,Spring会将切点注入到ProceedingJoinPoint对象中去,这样使得我们能够在切面中操纵切点本身的行为,比如进行重试策略。

    使用这个环绕通知我们代替了之前的几个通知,使代码变得更简洁了,同时没有对业务代码进行任何侵入。

原理

实现AOP的核心思路是使用代理,换句话说,在AOP中调用切点方法时,我们并不是真正的在访问提供方法的那个类,而是在访问它的一个代理,如下图,代理类将真正提供方法的对象包裹了起来,从而为其提供了注入通知的能力。

目前AOP有两种主要的实现方式,一种是AspectJ提供的静态代理,另一种是Spring AOP提供的动态代理。两者的区别主要在于代理生成的时间和方法,静态代理在编译时通过修改字节码来实现,动态代理则是在运行时阶段使用反射机制来创建动态代理类实现。

  1. 静态代理(编译时增强)

    AspectJ框架提供了静态代理的方式来实现AOP,比如下面一个类:

     public class Hello {
         public void sayHello() {
             System.out.println("hello");
         }
    	
         public static void  main(String[] args) {
             Hello h = newHello();
             h.sayHello();
         }
     }
    

    如果通过AspectJ为其添加一个环绕通知,那么这个类编译后生成的class文件会是这样的:

     public class Hello {
         public Hello() {}
    	
         public void sayHello() {
             System.out.println("hello");
         }
    	
         public static void  main(String[] args) {
             Hello h = new Hello();
             sayHello_aroundBody1$advice(h, TxAspect.aspectOf(), (AroundClosure)null);
         }
    	    
     }
    

    可以看到在编译后,这个类被附加了一些方法,从而实现了AOP的织入。最终我们访问的代理类就是原始类自身的一个增强。

  2. 动态代理(运行时增强)

    Spring AOP利用了JDK提供的动态代理机制,来给每个应用到AOP中的类生成一个代理类,从而实现织入。下面给一个例子:

     public class DynaProxyHello implements InvocationHandler{
    
         private Object target; //目标对象
         /**
          * 通过反射来实例化目标对象
          * @param object
          * @return
          */
         public Object bind(Object object){
             this.target = object;
             return Proxy.newProxyInstance(this.target.getClass().getClassLoader(), this.target.getClass().getInterfaces(), this);
         }
    	    
         @Override
         public Object invoke(Object proxy, Method method, Object[] args)
                 throws Throwable {
             Object result = null;
             Logger.start();//添加额外的方法
             //通过反射机制来运行目标对象的方法
             result = method.invoke(this.target, args);
             Logger.end();
             return result;
         }
    	    
     }
    

    最终调用时的方式如下:

     public class DynaTest {
         public static void main(String[] args) {
             IHello hello = (IHello) new DynaProxyHello().bind(new Hello());//如果我们需要日志功能,则使用代理类
             //IHello hello = new Hello();//如果我们不需要日志功能则使用目标类
             hello.sayHello("明天");
         }
     }
    

    这样我们就基于反射和动态代理类实现了一个环绕通知。

  3. 对比

    这两种方式本身提供的代理类不同,一个是原始类本身的增强,另一个则是原始类的包装,可以用下图来表述:

    从实现的原理上不难得出结论:静态代理性能更好,但需要特殊的编译框架支持;动态代理更加灵活,但性能相对而言要差一些。

小结

正如前言所说,AOP是对面向对象编程思想的一个补充。它比较适合用于存在交叉关注点的场景,来增加代码的复用性。

但AOP本身对于性能有所损耗,同时实现起来也需要一定的成本,因此究竟是否使用AOP,还需要根据具体的应用场景来判断。


评论

还没有评论