美文网首页JAVA随笔
实现MVC: 4. AOP的设计与实现

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

作者: 一灰灰blog | 来源:发表于2017-08-28 12:13 被阅读12次

    设计

    我们将结合日常使用的姿势来设计切面的实现方式,由于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

    相关博文:

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

    公众号获取更多:

    个人信息个人信息

    相关文章

      网友评论

        本文标题:实现MVC: 4. AOP的设计与实现

        本文链接:https://www.haomeiwen.com/subject/mtowdxtx.html