文档章节

AOP 实现原理

黄勇
 黄勇
发布于 2013/09/12 01:02
字数 2006
阅读 10523
收藏 130

本文是《轻量级 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:在退出方法时执行

© 著作权归作者所有

共有 人打赏支持
黄勇

黄勇

粉丝 6213
博文 121
码字总数 216155
作品 1
浦东
CTO(技术副总裁)
加载中

评论(33)

865871137
865871137
有个需求是实现一个aop,能够拦截dao层的所有crud操作(dao层的代码公司都是公司DAL框架自动生成的,全部继承了同一个父类,也就是说我只知道这个公共的父类,不知道它的具体子类),用户只需要添加相关依赖,然后不需要再写任何代码或者配置文件(比如说不能在各个类上注解或者像spring一样写xml配置文件),就可以实现拦截。这样一来,只要各个项目使用的是公司的DAL框架,并且依赖了我这个aop类库,那所有的增删方法查改都能被拦截并增强。请教一下这个有实现思路吗?
Honwhy
Honwhy
我可以评论吗
r
rain082900
受益匪浅
pwqok
pwqok
哦,发现有个AspectOrder,哈哈
pwqok
pwqok
怎么给Aspect排优先级啊
Xsank
Xsank
nice~
时间对面
时间对面

引用来自“tangkf”的评论

引用来自“oysterouy”的评论

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

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

你好,请问你会linux服务器部署搭建吗?我们在重庆
黄勇
黄勇

引用来自“Larry_OS”的评论

在 BaseAspect 中增加 begin() 与 final() 方法:
看下面的源码,final()应该是end()
多谢指正!现已修改。博文仅提供设计思路与开发过程,细节的代码请查看源码: http://git.oschina.net/huangyong/smart-framework/blob/master/src/main/java/org/smart4j/framework/aop/AspectProxy.java
Larry_OSC
Larry_OSC
在 BaseAspect 中增加 begin() 与 final() 方法:
看下面的源码,final()应该是end()
黄勇
黄勇

引用来自“哈库纳”的评论

引用来自“黄勇”的评论

引用来自“刘志成”的评论

引用来自“哈库纳”的评论

引用来自“liushicheng”的评论

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

赞同,虽然我没用过AspectJ ,不过在看到那个复杂的表达式配置时。我还是觉得 JFinal那样 标记一个@Before更实际。 还有就是基于Guice下Aop的,它根本不需要声明什么表达式。

赞同,jFinal太简洁了,而且只依赖java自带的annotation

Smart 是不是也要来一个 @Before、@After、@Around 之类的注解呢?

建议支持,但是不建议搞三个 @Before、@After、@Around 。

设计一个 @Aop 用 过滤器那种方式去支持 Aop 比 JFianl @Before 更实用。 如果顺手的话把 @Aop 做成支持 Aop 链的 就OK。

很好的思路与建议,非常感谢!
Spring核心技术原理-(2)-通过Web开发演进过程了解一下为什么要有Spring AOP?

Spring核心技术原理-(2)-通过Web开发演进过程了解一下为什么要有Spring AOP? Harries Blog™2017-12-251 阅读 httpIOSpringApphttpsAOPjavaioc 上一篇: Spring核心技术原理-(1)-通过Web...

Harries Blog™
2017/12/25
0
0
Spring架构揭秘-AOP

1、AOP概述 2、Spring AOP原理 3、Spring AOP架构解析 一、AOP概述 Java程序员在写代码的时候通常都是使用新建对象类来描述业务特性,然后通过对象的继承、组合、扩展等手段来实现业务需求,...

sgkbkega
2016/09/08
25
0
微热山丘,探索 IoC、AOP 实现原理(一)

微热山丘,探索 IoC、AOP 实现原理(一) 码蜂笔记2017-12-241 阅读 IoCAOP 一. 简介及项目设定 1.1 微热山丘 介绍 warmhill(微热山丘) 是一个参考 Spring 实现 IoC、AOP 特性的小项目。 ...

码蜂笔记
2017/12/24
0
0
dojo1.7功能介绍:面向方面编程(AOP)功能与原理

日前发布的dojo 1.7版本对其源码进行了很大的变更。在迈向2.0版本之际,dojo提供了许多新的功能,也对许多已有的功能进行了修改,具体来说新版本更好地支持AMD规范、提供了新的事件处理系统(...

bigYuan
2012/04/13
0
0
秋色园QBlog技术原理解析:系列终结篇:最后的AOP策略(十九)

开篇闲话: 好几个月没写文章了,从9月15号发布新浪“微博粉丝精灵”V1.0后,持续的几个月都在折腾它,现在都折腾到V3.4版本了。 因此,本篇迟来了三个月了,同时,本篇也是本系列的最后一篇...

晨曦之光
2012/03/09
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

java工程师用spring boot和web3j构建以太坊区块链应用

区块链最近IT世界的流行语之一。这项有关数字加密货币的技术,并与比特币一起构成了这个热门的流行趋势。它是去中心化的,不可变的分块数据结构,这是可以安全连接和使用的密码算法。在这种结...

笔阁
3分钟前
0
0
聊聊sentinel的SentinelWebAutoConfiguration

序 本文主要研究一下sentinel的SentinelWebAutoConfiguration SentinelWebAutoConfiguration spring-cloud-alibaba-sentinel-autoconfigure-0.2.0.BUILD-SNAPSHOT-sources.jar!/org/springf......

go4it
6分钟前
0
0
java ArrayList 根据对象内的属性排序

//根据修改时间排序Comparator com = new Comparator<ReleaseInfo>() {public int compare(ReleaseInfo reInfo1, ReleaseInfo reInfo2) { //return reInfo2.getModifyTime().c......

成长中的小白
6分钟前
0
0
PowerDesigner p f m

(非原创) P:PirmaryKey 主键 F:ForeignKey 外键 M:Mandatory 强制要求(不能为空) 主键: 主键是数据表的唯一索引,比如学生表里有学号和姓名,姓名可能有重名的,但学号确是唯一的,你要从...

森火
6分钟前
0
0
Nexus Repository Manager 搭建私有docker仓库

Nexus Repository Manager 搭建私有docker仓库 2018年05月08日 14:44:23 阅读数:115 1.下载nexus3的镜像: docker pull sonatype/nexus3 2.使用镜像启动一个容器: docker run -d --name n...

linjin200
7分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部