AOP 实现原理

原创
2013/09/12 01:02
阅读数 1.7W

本文是《轻量级 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:在退出方法时执行
展开阅读全文
加载中
点击加入讨论🔥(33) 发布并加入讨论🔥
打赏
33 评论
131 收藏
10
分享
返回顶部
顶部