AOP 实现原理

来源:互联网 发布:react router js跳转 编辑:程序博客网 时间:2024/05/22 01:27

本文是《轻量级 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 类吧:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
@Aspect(pkg = "com.smart.sample.action", cls = "ProductAction")
publicclass ProductActionAspect extendsBaseAspect {
 
    @Override
    protectedObject advice(Pointcut pointcut, Object proxy, Object[] args) {
        longbegin = System.currentTimeMillis();
 
        Object result = pointcut.invoke(proxy, args);
 
        System.out.println("Time: " + (System.currentTimeMillis() - begin) + "ms");
 
        returnresult;
    }
}

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

下面再看看 BaseAspect 吧:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
publicabstract class BaseAspect implementsMethodInterceptor {
 
   @SuppressWarnings("unchecked")
   public<T> T getProxy(Class<T> cls) {
        return(T) Enhancer.create(cls, this);
   }
 
   @Override
   publicObject intercept(Object proxy, Method methodTarget, Object[] args, MethodProxy methodProxy)throwsThrowable {
        returnadvice(newPointcut(methodTarget, methodProxy), proxy, args);
   }
 
   protectedabstract Object advice(Pointcut pointcut, Object proxy, Object[] args);
 
   protectedclass Pointcut {
 
        privateMethod methodTarget;
        privateMethodProxy methodProxy;
 
        publicPointcut(Method methodTarget, MethodProxy methodProxy) {
            this.methodTarget = methodTarget;
            this.methodProxy = methodProxy;
        }
 
        publicMethod getMethodTarget() {
            returnmethodTarget;
        }
 
        publicMethodProxy getMethodProxy() {
            returnmethodProxy;
        }
 
        publicObject invoke(Object proxy, Object[] args) {
            Object result = null;
            try{
                result = methodProxy.invokeSuper(proxy, args);
            }catch(Throwable e) {
                e.printStackTrace();
            }
            returnresult;
        }
   }
}

这里用到了 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 注解呢?请看最后一段代码吧:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
publicclass 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 = newArrayList<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 类,从这个类中可以获取被拦截的方法,进而加以判断就可以实现对方法的过滤了。代码如下:

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

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

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
publicabstract class BaseAspect implementsMethodInterceptor {
 
   @SuppressWarnings("unchecked")
   public<T> T getProxy(Class<T> cls) {
        return(T) Enhancer.create(cls, this);
   }
 
   @Override
   publicObject intercept(Object proxy, Method methodTarget, Object[] args, MethodProxy methodProxy)throwsThrowable {
        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);
        }
        returnresult;
   }
 
   protectedboolean filter(Method method, Object[] args) {
        returntrue;
   }
 
   protectedvoid before(Method method, Object[] args) {
   }
 
   protectedvoid after(Method method, Object[] args) {
   }
 
   protectedvoid error(Method method, Object[] args, Exception e) {
   }
}

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

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

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

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

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

以下就是修改后的 ProductActionAspect:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Bean
@Aspect(pkg = "com.smart.sample.action", cls = "ProductAction")
publicclass ProductActionAspect extendsBaseAspect {
 
   privatelong begin;
 
   @Override
   protectedboolean filter(Method method, Object[] args) {
        returnmethod.getName().equals("getProducts");
   }
 
   @Override
   protectedvoid before(Method method, Object[] args) {
        begin = System.currentTimeMillis();
   }
 
   @Override
   protectedvoid after(Method method, Object[] args) {
        System.out.println("Time: " + (System.currentTimeMillis() - begin) + "ms");
   }
 
   @Override
   protectedvoid error(Method method, Object[] args, Exception e) {
        System.out.println("Error: " + e.getMessage());
   }
}
为了让示例更加全面,我同时覆盖了父类中提供的所有方法(其实一个都不覆盖都行),分别在每个方法中写了一点内容。

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


补充(2013-09-17)

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
publicabstract class BaseAspect implementsMethodInterceptor {
 
    ...
 
    @Override
    publicObject intercept(Object proxy, Method methodTarget, Object[] args, MethodProxy methodProxy)throwsThrowable {
        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);
        }
        returnresult;
    }
 
    ...
}
总结一下,轻量级 AOP 框架中,目前可提供以下横切方法:
  1. begin:在进入方法时执行
  2. filter:用于设置拦截过滤条件
  3. before:在目标方法调用前执行
  4. after:在目标方法调用后执行
  5. error:在抛出异常时执行
  6. end:在退出方法时执行
转载:http://my.oschina.net/huangyong/blog/160769?p=3#comments
0 0