动手实现MVC: 4. AOP的设计与实现
2019独角兽企业重金招聘Python工程师标准>>>
设计
我们将结合日常使用的姿势来设计切面的实现方式,由于spring-mvc切面比较强大,先将切面规则这一块单独拎出来,后面单独再讲;本篇博文主要集中在如何实现切面的基本功能
几种切面
- Before
- 前置切面,在方法执行之前被调用
- Around
- 环绕切面,可以在切面内部执行具体的方法,因此可以在具体方法执行前后添加自己需要的东西
- After
- 后置切面,在方法执行完成之后被调用
基本功能
- 上面基础的三中切面支持
- 一个方法上可以被多个切面拦截(即一个方法可以同时对应多个
Before,After,Around
切面) - 切面执行的先后顺序(一般而言,
before优先around优先after
) - 被切的方法只能执行一遍(为什么单独拎出来?主要是
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 {@Overridepublic 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也支持iocclzBeanMap.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 {@LogDotpublic void print(String ans) {System.out.println(ans);}@LogDotpublic int add(int a, int b) {System.out.println("add");return a + b;}}
对应的测试方法
public class BeanScanIocTest {private ApplicationContext apc;private AspectDemoService aspectDemoService;@Beforepublic void setup() throws Exception {apc = new ApplicationContext();aspectDemoService = apc.getBean("aspectDemoService", AspectDemoService.class);}@Testpublic 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的功能,但是并不完善,且存在一些问题
- 切面的顺序指定没有实现(这个实际上还是比较简单的)
- 拦截规则,目前只支持自定义注解的拦截(后面单独说这一块)
- around切面的设计方式,先天缺陷
- 比如我有两个around切面,第一个打印输入输出参数,第二个around切面用于计算方法执行耗时,那么第二个切面的计算就会有问题,因为具体的方法执行是在第一个切面中完成的,那么第二个切面执行方法时,直接复用了前面计算结果,所以无法得到预期
- 如果方法的执行,会改变输入参数,那么多个around前面也会有问题
其他
一期改造到此基本结束,上面的问题留待下篇解决
源码地址: https://github.com/liuyueyi/quick-mvc
相关博文:
- 动手实现MVC: 1. Java 扫描并加载包路径下class文件
- 动手实现MVC: 2. bean加载, IoC依赖注入
- 动手实现MVC: 3. AOP实现准备篇动态代理
个人博客:一灰的个人博客
公众号获取更多:
转载于:https://my.oschina.net/u/566591/blog/1524861
动手实现MVC: 4. AOP的设计与实现相关推荐
- ASP.NET MVC:缓存功能的设计及问题
ASP.NET MVC:缓存功能的设计及问题 这是非常详尽的asp.net mvc中的outputcache 的使用文章. [原文:陈希章 http://www.cnblogs.com/chenxiz ...
- php基础知识【oop/mvc/orm/aop】
OOP 面向对象编程是一种计算机编程架构.OOP 的一条基本原则是计算机程序是由单个能够起到子程序作用的单元或对象组合而成.OOP 达到了软件工程的三个主要目标:重用性.灵活性和扩展性.为了实现整体运 ...
- 给MVC中的V设计一个框架
2019独角兽企业重金招聘Python工程师标准>>> 在前文中(基于MVC三层架构结合自己理念生成的四层架构)中,我大体的说了说整个架构上的设计,但是我们没怎么谈论具体各个层面上的 ...
- 3.2 Spring AOP的设计与实现
2019独角兽企业重金招聘Python工程师标准>>> JVM的动态代理特性 在Spring AOP实现中,使用的核心技术是动态代理,这实际上是JDK的一个特性(JDK1.3以上的版 ...
- spring MVC 及 AOP 原理
SpringMVC工作原理 https://www.cnblogs.com/xiaoxi/p/6164383.html spring MVC 原理 https://blog.csdn.net/y199 ...
- 菜鸟不菜学习mvc(二)(权限设计插曲)
上次运行完以后随便点了点就没看,时隔一个周,继续学习这传说优点>缺点的mvc 本来想按老步子走,链接数据库>增加>查询>更改>删除,但这些东西之前还有个数据库要建立,建咱 ...
- MVC过滤器-AOP思想初探
AOP:面向切面编程."给程序统一添加新功能的一种思想". 它是对传统OOP编程的一种补充. OOP关注需求功能模块,封装良好的类,并让他们有属于自己的行为,侧重业务逻辑的实现. ...
- spring AOP 之五:Spring MVC通过AOP切面编程来拦截controller
示例1:通过包路径及类名规则为应用增加切面 该示例是通过拦截所有com.dxz.web.aop包下的以Controller结尾的所有类的所有方法,在方法执行前后打印和记录日志到数据库. 新建一个spr ...
- 使用PostSharp进行AOP框架设计:一个简单的原型
AOP已经不是一个什么新名词了,在博客园使用关键字搜索可以查出n多条关于AOP的介绍,这里就不再赘述了. 在Bruce Zhang's Blog里面有很多关于AOP介绍及其在.net下实现研究,总觉得 ...
- spring mvc通过aop在控制台打印log日志,包含请求controller、method、url、remoteaddr、返回值,方便调试
1:导入对应的包 在maven xml文件中加入 <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjrt -->< ...
最新文章
- html css 让元素居中显示,html – CSS:如何在居中元素周围对齐元素?
- python matplotlib.pyplot如何绘制实时图表?(实时绘制、更新图表、实时更新、动态窗口)plt.ion() plt.clf() plt.pause() plt.ioff()
- Linked List Cycle
- 利用PowerShell Empire实现Word文档DDE攻击控制(简单没啥用)
- LeetCode 20. Valid Parentheses(c++)
- 多重循环、缓冲区管理、数组(day06)
- 等级考试(一):三级网络---似曾相识
- 基于python的性能负载测试Locust-6-脚本编写之使用HTTP client
- ykhmi是什么触摸屏软件_触摸屏如何读写变频器参数?如何控制变频器?实操步骤及方法分享...
- Pycharm-SSH连接服务器
- win7 局域网共享文件
- 京东商城(mysql+python)
- VScode配置PHP运行环境
- 郑树生与李一男的对望
- Android五子棋开发实验报告,Android五子棋游戏实验报告.doc
- 物体识别全流程(Ubuntu16.04)结合ROS
- 锐龙r7 5800h和酷睿i7 11800h性能差多少 锐龙r75800h和i711800h跑分
- 【10. 信号量和管程】
- java时间戳与LocalDateTime常用转换方式
- Debian 修改系统语言
热门文章
- 【资源】吴恩达新书《Machine Learning Yearning》,附中文版PDF下载
- 基于 MVP + Glide + Retrofit + RxJava2.0 + butterknife 的C9MJ TV App
- Android studio中的一次编译报错’Error:Execution failed for task ':app:transformClassesWithDexForDebug‘,困扰了两天
- 计算机c程序题孔融让梨,幼儿园大班语言游戏教案《孔融让梨》含PPT课件
- java船_Java-货船
- 全自动高清录播服务器,全自动高清录播服务器 高清录播系统 一体化操作;易使用 操作简便...
- 鸿蒙os系统使用技巧,鸿蒙OS系统的四大技术特性介绍
- c语言会变颜色程序,【图片】(原创)用纯C变了个变色输出字符的程序。。。【c语言吧】_百度贴吧...
- java runnable 匿名_Java 开发者最困惑的四件事
- 西安工程大学计算机是几本专业,2016年西安工程大学计算机科学与技术(卓越班)专业在陕西录取分数线...