文档章节

动手实现MVC: 4. AOP的设计与实现

小灰灰Blog
 小灰灰Blog
发布于 2017/08/28 12:01
字数 2781
阅读 44
收藏 0

设计

我们将结合日常使用的姿势来设计切面的实现方式,由于spring-mvc切面比较强大,先将切面规则这一块单独拎出来,后面单独再讲;本篇博文主要集中在如何实现切面的基本功能

几种切面

  1. Before
  • 前置切面,在方法执行之前被调用
  1. Around
  • 环绕切面,可以在切面内部执行具体的方法,因此可以在具体方法执行前后添加自己需要的东西
  1. After
  • 后置切面,在方法执行完成之后被调用

基本功能

  1. 上面基础的三中切面支持
  2. 一个方法上可以被多个切面拦截(即一个方法可以同时对应多个Before,After,Around切面)
  3. 切面执行的先后顺序(一般而言,before优先around优先after
  4. 被切的方法只能执行一遍(为什么单独拎出来?主要是around切面内部显示的调用方法执行,如果一个方法有多个around切面,那么这个方法我们要求只执行一次)

实现

切面的实现依然是在 quick-mvc 这个项目中的,因此会利用到切面的Bean自动加载,IoC依赖注入等基本功能,与之相关的内容将不会详细说明,本片主要集中在如何设计并实现AOP上

1. 几个注解

沿用其他方式的注解定义, Before,After,Around,Aspect 如下,稍微注意的是切面的定义的规则目前只实现了注解的切面拦截,所以value对应的class应该是自定义的注解类

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Before {

    /**
     * 自定义的注解,方法上有这个注解,则会被改切面拦截
     * @return
     */
    Class value();

    /**
     * 排序,越小优先级越高
     * @return
     */
    int order() default 0;
}



@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Around {

    /**
     * 自定义的注解,方法上有这个注解,则会被改切面拦截
     * @return
     */
    Class value();

    /**
     * 排序,越小优先级越高
     * @return
     */
    int order() default 0;
}


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface After {
    /**
     * 自定义的注解,方法上有这个注解,则会被改切面拦截
     * @return
     */
    Class value();

    /**
     * 排序,越小优先级越高
     * @return
     */
    int order() default 0;
}


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Aspect {
}

2. 辅助类

实现AOP,肯定少不了代理,因此我们需要定义一个类,来包装执行方法相关的信息,用于传递给各个切面类中的方法

JoinPoint 包含实际对象,执行方法,参数

@Data
public class JoinPoint {

    private Object[] args;

    private Method method;

    private Object obj;
}

ProceedingJoinPoint, 继承JoinPoint, 此外包含代理类,返回结果,这个主要是用于Around切面,提供的proceed方法,就是给Around切面中执行目标方法的操作方式(下面使用了加锁双重判断的方式,保证实际方法只执行一次,这里有个问题,后面细说)

@Data
public class ProceedingJoinPoint extends JoinPoint {

    private MethodProxy proxy;

    private Object result;


    // 是否已经执行过方法的标记
    private volatile boolean executed;


    public Object proceed() throws Throwable {
        if (!executed) {
            synchronized (this) {
                if (!executed) {
                    result = proxy.invokeSuper(getObj(), getArgs());
                    executed = true;
                }
            }
        }

        return result;
    }

}

3. 代理类

下图给出了切面执行的顺序,流程比较清晰,而整个流程,则由代理类进行控制

流程

代理工程类,利用Cglib用于生成对应的代理类 CglibProxyFactory

public class CglibProxyFactory {

    private static Enhancer enhancer = new Enhancer();

    private static CglibProxy cglibProxy = new CglibProxy();

    @SuppressWarnings("unchecked")
    public static <T> T getProxy(Class<T> clazz){
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(cglibProxy);
        return (T) enhancer.create();
    }
}

代理类, 暂时只给出大体框架

public class CglibProxy implements MethodInterceptor {


    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

        JoinPoint joinPoint = new JoinPoint();
        joinPoint.setArgs(args);
        joinPoint.setMethod(method);
        joinPoint.setObj(o);

        processBeforeAdvice(joinPoint);

        Object result = processAroundAdvice(joinPoint, methodProxy);

        processAfterAdvice(joinPoint, result);
        return result;
    }


    private void processBeforeAdvice(JoinPoint joinPoint) {
       //...
    }


    private Object processAroundAdvice(JoinPoint joinPoint, MethodProxy proxy) throws Throwable {
        // ....
        ProceedingJoinPoint proceddingJoinPoint = new ProceedingJoinPoint();
        proceddingJoinPoint.setArgs(joinPoint.getArgs());
        proceddingJoinPoint.setMethod(joinPoint.getMethod());
        proceddingJoinPoint.setObj(joinPoint.getObj());
        proceddingJoinPoint.setProxy(proxy);
        // ....
    }
    
    
    private void processAfterAdvice(JoinPoint joinPoint, Object result) {
        // ...
    }
}

4. 切面类执行

到这里,上面的设计与实现都还是比较容易理解的,接下来需要关注上面代理类中,具体的切面执行逻辑,即 processBeforeAdvice, processAroundAdvice, processAfterAdvice

1. Before切面执行

获取切面

在执行Before切面之前,我们需要获取这个方法对应的Before切面,我们借助前面的Bean加载,实现完成这个,具体实现后面给出

依次执行切面

在获取到对应的切面之后,执行切面方法,怎么执行呢?

我们定义了一个 BeforeProcess, 其中包含切面对象aspect,切面方法method,对应的切点信息JoinPoint

JoinPoint作为参数传递给Before方法,因此在Before方法中,可以查询被切的方法基本信息(如方法签名,参数回信息,返回对象等)

@Data
public class BeforeProcess implements IAopProcess {

    /**
     * 切面类
     */
    private Object aspect;


    /**
     * 切面类中定义的Before方法
     */
    private Method method;


    /**
     * 被切面拦截的切点信息
     */
    private JoinPoint joinPoint;

    public BeforeProcess() {
    }

    public BeforeProcess(BeforeProcess process) {
        aspect = process.getAspect();
        method = process.getMethod();
        joinPoint = null;
    }


    /**
     * 执行切面方法
     *
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    public void process() throws InvocationTargetException, IllegalAccessException {
        method.invoke(aspect, joinPoint);
    }

}

因此 processBeforeAdvice 的实现就比较简单了,代码如下

private void processBeforeAdvice(JoinPoint joinPoint) {
    List<BeforeProcess> beforeList = new ArrayList<>();
    Annotation[] anos = joinPoint.getMethod().getAnnotations();
    for (Annotation ano: anos) {
        if (AspectHolder.getInstance().processCollectMap.containsKey(ano.annotationType())) {
            beforeList.addAll(AspectHolder.getInstance().processCollectMap.get(ano.annotationType()).getBeforeList());
        }
    }

    if (beforeList == null || beforeList.size() == 0) {
        return;
    }


    BeforeProcess temp;
    for (BeforeProcess b: beforeList) {
        temp = new BeforeProcess(b);
        temp.setJoinPoint(joinPoint);
        try {
            temp.process();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2. After切面

看了上面的Before切面之后,这个就比较简单了,首先是定义 AfterProcess, 很明显,相交于BeforeProcess,应该多一个返回结果的参数

@Data
public class AfterProcess implements IAopProcess {

    private Object aspect;

    private Method method;

    private JoinPoint joinPoint;

    private Object result;

    public AfterProcess() {
    }

    public AfterProcess(AfterProcess process) {
        aspect = process.getAspect();
        method = process.getMethod();
    }

    public void process() throws InvocationTargetException, IllegalAccessException {
        if (method.getParameters().length > 1) {
            method.invoke(aspect, joinPoint, result);
        } else {
            method.invoke(aspect, joinPoint);
        }
    }
}

对应的执行方法 processAfterAdvice

private void processAfterAdvice(JoinPoint joinPoint, Object result) {
    List<AfterProcess> afterList = new ArrayList<>();
    Annotation[] anos = joinPoint.getMethod().getAnnotations();
    for (Annotation ano : anos) {
        if (AspectHolder.getInstance().processCollectMap.containsKey(ano.annotationType())) {
            afterList.addAll(AspectHolder.getInstance().processCollectMap.get(ano.annotationType()).getAfterList());
        }
    }

    if (CollectionUtils.isEmpty(afterList)) {
        return;
    }


    AfterProcess temp;
    for (AfterProcess f : afterList) {
        temp = new AfterProcess(f);
        temp.setJoinPoint(joinPoint);
        temp.setResult(result);

        try {
            temp.process();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3. Around 切面

Around切面相比较之前,有一点区别,around切面内部会显示调用具体的方法,因此当一个方法存在多个Around切面时,就有点蛋疼了, 这里采用比较猥琐的方式,around切面也是顺序执行,不过只有第一个around切面中是真的执行了具体方法调用,后面的around切面都是直接使用了第一个around执行后的结果(直接看ProceedingJoinPoint源码就知道了)

AroundProcess

@Data
public class AroundProcess implements IAopProcess {

    private Object aspect;

    private Method method;

    private ProceedingJoinPoint joinPoint;

    public AroundProcess() {
    }

    public AroundProcess(AroundProcess process) {
        aspect = process.getAspect();
        method = process.getMethod();
    }

    public Object process() throws InvocationTargetException, IllegalAccessException {
        return method.invoke(aspect, joinPoint);
    }
}

对应的processAroundAdvice, 对比之前,唯一的区别是当不存在Around切面时,并不是直接返回,需要执行方法

private Object processAroundAdvice(JoinPoint joinPoint, MethodProxy proxy) throws Throwable {
    ProceedingJoinPoint proceddingJoinPoint = new ProceedingJoinPoint();
    proceddingJoinPoint.setArgs(joinPoint.getArgs());
    proceddingJoinPoint.setMethod(joinPoint.getMethod());
    proceddingJoinPoint.setObj(joinPoint.getObj());
    proceddingJoinPoint.setProxy(proxy);

    List<AroundProcess> aroundList = new ArrayList<>();
    Annotation[] anos = joinPoint.getMethod().getAnnotations();
    for (Annotation ano : anos) {
        if (AspectHolder.getInstance().processCollectMap.containsKey(ano.annotationType())) {
            aroundList.addAll(AspectHolder.getInstance().processCollectMap.get(ano.annotationType()).getAroundList());
        }
    }

    if (CollectionUtils.isEmpty(aroundList)) {
        return proxy.invokeSuper(joinPoint.getObj(), joinPoint.getArgs());
    }


    Object result = null;
    AroundProcess temp;
    for (AroundProcess f : aroundList) {
        temp = new AroundProcess(f);
        temp.setJoinPoint(proceddingJoinPoint);

        try {
            result = temp.process();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    return result;
}

5. 切面加载

到这里,上面基本上将主要的AOP功能实现了,也就是说上面的代码已经足够支持简单的AOP功能了,现在遗留的一个问题就是如何自动加载切面,生成代理类,维护切面关系

这一块直接承接之前的Bean扫描加载,主要思路是

  • 扫描指定包下的所有类
  • 确定所有的切面类(即类上有 Aspect 注解)
  • 解析切面类中的方法,根据方法上的注解(Before, After, Around)来确定拦截的映射关系
    /**
       * 自定义注解 及 切该注解的所有切面的映射关系
       * <p>
       * key 为切面拦截的自定义注解的class对象
       * value 为该注解所对应的所有切面集合
       */
      private Map<Class, ProcessCollect> aspectAnoMap = new ConcurrentHashMap<>();
    
  • 加载其他的bean,判断bean中是否有方法上的注解,在上面的Map中
    • 若存在,则表示这个bean中的这个方法被切面拦截,因此生成代理对象
    • 若不存在,则表示这个bean没有方法被切面拦截,则直接生成实例对象
  • 实现IoC依赖注入

这一块并不是主要的逻辑,因此只给出 Aspect的加载代码

private void instanceAspect() throws IllegalAccessException, InstantiationException {
    clzBeanMap = new ConcurrentHashMap<>();
    Object aspect;
    for (Class clz : beanClasses) {
        if (!clz.isAnnotationPresent(Aspect.class)) {
            continue;
        }

        aspect = clz.newInstance();


        // 将aspect 丢入bean Map中, 因此aspect也支持ioc
        clzBeanMap.put(clz, aspect);

        // 扫描切面的方法
        Class ano;
        ProcessCollect processCollect;
        Method[] methods = clz.getDeclaredMethods();
        for (Method method : methods) {
            Before before = method.getAnnotation(Before.class);
            if (before != null) {
                BeforeProcess beforeProcess = new BeforeProcess();
                beforeProcess.setAspect(aspect);
                beforeProcess.setMethod(method);

                ano = before.value();
                processCollect = aspectAnoMap.computeIfAbsent(ano, k -> new ProcessCollect());
                processCollect.addBeforeProcess(beforeProcess);
            }


            After after = method.getAnnotation(After.class);
            if (after != null) {
                AfterProcess afterProcess = new AfterProcess();
                afterProcess.setAspect(aspect);
                afterProcess.setMethod(method);


                ano = after.value();
                processCollect = aspectAnoMap.computeIfAbsent(ano, k -> new ProcessCollect());
                processCollect.addAfterProcess(afterProcess);
            }


            Around around = method.getAnnotation(Around.class);
            if (around != null) {
                AroundProcess aroundProcess = new AroundProcess();
                aroundProcess.setAspect(aspect);
                aroundProcess.setMethod(method);

                ano = around.value();
                processCollect = aspectAnoMap.computeIfAbsent(ano, k -> new ProcessCollect());
                processCollect.addAroundProcess(aroundProcess);
            }

        }
    }


    AspectHolder.getInstance().processCollectMap = aspectAnoMap;
}

测试

实现了基本的功能之后,开始愉快的测试

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogDot {
}


@Aspect
public class LogAspect {

    @Before(LogDot.class)
    public void before1(JoinPoint joinPoint) {
        System.out.println("before1! point: " + Arrays.asList(joinPoint.getArgs()));
    }

    @Before(LogDot.class)
    public void before2(JoinPoint joinPoint) {
        System.out.println("before2! point: " + Arrays.asList(joinPoint.getArgs()));
    }

    @After(LogDot.class)
    public void after1(JoinPoint joinPoint) {
        System.out.println("after! point: " + Arrays.asList(joinPoint.getArgs()));
    }

    @After(LogDot.class)
    public void after2(JoinPoint joinPoint) {
        System.out.println("after2! point: " + Arrays.asList(joinPoint.getArgs()));
    }


    @Around(LogDot.class)
    public Object around1(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("around1! point: " + Arrays.asList(joinPoint.getArgs()));
        Object result = joinPoint.proceed();
        System.out.println("around1 over! ans: " + result);
        return result;
    }

    @Around(LogDot.class)
    public Object around2(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("around2! point: " + Arrays.asList(joinPoint.getArgs()));
        Object result = joinPoint.proceed();
        System.out.println("around2 over! ans: " + result);
        return result;
    }
}


@Service
public class AspectDemoService {

    @LogDot
    public void print(String ans) {
        System.out.println(ans);
    }

    @LogDot
    public int add(int a, int b) {
        System.out.println("add");
        return a + b;
    }

}

对应的测试方法

public class BeanScanIocTest {

    private ApplicationContext apc;

    private AspectDemoService aspectDemoService;


    @Before
    public void setup() throws Exception {
        apc = new ApplicationContext();
        aspectDemoService = apc.getBean("aspectDemoService", AspectDemoService.class);
    }

    @Test
    public void testApc() throws Exception {
        System.out.println("------------\n");
        aspectDemoService.print("aspectdemoservice------");
        System.out.println("------------\n");


        System.out.println("------------\n");
        aspectDemoService.add(10, 20);
        System.out.println("-------------\n");
    }

}

输出结果

------------

before1! point: [aspectdemoservice------]
before2! point: [aspectdemoservice------]
around1! point: [aspectdemoservice------]
aspectdemoservice------
around1 over! ans: null
around2! point: [aspectdemoservice------]
around2 over! ans: null
after! point: [aspectdemoservice------]
after2! point: [aspectdemoservice------]
------------

------------

before1! point: [10, 20]
before2! point: [10, 20]
around1! point: [10, 20]
add
around1 over! ans: 30
around2! point: [10, 20]
around2 over! ans: 30
after! point: [10, 20]
after2! point: [10, 20]
-------------

缺陷与改进

上面虽然是实现了AOP的功能,但是并不完善,且存在一些问题

  1. 切面的顺序指定没有实现(这个实际上还是比较简单的)
  2. 拦截规则,目前只支持自定义注解的拦截(后面单独说这一块)
  3. around切面的设计方式,先天缺陷
  • 比如我有两个around切面,第一个打印输入输出参数,第二个around切面用于计算方法执行耗时,那么第二个切面的计算就会有问题,因为具体的方法执行是在第一个切面中完成的,那么第二个切面执行方法时,直接复用了前面计算结果,所以无法得到预期
  • 如果方法的执行,会改变输入参数,那么多个around前面也会有问题

其他

一期改造到此基本结束,上面的问题留待下篇解决

源码地址: https://github.com/liuyueyi/quick-mvc

相关博文:

个人博客:一灰的个人博客

公众号获取更多:

个人信息

© 著作权归作者所有

小灰灰Blog
粉丝 202
博文 211
码字总数 379645
作品 0
武汉
程序员
私信 提问
Spring基础知识——是什么、为什么用以及体系结构

一、Spring是什么 Spring是分层的Java SE/EE应用一站式的轻量级开源框架,以IoC(Inverse of Control:反转控制)和AOP(Aspect Oriented Programming:面向切面编程)为内核,提供了展现层S...

littleant2
2016/01/06
87
0
手写Spring基本体系IOC+AOP+MVC+事物管理等简单实现。

注:由于本工程包含的内容较多。大部分设计图、代码示例不能提供。 请移步至码云clone查看:https://gitee.com/Coodyer/Coody-Framework 本文仅介绍实现思路。 By:Coody 644556636@qq.com ...

Coody
2018/03/01
0
1
Spring、Spring MVC、Struts2、、优缺点整理

Spring 及其优点 大部分项目都少不了Spring的身影,为什么大家对他如此青睐,而且对他的追捧丝毫没有减退之势呢 Spring是什么: Spring是一个轻量级的DI和AOP容器框架。 说它轻量级有一大部分...

架构师springboot
03/21
0
0
35、最简单的mvc框架tiny,V2版原理图、设计

在前面的v1版,由于我们临时起意,存在不少问题,我重新设计框架v2版chen(重名问题改名为chen)。 原理图如下: 先说下chen框架的功能: restful地址支持(chen中叫路由)。 mvc功能,使用简...

青青小树
2014/04/26
0
6
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

没有更多内容

加载失败,请刷新页面

加载更多

线程池之ThreadPoolExecutor使用

ThreadPoolExecutor提供了四个构造方法: ThreadPoolExecutor构造方法.png 我们以最后一个构造方法(参数最多的那个),对其参数进行解释: public ThreadPoolExecutor(int corePoolSize, /...

天王盖地虎626
24分钟前
1
0
小程序登陆流程

http://www.bubuko.com/infodetail-2592845.html

为何不可1995
33分钟前
1
0
Consul+Spring boot的服务注册和服务注销

一图胜千言 先看一看要做事情,需要在Consul上面实现注册中心的功能,并以2个Spring boot项目分别作为生产者,消费者。 Consul 假设已经完成文章《Consul的开发者模式之Docker版》中的所有的...

亚林瓜子
39分钟前
4
0
MySQL高可用之基于Galera复制跨地域节点分布的滥用

mysql使用教程 MySQL高可用之基于Galera复制跨地域节点分布的滥用 2018-11-22 02:15 8335 85 让我们再一次讨论MySQL高可用性(HA)和同步复制。 它是地理上分布区域上一些高可用性参考架构解...

rootliu
50分钟前
1
0
js判断pc还是移动端

var pcyidong =/(iPhone|iPad|iPod|iOS|Android)/i.test(navigator.userAgent); 如果pcyidong的值为false则用户的浏览器为pc端 如果pcyidong的值为true则用户浏览器为移动端 if (pcyidong =...

流年那么伤
今天
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部