• (十五)Spring之面向切面编程AOP



    Spring学习目录

    上一篇:(十四)Spring之回顾代理模式

    下一篇:(十六)Spring对事务的支持

    IoC使软件组件松耦合。AOP让你能够捕捉系统中经常使用的功能,把它转化成组件。
    AOP(Aspect Oriented Programming):面向切面编程,面向方面编程。(AOP是一种编程技术)
    AOP是对OOP的补充延伸。
    AOP底层使用的就是动态代理来实现的。
    Spring的AOP使用的动态代理是:JDK动态代理 + CGLIB动态代理技术。Spring在这两种动态代理中灵活切换,如果是代理接口,会默认使用JDK动态代理,如果要代理某个类,这个类没有实现接口,就会切换使用CGLIB。当然,你也可以强制通过一些配置让Spring只使用CGLIB。

    基础环境

    spring6里程碑版本的仓库
    依赖:spring context依赖、junit依赖、log4j2依赖
    log4j2.xml文件放到类路径下。

    AOP介绍

    一般一个系统当中都会有一些系统服务,例如:日志、事务管理、安全等。这些系统服务被称为:交叉业务
    这些交叉业务几乎是通用的,不管你是做银行账户转账,还是删除用户数据。日志、事务管理、安全,这些都是需要做的。
    如果在每一个业务处理过程当中,都掺杂这些交叉业务代码进去的话,存在两方面问题:

    • 第一:交叉业务代码在多个业务流程中反复出现,显然这个交叉业务代码没有得到复用。并且修改这些交叉业务代码的话,需要修改多处。
    • 第二:程序员无法专注核心业务代码的编写,在编写核心业务代码的同时还需要处理这些交叉业务。

    使用AOP可以很轻松的解决以上问题。
    在这里插入图片描述
    总结AOP:将与核心业务无关的代码独立的抽取出来,形成一个独立的组件,然后以横向交叉的方式应用到业务流程当中的过程被称为AOP。

    AOP的优点:

    • 第一:代码复用性增强。
    • 第二:代码易维护。
    • 第三:使开发者更关注业务逻辑。

    AOP的七大术语

    • 1.连接点 Joinpoint:描述的是位置
      在程序的整个执行流程中,可以织入切面的位置。方法的执行前后,异常抛出之后等位置。

    • 2.切点 Pointcut:本质上就是方法
      在程序执行流程中,真正织入切面的方法。(一个切点对应多个连接点)

    • 3.通知 Advice:本质上就是增强代码

      通知又叫增强,就是具体你要织入的代码。
      通知包括:

      • 前置通知:切点(方法)之前
      • 后置通知:切点(方法)之后
      • 环绕通知:前置后置都有通知
      • 异常通知:catch里面
      • 最终通知:finally里面
    • 4.切面 Aspect
      切点 + 通知就是切面。

    • 5.织入 Weaving
      把通知应用到目标对象上的过程。

    • 6.代理对象 Proxy
      一个目标对象被织入通知后产生的新对象。

    • 7.目标对象 Target
      被织入通知的对象。

    代码体现:

    public class UserService{
        public void do1(){
            System.out.println("do 1");
        }
        public void do2(){
            System.out.println("do 2");
        }
        public void do3(){
            System.out.println("do 3");
        }
        public void do4(){
            System.out.println("do 4");
        }
        public void do5(){
            System.out.println("do 5");
        }
        // 核心业务方法
        public void service(){
            try {
                //连接点 Joinpoint
                // 对于do1()来说 前置通知
                do1();//切点 Pointcut  (1)前置通知和后置通知都有叫环绕通知    (2)这一整个叫做切面(切点+通知)
                // 对于do1()来说 后置通知
                //连接点 Joinpoint
    
                do2();//切点 Pointcut
                //连接点 Joinpoint
                do3();//切点 Pointcut
                //连接点 Joinpoint
                do5();//切点 Pointcut
                //连接点 Joinpoint
            }catch (Exception e){
                //连接点 Joinpoint   异常通知
    
            }finally {
                //连接点 Joinpoint  最终通知
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    切点表达式

    切点表达式用来定义通知(Advice)往哪些方法上切入。
    切入点表达式语法格式:

    execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])
    
    • 1

    访问控制权限修饰符:

    • 可选项。
    • 没写,就是4个权限都包括。
    • 写public就表示只包括公开的方法。

    返回值类型:

    • 必填项。
    • *表示返回值类型任意。

    全限定类名:

    • 可选项。
    • 两个点“…”代表当前包以及子包下的所有类。
    • 省略时表示所有的类。

    方法名:

    • 必填项。
    • *表示所有方法。
    • set*表示所有的set方法。

    形式参数列表:

    • 必填项
    • () 表示没有参数的方法
    • (…) 参数类型和个数随意的方法
    • (*) 只有一个参数的方法
    • (*, String) 第一个参数类型随意,第二个参数是String的。

    异常:

    • 可选项。
    • 省略时表示任意异常类型。

    例如:
    表示service包下所有的类中以delete开始的所有方法

    execution(public * com.mall.service.*.delete*(..))
    
    • 1

    所有类的所有方法

    execution(* *(..))
    
    • 1

    mall包下所有的类的所有的方法

    execution(* com.mall..*(..))
    
    • 1

    Spring的AOP的使用

    Spring对AOP的实现包括以下3种方式:

    • 第一种方式:Spring框架结合AspectJ框架实现的AOP,基于注解方式。(常用)
    • 第二种方式:Spring框架结合AspectJ框架实现的AOP,基于XML方式。(不常用)
    • 第三种方式:Spring框架自己实现的AOP,基于XML配置方式。

    实际开发中,都是Spring+AspectJ来实现AOP。所以我们重点学习第一种和第二种方式。而实际开发基本使用注解方式,最重点的是注解方式的学习。
    什么是AspectJ?(Eclipse组织的一个支持AOP的框架。AspectJ框架是独立于Spring框架之外的一个框架,Spring框架用了AspectJ)
    AspectJ项目起源于帕洛阿尔托(Palo Alto)研究中心(缩写为PARC)。该中心由Xerox集团资助,Gregor Kiczales领导,从1997年开始致力于AspectJ的开发,1998年第一次发布给外部用户,2001年发布1.0 release。为了推动AspectJ技术和社团的发展,PARC在2003年3月正式将AspectJ项目移交给了Eclipse组织,因为AspectJ的发展和受关注程度大大超出了PARC的预期,他们已经无力继续维持它的发展。

    环境准备

    使用Spring+AspectJ的AOP除了还需要引入aop依赖和aspects依赖:
    而如果使用Maven则只需引入context依赖就会自动关联spring-aop依赖,没有使用Maven则需要把aop的jar包引入项目。

        <!--spring aspects依赖-->
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-aspects</artifactId>
          <version>6.0.0-M2</version>
        </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    Spring配置文件中添加context命名空间和aop命名空间:

    <?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:context="http://www.springframework.org/schema/context"
           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/context http://www.springframework.org/schema/context/spring-context.xsd
                               http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    
    </beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    基于AspectJ的AOP注解式开发

    首先需要创建spring.xml文件:

    • 1.引入context命名空间和aop命名空间
    • 2.添加组件扫描
    • 3.开启自动代理
      开启之后,Spring容器在扫描类的时候,会查看类上是否有@Aspect注解,如果有,则会自动给这个类生成代理对象。
      使用aop:aspectj-autoproxy开启自动代理
      • proxy-target-class属性:默认为false
        • true:表示强制使用CGLIB动态代理

        • false:表示,接口使用JDK动态代理,类使用CGLIB动态代理

    <?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:context="http://www.springframework.org/schema/context"
           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/context http://www.springframework.org/schema/context/spring-context.xsd
                               http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
        <!--添加组件扫描-->
        <context:component-scan base-package="com.aop.annotation.service"/>
        <!--
            开启aspectj自动代理
        -->
        <aop:aspectj-autoproxy proxy-target-class="true"/>
    </beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    创建目标类和目标方法,纳入spring管理:

    @Service
    public class UserService {//目标类
    
    
        public void login(){//目标方法
            System.out.println("正在登录。。。");
        }
    
        public void logout(){
            System.out.println("正在退出。。。");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    创建切面类:纳入spring管理,除此之外,需要加一个@Aspect注解,通知Spring框架这个类是一个切面类,到时候这个切面类会编写通知,添加切点表达式

    @Component
    @Aspect
    public class LogAspect {//切面=切点+通知
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    通知类型

    通知类型包括:

    • 前置通知:@Before 目标方法执行之前的通知
    • 后置通知:@AfterReturning 目标方法执行之后的通知
    • 环绕通知:@Around 目标方法之前添加通知,同时目标方法执行之后添加通知。
    • 异常通知:@AfterThrowing 发生异常之后执行的通知
    • 最终通知:@After 放在finally语句块中的通知
    前置通知@Before

    @Before注解代表该通知为前置通知,注解里面需要写切点表达式
    在LogAspect 切面类添加前置通知:

    	//前置通知
    	@Before("execution(* com.aop.annotation.service..*(..))")
        public void beforeAdvice(){
            System.out.println("前置通知。。。增强代码。。。");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    测试程序:

        @Test
        public void testAOP(){
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
            UserService userService = applicationContext.getBean("userService", UserService.class);
    
            userService.login();
            System.out.println("---------------------------------");
            userService.logout();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    请添加图片描述

    后置通知@AfterReturning

    @AfterReturning代表该通知为后置通知,注解里面需要写切点表达式
    在LogAspect 切面类添加后置通知:

    //后置通知
        @AfterReturning("execution(* com.aop.annotation.service..*(..))")
        public void afterReturningAdvice(){
            System.out.println("后置通知。。。增强代码。。。");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    再次运行测试程序:
    请添加图片描述

    环绕通知@Around

    @Around代表该通知为环绕通知,注解里面需要写切点表达式
    环绕通知会收到一个ProceedingJoinPoint参数,用来执行目标方法。
    在LogAspect 切面类添加环绕通知:

        @Around("execution(* com.aop.annotation.service..*(..))")
        public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
            //前面代码
            System.out.println("前环绕。。。");
            //执行目标
            joinPoint.proceed();
            //后面代码
            System.out.println("后环绕。。。");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    再次运行测试程序:
    可见环绕是最大的通知,在前置之前,在后置之后。
    请添加图片描述

    异常通知@AfterThrowing

    @AfterThrowing代表该通知为异常通知,注解里面需要写切点表达式
    为了更好演示,重新创建一个目标类和目标方法:该类抛一个异常

    @Service
    public class OrderService {
        public void generate(){
            System.out.println("正在生成订单。。。");
            throw new RuntimeException("异常");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在LogAspect 切面类添加异常通知:

    	@AfterThrowing("execution(* com.aop.annotation.service..*(..))")
        public void afterThrowingAdvice(){
            System.out.println("异常通知。。。");
        }
    
    • 1
    • 2
    • 3
    • 4

    修改测试程序:

        @Test
        public void testAOP(){
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
            UserService userService = applicationContext.getBean("userService", UserService.class);
    
            userService.login();
            System.out.println("---------------------------------");
            userService.logout();
            System.out.println("---------------------------------");
            OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
            orderService.generate();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    运行程序:发现发生异常后,后环绕和后置通知已经没了,这是因为执行方法的时候抛异常之后不会再往下执行通知
    请添加图片描述

    最终通知@After

    @After代表该通知为最终通知,注解里面需要写切点表达式
    在LogAspect 切面类添加最终通知:

    	@After("execution(* com.aop.annotation.service..*(..))")
        public void afterAdvice(){
            System.out.println("最终通知。。。");
        }
    
    • 1
    • 2
    • 3
    • 4

    再次运行测试程序:
    可见最终通知一定会执行,不管有没有异常,最终通知,在后置之后,后环绕之前,由此可见环绕通知确实是最大的通知
    请添加图片描述

    关于JoinPoint

    JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象.。
    JoinPoint是在Spring调用的切面方法时候自动传过来的,我们可以直接使用

    方法名功能
    Signature getSignature();获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
    Object[] getArgs();获取传入目标方法的参数对象
    Object getTarget();获取被代理的对象
    Object getThis();获取代理对象

    ProceedingJoinPoint对象是JoinPoint的子接口,该对象只用在@Around的切面方法中, 添加了两个方法.

    Object proceed() throws Throwable //执行目标方法 
    Object proceed(Object[] var1) throws Throwable //传入的新的参数去执行目标方法 
    
    • 1
    • 2

    切面的先后顺序

    我们知道,业务流程当中不一定只有一个切面,可能有的切面控制事务,有的记录日志,有的进行安全控制,如果多个切面的话,顺序如何控制:可以使用@Order注解来标识切面类,为@Order注解的value指定一个整数型的数字,数字越小,优先级越高。

    在LogAspect切面类添加注解:

    @Order(2)
    
    • 1

    把OrderService目标类的异常注释掉:

    @Service
    public class OrderService {
        public void generate(){
            System.out.println("正在生成订单。。。");
            //throw new RuntimeException("异常");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    再定义一个切面类:优先级设置为1,使用一下JoinPoint

    /**
     * 安全切面
     */
    @Component
    @Aspect
    @Order(1)//数字越小优先级越高
    public class SecurityAspect {
        @Before("execution(* com.aop.annotation.service..*(..))")
        public void beforeAdvice(JoinPoint joinPoint){
            System.out.println("安全的前置通知。。。");
            
            Signature signature = joinPoint.getSignature();//获取目标方法的签名
            System.out.println(signature.getName());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    测试程序:

        @Test
        public void test(){
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
            OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
            orderService.generate();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    请添加图片描述
    如果把优先级换一下,再次运行:
    请添加图片描述

    通用切点表达式

    回顾之前的切点表达式,发现切点表达式是重复的
    这样的缺点是:

    • 第一:切点表达式重复写了多次,没有得到复用。
    • 第二:如果要修改切点表达式,需要修改多处,难维护。

    怎么解决,可以使用通用切点表达式:@Pointcut注解将切点表达式单独的定义出来,在需要的位置引入即可。
    在LogAspect 切面类添加一个方法使用@Pointcut注解:
    方法只是个标记,方法名随意,方法体不需要写任何代码,只需要使用一个@Pointcut注解标注

        //通用切点表达式
        @Pointcut(value = "execution(* com.aop.annotation.service..*(..))")
        public void execution_use(){
        }
    
    • 1
    • 2
    • 3
    • 4

    怎么引用,只需要在通知注解中写该方法即可,例如:

        //@After("execution(* com.aop.annotation.service..*(..))")
        @After("execution_use()")
    
    • 1
    • 2

    这个通用表达式配置,只要一配置,任何切面类都可以使用,例如刚才的安全切面类,可以这样写。

    /**
     * 安全切面
     */
    @Component
    @Aspect
    @Order(3)//数字越小优先级越高
    public class SecurityAspect {
        //@Before("execution(* com.aop.annotation.service..*(..))")
        @Before("com.aop.annotation.service.LogAspect.execution_use()")
        public void beforeAdvice(JoinPoint joinPoint){
            System.out.println("安全的前置通知。。。");
            /*
                这个JoinPoint是在Spring调用的这个方法时候自动传过来的
                我们可以直接使用
             */
            Signature signature = joinPoint.getSignature();//获取目标方法的签名
            System.out.println(signature.getName());
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    再次运行test的测试程序:
    请添加图片描述

    全注解式开发AOP

    就是编写一个类,在这个类上面使用大量注解来代替spring的配置文件,spring配置文件消失了,如下:

    @Configuration //代替Spring配置文件
    @ComponentScan("com.aop.annotation.service")//代替注解扫描
    @EnableAspectJAutoProxy(proxyTargetClass = true)//代替开启aspectj自动代理,
    public class SpringConfig {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    测试程序:

        @Test
        public void testNoXML(){
            ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
            OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
            orderService.generate();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    请添加图片描述

    基于XML配置方式的AOP(了解)

    第一步:编写目标类

    /**
     * 目标类
     */
    public class UserService {
        public void login(){
            System.out.println("正在登录。。。");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    第二步:编写切面类,并且编写通知

    /**
     * 切面
     */
    public class TimerAspect {
        //通知
        public void arountAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
            //前环绕
            long begin = System.currentTimeMillis();
            joinPoint.proceed();//执行目标
            //后环绕
            long end = System.currentTimeMillis();
            System.out.println("耗时:"+ (end-begin));
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    第三步:编写spring配置文件

    <?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:context="http://www.springframework.org/schema/context"
           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/context http://www.springframework.org/schema/context/spring-context.xsd
                               http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
        <!--配置bean-->
        <bean id="userService" class="com.spring.aspectj.service.UserService"></bean>
        <bean id="timerAspect" class="com.spring.aspectj.service.TimerAspect"></bean>
    
        <!--aop配置-->
        <aop:config>
            <!--切点表达式-->
            <aop:pointcut id="mypointcut" expression="execution(* com.spring.aspectj.service..*(..))"/>
            <!--切面-->
            <aop:aspect ref="timerAspect">
                <!--哪个通知就配哪个-->
                <aop:around method="arountAdvice" pointcut-ref="mypointcut"/>
            </aop:aspect>
        </aop:config>
    </beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    测试程序:

        @Test
        public void testXML(){
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
            UserService userService = applicationContext.getBean("userService", UserService.class);
            userService.login();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    请添加图片描述

  • 相关阅读:
    LaTeX学习笔记
    20231120_python练习_天气网爬取城市近七天温度情况
    HTML小游戏14 —— H5横版冒险游戏《无限生机》(附完整源码)
    MySQL:锁机制 |表级锁、行级锁 | 排它锁、共享锁 | 间隙锁
    CMake中while/continue/break的使用
    【星海出品】flask (四) 三方工具使用
    React-Hooks进阶:useContext、useState回调、useEffect发送网络请求和useRef
    力扣371周赛
    houdini布料解算 质感像塑料
    如何使用工程仪器设备在线监测管理系统
  • 原文地址:https://blog.csdn.net/weixin_45832694/article/details/128039889