首页 > 编程语言 >深入浅出Spring AOP:面向切面编程的实战与解析

深入浅出Spring AOP:面向切面编程的实战与解析

时间:2024-03-31 13:30:28浏览次数:28  
标签:Spring 深入浅出 代理 切面 AOP 方法 public

导语

Spring AOP(面向切面编程)作为Spring框架的核心特性之一,提供了强大的横切关注点处理能力,使得开发者能够更好地解耦系统架构,将非功能性需求(如日志记录、事务管理、权限控制等)从主业务逻辑中抽离出来,实现模块化的交叉关注点处理。本文将带你逐步探索Spring AOP的关键技术要点及其实战应用。

 

一、AOP基础概念

在Spring AOP中,有几个基础概念对于理解和使用AOP至关重要。以下是对这些概念的详细解释,并配合Java示例代码说明:


1.切面(Aspect)

  • 定义:切面是关注点的模块化,包含了通知(Advice)和切入点(Pointcut)的定义。它是AOP的核心部分,代表了应用中的某个特定关注点,比如事务管理、日志记录等。
   @Aspect
   public class LoggingAspect {

       // 这个类就是一个切面
   }
   

2.连接点(Join Point)

  • 定义:连接点是在程序执行过程中明确的一个点,例如一个方法调用、字段访问等。在Spring AOP中,仅支持方法级别的连接点。
   // 比如,在整个应用中有成千上万个方法调用,每一个都是潜在的连接点
   public class UserService {
       public void addUser(User user) {...}
   }
   

3.切入点(Pointcut)

  • 定义:切入点是一个或多个连接点的集合,定义了哪些连接点将被执行增强。在Spring AOP中,使用 AspectJ 表达式来指定切入点。
   @Pointcut("execution(* com.example.service.*.*(..))")
   public void anyServiceMethod() {}

   // 上述表达式表示所有位于com.example.service包及其子包下的任何公共方法
   

4.通知(Advice)

  • 定义:通知是在特定连接点上执行的操作,它可以是方法级别的拦截器,根据不同时机有不同的类型:
    • 前置通知(Before Advice):在方法执行前执行。
     @Before("anyServiceMethod()")
     public void logBefore(JoinPoint joinPoint) {
         System.out.println("Before executing: " + joinPoint.getSignature().getName());
     }
     
  • 后置通知(After Advice):无论方法是否抛出异常都会执行,但在方法返回结果之后。
    • 返回通知(AfterReturning Advice):在方法成功执行并返回结果后执行。
    • 异常通知(AfterThrowing Advice):在方法抛出异常后执行。
    • 环绕通知(Around Advice):最强大的一种通知,可以控制方法的执行流程,决定方法是否执行,何时执行以及如何执行。
     @Around("anyServiceMethod()")
     public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
         long start = System.currentTimeMillis();
         try {
             Object result = pjp.proceed(); // 执行目标方法
             System.out.println("Method executed in: " + (System.currentTimeMillis() - start) + "ms");
             return result;
         } catch (Throwable ex) {
             System.err.println("Exception caught: " + ex.getMessage());
             throw ex;
         }
     }
     

二、Spring AOP实现机制

Spring AOP 实现机制主要是基于代理模式来实现的。它有两种主要的代理实现方式:JDK动态代理和CGLIB代理。


JDK动态代理
JDK动态代理通过实现java.lang.reflect.Proxy接口来创建代理对象。当目标类实现了至少一个接口时,Spring AOP会优先使用JDK动态代理。


示例代码:

public interface MyService {
    void doSomething();
}

@Service
public class MyServiceImpl implements MyService {
    @Override
    public void doSomething() {
        // 主业务逻辑
    }
}

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.MyService.doSomething(..))")
    public void logBefore() {
        System.out.println("Before method execution");
    }
}

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = false) // 默认情况下使用JDK代理
public class AppConfig {
    // ...
}

在这个例子中,MyServiceImpl类实现了MyService接口,Spring AOP通过JDK动态代理为MyService接口创建一个代理对象。当调用doSomething方法时,代理对象会在调用真实方法之前执行LoggingAspect切面中的logBefore方法。

CGLIB代理
当目标类没有实现任何接口时,Spring AOP会选择CGLIB库来生成一个代理子类,扩展自目标类并在其中插入横切逻辑。


示例代码(CGLIB代理需要显式指定):

@Service
public class NonInterfaceService {
    public void doSomething() {
        // 主业务逻辑
    }
}

// 由于NonInterfaceService没有实现接口,Spring AOP将使用CGLIB代理
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {
    // ...
}

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.NonInterfaceService.doSomething(..))")
    public void logBefore() {
        System.out.println("Before method execution");
    }
}

在CGLIB代理的例子中,尽管NonInterfaceService类没有实现任何接口,Spring AOP依然可以通过CGLIB生成它的子类代理,在调用doSomething方法时插入切面逻辑。


工作原理
无论是JDK动态代理还是CGLIB代理,Spring AOP都是通过在代理对象的方法调用时,插入切面逻辑来实现横切关注点的处理。代理对象在运行时“拦截”方法调用,执行对应的切面通知,然后再调用实际的目标方法。这样就达到了在不修改原有业务逻辑代码的情况下,添加通用处理逻辑的目的。

三、Spring AOP API

Spring AOP API主要包括一系列注解和接口,用于定义切面、切入点、通知等。以下是关键API元素的示例代码和详细讲解:


1. 定义切面(Aspect) - 使用@Aspect注解

import org.aspectj.lang.annotation.Aspect;

@Aspect
public class LoggingAspect {

    // 切面内部包含各种通知方法
}

@Aspect注解标记了一个Java类作为切面,表示这个类中包含了一系列与横切关注点相关的通知。


2. 定义切入点(Pointcut) - 使用@Pointcut注解

@Pointcut("execution(* com.example.service.*.*(..))")
public void businessServiceOperation() {}

@Pointcut注解定义了一个切入点表达式,该表达式标识了那些方法调用会被切面影响。上面的表达式匹配了com.example.service包及其子包下所有类的所有方法。


3. 定义通知(Advice) - 使用不同类型的注解

  • 前置通知(Before Advice):在目标方法执行前调用。
@Before("businessServiceOperation()")
public void beforeAdvice(JoinPoint joinPoint) {
    System.out.println("Executing Before advice on method: " + joinPoint.getSignature().getName());
}
  • 后置通知(After Advice):在目标方法执行完后(无论是否有异常)调用,无法访问到方法的返回值。
@After("businessServiceOperation()")
public void afterAdvice(JoinPoint joinPoint) {
    System.out.println("Executing After advice on method: " + joinPoint.getSignature().getName());
}
  • 返回后通知(AfterReturning Advice):在目标方法成功执行并返回后调用,可以访问到方法的返回值。
@AfterReturning(pointcut = "businessServiceOperation()", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
    System.out.println("Executing AfterReturning advice on method: " + joinPoint.getSignature().getName() + ", Result: " + result);
}
  • 异常抛出通知(AfterThrowing Advice):在目标方法抛出异常后调用,可以访问到抛出的异常。
@AfterThrowing(pointcut = "businessServiceOperation()", throwing = "exception")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception exception) {
    System.out.println("Executing AfterThrowing advice on method: " + joinPoint.getSignature().getName() + ", Exception: " + exception.getMessage());
}
  • 环绕通知(Around Advice):最强大的通知类型,可以完全控制目标方法的执行,可以选择是否执行目标方法,也可以修改方法的返回值。
@Around("businessServiceOperation()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("Starting Around advice on method: " + joinPoint.getSignature().getName());

    try {
        // 前置逻辑
        Object result = joinPoint.proceed(); // 调用目标方法
        
        // 后置逻辑
        System.out.println("Completed Around advice on method, Result: " + result);

        return result;
    } catch (Throwable throwable) {
        // 处理异常逻辑
        System.out.println("Exception thrown from method: " + joinPoint.getSignature().getName());
        throw throwable;
    }
}

4. Spring AOP自动代理
为了使以上切面生效,你需要将此切面类加入Spring容器,并启用AOP代理。在Spring Boot中,通常通过@EnableAspectJAutoProxy注解开启自动代理:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
    // ...
}

这样,Spring容器在创建bean时,会为符合条件的bean生成代理对象,当调用代理对象的方法时,就会触发相应的切面通知。

四、配置AOP

Spring AOP可以通过Java注解和XML配置两种方式进行配置。这里我们分别介绍这两种配置方式的示例代码和详细讲解。


1. Java注解方式配置AOP


a. 创建切面类

@Aspect
@Component
public class LoggingAspect {

    @Pointcut("execution(* com.example.service.*.*(..))")
    public void businessServiceMethods() {}

    @Before("businessServiceMethods()")
    public void logBefore(JoinPoint joinPoint) {
        // 前置通知逻辑
    }

    @AfterReturning(pointcut = "businessServiceMethods()", returning = "result")
    public void logAfterReturning(Object result) {
        // 后置通知逻辑
    }

    // 其他通知类型的实现...
}
  • @Aspect 注解表示这是一个切面类。
  • @Component 注解使切面类成为Spring容器中的一个bean。
  • @Pointcut 定义了一个切入点表达式,标识了所有在com.example.service包下定义的方法。
  • @Before 和 @AfterReturning 分别定义了在方法执行前和执行后返回后的通知方法。

 

b. 配置Spring扫描并启用AOP
在Spring Boot应用中,通常只需要添加@EnableAspectJAutoProxy注解在启动类上即可自动扫描带有@Aspect注解的类并启用AOP。

@SpringBootApplication
@EnableAspectJAutoProxy
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

2. XML配置方式配置AOP


a. 配置切面类

<aop:config>
    <aop:aspect id="loggingAspect" ref="loggingAspectBean">

        <!-- 定义切入点 -->
        <aop:pointcut id="businessServiceMethods"
                      expression="execution(* com.example.service.*.*(..))"/>

        <!-- 前置通知 -->
        <aop:before method="logBefore" pointcut-ref="businessServiceMethods"/>

        <!-- 后置通知 -->
        <aop:after-returning method="logAfterReturning"
                             pointcut-ref="businessServiceMethods"
                             returning="result"/>

        <!-- 其他通知类型的配置... -->
    </aop:aspect>
</aop:config>

<!-- 配置切面类 bean -->
<bean id="loggingAspectBean" class="com.example.aspect.LoggingAspect"/>
  • <aop:config> 标签开始定义AOP配置区域。
  • <aop:aspect> 标签定义一个切面,id属性为其命名,ref属性引用切面类的bean。
  • <aop:pointcut> 定义一个切入点,expression属性内填写切入点表达式。
  • <aop:before> 和 <aop:after-returning> 分别定义前置通知和后置通知,method属性指向切面类中的通知方法,pointcut-ref属性引用前面定义的切入点。

 

b. 配置Spring扫描并启用AOP
在Spring的XML配置文件中,你需要启用AOP名称空间,并确保Spring能够扫描到包含切面类的包。

<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 http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 开启AOP自动代理 -->
    <aop:aspectj-autoproxy/>

    <!-- 包扫描配置 -->
    <context:component-scan base-package="com.example"/>

    <!-- 其他配置... -->
</beans>

在这里,<aop:aspectj-autoproxy/> 开启了AOP自动代理功能,而 <context:component-scan> 标签用于指定Spring应该扫描哪些包以查找标注了@Component、@Service等注解的bean,这其中包括我们的切面类。

五、实战应用

Spring AOP 在实战中主要用于处理横切关注点,比如日志记录、事务管理、权限验证、性能统计等。下面给出一个使用Spring AOP进行日志记录的实战应用示例,并详细讲解。
假设有一个简单的服务接口UserService,以及其实现类UserServiceImpl,现在希望在调用服务方法时自动记录日志。


定义服务接口和服务实现

package com.example.service;

public interface UserService {
    void addUser(User user);
    void updateUser(User user);
    void deleteUser(Long id);
}

@Service
public class UserServiceImpl implements UserService {
    // 实现具体的业务逻辑
    @Override
    public void addUser(User user) {
        // 添加用户的逻辑...
    }

    @Override
    public void updateUser(User user) {
        // 更新用户的逻辑...
    }

    @Override
    public void deleteUser(Long id) {
        // 删除用户的逻辑...
    }
}

 

创建日志切面

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    private final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

    // 定义切入点,这里是所有`UserService`接口方法的执行
    @Pointcut("execution(* com.example.service.UserService.*(..))")
    public void userServiceMethods() {
    }

    // 定义前置通知,在执行方法前记录日志
    @Before("userServiceMethods()")
    public void logBefore(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        logger.info("方法名:{},准备执行...", signature.getName());
        // 可以进一步获取方法参数等信息并加入日志
    }
}

在上面的代码中:

  • @Aspect 表明LoggingAspect是一个切面类。
  • @Component 将这个切面类注册为Spring Bean,以便Spring AOP能自动发现和管理。
  • @Pointcut 定义了一个切入点表达式,匹配UserService接口的所有方法。
  • @Before 注解的方法会在符合userServiceMethods切入点条件的方法执行前调用,负责记录日志。

 

通过这种方式,每当调用UserService接口中的任何一个方法时,都会先执行logBefore方法记录日志,然后才执行实际的服务方法。这种做法极大地提高了代码复用性和可维护性,同时降低了侵入性,让业务代码更加清晰简洁。

六、Spring Boot整合AOP

在Spring Boot项目中整合Spring AOP非常简单,因为Spring Boot已经内置了对AOP的支持。下面是一个Spring Boot整合Spring AOP的完整示例,包括创建切面类、定义切入点和通知,以及启动Spring Boot项目时自动启用AOP代理。


Step 1: 添加依赖
在pom.xml文件中,如果使用Maven构建项目,确保已经引入了Spring AOP相关的起步依赖:

<dependencies>
    <!-- Spring Boot Starter Web 或其他组件可能已经包含了此依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
</dependencies>

 

Step 2: 创建切面类

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @Around("execution(* com.example.service..*.*(..))")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        
        // 方法执行前的逻辑
        System.out.println("方法 " + joinPoint.getSignature().getName() + " 开始执行,时间:" + start);
        
        try {
            // 调用方法并获取返回值
            Object result = joinPoint.proceed();

            // 方法执行后的逻辑
            long end = System.currentTimeMillis();
            System.out.println("方法 " + joinPoint.getSignature().getName() + " 执行结束,耗时:" + (end - start) + " ms");

            return result;
        } catch (IllegalArgumentException e) {
            // 处理自定义异常
            System.err.println("非法参数异常:" + e.getMessage());
            throw e;
        }
    }
}

在上述代码中:

  • 使用@Aspect注解声明LoggingAspect是一个切面类。
  • 使用@Component注解将其注入Spring容器,以便Spring管理其生命周期。
  • @Around注解的方法是一个环绕通知,其表达式execution(* com.example.service..*.*(..))表示切入点,即在com.example.service及其子包下所有类的所有方法执行时触发此通知。


Step 3: 启用AOP代理
在Spring Boot项目中,默认已启用AOP代理,所以通常不需要额外的配置。但如果你在某些特殊情况下需要禁用或定制AOP配置,可以在主配置类或者配置文件中进行调整。
例如,在application.properties中,你可以设置:

spring.aop.auto=true # 默认为true,表示启用AOP代理

或者在主配置类上使用@EnableAspectJAutoProxy注解:

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy // 启用AOP代理
public class AppConfig {
    // ...
}

通过以上步骤,当你运行Spring Boot应用并调用com.example.service包下某个服务类的方法时,将会触发LoggingAspect切面类中的logAround方法,在方法执行前后打印日志。这就是Spring Boot整合Spring AOP的基本过程。

七、Spring AOP限制

Spring AOP存在一些内在的技术限制,了解这些限制有助于我们在实际开发中合理地设计和使用AOP。以下是Spring AOP的一些主要限制及相应示例说明:


1.不能拦截final方法:

Spring AOP基于代理机制实现,对于final方法,由于Java语言特性,子类不能覆盖final方法,因此Spring AOP也无法通过代理的方式在其前后增加额外的行为。
示例:

   public final class FinalClass {
       public final void finalMethod() {
           // 主要业务逻辑
       }
   }
   

上述FinalClass中的finalMethod方法无法被Spring AOP所拦截。


2.不能直接代理静态方法:

Spring AOP是基于代理(JDK动态代理或CGLIB)的方式来实现代理功能的,静态方法属于类级别的方法,而非对象实例方法,因此不能通过代理的方式对其进行增强。
示例:

   public class StaticService {
       public static void staticMethod() {
           // 主要业务逻辑
       }
   }
   

上述StaticService中的staticMethod方法无法被Spring AOP所拦截。


3.代理对象内部方法调用的问题:

如果在一个类的内部方法中调用了同一个类的另一个方法,而不是通过代理对象去调用,那么AOP将不会生效,因为此时调用的是实际对象而非代理对象的方法。
示例:

   @Service
   public class SelfCallService {
       
       public void publicMethod() {
           internalMethod(); // 直接内部调用,AOP将不会对此内部调用进行拦截
       }
       
       private void internalMethod() {
           // 主要业务逻辑
       }
   }
   

在此示例中,如果只对publicMethod进行了AOP增强,那么internalMethod的调用将不会受到AOP通知的影响。


4.只能代理Spring管理的bean:

Spring AOP仅能增强那些由Spring IoC容器管理的对象。这意味着非Spring管理的实例,或者通过new关键字直接创建的对象,其方法不会被AOP拦截器处理。


5.CGLIB代理与final类和方法:

虽然CGLIB代理可以代理没有实现接口的类,但它仍然不能代理final类和final方法。


6.构造器注入问题:

由于AOP代理是动态生成的,所以在构造器注入时,注入的将是原始类型而非代理类型。为避免这个问题,推荐使用setter或field注入。


总的来说,Spring AOP在大多数常规应用场景下是非常有效的,但在遇到上述限制时,可能需要考虑更强大的AOP框架如AspectJ,或者重新审视设计,确保业务逻辑适合使用AOP的代理模式。

标签:Spring,深入浅出,代理,切面,AOP,方法,public
From: https://blog.csdn.net/2301_78813969/article/details/137111174

相关文章

  • 深入浅出的揭秘游标尺模式与迭代器模式的神秘面纱 ✨
    ​......
  • SMU 2024 spring 天梯赛3
    SMU2024spring天梯赛37-1重要的话说三遍-SMU2024spring天梯赛3(pintia.cn)I'mgonnaWIN!I'mgonnaWIN!I'mgonnaWIN!7-2两小时学完C语言-SMU2024spring天梯赛3(pintia.cn)#include<bits/stdc++.h>usingnamespacestd;usingi64=longlong;......
  • @Transactional详解(作用、失效场景与解决方法)| 事务注解实际原理(AOP)解析
    开发中代码实现事务的方式,理论上说有两种:编程式事务、注解式事务。但是实际上使用最多的还是注解实现的事务控制; 1、编程式事务(开发用的很少了)基于底层的API,如PlatformTransactionManager、TransactionDefinition 和 TransactionTemplate 等核心接口,开发者完全可以通过编......
  • 基于java+springboot+vue实现的房屋租赁系统(文末源码+Lw+ppt)23-397
    摘要随着社会的不断进步与发展,人们经济水平也不断的提高,于是对各行各业需求也越来越高。特别是从2019年新型冠状病毒爆发以来,利用计算机网络来处理各行业事务这一概念更深入人心,由于工作繁忙以及疫情的原因,用户到房源公司进行房屋求租也是比较难实施的。如果开发一款房屋租赁......
  • 基于java+springboot+vue实现的付费自习室管理系统(文末源码+Lw+ppt)23-400
    摘 要付费自习室管理系统采用B/S架构,数据库是MySQL。网站的搭建与开发采用了先进的java进行编写,使用了springboot框架。该系统从两个对象:由管理员和用户来对系统进行设计构建。主要功能包括:个人信息修改,对用户信息、自习室准则、自习室、自习计划、留言反馈、订单等功能进行......
  • 基于java+springboot+vue实现的房屋租赁系统(文末源码+Lw+ppt)23-397
    摘要随着社会的不断进步与发展,人们经济水平也不断的提高,于是对各行各业需求也越来越高。特别是从2019年新型冠状病毒爆发以来,利用计算机网络来处理各行业事务这一概念更深入人心,由于工作繁忙以及疫情的原因,用户到房源公司进行房屋求租也是比较难实施的。如果开发一款房屋租赁......
  • 基于java+springboot+vue实现的付费自习室管理系统(文末源码+Lw+ppt)23-400
     摘 要付费自习室管理系统采用B/S架构,数据库是MySQL。网站的搭建与开发采用了先进的java进行编写,使用了springboot框架。该系统从两个对象:由管理员和用户来对系统进行设计构建。主要功能包括:个人信息修改,对用户信息、自习室准则、自习室、自习计划、留言反馈、订单等功能进......
  • 基于java+springboot+vue实现的电商个性化推荐系统(文末源码+Lw+ppt)23-389
    摘 要伴随着我国社会的发展,人民生活质量日益提高。于是对电商个性化推荐进行规范而严格是十分有必要的,所以许许多多的信息管理系统应运而生。此时单靠人力应对这些事务就显得有些力不从心了。所以本论文将设计一套电商个性化推荐系统,帮助商家进行商品信息、在线沟通等繁琐又......
  • 基于java+springboot+vue实现的房屋租赁系统(文末源码+Lw+ppt)23-397
    摘要随着社会的不断进步与发展,人们经济水平也不断的提高,于是对各行各业需求也越来越高。特别是从2019年新型冠状病毒爆发以来,利用计算机网络来处理各行业事务这一概念更深入人心,由于工作繁忙以及疫情的原因,用户到房源公司进行房屋求租也是比较难实施的。如果开发一款房屋租赁......
  • 基于java+springboot+vue实现的付费自习室管理系统(文末源码+Lw+ppt)23-400
     摘 要付费自习室管理系统采用B/S架构,数据库是MySQL。网站的搭建与开发采用了先进的java进行编写,使用了springboot框架。该系统从两个对象:由管理员和用户来对系统进行设计构建。主要功能包括:个人信息修改,对用户信息、自习室准则、自习室、自习计划、留言反馈、订单等功能进......