AOP 实现原理
博客专区 > 黄勇 的博客 > 博客详情
AOP 实现原理
黄勇 发表于4年前
AOP 实现原理
  • 发表于 4年前
  • 阅读 9669
  • 收藏 128
  • 点赞 8
  • 评论 33

腾讯云 技术升级10大核心产品年终让利>>>   

本文是《轻量级 Java Web 框架架构设计》的系列博文。

最近两天都在研究 AOP,很想做一个轻量级的 AOP,今天尝试了一天,用到了 CGLib、ASM、Javassist 等技术,但都已失败而告终。

有人会问我:Spring 都选择了知名的 AspectJ 开源 AOP 类库,而你为何不尝试一下呢?

原因其实很简单,AspectJ 的 jar 将近 2M,功能肯定是非常强大了,尤其是切点表达式,但文件实在太大,我认为不够轻量级。还有一个问题就是,如果要使用 AspectJ,又不想集成 Spring 的话,那就必须使用 AspectJ 给我们提供的 Java 语法扩展,也就是 aspect 类了,这就意味着我们还要多学一门语言。所以我果断的放弃了 AspectJ。

后来我又看了一下 AspectJ 的前身 AspectWerkz,这个项目早在 2005 年就没有升级过了,虽然很轻量级,jar 包将近 700K。当然唯一让我不爽的是,要用 Doclet,确实够老的技术了。所以最终我也放弃了它。

无奈之下,我将目光转回到 CGLib,这个类库我还是比较看好的,只要不是 final 类或 final 方法,它都可以生成动态代理,而且用法也比较简单。

那么最后我又是如何实现轻量级 AOP 的呢?先看看我设计的这个 Aspect 类吧:

@Bean
@Aspect(pkg = "com.smart.sample.action", cls = "ProductAction")
public class ProductActionAspect extends BaseAspect {

    @Override
    protected Object advice(Pointcut pointcut, Object proxy, Object[] args) {
        long begin = System.currentTimeMillis();

        Object result = pointcut.invoke(proxy, args);

        System.out.println("Time: " + (System.currentTimeMillis() - begin) + "ms");

        return result;
    }
}

很简单,我要横切(或成为“拦截”)的是 com.smart.sample.action 包下的 ProductAction 类。增强代码写在 advice 方法中,它是父类 BaseAspect 的一个抽象方法,必须由子类来实现。业务逻辑是,在调用切点的前后(也就是调用 ProductAction 所有方法的前后),打印一下调用时长。应该很好理解吧。

下面再看看 BaseAspect 吧:

public abstract class BaseAspect implements MethodInterceptor {

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

    @Override
    public Object intercept(Object proxy, Method methodTarget, Object[] args, MethodProxy methodProxy) throws Throwable {
        return advice(new Pointcut(methodTarget, methodProxy), proxy, args);
    }

    protected abstract Object advice(Pointcut pointcut, Object proxy, Object[] args);

    protected class Pointcut {

        private Method methodTarget;
        private MethodProxy methodProxy;

        public Pointcut(Method methodTarget, MethodProxy methodProxy) {
            this.methodTarget = methodTarget;
            this.methodProxy = methodProxy;
        }

        public Method getMethodTarget() {
            return methodTarget;
        }

        public MethodProxy getMethodProxy() {
            return methodProxy;
        }

        public Object invoke(Object proxy, Object[] args) {
            Object result = null;
            try {
                result = methodProxy.invokeSuper(proxy, args);
            } catch (Throwable e) {
                e.printStackTrace();
            }
            return result;
        }
    }
}

这里用到了 CGLib,见 getProxy() 方法。也很简单,只是创建一个动态代理类而已。

由于实现了 CGLib 的 MethodInterceptor 接口,所以必须实现 intercept() 方法。我在这个方法中调用了自定义的 advice() 方法,然而这个 advice() 方法还是一个 abstract 方法,那么 BaseAspect 的子类就必须实现 advice() 方法了。注意:这里使用了 Template 设计模式。

最后,我在这里定义了一个 protected 的内部类 Pointcut,也就是切点了。在切点里封装了 intercept() 方法的 MethodProxy 参数,并在自定义的 invoke() 方法中代理了 MethodProxy 的 invokeSuper() 方法,此外也顺便处理了异常。

做了以上这些处理,ProductActionAspect 的 advice() 方法的参数才会如此简单,甚至程序员在使用的时候,都无需知道 CGLib 的存在。

顺便说明一下,在 @Aspect 注解中 pkg 字段是必须的,而 cls 字段是可选的。若 cls 不写,则表示横切 pkg 下所有的类,否则只横切指定类。

好了,到这里为止还差一步,就是框架如何处理 @Aspect 注解呢?请看最后一段代码吧:

public class AOPHelper {

    static {
        try {
            // 获取带有 @Aspect 注解的类(切面类)
            List<Class<?>> aspectClassList = ClassHelper.getClassListByAnnotation(Aspect.class);
            // 遍历所有切面类
            for (Class<?> aspectClass : aspectClassList) {
                // 获取 @Aspect 注解中的属性值
                Aspect aspect = aspectClass.getAnnotation(Aspect.class);
                String pkg = aspect.pkg(); // 包名
                String cls = aspect.cls(); // 类名
                // 初始化目标类列表
                List<Class<?>> targetClassList = new ArrayList<Class<?>>();
                if (StringUtil.isNotEmpty(pkg) && StringUtil.isNotEmpty(cls)) {
                    // 如果包名与类名均不为空,则添加指定类
                    targetClassList.add(Class.forName(pkg + "." + cls));
                } else {
                    // 否则(包名不为空)添加该包名下所有类
                    targetClassList.addAll(ClassHelper.getClassListByPackage(pkg));
                }
                // 遍历目标类列表
                if (CollectionUtil.isNotEmpty(targetClassList)) {
                    // 创建父切面类
                    BaseAspect baseAspect = (BaseAspect) aspectClass.newInstance();
                    for (Class<?> targetClass : targetClassList) {
                        // 获取目标实例
                        Object targetInstance = BeanHelper.getBean(targetClass);
                        // 创建代理实例
                        Object proxyInstance = baseAspect.getProxy(targetClass);
                        // 复制目标实例中的字段到代理实例中
                        for (Field field : targetClass.getDeclaredFields()) {
                            field.setAccessible(true); // 可操作私有字段
                            field.set(proxyInstance, field.get(targetInstance));
                        }
                        // 用代理实例覆盖目标实例
                        BeanHelper.getBeanMap().put(targetClass, proxyInstance);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这个 AOPHelper 的作用非常大,它用于识别用户编写的 Aspect 类,并将其织入到目标类中,当然这一切都归功于 CGLib。

代码逻辑我在此就不做解释了,若有疑问,请大家给我留言。非常感谢您的关注!


补充(2013-09-12)

估计会有网友提出这样的问题:你这里只是对指定包和指定类进行了拦截,实际上就是拦截了类中所有的方法,那我只想拦截特定的方法,应该如何实现呢?

这个问题非常好,其实我已经封装了一个 Pointcut 类,从这个类中可以获取被拦截的方法,进而加以判断就可以实现对方法的过滤了。代码如下:

@Override
protected Object advice(Pointcut pointcut, Object proxy, Object[] args) {
    Object result;
    Method method = pointcut.getMethodTarget();
    if (method.getName().equals("getProducts")) {
        long begin = System.currentTimeMillis();
        result = pointcut.invoke(proxy, args);
        System.out.println("Time: " + (System.currentTimeMillis() - begin) + "ms");
    } else {
        result = pointcut.invoke(proxy, args);
    }
    return result;
}
以上我只拦截了 getProducts() 方法,在调用该方法前后进行了计时。这是通过方法名来判断的,其实也可以做成自定义注解的方式来判断。

我越看这段代码越觉得不够优雅,比如 result = pointcut.invoke(proxy, args); 在 if...else... 语句中都重复出现过,只不过在 if 中稍微有些不一样罢了。

于是我大胆地对这个 BaseAspect 进行了重构,充分运用到了 Template 设计模式。修改后的 BaseAspect 如下:

public abstract class BaseAspect implements MethodInterceptor {

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

    @Override
    public Object intercept(Object proxy, Method methodTarget, Object[] args, MethodProxy methodProxy) throws Throwable {
        Object result = null;
        if (filter(methodTarget, args)) {
            before(methodTarget, args);
            try {
                result = methodProxy.invokeSuper(proxy, args);
            } catch (Exception e) {
                e.printStackTrace();
                error(methodTarget, args, e);
            }
            after(methodTarget, args);
        } else {
            result = methodProxy.invokeSuper(proxy, args);
        }
        return result;
    }

    protected boolean filter(Method method, Object[] args) {
        return true;
    }

    protected void before(Method method, Object[] args) {
    }

    protected void after(Method method, Object[] args) {
    }

    protected void error(Method method, Object[] args, Exception e) {
    }
}

首先,通过一个 filter() 方法对目标方法进行过滤(默认为 true,无过滤)。

然后,在头部增加了 before() 方法。

随后,在尾部增加了 after() 方法。

最后,将 result = methodProxy.invokeSuper(proxy, args); 语句用一个 try...catch... 来包一下,在 catch 中调用了 error() 方法。

以上这四个方法都是 protected 的,且方法中没有任何代码。也就意味着,它们可以自由地让用户来覆盖(继承)。

以下就是修改后的 ProductActionAspect:

@Bean
@Aspect(pkg = "com.smart.sample.action", cls = "ProductAction")
public class ProductActionAspect extends BaseAspect {

    private long begin;

    @Override
    protected boolean filter(Method method, Object[] args) {
        return method.getName().equals("getProducts");
    }

    @Override
    protected void before(Method method, Object[] args) {
        begin = System.currentTimeMillis();
    }

    @Override
    protected void after(Method method, Object[] args) {
        System.out.println("Time: " + (System.currentTimeMillis() - begin) + "ms");
    }

    @Override
    protected void error(Method method, Object[] args, Exception e) {
        System.out.println("Error: " + e.getMessage());
    }
}
为了让示例更加全面,我同时覆盖了父类中提供的所有方法(其实一个都不覆盖都行),分别在每个方法中写了一点内容。

在重构 BaseAspect 时顺手也把 Pointcut 给干掉了(减少词汇,降低难度),这样是不是比以前更加优雅了呢?请大家指教!


补充(2013-09-17)

在 BaseAspect 中增加 begin() 与 end() 方法:

public abstract class BaseAspect implements MethodInterceptor {

    ...

    @Override
    public Object intercept(Object proxy, Method methodTarget, Object[] args, MethodProxy methodProxy) throws Throwable {
        begin(methodTarget, args);
        Object result = null;
        try {
            if (filter(methodTarget, args)) {
                before(methodTarget, args);
                result = methodProxy.invokeSuper(proxy, args);
                after(methodTarget, args);
            } else {
                result = methodProxy.invokeSuper(proxy, args);
            }
        } catch (Exception e) {
            e.printStackTrace();
            error(methodTarget, args, e);
        } finally {
            end(methodTarget, args);
        }
        return result;
    }

    ...
}
总结一下,轻量级 AOP 框架中,目前可提供以下横切方法:
  1. begin:在进入方法时执行
  2. filter:用于设置拦截过滤条件
  3. before:在目标方法调用前执行
  4. after:在目标方法调用后执行
  5. error:在抛出异常时执行
  6. end:在退出方法时执行
共有 人打赏支持
黄勇
粉丝 5635
博文 114
码字总数 196279
作品 1
评论 (33)
黄正文
doclet是什么?不是生成文件的?
sunnytu
写的不错:)
黄勇

引用来自“黄正文”的评论

doclet是什么?不是生成文件的?

早在 JDK 1.4 的时代,那时还没有注解,于是有些牛逼的人发明了在 JavaDoc 里写注解。风靡了很长一段时间后,Sun 公司看到这是一个 good idea,所有就开发了注解。
wangzunren
强大
oysterouy
曾经觉得AOP这个概念很神奇,认真研究过之后发现现实实现总有很多限制,越来越觉得这个不就是管道事件的另一个说法吗?区别就是事件得预先定义,AOP可以后续注入,但总觉得后续注入这个需求是程序员自己偷懒的做法,不是业务模型架构该有的设计,不值得提倡啊
黄正文

引用来自“黄勇”的评论

引用来自“黄正文”的评论

doclet是什么?不是生成文件的?

早在 JDK 1.4 的时代,那时还没有注解,于是有些牛逼的人发明了在 JavaDoc 里写注解。风靡了很长一段时间后,Sun 公司看到这是一个 good idea,所有就开发了注解。

说嘎
黄开源中国

引用来自“oysterouy”的评论

曾经觉得AOP这个概念很神奇,认真研究过之后发现现实实现总有很多限制,越来越觉得这个不就是管道事件的另一个说法吗?区别就是事件得预先定义,AOP可以后续注入,但总觉得后续注入这个需求是程序员自己偷懒的做法,不是业务模型架构该有的设计,不值得提倡啊

这个是用来补充oo的不足啊。。我理解更多是为了更加效率的开发。。。特别是面对一些需求的更新。。
never_say
AspectJ 很强大,切点很灵活,虽然强大,但是太复杂了,用上的可能性太小了
IT民工_
636666
hdairong
虽然我不懂,但看起来很厉害的样子
tangkf

引用来自“oysterouy”的评论

曾经觉得AOP这个概念很神奇,认真研究过之后发现现实实现总有很多限制,越来越觉得这个不就是管道事件的另一个说法吗?区别就是事件得预先定义,AOP可以后续注入,但总觉得后续注入这个需求是程序员自己偷懒的做法,不是业务模型架构该有的设计,不值得提倡啊

我同意你的观点,AOP的应用场景很窄,不应该大规模出现在业务系统中
哈库纳

引用来自“liushicheng”的评论

AspectJ 很强大,切点很灵活,虽然强大,但是太复杂了,用上的可能性太小了

赞同,虽然我没用过AspectJ ,不过在看到那个复杂的表达式配置时。我还是觉得 JFinal那样 标记一个@Before更实际。 还有就是基于Guice下Aop的,它根本不需要声明什么表达式。
哈库纳
博主还尝试了 ASM? 那家伙是很不错,性能可以发挥到淋漓尽致,性能甚至和静态代理相媲美。不过上手很难啊,需要去翻Java字节码。

我用那个家伙写过一个 类似 CGlib的工具, 说实话我觉得ASM确实不适合用来当作Smart Framework 的核心 AOP实现。不过要想追求极致的性能还真就非它莫属了。
哈库纳

引用来自“tangkf”的评论

引用来自“oysterouy”的评论

曾经觉得AOP这个概念很神奇,认真研究过之后发现现实实现总有很多限制,越来越觉得这个不就是管道事件的另一个说法吗?区别就是事件得预先定义,AOP可以后续注入,但总觉得后续注入这个需求是程序员自己偷懒的做法,不是业务模型架构该有的设计,不值得提倡啊

我同意你的观点,AOP的应用场景很窄,不应该大规模出现在业务系统中

赞同,业务场景中应用Aop 无外乎 : 方法级权限、方法结果缓存、数据库事务、日志记录、性能统计。
黄勇
其实没有 AOP 也是可以的,但有了它会让您写的代码更少,更关注与业务本身。
叫我蝴蝶吧
Pointcut还是别干掉了,毕竟老外选词来描述特性还是很深刻的,以前我就不能理解,慢慢就好了~
黄勇

引用来自“叫我蝴蝶吧”的评论

Pointcut还是别干掉了,毕竟老外选词来描述特性还是很深刻的,以前我就不能理解,慢慢就好了~

其实我已经将 Pointcut 写在 @Aspect 注解中了:
@Aspect(pkg = "com.smart.sample.action", cls = "ProductAction")
这里的 pkg + cls 就相当于 Pointcut,也就是横切条件了。
罗盛力

引用来自“黄开源中国”的评论

引用来自“oysterouy”的评论

曾经觉得AOP这个概念很神奇,认真研究过之后发现现实实现总有很多限制,越来越觉得这个不就是管道事件的另一个说法吗?区别就是事件得预先定义,AOP可以后续注入,但总觉得后续注入这个需求是程序员自己偷懒的做法,不是业务模型架构该有的设计,不值得提倡啊

这个是用来补充oo的不足啊。。我理解更多是为了更加效率的开发。。。特别是面对一些需求的更新。。

如果采用aop可以很方便的做一个日志监控
幻影浪子

引用来自“tangkf”的评论

引用来自“oysterouy”的评论

曾经觉得AOP这个概念很神奇,认真研究过之后发现现实实现总有很多限制,越来越觉得这个不就是管道事件的另一个说法吗?区别就是事件得预先定义,AOP可以后续注入,但总觉得后续注入这个需求是程序员自己偷懒的做法,不是业务模型架构该有的设计,不值得提倡啊

我同意你的观点,AOP的应用场景很窄,不应该大规模出现在业务系统中

AOP不就是为了减少一些不该出现在业务系统中的东西才诞生的嘛
mn_1127
楼主果然是高手呀! aop那么难理解的东西,被楼主几行代码便交代清楚了!
×
黄勇
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: