造一个方形轮子文章目录:造一个方形的轮子
01、解决遗留问题
上一篇的最后说了几个问题,没有处理500、没有处理from表单、没有处理文件上传,文件上传和表单可以算是同一个问题了,偷个懒就先不解决了,思路的话就是把文件单独封装一个接收对象放内存或临时缓存,from表单可以按参数名封装接收参数bean。这里先解决一下500的问题。
程序触发500的情况是在处理Controller方法及后续调用方法的时候产生的,也就是说只要在DispatcherServlet里catch一下invoke()方法,记录一下日志并设置一下返回结果就可以了。
1 |
|
日志输出错误的请求uri,并返回500错误。
02、Aop功能整理
在开始开发Aop功能之前先来回顾一下Aop的知识点,一般来说,Aop指的就是面向切面编程,也就是程序中找出一个点或一条线(由多个点组成)来,针对性的做程序处理,一般用来做统一鉴权、日志记录之类的。
上边是从使用的角度考虑,当然如果开发的话要考虑的还是有点多的,我只是简单实现了一下,而且遗留问题还不少,只是基本能跑了,还有N多种情况是没有处理的,只有真正写完才觉得Spring真的很赞,它把那么多基本功能提供出来,让开发人员能够专注在业务开发上。
事先声明我实现的方式依然是简单粗暴不要拿来和其它成熟框架做对比,当然觉得有问题的也欢迎指正,写的仓促我已知的问题已经不少,大家能提出来一起讨论也好。
说一下设计Aop功能的思路:
1、定义Aop相关注解,并在初始化Bean之前先加载Aop切面
2、初始化Bean阶段判断package是否有匹配的切面,如果有则生成代理类,保存原始对象和代理对象
3、处理DI阶段
向原始对象里注入代理(原始)对象
(这句话后边结合代码详细说)4、程序调用时如果是代理类,在代理类里判断当前调用方法是否有匹配的切面规则,有则按规则执行切面
这个思路可能不太好理解,还是结合代码看一下吧。
03、添加注解
新增三个注解,一个作用在类上的Aspect标识当前类为切面类,一个Pointcut标记切入点,和一个Before标记方法在切入点的执行动作,这里只实现了一个Before对应的After、 Around都类似,就没都实现。
Aspect.java:
1 | package com.jisuye.annotations.aop; |
Before.java:
1 | package com.jisuye.annotations.aop; |
Pointcut.java:
1 | package com.jisuye.annotations.aop; |
04、添加代理类、切面对象、HTTP请求上下文
切面对象也是放在容器里的,因为后边代理对象里会使用,切面对象保存的是切面的规则信息,及对应的切面类实例,动作方法等信息。
AspectObject.java:
1 | package com.jisuye.core; |
代理类这里只使用jdk自代的动态代理,也就是说代理的类必须实现接口。
invoke内部执行过程:
1、根据package从容器中获取对应的AspectObject
2、判断当前类、方法、返回类型是否有匹配的切入点
3、如果有,获取使用当前切入点的方法,并按逻辑执行
SquareProxyHandler.java:
1 | package com.jisuye.core; |
HTTP请求上下文的类,是为了保存当前请求线程的Request 及Response 方便在后续方法中使用。
RequestContextHolder.java:
1 | package com.jisuye.core; |
05、修改容器初始化逻辑
BeansMap中添加aopMap:
1 | public class BeansMap { |
BeansInitUtil.init()方法中在处理IOC/DI之前先加载Aop切面保存到BeansMap.aops中,这块的代码有点长 我分段给出,完整代码可以去代码库中查找。
下边这段代码的逻辑是遍历目录,查找有@Aspect注解的类,并分析带@Pointcut注解的方法,做为切入点,以及使用该切入点的带@Before注解的方法做为动作执行方法,构造一下AspectObject对象,保存到BeansMap.aops中。
BeansInitUtil.java:
1 | //...... |
接下来看一下获取完Aop切面信息之后在哪里使用,修改BeansInitUtil.loadClass()方法,也就是初始化Bean的方法,原来方法里获取反射对象实例直接使用了clzz.newInstance()
,现在要再加一个判断他是不是有匹配的切面信息的逻辑,如果符合某一个切面,则要生成一个代码类,然后保存原始对象及代理对象。
BeansInitUtil.java:
1 | //... |
可以看到getInstance()方法返回的内容有可能是代理类,也有可能是传入对象本身,这里是做了兼容处理,还有beanObject.setObject()方法由原来的一个参数改成了两个参数,这是因为添加了一个代理对象,下边马上就能讲到为什么加这个对象。
BeanObject.java:
1 | public class BeanObject { |
正常来说我们原来使用的是反射生成的对象,现在添加了Aop直接替换为代理对象就可以了,但是这里有一个问题。在处理DI关系的时候,是不可以向代理对象里注入依赖的,好在代理对象也是包装的原始对象,也就是说我们把原始对象的依赖注入成功后,调用代理对象的方法也就不会出现空指针的问题了,看一下相关代码:
BeansInitUtil.java:
1 | //... |
主要的区别就是field.set()
方法的对数,原来取的是beanObject.getObject()
(代理对象)现在取的是beanObject.srcObj()
(原始对象)
06、测试AOP
创建切面类
WebLogAspect.java:
1 | package com.jisuye.service.aop; |
修改Abc.java:
1 | public interface Abc { |
添加了两个方法,AbcImpl里只是空实现就不放代码了,看一下TestController.java:
1 |
|
添加调用abc及abc2的接口。
启动程序
调用接口:http://localhost:8888/abc/test/abc?name=ixx
查看控制台输出:
1 | 14:31:36.102 [http-nio-8888-exec-4] INFO com.jisuye.service.aop.WebLogAspect - AOP exe... url:/abc/test/abc, params:{"name":["ixx"]} |
调用接口:http://localhost:8888/abc/test/abc2?name=ixx
查看控制台输出:
1 | 14:32:07.156 [http-nio-8888-exec-6] INFO com.jisuye.service.aop.WebLogAspect - AOP exe... url:/abc/test/abc2, params:{"name":["ixx"]} |
调用接口:http://localhost:8888/abc/test/hello?name=ixx
查看控制台输出:
1 | 14:33:31.186 [http-nio-8888-exec-10] INFO com.jisuye.core.DispatcherServlet - http request path:get:/test/hello |
可以看到符合我们规则的方法调用都输出了AOP日志。
07、遗留问题
AOP的基本功能实现了,但问题点太多了,比如只实现了一个Before动作,没有处理方法参数,加入了aop后导致多了一遍目录遍历,而且BeansInitUtil类快看不下去了….下一篇之前找时候看看再梳理一下代码吧。
本篇代码地址: https://github.com/iuv/square/tree/square8
本文链接: http://blog.jisuye.com/2019/08/10/square8/
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!