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(一般不用)

先创建一个类:

然后创建增强类:

配置前置通知:

 

posted @ 2021-04-27 23:59  wenxuehai  阅读(365)  评论(0编辑  收藏  举报
//右下角添加目录