AOP层层递进 第二部分 切面

一 AOP由来

  AOP全称Aspect-Oriented Programming,和OOP相对应,前者是面向切面编程,后者面向切面编程,若要更加直白的描述两者的区别,个人认为面向对象在纵向通过派生来实现个性化逻辑,而面向切面则在横向通过代理实现统一化逻辑。

  事实上AOP的确也表示从业务逻辑中分离出来的横切逻辑,包括但不限于性能监控、日志记录、权限控制等等。这些功能在各个独立的业务功能模块中都可能存在,AOP就是为了将这部分公用且重复的逻辑从业务逻辑中剥离开来,实现代码解耦,让业务职责更加单一。

  AOP的概念很早就有了,Java圈子里最出名的非AspectJ莫属,它的前身是AspectWerkz,而AspectWerkz早在2005年就停止更新了,所以说它是AOP的发源地并不为过。让AOP广为流传的则是Rod Johnson,这个人写了一个叫Spring的框架,因此一炮而红,他不仅在Spring中设计了一套IOC框架,还在此基础上设计了一套AOP框架,然而AOP效果并不理想,后来他采纳了网友们的建议在Spring中集成了AspectJ,所以才出现了如今Spring+AspectJ的经典组合。

二 如何实现AOP

  假如我们现在要对一个函数执行过程进行性能监控,那么新人们大致会写出如下代码:

public class DemoClass {
    public static void main(String[] args) {
        DemoClass demoClass = new DemoClass();
        demoClass.process();
    }

    private void process() {
        long start = System.currentTimeMillis();
        System.out.println("我被统计耗时了");
        System.out.println("耗时:" + (System.currentTimeMillis() - start));
    }
}

  这种写死的代码有哪些缺陷呢?首先process方法应该专注于业务逻辑的实现,将性能统计代码强行插入其间使其职责混乱;其次当需要被统计的函数不仅仅为DemoClass的process函数时,需要修改各个函数逻辑,这违反了开闭原则;最后统计逻辑未必在生产中使用,此时再将函数调整,那么测试将变得毫无意义。

  怎么解决?考虑如何在不侵入原函数逻辑的前提下,对函数功能进行增强——代理,没错就是上一章节提到的代理,那么将上面的实现调整为静态代理,我们需要先将目标函数抽象成为接口定义,然后实现代理类设计,那么调整后的代码应该长成下面的样子:

interface Process {
    public void process();
}

class DemoClassProxy implements Process {
    private Process impl;

    public DemoClassProxy(Process impl) {
        this.impl = impl;
    }

    @Override
    public void process() {
        long start = System.currentTimeMillis();
        impl.process();
        System.out.println("耗时:" + (System.currentTimeMillis() - start));
    }
}

public class DemoClass implements Process {
    public static void main(String[] args) {
        Process processImpl = new DemoClassProxy(new DemoClass());
        processImpl.process();
    }

    public void process() {
        System.out.println("我被统计耗时了");
    }
}

  当然按照上一章节所说,静态代理是存在缺陷的,如果希望对多个类型的目标方法进行耗时统计,那么我们需要写很多Proxy,所以再次改进,用JDK动态代理实现:

class DynamicProxy implements InvocationHandler {
    private Object target;

    public DynamicProxy(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 {
        long start = System.currentTimeMillis();
        Object result = method.invoke(target, args);
        System.out.println("耗时:" + (System.currentTimeMillis() - start));
        return result;
    }
}

public class DemoClass implements Process {
    public static void main(String[] args) {
        Process processImpl = new DynamicProxy(new DemoClass()).getProxy();
        processImpl.process();
    }

    public void process() {
        System.out.println("我被统计耗时了");
    }
}

  如此一来但凡需要进行耗时统计的函数,只需要在应用程序调用处调整为DynamicProxy来返回接口实例对象即可,然而……如果被代理类没有实现任何接口呢?JDK动态代理无法做到非接口模式的代理,那么只能选择CGLIB动态代理了,重新调整:

class CGLIBProxy implements MethodInterceptor {

    public CGLIBProxy() {
    }

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

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = methodProxy.invokeSuper(o, objects);
        System.out.println("耗时:" + (System.currentTimeMillis() - start));
        return result;
    }

}

public class DemoClass implements Process {
    public static void main(String[] args) {
        Process processImpl = new CGLIBProxy().getProxy(DemoClass.class);
        processImpl.process();
    }

    public void process() {
        System.out.println("我被统计耗时了");
    }
}

  到此为止,掰开了揉碎了,看似都在搞代理,这和AOP有啥关系呢?其实我们上面实现的就是AOP,在AOP中横向抽取出来的公共逻辑实际上就是原函数的一种增强,所以你会在SpringAOP中常常听到三个名词:

  1. 前置增强-BeforeAdvice
  2. 后置增强-AfterAdvice
  3. 环绕增强-AroundAdvice

  稍微调整下上面的程序,我们将long start = System.currentTimeMillis()封装为函数before(), 将System.out.println(“耗时:” + (System.currentTimeMillis() - start))封装为函数after():

public long befor() {
	return System.currentTimeMillis();
}

public void after(long start) {
	System.out.println("耗时:" + (System.currentTimeMillis() - start));
}

  那么前置增强就是before函数,后置增强就是after函数,环绕增强就是把这两个合起来。这样去理解AOP是不是就很清晰了?

三 Spring AOP

  在了解了AOP的实现机制后,我们在来说说Spring是如何实现AOP的,原理自不必说,先说说如何使用。SpringAOP的使用分为编程式和配置式。

3.1 编程式

  以编程模式使用SpringAOP要求依赖spring-aop组件,并且应用需要预先准备前置增强和后置增强接口的实现,写法如下:

class BeforeAdvice implements MethodBeforeAdvice {

    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("前置增强");
    }
}

class AfterAdvice implements AfterReturningAdvice {

    @Override
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
        System.out.println("后置增强");
    }
}

public class DemoClass {
    public static void main(String[] args) {
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.setTarget(new DemoClass());
        proxyFactory.addAdvice(new BeforeAdvice());
        proxyFactory.addAdvice(new AfterAdvice());

        DemoClass demoClass = (DemoClass) proxyFactory.getProxy();
        demoClass.process();
    }

    public void process() {
        System.out.println("我被测试着呢");
    }
}

  按如上方式就可以实现对DemoClass的process方法进行前后两次增强了,那么如果我们将MethodBeforeAdvice和AfterReturningAdvice两个接口合并是不是就实现了环绕增强咯,没错,只不过我们无需额外定义一个环绕接口了,org.aopalliance.intercept.MethodInterceptor已经帮我们做了这件事儿。这个接口不是Spring提供的,它归属AOP联盟,对接口的一个简单实现如下:

class AroundAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        before();
        Object result = methodInvocation.proceed();
        after();
        return result;
    }
    
    private void before() {
        System.out.println("前置增强");
    }
    
    private void after() {
        System.out.println("后置增强");
    }
}

  上面的篇幅都是以代码方式实现的AOP,其本质上依然是通过代理的方式来对目标函数进行增强而已,所以说AOP本身没有什么很厉害的东西,接下来我们在看看Spring大肆宣扬的配置文件方式实现AOP(个人不是很接受大篇幅的配置,代码可读性不高,配置维护困难,看似降低了开发者工作成本,实际上也拦住了开发者晋升的渠道,大量使用反射性能低下,代码行数看似很少,性能却无法掌控,优化耗时几乎将前期节约的资源成本都耗尽了)。

3.2 配置式

  说实话,这段不想写了,我是真的十分十分讨厌配置,就这么任性,反正Spring这套鬼东西网上资料多的是,看官有兴趣就自己搜搜看吧,我简单列一下SpringAOP中的一些关键点,大家按关键词搜索资料就好:

3.2.1 抛出增强

  主要用于统一的异常处理,举个例子,程序中各处的异常处理要么继续向上抛出,要么拦截记录日志,这些对异常的处理行为是一致的,所以没有必要在各处实现,而且为了统一异常处理模式,莫不如直接将其剥离出来成为切面,抛出增强就是解决这个问题的。

3.2.2 引入增强

  在这个小节之前都是对方法进行增强的,这个过程Spring称之为织入——Weaving。那么引入这个概念则是对类型进行增强,Spring称之为引入增强——IntroductionAdvice,啥意思?

  1. 存在接口A和B
  2. 存在类C实现了接口A
  3. 现在C不想实现接口B,但是又想通过C调用B接口的方法

  够清楚了吗?话说回来我真是不知道Spring到底方便了开发者,还是坑了开发者,整个架构观念都偏的不行。

3.2.3 切面切点

  再说说AOP中一个较难解决的问题,之前我们所写的案例都是单方法的,所以通过代理实现AOP也不复杂,但实际上很多类型的方法定义是非常多的,想对目标方法进行增强就必须要定位到目标方法,切面就是用来解决这个问题的。

  所谓切面就是一组筛选过滤的条件,这些过滤条件加上目标类型就可以定位到具体的方法了,比如说目标类型DemoClass,过滤条件方法名为process,这个组合就叫切面——Advisor。

  那么如何描述筛选过滤条件呢?切点呗——PointCut,那么总结起来切面将增强的逻辑和切点包装起来,Spring将其配置到ProxyFactory中,从而生成代理,以此来实现对目标类型的目标函数进行逻辑增强。

四 Spring+AspectJ

  整个章节三其实并不能完全将SpringAOP描述清楚,我也懒得去写,比如说SpringAOP还有自动代理模式,可以扫描Bean,也可以扫描切面配置等等,万变不离其宗,就是各种配配配,配到最后文件乱了,开发者不仅编程水平没提升,连文件配置都干懵逼了,以至于Rod Johnson意识到在这样下去就特么的没办法忽悠了呀,得想办法把切面配置从Spring体系中干掉,然后他打开自己的留言板——嗯,既然网友们都喜欢AspectJ,那我也用吧。

  至此Spring+AspectJ黄金搭档横空出世。大家都不用去配置各种切面文件了,改为各种注解来描述,程序员们载歌载舞,写注解归根到底也是写代码,总比写配置强,舒服。

  这真的舒服吗?我个人看待这个事情还是举保留态度的,注解虽然以代码的方式来描述代码的元信息,但我们必须意识到注解的使用一定离不开反射,只要大量使用反射性能必然受损,合理和架构设计与所谓的精简代码相比,孰轻孰重很难说的清楚,个人更偏向于合理的架构设计,清晰合理的架构设计之下使用最基础的语法实现程序设计,不仅便于程序的阅读维护,也方便迁移。

  用注解不过是替代了配置文件,不论何种实现其本质都是代理,所以这里不再重点描述AspectJ的用法,我列一个AspectJ提供的用于AOP的注解列表,大家参考下吧:

增强类型接口注解配置
前置增强:BeforeAdviceMethodBeforeAdvice@Beforeaop:before
后置增强:AfterAdviceAfterReturningAdvice@Afteraop:after
环绕增强:AroundAdviceMethodInterceptor@Aroundaop:around
抛出增强:ThrowsAdviceThrowsAdvice@AfterThrowingaop:after-throwing
引入增强:IntroductionAdviceDelegatingIntroductionInterceptor@DeclareParentsaop:declare-parents

五 小结

  第一部分主要介绍了代理,是为第二部分铺路,第二部分简单的理一下Spring对AOP的支持,以及为何会出现配置、注解多种方式,那么在理清楚历史脉络之后,大家再去学习相关知识的时候就应该知道什么才是重点,说的更直接一些:

  1. 代理是根,想提升把代理吃透
  2. AOP是一种设计思想,SpringAOP沿用了Spring的配置体系,最终因配置过于复杂被抛弃
  3. AspectJ通过注解实现AOP,代码可读性更强,但是耦合度也随之变高

  最后我会在第三部分自行设计一个AOP框架,反正已经了解了代理机制,和Spring以及AscpectJ对AOP的支持,那么如果我们在项目实施的过程中,想实现更加轻量,更加可控的AOP框架,莫不如自行设计一个。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬睡客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值