浅谈服务埋点(1)——AOP

  年会圆满结束了,我们的年会系统整体表现也还算不错,但唯一遗憾的是到最后摇一摇的时候,系统卡住了,不过还好最后挺了过来。
  在以往编写应用程序的时候,我们通常会记录日志,以便出了问题之后事后有迹可循,这是一种静态分析。这次UIOC事件的发生,让我意识到系统性能的监控,或者说某一时刻运行的情况,比如当前系统中对外提供了多少次服务,这些服务的响应时间是多少,随时间变化的情况是什么样的,系统出错的频率是多少,这些动态的实时信息对于监控整个系统的运行健康状况来说多么的重要。
  我们发现我们要做的这些监控实际和我们的业务是毫无关系的,说到这里可能了解Spring的朋友们就会想到了AOP。
  所以这一篇我先谈一谈基础知识–AOP

—————————————请叫我大分割线———————————–

一.为什么要用AOP

  面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。
  但是人们也发现,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。
  也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?

二.什么是AOP

  这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

  一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。
  这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码,有了AOP,OOP变得立体了。
  实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。

  接下来是一张在知乎上比较传神的图片。
  这里写图片描述
  面向切面,面向方面,也叫刀削面。

这里白面条好比系统的主流业务代码,各种调料和卤汁则好比那些处理琐碎事务的代码。这种将业务代码和琐碎事务代码分离的机制,能够让你在制作的时候只需要考虑自己这一部分的好坏,而不需要考虑其他。而且我需要加一个其他的琐碎的功能就非常方便(加一个配菜即可,不需要重新做一碗)。

  
—————————————请叫我分割线———————————–

三、AOP应用实战

  前面说了一堆的理论,接下来我们看看如何应用。

1、写死代码

先一接口

public interface Greeting {
    void sayHello(String name);
}

实现类

public class GreetingImpl implements Greeting {

    @Override
    public void sayHello(String name) {
        before();
        System.out.println("Hello! " + name);
        after();
    }

    private void before() {
        System.out.println("Before");
    }

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

这不必多说,这样的代码无疑满足了所有low代码的特点,程序员被累死,架构师被气死。
接下来提出三个重构的解决方案:

2、静态代理

最简单的方法是采用设置模式中的代理模式:

public class GreetingProxy implements Greeting {

    private GreetingImpl greetingImpl;

    public GreetingProxy(GreetingImpl greetingImpl) {
        this.greetingImpl = greetingImpl;
    }

    @Override
    public void sayHello(String name) {
        before();
        greetingImpl.sayHello(name);
        after();
    }

    private void before() {
        System.out.println("Before");
    }

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

用GreetingProxy这个代理类去代理GreetingImpl实现类。接着,客户端调用:

public class Client {

    public static void main(String[] args) {
        Greeting greetingProxy = new GreetingProxy(new GreetingImpl());
        greetingProxy.sayHello("Jack");
    }
}

这样做的问题也很明显:XXXProxy这样的代理类会越来越多。
这时需要用到JDK提供的动态代理了:

3、JDK动态代理

public class JDKDynamicProxy implements InvocationHandler {

    private Object target;

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

    @SuppressWarnings("unchecked")
    public <T> T getProxy() {
        return (T) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            this
        );
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);
        after();
        return result;
    }

    private void before() {
        System.out.println("Before");
    }

    private void after() {
        System.out.println("After");
    }
}
public class Client {

    public static void main(String[] args) {
        Greeting greeting = new JDKDynamicProxy(new GreetingImpl()).getProxy();
        greeting.sayHello("Jack");
    }
}

JDKDynamicProxy实现了InvocationHandler接口,那么必须实现该接口的invoke方法,该方法直接通过反射去invoke method,在调用前后分别处理before和after,最后返回result。
它的好处在于接口变了,这个动态的代理类不用动;而静态代理则是接口变了,实现类也要动代理类也要动。但jdk动态代理的缺陷是:它只能代理接口,而不能没有代理没有接口的类。

4、CGlib动态代理

我们使用开源的 CGLib 类库可以代理没有接口的类,这样就弥补了 JDK 的不足。CGLib 动态代理类是这样玩的:

public class CGLibDynamicProxy implements MethodInterceptor {

    private static CGLibDynamicProxy instance = new CGLibDynamicProxy();

    private CGLibDynamicProxy() {
    }

    public static CGLibDynamicProxy getInstance() {
        return instance;
    }

    @SuppressWarnings("unchecked")
    public <T> T getProxy(Class<T> cls) {
        return (T) Enhancer.create(cls, this);
    }

    @Override
    public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        before();
        Object result = proxy.invokeSuper(target, args);
        after();
        return result;
    }

    private void before() {
        System.out.println("Before");
    }

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

以上代码中了 Singleton 模式,那么客户端调用也更加轻松了:

public class Client {

    public static void main(String[] args) {
        Greeting greeting = CGLibDynamicProxy.getInstance().getProxy(GreetingImpl.class);
        greeting.sayHello("Jack");
    }
}

与JDKDynamicProxy类似,在CGLibDynamicProxy 中也添加一个泛型的getProxy方法,便于我们可以快速地获取自动生成的代理对象。

以上是基于代理的经典AOP,当引入了简单的声明式AOP和基于注解的AOP后,这种代理方式看起来非常笨重和复杂,感觉现在应该很少用到了,只不过我都看了,就写上来了。


5、声明式SpringAop

5.1、配置切入点Pointcut

Spring所有的切面和通知其都必须放在一个<aop:config> 内(可以配置包含多个<aop:config>元素),每一个<aop:config>可以包含pointcut,advisor和aspect元素。
(它们必须按照顺序声明)

切点用于定位应该在什么地方应用切面的通知,切点和通知是切面最基本的元素。
在SpringAOP中,需要使用AspectJ的切点表达式语言来定义切点。

这里写图片描述

最为常用的是excution()

这里写图片描述

我们使用execution()指示器选择Instrument的play()方法。*和(..)表示了我们不关心返回值和参数列表。
现在假设我们需要配置切点仅匹配com.springinaction.springidol包,在此场景下,可以使用within()指示器来限制匹配。

这里写图片描述

同理可以使用 || 和!操作符。

如何声明:

<aop:config>
    <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service..(..))"/>
</aop:config>

5.2、配置切面asspect

  • <aop:aspect>
<aop:config>
    <aop:aspect id="myAspect" ref="aBean">
        <aop:pointcut id="businessService"
             expression="execution(* com.xyz.myapp.service..(..))"/>
         ...
    </aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>

通知advice 分为以下几种:
这里写图片描述

前置、后置、环绕

首先有个观众类(Audience),有入座、关机、鼓掌、要求退款 等功能。
这里写图片描述

把他注册为Spring应用上下文的一个Bean。接下来利用AOP配置,将其变成一个切面。

这里写图片描述
这里写图片描述

这样的配置就能在表演者Perform的前、后、以及异常的时候调用观众的方法,而这些事件表演者毫不知情(做到真正意义的解耦)。
由于我们的这些切点都是一样的,所以可以做如下提取。

这里写图片描述

环绕通知

如果希望观众一直关注演出,并汇报每个表演的时长,那么前置/后置通知的方法是:前置通知保存开始时间,后置通知通过减法汇报时长。这样做不仅麻烦、而且容易引发线程安全问题。
而环绕通知可以很好的解决,它只需要在一个方法中实现,所以不需要使用成员变量保存状态。

这里写图片描述

从代码中我们可以看出,环绕通知有一个特点:它要求通知方法的第一个参数必须是ProceedingJoinPoint类型,然后joinpoint.proceed()是执行被通知的方法。
在这个切面中,watchPerformance()方法包含之前的4个通知方法的所有逻辑,但所有的逻辑都放在一个方法中了,而且该方法还负责自身的异常处理。接下来要做的就是配置<aop:around>了。

这里写图片描述

通知传参
有时候通知并不仅仅是对方法进行简单包装,还需要校验传递给方法的参数值,这时候就需要为通知传递参数。
现在有个实现MindReader接口的魔术表演者Magican,他想表演监听观众的内心感应和显示他们在想什么。

实现了Thinker的志愿者Volunteer参与了这次表演。

这里写图片描述

接下来便是Magican利用Spring AOP技术来实现监听Volunteer内心感应的表演了。

这里写图片描述

这里切点标识了thinkOfSomething()方法,指定了thoughts参数。before标签的arg-names元素意味着该参数必须传递给Magician的interceptThoughts()方法。


由于篇幅原因,还有一些利用AOP引入新方法(introduction)、注解AOP、AspectJ等内容以后再总结吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章