术语

术语不是为了限制你的设计,而是借此可以更方便的讨论和思考这个问题。这是我早上在《游戏设计要则探秘》上看的一句话,当然术语也不能华而不实,最好做到顾名思义或指出本质。

在使用 AOP 之前,我们需要了解AOP涉及的相关概念。

  • JoinPint:AOP的功能模块要植入到OOP的模块中,需要知道在哪些执行点上进行植入,这些执行点就是JoinPoint。JoinPoint可以是方法的调用,字段设置,异常处理执行等等。

  • PointCut:PointCut是对JoinPoint的表现形式,可以直接用方法名,正则表达式还有特定的PointCut表述语言。

  • Advice:Advice就是横切点功能的载体。包括,Before Advice,AAfter Advice,Around Advice。这三个切入的时间不同,可顾名思义。

  • Aspect:Aspect可以理解为切面,它可以包含多个PointCut和Advice。

Spring AOP 中的代理模式

Spring 文档里这样一句话:“It is important to grasp the fact that Spring AOP is proxy-based. ”Spring AOP 是基于代理的 AOP。配置AOP可以分为@AspecJ和XML格式。

Spring AOP 的实现机制同时使用JDK的动态代理CGLIB的动态字节码生成,当需要切入的类有实现某个接口时,Spring 会选择使用动态代理,为其动态生成代理类。当没有实现任何接口时,使用 CGLIB 动态生成其子类,并进行扩展。

代理模式

复习一下代理模式,代理模式是常见的一种设计模式。最近在Android开发中也有遇到。我们来看UML图:

pattern

从图中,我们看到不直接使用SubjectImpl而是通过SubjectProxy,这样SubjectProxy可以在其方法内对SubjectImpl进行扩展。

public class SubjectProxy implements ISubject {
private ISubject subject;

@Override
public String request(String header) {
	if (!header.startsWith("https")) {
		return null;
	}
	return "Proxy: " + subject.request(header);
}

public void setSubject(ISubject subject) {
    this.subject = subject;
}
}
public class Client {

public static void main(String[] args) {
    SubjectProxy subjectProxy = new SubjectProxy();
    subjectProxy.setSubject(new SubjectImpl());
    System.out.println(subjectProxy.request("https://leer.moe"));
    System.out.println(subjectProxy.request("http://leer.moe"));
}
}

这里我们的JoinPoint是request()方法,如果其他的接口也有这个方法,它也是我们的JoinPoint,就需要为每个接口编写代理类。为了解决这个问题,Java在JDK1.3之后提供了动态代理。

动态代理

动态代理的实现主要由一个类和一个接口实现:java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler。

上面的代理模式可用动态代理实现,编写我们自己的InvocationHandler:

public class RequestInvocationHandler implements InvocationHandler {
  private Object target;

  public RequestInvocationHandler(Object target) {
    this.target = target;
  }

  @Override
  public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
    if (method.getName().equals("request")) {
      if (!(args[0] instanceof  String) || !((String)args[0]).startsWith("https"))
        return null;
      return "Proxy: " + method.invoke(target, args);
    }
    return null;
  }
}

对不同目标对象,但是都拥有request方法JoinPoint的对象(比如下面代码中的SubjectImplRequsetableImpl),我们不再需要为他们分别创建代理类了,动态代理机制会为它们动态的创建代理实例,当调用request方法时,InvocationHandler就会对相应的方法进行拦截和处理。

  static void dynamic() {
    ISubject subject = (ISubject)Proxy.newProxyInstance(
        Client.class.getClassLoader(),
        new Class[]{ISubject.class},
        new RequestInvocationHandler(new SubjectImpl())
    );
    System.out.println(subject.request("https://leer.moe"));
    System.out.println(subject.request("http://leezoom.xyz"));

    Requestable requestable = (Requestable)Proxy.newProxyInstance(
        Client.class.getClassLoader(),
        new Class[]{Requestable.class},
        new RequestInvocationHandler(new RequsetableImpl())
    );
    System.out.println(requestable.request("https://leer.moe"));
    System.out.println(requestable.request("http://leezoom.xyz"));
  }

不过,动态代理也有无能无力的时候,如果一个类没有实现任何接口的话,就无法生成代理类了,Spring AOP 在无法使用动态代理的时候,会使用CGLIB为目标对象生成子类。

CGLIB

CGLIB 的使用请自己搜索

为了演示CGLIB的使用,我们定义的目标对象如下:

public class Request {
  public void request(String headers) {
    System.out.println("request " + headers);
  }
}
public class RequestCallback implements MethodInterceptor {

  public boolean before(Object arg) {
    System.out.println("before: check https....");
    return arg instanceof String && ((String) arg).startsWith("https");
  }

  public void after() {
    System.out.println("after");
  }

  @Override
  public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    if (method.getName().equals("request")) {
      if (before(args[0])) {
        Object result = methodProxy.invokeSuper(o, args);
        after();
        return result;
      } else {
        System.out.println("Check failed");
        return null;
      }
    }
    return null;
  }
}

可以看到,我增加了before和after方法,这就类似AOP中Advice的雏形了,

分别在 methodProxy.invokeSuper(o, args) 的前后调用。

接下来在客户端中使用CGLIB:

  static void cglib() {
    Request requestInstance = (Request) Enhancer.create(Request.class, new RequestCallback());
    requestInstance.request("https://leer.moe");
    requestInstance.request("http://leezoom.xyz");
  }

CGLIB为 Request生成子类,并对request方法按我们的需求进行扩展。GCLIB的唯一限制是无法对final方法进行扩展。输入结果如下:

cglib

小试 Spring AOP

在对Spring AOP的实现机制有所了解之后,我们开始实战使用一下Spring AOP吧。

简单起见,我使用@AspectJ风格的AOP配置,它是使用 Annotation 来配置AOP的。

Spring AOP可以使用@AspectJ风格是配置也可以用XML,但是底层还是基于动态代理和CGLIB的实现,并不是使用AspectJ的专门编译器来实现的。当然Spring 也支持完全使用AspectJ,但是这已经不是Spring AOP了,而是AspectJ的内容。

定义一个Aspect:

@Aspect
@Configuration
public class UserAccessAspect {
  private Logger logger = LoggerFactory.getLogger(this.getClass());


  @Before("execution(* moe.leer.springdemo.aop.service.*.*(..))")
  public void before(JoinPoint joinPoint) {
    //Advice
    logger.info("Check for user access");
    logger.info("Allowed execution for {}", joinPoint);
  }

  @After("execution(* moe.leer.springdemo.aop.service.*.*(..))")
  public void after(JoinPoint joinPoint) {
    //Advice
    logger.info("Execution finished {}", joinPoint);
  }
}

PointCut的定义使用正则表达式,execution(* moe.leer.springdemo.aop.service.*.*(..)) 表示匹配service包下的所有方法。@Before 和 @After 注解的两个方法分别对应Before Advice 和After Advice,来模拟对用户的检查和在执行完成后打印。

我们再来看Service包,里面就是简单测试业务:

@Service
public class Business1 {
  private Logger logger = LoggerFactory.getLogger(this.getClass());

  @Autowired
  private Dao1 dao1;

  public String calculateSomething() {
    String val = dao1.returnSomething();
    logger.info("In Business1 -{}", val);
    return val;
  }
}

接着编写测试代码,对其进行测试:

@RunWith(SpringRunner.class)
@SpringBootTest
public class BusinessAopTests {
  private Logger logger = LoggerFactory.getLogger(this.getClass());

  @Autowired
  private Business1 business1;

  @Autowired
  private Business2 business2;

  @Test
  public void invokeAOP() {
    logger.info(business1.calculateSomething());
    logger.info(business2.calculateSomething());
  }
}

aop_test

在 calculateSomething方法前后,植入了两个Advice。细心一点你可以发现,打印的Log中有“BySpringCGLIB”,很好的验证了我们之前的说法:Spring AOP 在无法使用动态代理的时候,会使用CGLIB为目标对象生成子类。