自己设计的Aop简介+求助帖

154 阅读6分钟

前言

很抱歉拖了这么久才更新,不只是因为大四实习工作任务多,更重要的是设计一个框架确实很有挑战性(太菜了没办法)。在编写的过程中确实有Spring源码做参考,也翻了大量帖子,还有《Spring技术内幕》这本书(说实话这本书真的没咋看懂,感觉很晦涩),但是大部分的时候还是我自己在设计各种接口和类,放飞了自我,来实现我脑中的以及写在纸上的构想。自己写的BeanFactory与Spring的BeanFactory简直相差的十万八千里。但好在Ioc以及Aop的功能基本上实现了。至于有没有bug,是不是高可用的就需要大佬们赐教了。

先说明,本文所有代码以及项目源码都在 gitee.com/Alan_ZhangA… 中能找到,github 也有,我在gitee上设了镜像仓库,但不确定啥时候就不能白嫖了。

回到标题,之所以要自己设计Aop,原因有几点:

  1. @Pointcut中的 execution表达式我第一次看到的时候没看懂咋写的,写起来挺复杂的
  2. @Before和 @After中的方法好像只能填自己所在类中的其他方法(某个方法只能切入自己所在类中的其他方法中),不太自由(看了好多Aop教程都没有切入其他类中的方法的例子)
  3. 一个方法好像只能切入一个方法中,不能同时切入多个方法,不太自由(同上)

功能演示(使用方法)

所以先介绍一下我自己设计的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的功能演示

原理

接下来简单介绍一下原理,先看一下所有的文件

image.png

以及最重要的

image.png

首先,被@Pointcut@Before@After注释的每一个方法都会被封装为一个MethodInvocation方法执行器;被@Before@After注释的方法又因为含有参数再次封装为一个JoinPoint

InterceptorFactory中存在三个Map:

存放所有被@Before注释的方法的BEFORE_METHODS_MAP

存放所有被@After注释的方法的AFTER_METHODS_MAP

存放所有被@Pointcut注释的方法的POINTCUT_METHODS_MAP

InterceptorFactory中,一开始从CLASSES中获取被@Aspect注释的类的集合,再遍历集合,对每个类提取方法,再遍历每个方法提取出被那三个注解注释的方法,提取信息封装成对应的MethodInvocationJoinPoint,以各自完整唯一的方法名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.javatest1()方法中的形式运行,但Spring Aop中运行代理方法的方式却是这样的

image.png

我是真的不知道Spring是怎么做到在JVM运行一个明确给定的方法时给拦住并增加新的东西的,它真的没有重写userDao.add()方法哇。

网上有很多帖子,像这样的:

image.png

看得我一脸懵逼,所以有没有能理解这些的大佬在我的这个东西的基础上再怎么封装,真正做到像Spring Aop那样直接调用方法就能运行代理方法。

谢谢大家!