前言
很抱歉拖了这么久才更新,不只是因为大四实习工作任务多,更重要的是设计一个框架确实很有挑战性(太菜了没办法)。在编写的过程中确实有Spring源码做参考,也翻了大量帖子,还有《Spring技术内幕》这本书(说实话这本书真的没咋看懂,感觉很晦涩),但是大部分的时候还是我自己在设计各种接口和类,放飞了自我,来实现我脑中的以及写在纸上的构想。自己写的BeanFactory与Spring的BeanFactory简直相差的十万八千里。但好在Ioc以及Aop的功能基本上实现了。至于有没有bug,是不是高可用的就需要大佬们赐教了。
先说明,本文所有代码以及项目源码都在 gitee.com/Alan_ZhangA… 中能找到,github 也有,我在gitee上设了镜像仓库,但不确定啥时候就不能白嫖了。
回到标题,之所以要自己设计Aop,原因有几点:
- @Pointcut中的 execution表达式我第一次看到的时候没看懂咋写的,写起来挺复杂的
- @Before和 @After中的方法好像只能填自己所在类中的其他方法(某个方法只能切入自己所在类中的其他方法中),不太自由(看了好多Aop教程都没有切入其他类中的方法的例子)
- 一个方法好像只能切入一个方法中,不能同时切入多个方法,不太自由(同上)
功能演示(使用方法)
所以先介绍一下我自己设计的Aop的使用方法
先上代码
cn.zay.demo.java.pojo.People
@Aspect
@Component
@Slf4j
public class People {
@Before({"cn.zay.demo.java.pojo.People.working","cn.zay.demo.java.pojo.People.haveLunch"})
public void goToWork(){
log.info("上班打卡");
}
@Pointcut
public void working(){
log.info("人在上班");
}
@Pointcut
public void haveLunch(String time){
log.info("人在"+time+"吃午饭");
}
@After({"cn.zay.demo.java.pojo.People.working","cn.zay.demo.java.pojo.People.haveLunch"})
public void goOffWork(){
log.info("下班打卡");
}
}
cn.zay.demo.java.pojo.Student
@Slf4j
@Aspect
@Component(name = "ZAY")
public class Student{
private String name;
private int age;
public Student(){
this.name = "ZAY";
this.age = 0;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@After({"cn.zay.demo.java.pojo.People.working"})
public void sayHello(){
log.info("Hello,我是学生{}.",getName());
}
public void eat() {
log.info("学生{}吃饭",name);
}
public void sleep(){
log.info("学生{}在睡觉",name);
}
@Override
@Pointcut
public String toString() {
return "Student{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
}
解释:
- 一个类必须被
@Component注释才能被容器统一管理,必须被@Aspect注释才能被框架识别到类中方法上的@Before和@After @Pointcut声明该方法为切点方法,也就是要被修饰、代理的方法@Befoore和@After中的参数为一个字符串组,用来指定该方法要在哪些方法之前或之后运行- 字符串组中指定方法名,格式为: 包路径+"."+类名+"."+方法名,参考上面代码
- 指定的方法必须存在且被
@Pointcut注释,否则会报UnrecognizedPointcutMethodException异常 - 被
@Pointcut注释的方法可以有参数,可以有返回值;被@After或@Before注释的方法不能有参数,返回值只能为void
然后执行测试文件夹中的AopTest.java中的test1()方法:
@Test
public void test1(){
try{
String[] packageNames=new String[]{"cn.zay.demo.java.pojo"};
BeanFactory.loadClass(packageNames);
BeanFactory.loadBeans();
InterceptorFactory.loadInterceptors(packageNames);
InterceptorFactory.INTERCEPTORS.get("cn.zay.demo.java.pojo.People.working").agent();
InterceptorFactory.runProxyMethodWithReturnValueOfVoid("cn.zay.demo.java.pojo.People.working");
InterceptorFactory.runProxyMethodWithReturnValueOfVoid("cn.zay.demo.java.pojo.People.haveLunch", "12:00");
log.info((String)InterceptorFactory.runProxyMethodWhoseReturnValueIsNotVoid("cn.zay.demo.java.pojo.Student.toString"));
}catch (Exception e){
log.error("异常!",e);
}
}
执行结果为:
2022-09-09 21:04:05 [main] INFO cn.zay.demo.java.pojo.People - 上班打卡
2022-09-09 21:04:05 [main] INFO cn.zay.demo.java.pojo.People - 人在上班
2022-09-09 21:04:05 [main] INFO cn.zay.demo.java.pojo.People - 下班打卡
2022-09-09 21:04:05 [main] INFO cn.zay.demo.java.pojo.Student - Hello,我是学生ZAY.
2022-09-09 21:04:05 [main] INFO cn.zay.demo.java.pojo.People - 上班打卡
2022-09-09 21:04:05 [main] INFO cn.zay.demo.java.pojo.People - 人在上班
2022-09-09 21:04:05 [main] INFO cn.zay.demo.java.pojo.People - 下班打卡
2022-09-09 21:04:05 [main] INFO cn.zay.demo.java.pojo.Student - Hello,我是学生ZAY.
2022-09-09 21:04:05 [main] INFO cn.zay.demo.java.pojo.People - 上班打卡
2022-09-09 21:04:05 [main] INFO cn.zay.demo.java.pojo.People - 人在12:00吃午饭
2022-09-09 21:04:05 [main] INFO cn.zay.demo.java.pojo.People - 下班打卡
2022-09-09 21:04:05 [main] INFO AopTest - Student{name='ZAY', age=0}
以上就是我设计的Aop的功能演示
原理
接下来简单介绍一下原理,先看一下所有的文件
以及最重要的
首先,被@Pointcut、@Before、@After注释的每一个方法都会被封装为一个MethodInvocation方法执行器;被@Before和@After注释的方法又因为含有参数再次封装为一个JoinPoint。
在InterceptorFactory中存在三个Map:
存放所有被
@Before注释的方法的BEFORE_METHODS_MAP存放所有被
@After注释的方法的AFTER_METHODS_MAP存放所有被
@Pointcut注释的方法的POINTCUT_METHODS_MAP
在InterceptorFactory中,一开始从CLASSES中获取被@Aspect注释的类的集合,再遍历集合,对每个类提取方法,再遍历每个方法提取出被那三个注解注释的方法,提取信息封装成对应的MethodInvocation或JoinPoint,以各自完整唯一的方法名为key存入对应的Map中。
接着来到核心了,给每一个 POINTCUT_METHODS_MAP中的方法生成拦截器,拦截器InternallyAspectInterceptor中又有两个ArrayList:
beforeMethods: 用来存放所有要切入被代理方法之前的方法的执行器 (被 @Before注释的方法)
afterMethods: 用来存放所有要切入被代理方法之后的方法的执行器 (被 @After注释的方法)
每new一个InternallyAspectInterceptor就会执行init()方法,在BEFORE_METHODS_MAP中寻找在该方法之前运行的方法,添加到beforeMethods中;在AFTER_METHODS_MAP中寻找在该方法之后运行的方法,添加到afterMethods中。再将生成的拦截器InternallyAspectInterceptor以该Pointcut方法的完整唯一的方法名为key存入InterceptorFactory中的第四个Map——INTERCEPTORS中。
只需要根据Poingcut方法的完整方法名在INTERCEPTORS中找到对应的拦截器并执行拦截器的agent()方法,就可以运行代理后的方法,实现Aop。
求助
目前的Aop运行代理方法还只能通过Test测试文件夹中AopTest.java中test1()方法中的形式运行,但Spring Aop中运行代理方法的方式却是这样的
我是真的不知道Spring是怎么做到在JVM运行一个明确给定的方法时给拦住并增加新的东西的,它真的没有重写userDao.add()方法哇。
网上有很多帖子,像这样的:
看得我一脸懵逼,所以有没有能理解这些的大佬在我的这个东西的基础上再怎么封装,真正做到像Spring Aop那样直接调用方法就能运行代理方法。
谢谢大家!