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


    1、AOP的基本介绍

    AOP是Aspect Oriented Programming,即面向切面编程。AOP是OOP(面向对象编程)的延续,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。OOP作为面向对象编程的模式,获得了巨大的成功,OOP的主要功能是数据封装、继承和多态。而AOP是一种新的编程方式,它和OOP不同,OOP把系统看作多个对象的交互,AOP把系统分解为不同的关注点,或者称之为切面(Aspect)。

    AOP 要达到的效果是,保证开发者在不修改源代码的前提下,去为系统中的业务组件添加某种通用功能。AOP 的本质是由 AOP 框架修改业务组件的多个方法的源代码。

    AOP技术看上去比较神秘,但实际上,它本质就是一个动态代理。

    1.1、spring AOP的代理机制

    按照 AOP 框架修改源代码的时机,可以将其分为两类:

    • 静态 AOP 实现:AOP 框架在编译阶段对程序源代码进行修改,生成了静态的 AOP 代理类。此时生成的 *.class 文件已经被改掉了,需要使用特定的编译器,比如 AspectJ。
    • 动态 AOP 实现:AOP 框架在运行阶段对动态生成代理对象,在内存中以 JDK 动态代理,或 CGlib 动态地生成 AOP 代理类,如 SpringAOP。目标对象和切面都是普通Java类,通过JVM的动态代理功能或者第三方库实现运行期动态织入。

    最简单的方式就是动态 AOP 实现,Spring的AOP实现就是基于JVM的动态代理。JVM的动态代理要求必须实现接口,所以如果一个普通类并没有实现任何借口,那么就需要通过CGLIB或者Javassist这些第三方库来实现 AOP。

    如果要被代理的对象是个实现类,Spring 会自动使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制);如果要被代理的对象是个普通类,即不是实现类,那么 Spring 会强制使用 CGLib 来实现动态代理。

    通过配置Spring的中<aop:config>标签可以显式地指定使用什么代理机制,proxy-target-class=true 表示使用CGLib代理,如果为 false 就是默认使用JDK动态代理:

    1.2、AOP相关术语

    AOP 领域中的特性术语:

    • 通知(Advice,增强): 通知描述了切面何时执行以及如何执行增强处理。(比如给类中的某个方法进行了添加了一些额外的操作,这些额外操作就是增强)
    • 连接点(join point): 应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用,即哪些方法可以被增强,这些方法就可以称之为一个连接点。
    • 切入点(PointCut): 可以插入增强处理的连接点。实际被真正增强了的方法,称为切入点。(连接点都可被增强,但实际应用可能只增强了类中的某个方法,则该方法就被称为切入点)
    • 切面(Aspect): 切面是通知和切点的结合。把通知(增强)应用到切入点的过程就称为切面。
    • 引入(Introduction):引入允许我们向现有的类添加新的方法或者属性。
    • 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,这个过程就是织入。

    1.3、通知的五种类型(@Before、@After、@AfterReturning、@AfterThrowing、@Around)

    通知(增强)有五种类型:

    1. 前置通知(@Before):在目标方法运行之前运行。目标代码有异常也会执行,但如果拦截器抛异常,那么目标代码就不执行了;
    2. 后置通知(@After):在目标方法运行结束之后运行。无论目标代码是否抛异常,拦截器代码都会执行;
    3. 返回通知(@AfterReturning):在目标方法正常返回之后运行。和@After不同的是,只有当目标代码正常返回时,才执行拦截器代码,并且这个通知执行顺序在 @After 之后
    4. 异常通知(@AfterThrowing):在目标方法出现异常之后运行。和@After不同的是,只有当目标代码抛出了异常时,才执行拦截器代码;
    5. 环绕通知(@Around):增强的方法会将目标方法封装起来,能控制是否执行目标代码,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能。

    2、AOP的基本使用

    2.1、切入点表达式

    切入点表达式是 spring 用表达式来对指定的方法进行拦截。

    示例如下:

    execution([权限修饰符] [返回类型(可省略)] [完整类名].[方法名](参数列表))
    
    //示例如下,权限修饰符可用*表示任意权限,参数列表可用 .. 表示方法中的参数
    execution(public int com.demo.Test.*(..))  //作用于Test类中的所有方法
    
    execution(* com.demo.Test.add(..))  //作用于Test类中的add方法
    
    execution(* com.demo.*.*(..))  //作用于demo包下的所有类的所有方法

    2.2、AOP的使用

    spring 框架一般都是基于 AspectJ 实现 AOP 操作,AspectJ 不是 spring 的组成部分,一般把 AspectJ 和 spring 框架一起使用,进行 AOP 操作。

    基于 AspectJ  实现 AOP 操作有两种方式:

    1. 基于XML配置文件实现
    2. 基于注解方式实现

    先导入依赖包,spring 实现AOP不仅仅需要自己的jar包,还需要第三方的jar,将这三个jar包放入项目中就可以spring的aop编程了,如下所示:

    使用示例:

    先创建一个类(被增强的类):

    package webPackage;
    
    import org.springframework.stereotype.Component;
    
    //需要被增强的类
    @Component
    public class User {
        public void add() {
            System.out.println("user add...");
        }
    }

    然后创建增强类(切面类,编写增强逻辑):

    package webPackage;
    
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.stereotype.Component;
    
    @Component
    @Aspect  //生成代理对象
    public class UserProxy {
    
        //前置通知
        @Before("execution(* webPackage.User.add(..))")
        public void beforeHandler() {
            System.out.println("前置处理。。。");
        }
    }

    在 xml 配置文件中引入命名变量,并且开启组件注解扫描和 aspectj 切面支持:

    <?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/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
                               http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    
        <!--开启组件扫描-->
        <context:component-scan base-package="testMain, service, dao, webPackage"></context:component-scan>
    
        <!-- 开启aspectj切面支持 -->
        <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    </beans>

    验证代码:

    package testMain;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import webPackage.User;
    
    public class Test01 {
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("bean01.xml");
    
            User user = (User) context.getBean("user");
            user.add();   //将输出 前置处理。。。  user add...
    
        }
    }

    如果 User 类实现了某个接口,即是实现类,上面的用法可能会报错: com.sun.proxy.$Proxy11 cannot be cast to web.User。报错是因为不能用接口的实现类(Target)来转换Proxy的实现类,它们是同级,应该用共同的接口来转换。将获得Bean的接收类型改成接口类型即可。

    此时可以这么用:

    package test;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import web.User;
    import web.UserInter;
    
    public class Test {
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("bean01.xml");
            UserInter user = (UserInter) context.getBean("user");
            user.add();   //将输出 前置处理。。。  user add...
        }
    }

    上面实现的是前置通知,其他类型通知如下:

    package webPackage;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
    
    @Component
    @Aspect  //生成代理对象
    public class UserProxy {
    
        //前置通知
        @Before("execution(* webPackage.User.add(..))")
        public void beforeHandler() {
            System.out.println("前置处理。。。");
        }
    
        //后置通知
        @After("execution(* webPackage.User.add(..))")
        public void afterHandler() {
            System.out.println("后置处理。。。");
        }
    
        //返回通知
        @AfterReturning("execution(* webPackage.User.add(..))")
        public void afterReturnHandler() {
            System.out.println("返回处理。。。");
        }
    
        //异常通知
        @AfterThrowing("execution(* webPackage.User.add(..))")
        public void afterThrowReturnHandler() {
            System.out.println("异常处理。。。");
        }
    
        //环绕通知
        @Around("execution(* webPackage.User.add(..))")
        public void aroundThrowReturnHandler(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            System.out.println("环绕之前。。。");
    
            proceedingJoinPoint.proceed();  //执行目标方法
    
            System.out.println("环绕之后。。。");
        }
    }

    执行结果:环绕之前。。。     前置处理。。。    user add...     环绕之后。。。   后置处理。。。   返回处理。。。

    异常通知没有执行,因为目标方法并没有抛出异常,如果抛出异常,异常通知才会执行。

    2.3、抽取相同切入点(@Pointcut)

    使用多个通知时,可能需要重复写切入点表达式,此时我们可以通过 @Pointcut 注解来将切入点抽取出来进行复用:

    package webPackage;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
    
    @Component
    @Aspect  //生成代理对象
    public class UserProxy {
    
        @Pointcut("execution(* webPackage.User.add(..))")
        public void pointDemo() {}
    
        //前置通知
        @Before("pointDemo()")
        public void beforeHandler() {
            System.out.println("前置处理。。。");
        }
    
        //后置通知
        @After("pointDemo()")
        public void afterHandler() {
            System.out.println("后置处理。。。");
        }
    }

    2.4、多个切面(增强类)优先级

    如果有多个切面对同一个类的方法都进行了增强,我们可以用 @Order(num) 来定义各个切面的优先级,num越小,优先级越高。

    比如 UserProxy 和 UserProxy02 都对 User 的 add 方法进行了增强,我们就可以通过 @Order(num) 来指定哪个增强类先执行:

    UserProxy 代码:

    package webPackage;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;
    
    @Component
    @Aspect 
    @Order(2)
    public class UserProxy {
    
        @Pointcut("execution(* webPackage.User.add(..))")
        public void pointDemo() {}
    
        //前置通知
        @Before("pointDemo()")
        public void beforeHandler() {
            System.out.println("UserProxy 的前置处理。。。");
        }
    }

    UserProxy02 代码:

    package webPackage;
    
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;
    
    @Component
    @Aspect  //生成代理对象
    @Order(1)
    public class UserProxy02 {
        @Pointcut("execution(* webPackage.User.add(..))")
        public void pointDemo() {}
    
        //前置通知
        @Before("pointDemo()")
        public void beforeHandler() {
            System.out.println("UserProxy02 的前置处理。。。");
        }
    }

    验证代码:

    package testMain;
    
            import org.springframework.context.ApplicationContext;
            import org.springframework.context.support.ClassPathXmlApplicationContext;
            import webPackage.User;
    
    public class Test01 {
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("bean01.xml");
    
            User user = (User) context.getBean("user");
            user.add();  //将输出:UserProxy02 的前置处理。。。  UserProxy 的前置处理。。。  user add...
        }
    }

    2.5、完全注解开发(不需要xml配置文件)

    我们可以通过配置类来替代 xml 配置文件,实现完全注解开发:

    照着上面的例子,先建一个 User 类和一个增强类 UserProxy,然后用配置类替代 xml 配置文件。配置类如下:

    package webPackage;
    
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    @Configuration
    @ComponentScan(basePackages = {"testMain", "service", "dao", "webPackage"})
    @EnableAspectJAutoProxy(proxyTargetClass = true)
    public class AopConfig {
    }

    Spring的 IOC 容器看到 @EnableAspectJAutoProxy 注解,就会自动查找带有@Aspect的Bean,然后根据每个方法的@Before@Around等注解把AOP注入到特定的Bean中。

    使用 bean 跟用配置文件方式不太一样,代码如下:

    package testMain;
    
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import webPackage.AopConfig;
    import webPackage.User;
    
    public class Test01 {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
            User user = (User) context.getBean("user");
            user.add();
        }
    }

    2.6、直接通过配置文件实现AOP(一般不用)

    先创建一个类:

    然后创建增强类:

    配置前置通知:

  • 相关阅读:
    控制反转和依赖注入
    共识机制是什么?
    实用拜占庭容错算法PBFT
    三种框架对比react vue 和Angular对比
    go语言学习笔记
    激活方法总结
    钱包助记词
    简历中存在的问题的处理
    why we use Symbols in Hash
    compact过滤数组中的nil
  • 原文地址:https://www.cnblogs.com/wenxuehai/p/14711475.html
Copyright © 2020-2023  润新知