AOP

引入

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

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

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

一.术语

1.横切关注点

软件开发中,散布于应用中多处的功能被称为横切关注点,如事务、日志、安全。

2.advice(通知) what when

切面的工作被称为通知,他定义了切面的工作是什么和何时工作

5种类型通知:

  1. 前置通知before
  2. 后置通知after(不关心方法的输出是什么)
  3. 返回通知after-returning(方法成功执行)
  4. 异常通知after-throwing(方法返回异常)
  5. 环绕通知around(包裹被通知方法,之前和之后都自定义行为)

3.join point连接点

应用执行过程中,能够插入切面的所有“点”(时机),使用通知的时机,即触发通知的事件方法

4.poincut 切点 where

如果说通知定义切面的“什么”和“何时”,那么切点就定义了“何处”。切点的定义会匹配通知所要织入的一个或多个连接点。我们通常会使用明确的类和方法名称来指定这些切点,或是利用正则表达式定义匹配的类和方法名称模式来指定这些切点。

5.切面aspect = advice + poincut

切面是通知和切点的结合,通知和切点共同定义了关于切面的全部内容—-它是什么,在何时和何处完成其功能

通过 代理 来实现切面,代理类包裹了目标bean,先拦截对被通知方法的调用,再转发给真正的bean,在调用bean之前,先执行切面逻辑。直到需要被代理的bean时,才创建代理对象。正因为spring会在运行时才创建代理对象,所以不需要特殊的编译器织入springAOP切面。

定义切面

@AspectJ :表明该类不仅是一个pojo还是以个切面

@Pointcut 在一个@Aspect切面内定义可重用的切点

@Pointcut(“execution( concert.Performance.perform(..))”)
public void performance() {}
将切点依附于performance()方法,可以在定义切点表达式的时候用performance()代替execution(
concert.Performance.perform(..))

  • 环绕通知
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Aspect
    public class Audience {
    @Pointcut("excution(** concert.Performance.perform(..))")
    public void performance() {}

    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint jp) {
    try {
    System.out.println("Silencing cell phones");
    System.out.pringln("Taking seats");
    jp.proceed():
    Sytem.out.println("CLAP CLAP CLAP!!!");
    } catch (THrowable e) {
    SYstem.out.println("Demanding a refund");
    }
    }
    }

如果不调用proceed()方法,那么会阻塞被通知方法的调用。
可以不调用proceed()方法,也可以多次调用proceed()方法,这样的场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。

  • 切点拦截参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Pointcut("excution(** concert.Performance.perform(..)) && args(trackNumber")
    public void trackPlayed(int trackNumber) {}

    @Before("trackPlayed(trackNumber)")
    public void countTrack(int trackNumber) {
    int currentCount = getPlayCount(trackNumber);
    trackCounts.put(trackNumber, currentCount + 1);
    }
    public int getPlayCount(int trackNumber) {
    return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
    }

切点定义中的参数与切点方法中的参数名一样就可以实现从命名切点到通知方法的参数转移。

6.Introduction引入

向现有的类添加新方法和属性。

给bean添加一个新的功能,需要一个引入代理去实现相关的功能。

1
2
3
4
5
@Aspect 
public class EncoreableIntroducer {
@DeclareParents(value="concert.Performance+", defualtImpl=DefaultEncoreable.class)
public static Encoreable ecoreable;
}

  • value指定了哪种类型的bean要引入该接口。在这里是所有实现Performance的类型。加号表示Performance的所有子类而不是本身
  • defaultImplement指定了为引入功能提供实现的类 实现类
  • @DeclareParents注解所标注的静态属性指明了要 引入的接口

7.织入

把切面应用到目标对象并创建新的代理对象的过程。在目标生命周期有多个点可以进行织入:

  • 编译期
  • 类加载期
  • 运行期

二.spring对AOP的支持

Spring默认采取的动态代理机制实现AOP,当动态代理不可用时(代理类无接口)会使用CGlib机制。但Spring的AOP有一定的缺点,第一个只能对方法进行切入,不能对接口,字段,静态代码块进行切入(切入接口的某个方法,则该接口下所有实现类的该方法将被切入)。第二个同类中的互相调用方法将不会使用代理类。因为要使用代理类必须从Spring容器中获取Bean。第三个性能不是最好的

  • 基于代理的经典SpringAOP
  • 纯POJO切面
  • @AspectJ注解驱动的切面
  • 注入式AspectJ切面

由于Spring AOP构建在动态代理基础之上,所以其对AOP的支持局限于方法拦截。他不支持字段连接点和构造器连接点。

直到应用需要被代理的bean时,spring才会创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有bean的时候,Spring才会创建被dialing的对象。

动态代理

不修改原来的对象就能增加一系列新的功能

  1. jdk自带动态代理

Java在JDK1.3后引入的动态代理机制,使我们可以在运行期,目标类加载后动态的创建代理类

  1. CGlib

    使用动态字节码生成技术实现AOP原理是在运行期间目标字节码加载后,生成目标类的子类,将切面逻辑加入到子类中,所以使用Cglib实现AOP不需要基于接口。

JDK中动态代理实现流程

在java的动态代理机制中,有两个重要的类和接口,一个是 InvocationHandler(Interface)、另一个则是 Proxy(Class),这一个类和接口是实现我们动态代理所必须用到的。

使用方法:

  1. 创建调用处理器(InvocationHandler),在其中传入被代理对象
  2. 在调用处理器的invoke方法中加入代理逻辑,并调用被代理对象的被代理方法
  3. 通过Proxy.newProxyInstance方法获得代理对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class DynamicProxyHandler implements InvocationHandler {
private Object proxied;
public DynamicProxyHandler(Object proxied) {
this.proxied = proxied;
}
public Object
invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("**** proxy: " + proxy.getClass() +
", method: " + method + ", args: " + args);
if(args != null)
for(Object arg : args)
System.out.println(" " + arg);
return method.invoke(proxied, args);
}
}
class SimpleDynamicProxy {
public static void consumer(Interface iface) {
iface.doSomething();
iface.somethingElse("bonobo");
}

public static void main(String[] args) {
RealObject real = new RealObject();

Interface proxy = (Interface)Proxy.newProxyInstance(
Interface.class.getClassLoader(),
new Class[]{ Interface.class },
new DynamicProxyHandler(real));
consumer(proxy);
}

Proxy.newProxyInstance方法有三个参数,分别是类加载器,一个希望该代理实现的接口列表以及传入他的被代理对象的调用处理器。

通过 Proxy.newProxyInstance创建的代理对象是在jvm运行时动态生成的一个对象,它并不是我们的InvocationHandler类型,也不是我们定义的那组接口的类型,而是在运行是动态生成的一个对象,并且命名方式都是这样的形式,以$开头,proxy为中,最后一个数字表示对象的标号。

总体逻辑大概是:通过InvocationHandler来包装被代理的方法,再根据(InvocationHandler)和需要代理的接口生成相应的代理对象,通过将相应的调用转发到代理对象,从而实现功能的包装。

由于在Proxy.newProxyInstance中,所有的方法都是通过h.invoke()实现的,这样就让所有的接口方法都被包裹上代理逻辑,也就是说当执行被代理操作的时候在代理对象内部,实际上是使用invoke()方法将请求转发给了调用处理器进行操作。

静态代理

既然说了动态代理,那我们就顺便提一下静态代理,虽然他没有动态代理那么好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

package test;

public interface Subject
{
public void doSomething();
}
package test;

public class RealSubject implements Subject
{
public void doSomething()
{
System.out.println( "call doSomething()" );
}
}
package test;

public class SubjectProxy implements Subject
{
Subject subimpl = new RealSubject();
public void doSomething()
{
//可以在这里加入代理逻辑
subimpl.doSomething();
//可以在这里加入代理逻辑
}
}
package test;

public class TestProxy
{
public static void main(String args[])
{
Subject sub = new SubjectProxy();
sub.doSomething();
}
}

刚开始我会觉得SubjectProxy定义出来纯属多余,直接实例化实现类完成操作不就结了吗?后来随着业务庞大,你就会知道,实现proxy类对真实类的封装对于粒度的控制有着重要的意义。但是静态代理这个模式本身有个大问题,如果类方法数量越来越多的时候,代理类的代码量是十分庞大的。所以引入动态代理来解决此类问题。

动态代理相对于静态的好处:

  1. Proxy类的代码量被固定下来,不会因为业务的逐渐庞大而庞大;
  2. 可以实现AOP编程,实际上静态代理也可以实现,总的来说,AOP可以算作是代理模式的一个典型应用;
  3. 解耦,通过参数就可以判断真实类,不需要事先实例化,更加灵活多变。
    这也是为什么Spring这么受欢迎的一个原因,
    Spring容器代替工厂,Spring AOP代替JDK动态代理,让面向切面编程更容易实现。
    在Spring的帮助下轻松添加,移除动态代理,且对源代码无任何影响。