⚠️⚠️本日志已被作者用于“厦门理工学院软件工程学院”《J2EE架构与程序设计》课程激励计划,其他同学请勿在任何的方式进行利用本篇日志。

写在前面

这些天在学习 AOP,也就是面向切面编程。在理解之前,对于 AOP 的相关概念有点感觉,但是使用方式不是很了解。在参考那几篇博客,也仅仅只是对 AOP 的各项通知进行了打印输出,并没有写出真正的场景。也许是写不出一个很好的例子吧。之前在学习 spring 的时候很痛恨 AOP 。

昨天下午再对 AOP 进行了一个复习,然后自己编写了一个切面来对中午学习的知识进行了一个巩固。在写完之后,对面向切面编程有一个新的认识,仿佛自己来到了一个新的世界一样。希望随着后面的学习,对 AOP 也越来越深入。今天总算是写完了。

话不多说,先对 AOP 的相关概念进行一个回顾吧。

对啦,话说什么样的汉堡比较好吃呢??🥰🥰🥰

img

图片来源于网络

什么是 AOP

要理解切面编程,就需要先理解什么是切面,可以用汉堡来做类比,以下是一个只有肉的汉堡(图片来源于网络):

只有肉和一点点菜的汉堡也许还没那么好吃。如果我们要将汉堡包变得更好吃,那么这汉堡里面肯定有生菜,有沙拉,有煎蛋,有芝士等等。哎呀,说着说着口水都流出来了😂😂,放入这些肯定要比只有单纯的肉和菜好吃吧。

有馅的汉堡(图片来源于网络)

那么,切面来咯🤩

切面其实就是两个面包之间要放入的东西,两个面包之间的间隙就叫做切面。

AOP 相关术语

其实,AOP 的那些相关术语实在是难以理解,即使是对于 Spring 的作者也是如此,对下面的专业术语也是非常无语的。这也很正常的啦,他们也没办法。官方对于 aop 如是说:

These terms are not Spring-specific. Unfortunately, AOP terminology is not particularly intuitive; however, it would be even more confusing if Spring used its own terminology.

这些术语不是特定于 Spring 的。可惜 AOP 术语不是特别直观;然而,如果 Spring 使用自己的术语,那就更令人困惑了。

我写不出来呀,我可以不介绍吗 😂😂

实在是写不出来,在参考的博客中复制了一份出来,然后红色部分就是对概念进行理解。

  • Aspect:即切面, Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。类比汉堡:也就是只有两块面包之间的间隙

  • Join point:即连接点,表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 join point。类比汉堡:这个有点不怎么好类比。如果要将汉堡做的好看,那肯定不能像下图这样子放。这里的连接点就是汉堡的中心点,如果将这生菜,芝士这些面包的中心点对齐,这样就岂不是更好看了。上面说了一大通说的连接点就相当于是面包的中心、生菜的中心、芝士的中心点

    难看的汉堡(图片来自于网络)

    img

    整齐的汉堡(图片来自于网络):

    img

  • Pointcut:即切点,表示一组 join point,这些 join point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。类比汉堡:这个也有点不怎么好类比。按照上面的连接点的定义,把他想象成将这肉饼,生菜和芝士这些面包的中心点对齐。

  • Advice:即通知,定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。类比汉堡:把他想象成把汉堡的肉,生菜,沙拉,煎蛋

  • Target:织入 Advice 的目标对象。类比汉堡:把它想象成把汉堡合起来

  • Weaving:将 Aspect 和其他对象连接起来, 并创建增强对象的过程。类比汉堡:把它想象成已经完成的汉堡

通过上面的类比,感觉自己对上面的相关术语有一定的更好的认识,接下来再谈谈通知。通知也分为很多种类型。

AOP 的使用场景

相关的使用场景如下,话不多说,直接贴上去:

  • Authentication 权限
  • Caching 缓存
  • Context passing 内容传递
  • Error handling 错误处理
  • Lazy loading 懒加载
  • Debugging  调试
  • logging, tracing, profiling and monitoring 记录跟踪 优化 校准
  • Performance optimization 性能优化
  • Persistence  持久化
  • Resource pooling 资源池
  • Synchronization 同步
  • Transactions 事务

通知

我觉得通知叫 Advice 怪怪的,为何不叫 Notification 呢?在介绍通知之前,先来理解一下目标方法。

什么是目标方法

还记得第二张的汉堡吗?第二张汉堡只有肉,没有那么好吃。在汉堡中,肉饼就是目标方法。

五种通知类型

五种通知类型可以理解为将汉堡的生菜和芝士所放置的位置。

  • before:即前置通知,在目标方法被调用之前调用通知功能
  • after returning:即后置通知,在目标方法成功执行之后调用通知
  • after throwing:即异常通知,在目标方法抛出异常后调用通知
  • after(final):即最终通知,在目标方法完成之后调用通知,此时不会关心方法的输出是什么
  • around advice:即环绕通知,但是它包含了以上四种的全部通知。

通知相关介绍

先介绍环绕通知,然后通过环绕通知来介绍其它四种通知类型。毕竟环绕通知包含这四种通知类型,请看代码示例。以下的代码是介绍基于 aspectj 注解的配置方式,后续再进行回顾。

@Around(value = "pointCut()")
public Object aroundAsdf(ProceedingJoinPoint pjp) {
    Object object = null;
    System.out.println("我是生菜");
    try {
        System.out.println("我是芝士");
        System.out.println("我是沙拉");
        object = pjp.proceed(); // 肉饼
        System.out.println("我是芝士");
    } catch (Throwable throwable) {
        throwable.printStackTrace();
        System.out.println("我是洋葱");
    } finally {
        System.out.println("我是生菜");
    }
    return object;
}

在这个例子中 pjp.proceed() 就是目标对象,也就是汉堡中的肉饼,看起来的确是个汉堡。

肉饼之上生菜,芝士就是前置通知,即这些操作在目标对象之前;肉饼下层的芝士就是后置通知,即这些操作在目标对象之后。

catch 代码块中的洋葱就是异常通知。

finally 代码块中的生菜就是最终通知。

以下的代码示例展示了单独分开的四种通知类型。

其实我在理解 aop 之前,一直都认为 aop 的使用场景就仅仅是做日志记录的功能,所以之前就认为 aop 的用处并不是很大。其实现在很有必要去了解 aop,而且作为 aop 也不仅仅只是利用到这么 low 逼的功能上吧。

课上老师所教的 aop 还是各种博客上举出 aop 的使用例子,绝大多数都只是讲日志记录相关的例子。即使是知道了使用场景也不会说出来。

前置通知

在给这 4 个通知类型定义的时候,你可以默认给切面类中方法给一个 JoinPoint 参数,通过这个参数可以获取目标方法中的参数以及当前目标方法所在的对象。

@Before(value = "pointCut()")
public void checkPermission(JoinPoint pointcut) {
    Object[] objects = pointcut.getArgs();
    User user = (User) objects[1];
    if(user.getRole() == 0) {
        System.out.println("你没有操作学生的权限");
    }
}

后置通知

@AfterReturning("pointCut()")
public void afterReturn() {
    System.out.println("操作完成23333---------------------");
}

异常通知

@AfterThrowing(value = "pointCut()", throwing = "throwable")
public void afterThrowing(Throwable throwable) {
    System.out.println("操作完成,但是出现异常-----------------");
}

最终通知

@After("pointCut()")
public void after() {
    System.out.println("无论是否异常,都执行-----------------");
}

小总结

上面的代码示例不想写过多的文字描述,看看就行。

以上的例子和绝大多数的博客简直就是大同小异,看起来就都是模拟日志记录。也就让我们产生了错觉,aop 真的仅仅只有这种功能?然而我觉得如果要操纵目标对象中的方法的话,还是使用环绕通知的好。

以上四种的通知类型,也不是说没有用,毕竟存在即合理。

代码示例

开始之前

以下示例代码是基于注解的 AOP 编程,在此之前需要介绍一下几个注解。

  • @Aspect:切面类,这个直接标在切面类上
  • @PointCut:切入点表达式
  • @Before:前置通知的注解,需要传入一个切入点,也就是 PointCut
  • @AfterReturning:后置通知注解,需要传入一个切入点,也就是 PointCut
  • @AfterThrowing:异常通知注解,需要传入一个切入点,也就是 PointCut
  • @After:最终通知注解,需要传入一个切入点,也就是 PointCut
  • @Around:环绕通知注解,需要传入一个切入点,也就是 PointCut

开始

我现在要做一个简单的学生信息管理系统,这个功能不是很全面,但是有利用到 AOP 在里面。

好的,现在是这样的一个情况,管理员有两类,一个是管理员,管理员可以添加学生信息和查询信息,另一个是普通用户,普通用户不能添加学生信息,但是却可以查询学生信息。

学生实体类

首先建立一个学生实体类(setter 和 getter 省略)

public class Student {
    private int id;
    private String name;
    private int age;
}

用户类

建立了一个用户类(setter 和 getter 省略)

public class User {
   private String username;//用户名
   private String password;//密码
   private int role; //用户角色 0表示用户,1表示系统管理员
}

用户服务类

建立了一个 UserService 类,UserService 类中的成员变量定义了一个 List 用于保存学生信息。方法用于添加用户,删除用户和查询用户。

import java.util.ArrayList;
import java.util.List;

public class UserService {

    // 存储学生
    private List<Student> students = new ArrayList<>();

    {
        students.add(new Student(1, "xiaoqiang" + 1, 21));
        students.add(new Student(2, "xiaoqiang" + 2, 22));
        students.add(new Student(3, "xiaoqiang" + 3, 23));
        students.add(new Student(4, "xiaoqiang" + 4, 24));
        students.add(new Student(5, "xiaoqiang" + 5, 25));
    }

    // 添加学生
    public boolean addStudent(Student student, User user) {
        return students.add(student);
    }

    // 删除学生
    public boolean delStudent(Student student, User user) {
        return students.remove(student);
    }

    // 打印学生信息
    public void printStudents() {
        for (Student student: students) {
            System.out.println(student);
        }
    }

}

切面类

在切面类中我只定义了,环绕通知,因为我觉得吧环绕通知通过控制方法返回值来确定权限。

import com.itbaizhan.bean.User;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

@Aspect
public class MyAspect {

    // 配置切入点表达式,切入点表达式的匹配了 UserService 目标对象中的带 Student 的相关方法。
    @Pointcut("execution(* com.itbaizhan.aop.UserService.*Student(..))")
    public void pointCut() {
    }

    @Around(value = "pointCut()")
    public Object aroundAsdf(ProceedingJoinPoint pjp) {
        Object object = null;
        try {
            // 得到方法中的参数
            Object[] objects = pjp.getArgs();
            User user = null;
            for (Object o : objects) {
                if(o instanceof User) {
                    user = (User) o;
                }
            }
			// 判断是否为管理员
            if (user.getRole() == 1) {
                object = pjp.proceed();
                System.out.println("学生信息被更改!");
            } else {
                object = false;
                System.out.println("学生信息被更改失败,你没有权限!");
            }
        } catch (Throwable throwable) {
            System.out.println("操作完成,但是出现异常-----------------");
            throwable.printStackTrace();
        }
        return object;
    }

}

上述代码中 pjp.proceed(); 的返回值是一个 Object 类型,这个 proceed() 其实就是目标对象的调用,它的返回出来的值就是目标对象的返回值,之前一直对这个不懂,现在懂了。

bean 的相关配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置 UserService -->
    <bean class="com.itbaizhan.aop.UserService" id="userService" />

    <!-- 配置切面类 -->
    <bean class="com.itbaizhan.aop.MyAspect" id="myAspect" />

    <!-- 开启基于注解的配置 -->
    <aop:aspectj-autoproxy proxy-target-class="true" />

</beans>

主程序

import com.itbaizhan.bean.User;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.util.Scanner;

public class Entrance {

    static ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans2.xml");

    static UserService userService = (UserService) applicationContext.getBean("userService");

    static int index = 6;

    public static void main(String[] args) {

        Scanner sc = new Scanner(System.in);
        User user = new User();
        // 登录验证
        while(true){
            // 模拟登录
            String username = sc.nextLine();
            String password = sc.nextLine();
            if("xiaohehe".equals(username) && "123456".equals(password)) {
                System.out.println("登录成功,管理员");
                user.setUsername("xiaohehe");
                user.setPassword("123456");
                user.setRole(1);
                // 进入业务
                go(sc, user);
            } else if("hehe".equals(username) && "123".equals(password)) {
                System.out.println("登录成功,普通用户");
                user.setUsername("xiaohehe");
                user.setPassword("123456");
                user.setRole(0);
                go(sc, user);
            } else {
                System.out.println("登录失败,用户名或密码输错了!");
            }
        }
    }

    // 主程序的业务
    public static void go(Scanner sc, User user) {

        while(true) {
            System.out.print("\n请选择:");
            String select = sc.nextLine();
            if("add".equals(select)) {
                if(userService.addStudent(new Student(index, "xiaoqiang" + index, 20 + index), user)){
                    System.out.println("添加成功");
                } else {
                    System.out.println("添加失败");
                }
            } else if("del".equals(select)) {
                if(userService.delStudent(user)){
                    System.out.println("删除成功");
                } else {
                    System.out.println("删除失败");
                }
            } else if("print".equals(select)) {
                userService.printStudents();
            } else if("quit".equals(select)) {
                System.out.println("程序退出了!");
                System.exit(0);
            } else if("logout".equals(select)) {
                System.out.println("用户退出登录了!");
                break;
            } else {
                System.out.println("选项有误!");
            }
            index++;
        }
    }

}

执行过程

程序执行后,先输入用户名和密码进入程序,打印结果如下

xiaohehe
123456
登录成功,管理员

请选择:

然后我们先打印学生信息。

请选择:print
Student{id=1, name='xiaoqiang1', age=21}
Student{id=2, name='xiaoqiang2', age=22}
Student{id=3, name='xiaoqiang3', age=23}
Student{id=4, name='xiaoqiang4', age=24}
Student{id=5, name='xiaoqiang5', age=25}

执行添加操作,看,管理员可以进行添加学生。

请选择:add
学生信息被更改!
添加成功

请选择:print
Student{id=1, name='xiaoqiang1', age=21}
Student{id=2, name='xiaoqiang2', age=22}
Student{id=3, name='xiaoqiang3', age=23}
Student{id=4, name='xiaoqiang4', age=24}
Student{id=5, name='xiaoqiang5', age=25}
Student{id=8, name='xiaoqiang8', age=28}

请选择:

如果我们切换成普通管理员,再次添加试试。

请选择:logout
用户退出登录了!
hehe
123
登录成功,普通用户

请选择:

输入 add 的时候,发现添加失败了。输入 del 的时候也是如此!

然后看上面的 UserService 类中没有挨个判断权限的代码,却能知道你没有权限。这就是 aop 中定义的环绕通知的代码的功劳。

请选择:add
学生信息被更改失败,你没有权限!
添加失败

请选择:del
学生信息被更改失败,你没有权限!
删除失败

请选择:print
Student{id=1, name='xiaoqiang1', age=21}
Student{id=2, name='xiaoqiang2', age=22}
Student{id=3, name='xiaoqiang3', age=23}
Student{id=4, name='xiaoqiang4', age=24}
Student{id=5, name='xiaoqiang5', age=25}
Student{id=8, name='xiaoqiang8', age=28}

请选择:

示例总结

上面的权限验证的场景也是自己想出来的,我自己能够真真正正的体会到 AOP 的编写方法,同时也对上面的概念进行了一个类比,我觉得这里面的示例代码拿过来复制运行一下,体会体会并理解 aop 的道理,其实还是很重要的。

后记

自己在思考的过程中也想到过很多的类比方式,比如用牙签串起来的汉堡,羊肉串,西瓜,豆沙包,饺子等等。只要能真正的弄懂,那才是王道。

在理解 AOP 以及使用场景,参考了以下资料