6.AOP(面向切面编程)

本章目标

  1. AOP概念(理解)
  2. Spring中AOP的实现(理解)
  3. AspectJ开发(xml方式)(掌握)
  4. 基于注解的声明式AspectJ /ˈæspekt/(掌握)

本章内容:

一句话概括AOP:可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术

一、AOP相关概念

1、出现的背景

软件系统包含许多功能模块,它们有着不同的功能,处理着不同的业务,同时每个模块调用一些共同的系统级的功能,例如每个业务模块均调用日志功能。这些功能的调用分散在软件中各个地方,当日志功能修改(如改变接口参数类型)时,需要在软件中很多地方进行维护,产生较大的维护量。

2、AOP概念

AOP(面向切面编程, Aspect-oriented programming )是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率

在一个服务的流程中插入与业务逻辑无关的系统服务逻辑(例如 Logging 、 Security ) , 这样的逻辑称为Cross-cutting concerns(横切关切点 ),将Crossing-cutting concerns独立出来设计为一个对象,这样的特殊对象称之为Aspect,Aspect-oriented programming着重在Aspect的设计及与应用程序的织入(Weave)

3、应用场景

  • 在我们的程序中,经常存在一些系统性的需求,比如权限校验、日志记录、统计等,这些代码会散落穿插在各个业务逻辑中,非常冗余且不利于维护。例如下面这个示意图:

  • 有多少业务操作,就要写多少重复的校验和日志记录代码,这显然是无法接受的。当然,用面向对象的思想,我们可以把这些重复的代码抽离出来,写成公共方法,就是下面这样:

  • 这样,代码冗余和可维护性的问题得到了解决,但每个业务方法中依然要依次手动调用这些公共方法,也是略显繁琐。有没有更好的方式呢?有的,那就是AOP,AOP将权限校验、日志记录等非业务代码完全提取出来,与业务代码分离,并寻找节点切入业务代码中:

  • 我们将共同的操作和处理可以称为“切面”,AOP对这个切面提供集中统一的管理;各个业务处理组件就可以集中精力解决自己特定的问题,不再关心繁琐的一般业务。

4、AOP主要的功能

主要的意图是:将日志记录性能统计安全控制事务处理异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

AOP实际是设计模式的延续,设计模式是调用者和被调用者之间的解耦,提高代码的灵活性和可扩展性,AOP可以说也是这种目标的一种实现。

在Spring中提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。

5、AOP术语:

横切关注点(Cross-cutting Concerns) 在项目的例子中,记录的动作原先被横切(Cross-cutting)入至 HelloProxy本身所负责的业务流程之中,另外类似于记录这类的动作,如安全(Security)检查、事务(Transaction)等系统层面的服务(Service),在一些应用程序之中常被见到安插至各个对象的处理流程之中,这些动作在AOP的术语中被称之为Cross-cutting concerns

切面(Aspect ): 将散落于各个业务对象之中的 Cross-cutting concerns收集起来,设计各个独立可重用的对象,这些对象称之为Aspect(/æspekt/),比如,日志、事务、权限、异常等都可以看作切面。

可以这样理解:软件各模块需要嵌入共同的功能,由于各模块均需嵌入这个功能,就好像这个功能是一个切面“切入”各模块。

  • 连接点(Joinpoint)when(什么时候,比如方法执行之前): 在程序执行过程中某个特定的时间点,比如某方法调用的时候 在Spring AOP中,一个连接点总是代表一个方法的执行的某个时机,比如方法执行前,方法执行后、方法执行发生异常的时候
  • 通知(Advice) what(做什么操作,比如写日志): 在切面的某个特定的连接点上执行的动作,即切面功能的实现。通知有各种类型,其中包括around、before和after等。
  • 切入点(Pointcut)where(作用于哪些类的哪些方法): 通知应用于连接点的匹配说明,即匹配连接点的断言。通俗的说,就是将切面引用项目中哪些类的哪些方法,那么这些方法就称为切入点

目标对象(Target Object): 被一个或者多个切面所通知的对象。也有人把它叫做被通知对象。即应用通知的目标对象。

AOP代理(AOP Proxy): AOP框架创建的代理对象,其作用是代理目标对象,实现切面功能。

织入( Weaving ):Advice被应用至对象之上的过程称之为织入(Weave) ,在AOP中织入的方式有几个时间点。

  • 编译期:切面在目标对象编译时织入.这需要一个特殊的编译器.
  • 类装载期:切面在目标对象被载入JVM时织入.这需要一个特殊的类载入器.
  • 运行期:切面在应用系统运行时织入.

6、Spring 通知的 5 种类型

6.1、通知类型

通知 说明
前置通知(Before advice) 在某连接点之前执行的通知,但这个通知不能阻止连接点的执行(除非它抛出一个异常)
后置通知(After returning advice) 在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回
异常通知(After throwing advice) 在方法抛出异常退出时执行的通知
最终通知(After (finally) advice) 当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)
环绕通知 在连接点之前执行的通知,但是可以控制是否执行连接点

6.2、接口及业务操作

名称 说明
org.springframework.aop.MethodBeforeAdvice(前置通知) 在方法之前自动执行的通知称为前置通知,可以应用于权限管理等功能。
org.springframework.aop.AfterReturningAdvice(后置通知) 在方法之后自动执行的通知称为后置通知,可以应用于关闭流、上传文件、删除临时文件等功能。
org.aopalliance.intercept.MethodInterceptor(环绕通知) 在方法前后自动执行的通知称为环绕通知,可以应用于日志、事务管理等功能。
org.springframework.aop.ThrowsAdvice(异常通知) 在方法抛出异常时自动执行的通知称为异常通知,可以应用于处理异常记录日志等功能。
org.springframework.aop.AfterAdvice(最终通知) 在方法执行之后调用的通知,无论方法执行是否成功。

7、Java常用AOP框架 Spring AOP 和 AspectJ

Spring aop 旨在提供一个跨 Spring IoC 的简单的 aop 实现, 以解决程序员面临的最常见问题。它不打算作为一个完整的 AOP 解决方案 —— 它只能应用于由 Spring 容器管理的 bean。

AspectJ 是原始的 aop 技术, 目的是提供完整的 aop 解决方案。它更健壮, 但也比 Spring AOP 复杂得多。还值得注意的是, AspectJ 可以在所有域对象中应用。

8、Spring中AOP的三大方式

  1. 方式一:使用原生Spring API接口
  2. 方式二:自定义切面类
  3. 方式三:注解方式

二、Spring中AOP的实现

在 Spring 中创建 AOP 代理的底层是基于 org.springframework.aop.framework.ProxyFactoryBean. 这将完全控制切点和应用的通知及顺序.

1、Spring AOP 是基于动态代理模式实现

Spring AOP 是基于动态代理模式实现,采用两种,JDK动态代理、CGLIB的动态代理。

使用 JDK 的 Proxy 实现代理,要求目标类与代理类实现相同的接口。若目标类不存在接口,则无法使用该方式实现。对于无接口的类,要为其创建动态代理,就要使用 CGLIB 来实现。

CGLIB 代理的生成原理是生成目标类的子类,而子类是增强过的,这个子类对象就是代理对象。所以使用 CGLIB 生成动态代理,要求目标类必须能够被继承,即不能是 final 的类。

2、ProxyFactoryBean

ProxyFactoryBean的作用:针对目标对象来创建代理对象,将对目标对象方法的调用转到对相应代理对象方法的调用,并且可以在代理对象方法调用前后执行与之匹配的 各个通知器中定义好的方法

一句话概括:ProxyFactoryBean 就是来创建代理对象的

ProxyFactoryBean 与其他Spring FactoryBean 的实现一样,引入了一个间接层. 如果定义了一个名为 fooProxyFactoryBean, 那么引用 foo 的对象不是 ProxyFactoryBean 实例本身,而是由 ProxyFactoryBean 实现的 getObject() 方法创建的对象. 此方法将创建一个用于包装目标对象的 AOP 代理

常用属性

属性名称 描 述
target 代理的目标对象
proxyInterfaces 代理要实现的接口,如果有多个接口,则可以使用以下格式赋值:<list> <value ></value></list>
proxyTargetClass 是否对类代理而不是接口,设置为 true 时,使用 CGLIB 代理
interceptorNames 需要植入目标的 Advice
singleton 返回的代理是否为单例,默认为 true(返回单实例)
optimize 当设置为 true 时,强制使用 CGLIB

3、在pom.xml中添加依赖

 <properties>
         <maven.compiler.source>8</maven.compiler.source>
         <maven.compiler.target>8</maven.compiler.target>
         <spring.version>5.3.10</spring.version>
     </properties>
     <dependencies>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-beans</artifactId>
             <version>${spring.version}</version>
         </dependency>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-core</artifactId>
             <version>${spring.version}</version>
         </dependency>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-context</artifactId>
             <version>${spring.version}</version>
         </dependency>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-expression</artifactId>
             <version>${spring.version}</version>
         </dependency>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-aop</artifactId>
             <version>${spring.version}</version>
         </dependency>
         <!-- https://mvnrepository.com/artifact/aopalliance/aopalliance -->
         <dependency>
             <groupId>aopalliance</groupId>
             <artifactId>aopalliance</artifactId>
             <version>1.0</version>
         </dependency>
         <dependency>
             <groupId>log4j</groupId>
             <artifactId>log4j</artifactId>
             <version>1.2.17</version>
         </dependency>
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <version>4.12</version>
             <scope>test</scope>
         </dependency>
     </dependencies>

spring-aop:是spring为AOP提供的实现包

aopalliance:是AOP联盟提供的规范包

4、编写切面类:

在 Spring 通知中,环绕通知是一个非常典型的应用。下面通过环绕通知的案例演示 Spring 创建 AOP 代理的过程

 package com.woniuxy.aop;
 
 import org.aopalliance.intercept.MethodInterceptor;
 import org.aopalliance.intercept.MethodInvocation;
 import org.apache.log4j.Logger;
 
 import java.time.LocalDateTime;
 
 public class MyAspect implements MethodInterceptor {
     //构造log对象
     Logger logger = Logger.getLogger(MyAspect.class);
     @Override
     public Object invoke(MethodInvocation invocation) throws Throwable {
         logger.info(invocation.getMethod().getDeclaringClass()+"的方法"+invocation.getMethod().getName()+"在"+ LocalDateTime.now()+"开始执行了");
         Object proceed = invocation.proceed();
         logger.info(invocation.getMethod().getDeclaringClass()+"的方法"+invocation.getMethod().getName()+"在"+ LocalDateTime.now()+"执行结束了");
         return proceed;
     }
 }
 

5、配置文件

 <?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"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
         https://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/context
         https://www.springframework.org/schema/context/spring-context.xsd">
 
       <!-- 通知 advice,也可以直接在类上添加注解的方式来定义组件 -->
     <bean id="myAspect" class="com.woniuxy.aop.MyAspect"/>
     <!--生成代理对象 -->
     <bean id="userService" class="org.springframework.aop.framework.ProxyFactoryBean">
         <!--代理目标-->
         <property name="target" ref="userServiceImpl"/>
         <!--添加的功能-->
         <property name="interceptorNames" value="myAspect"/>
         <!--代理接口-->
         <property name="proxyInterfaces" value="com.woniuxy.service.UserService"/>
         <!-- 如何生成代理,true:使用cglib; false :使用jdk动态代理 -->
         <property name="proxyTargetClass" value="false" />
     </bean>
 </beans>

ProxyFactoryBean为工厂bean,返回的是代理的对象

注意:target对应的值为ref引用,其它的几个为value赋值

6、测试类:

 public class SSMApplication {
     public static void main(String[] args) {
         ApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
         UserService userService = ac.getBean("userService", UserService.class);
         userService.selectByPrimaryKey(1l);
     }
 }

此时实现已经实现了对userServiceImpl的代理操作,但这只能操作于当前这个接口,如果想应用到其它接口比如DeptService上,我们还需要再配置

7、使用原生Spring API接口

前面的例子我们发现只能针对一个接口,如果要生成另一外接口的代理类,需要重新配置,这样太麻烦了,既然使用Spring AOP,spring就应该帮我们解决了这个问题,那么我们看一下怎么配置:

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

      <!-- 通知 advice -->
     <bean id="myAspect" class="com.woniuxy.aop.MyAspect"/>

     <aop:config>
         <!--切入点,需要告诉方法在什么去执行
         expression="execution(* com.spring.service.impl.*.*(..))"
         第一个* 表示所有的返回值,然后就是包名
         第二个*表示所有的类对象
         第三个*表示类对象所有的方法
         第四个..表示所有方法下面的带参数的方法或者是不带参数的方法
         -->
         <aop:pointcut expression="execution(* com.woniuxy.sm.service.impl.*.*(..))" id="pointcut"/>
         <!-- 在所有的方法中都切入前置通知-->
         <aop:advisor advice-ref="myAspect" pointcut-ref="pointcut"/>
     </aop:config>
 </beans>

更多切点表达式说明,可以参考以下两个文档

切点表达式说明:

支持的切点符号参考

advisor : /ədˈvaɪzə/

三、AspectJ开发(面向切面的框架开发)

使用AspectJ实现AOP有两种方式:

  1. 一种是基于XML的声明式AspectJ
  2. 另外一种是基于注解的声明式AspectJ

1、pom.xml引入依赖

基于XML的声明式AspectJ在使用AspectJ框架之前先要在pom.xml文件中导入spring-aspects的相关依赖

 <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-aspects</artifactId>
         <version>${spring.version}</version>
 </dependency>

切面类接口和实现类同上

2、编写切面

配置文件说明可以查看官网

 package com.woniuxy.ssm.aop;
 
 import org.apache.log4j.Logger;
 import org.aspectj.lang.JoinPoint;
 import org.aspectj.lang.ProceedingJoinPoint;
 import org.aspectj.lang.annotation.After;
 import org.aspectj.lang.annotation.AfterReturning;
 import org.aspectj.lang.annotation.AfterThrowing;
 import org.aspectj.lang.annotation.Before;
 import org.springframework.web.context.request.RequestContextHolder;
 import org.springframework.web.context.request.ServletRequestAttributes;
 
 import java.time.LocalDateTime;
 import java.util.Arrays;
 
 public class LogAspect {
     Logger log = Logger.getLogger(LogAspect.class);
 
     //进入方法时间戳
     private Long startTime;
     //方法结束时间戳(计时)
     private Long endTime;
 
     public LogAspect() {
     }
 
     //前置通知,方法之前执行
     public void doBefore(JoinPoint joinPoint) {
         startTime = System.currentTimeMillis();
         log.info("请求开始时间:" + LocalDateTime.now());
         log.info("请求参数 : " + Arrays.toString(joinPoint.getArgs()));
     }
 
     //最终通知
     public void doAfter(JoinPoint joinPoint) {
         log.info("类:"+joinPoint.getSignature().getDeclaringTypeName());//执行的类名称
         log.info("Logger-->后置通知,方法名:" + joinPoint.getSignature().getName() + ",方法执行完毕");
     }
 
     //后置通知 正常结束时进入此方法
     public void doAfterReturning(Object ret) {
         endTime = System.currentTimeMillis();
         log.info("请求结束时间 : " + LocalDateTime.now());
         log.info("请求耗时 : " + (endTime - startTime));
         // 处理完请求,返回内容
         log.info("请求返回 : " + ret);
     }
 
     //异常通知: 在目标方法非正常结束,发生异常或者抛出异常时执行
     public void doAfterThrowing(Throwable throwable) {
         // 保存异常日志记录
         log.error("发生异常时间 : " + LocalDateTime.now());
         log.error("抛出异常 : " + throwable.getMessage());
     }
 
     //环绕通知,必须有返回值,否则程序无法继续往下执行,返回空指针异常
     public Object doAround(ProceedingJoinPoint jp) throws Throwable {
         log.info("权限管理");
         //执行目标方法proceed
         Object proceed = jp.proceed();
         log.info("日志记录");
         return proceed;
     }
 }

测试过程中可以一个个的测试。

3、配置文件

配置文件说明可以查看官网

 <?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-3.0.xsd
 http://www.springframework.org/schema/aop
 http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
     <bean id="logAspect" class="com.woniuxy.ssm.aop.LogAspect"/>
     <aop:config>
         <aop:aspect  ref="logAspect">
             <!--切入点-->
             <aop:pointcut id="bs"  expression="execution(* com.woniuxy.ssm.service.*.*(..))"/>
             <!--前置通知-->
             <aop:before method="doBefore" pointcut-ref="bs" />
             <!--后置通知,方法正常执行完成会做的事情-->
             <aop:after-returning method="doAfterReturning" pointcut-ref="bs" returning="ret"/>
             <!--最终通知,总会执行-->
             <aop:after method="doAfter" pointcut-ref="bs"/>
             <!--环绕通知,方法执行前后都会执行-->
             <aop:around method="doAround" pointcut-ref="bs"/>
             <!--方法执行过程中发生异常的时候会执行的通知-->
             <aop:after-throwing method="doAfterThrowing" pointcut-ref="bs" throwing="throwable"/>
         </aop:aspect>
     </aop:config>
 </beans>

4、测试类

 public static void main(String[] args) {
     ApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
     SysUserService sysUserService = ac.getBean("sysUserService", SysUserService.class);
        List<SysUser> list = sysUserService.queryAll();
         for (SysUser user:list) {
             System.out.println(user);
         }
     }

四、基于注解的声明式AspectJ

1、AOP常用注解

名称 解释
@Aspect 注解注释的Class被标识为切面类
@Before 前置通知 方法签名有 JoinPoint 参数
@AfterReturning 后置通知,@AfterReturning注解有 returning 属性,可以在切面方法结束后,返回结果。最好定义为Object
@Around 环绕通知,被增强的方法有 ProceedingJoinPoint 参数
@AfterThrowing 异常通知,注解中有 throwing 属性。在目标方法抛出异常后执行。该注解的 throwing 属性用于指定所发生的异常类对象
@After 最终通知,无论目标方法是否抛出异常,该增强均会被执行。个人理解为try catch finally里的finally一样
@Pointcut 定义切入点
@EnableAspectJAutoProxy 启用AspectJ自动代理功能

2、AOP切面编程控制日志

 package com.woniuxy.ssm.aop;
 
 import org.apache.log4j.Logger;
 import org.aspectj.lang.JoinPoint;
 import org.aspectj.lang.ProceedingJoinPoint;
 import org.aspectj.lang.annotation.*;
 import org.springframework.stereotype.Component;
 
 
 import java.time.LocalDateTime;
 import java.util.Arrays;
 @Aspect  // 表明是一个切面类
 @EnableAspectJAutoProxy//启动自动代理注解的支持
 @Component
 public class LogAspect {
     Logger log = Logger.getLogger(LogAspect.class);
 
     //进入方法时间戳
     private Long startTime;
     //方法结束时间戳(计时)
     private Long endTime;
     /**
      * 定义切入点表达式
      * 访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
      * 权限修饰符可以使用默认 第一个*表示返回值类型  ..表示当前包以及其子包下 第二个*表示任意方法 (..)表示任意参数列表
      */
     private final String POINTCUT = "execution(* com.woniuxy.ssm.service.*.*(..))";
 
     public LogAspect() {
     }
     //配置切点的第二种方式
     @Pointcut("execution(* com.woniuxy.sm.service.impl.*.*(..))")
     public void setPointcut(){
 
     }
 
     //前置通知,方法之前执行
     @Before("setPointcut()")
     public void doBefore(JoinPoint joinPoint) {
         startTime = System.currentTimeMillis();
         log.info("请求开始时间:" + LocalDateTime.now());
         log.info("请求参数 : " + Arrays.toString(joinPoint.getArgs()));
     }
 
     //后置通知
     @After(POINTCUT)
     public void doAfter(JoinPoint joinPoint) {
         log.info("类:"+joinPoint.getSignature().getDeclaringTypeName());//执行的类名称
         log.info("Logger-->后置通知,方法名:" + joinPoint.getSignature().getName() + ",方法执行完毕");
     }
 
     //返回通知 正常结束时进入此方法
     @AfterReturning(returning = "ret", pointcut = POINTCUT)
     public void doAfterReturning(Object ret) {
         endTime = System.currentTimeMillis();
         log.info("请求结束时间 : " + LocalDateTime.now());
         log.info("请求耗时 : " + (endTime - startTime));
         // 处理完请求,返回内容
         log.info("请求返回 : " + ret);
     }
 
     //异常通知: 1. 在目标方法非正常结束,发生异常或者抛出异常时执行
     @AfterThrowing(pointcut = POINTCUT, throwing = "throwable")
     public void doAfterThrowing(Throwable throwable) {
         // 保存异常日志记录
         log.error("发生异常时间 : " + LocalDateTime.now());
         log.error("抛出异常 : " + throwable.getMessage());
     }
 
     //环绕通知,必须有返回值,否则程序无法继续往下执行,返回空指针异常
     @Around(value = POINTCUT)
     public Object doAround(ProceedingJoinPoint jp) throws Throwable {
         log.info("权限管理");
         //执行目标方法proceed
         Object proceed = jp.proceed();
         log.info("日志记录");
         return proceed;
     }
 }

3、开启AOP自动代理

方式一:

直接在类上添加注解@EnableAspectJAutoProxy,便可开启自动注解

方式二:

修改spring-config.xml文件,开启自动代理

 <!--开启aop自动代理-->
 <aop:aspectj-autoproxy/>
posted @ 2025-04-21 17:50  icui4cu  阅读(35)  评论(0)    收藏  举报