当前位置:首页 > 技术 > 正文内容

Spring AOP:通过注解与XML实现横切关注点

访客 技术 2026年6月20日 1

面向切面编程(Aspect-Oriented Programming, AOP)是面向对象编程(OOP)的一种补充,它旨在将横切关注点(如日志、事务、安全)与业务逻辑代码解耦。通过AOP,我们可以在不修改原有业务代码的情况下,动态地为这些代码织入额外的功能,从而提高代码的模块化和可维护性。AOP的核心思想是代理模式,它允许我们在目标对象的方法执行前后或异常时插入自定义逻辑。

AOP的核心概念包括:

  • 切面(Aspect):一个模块化的关注点,例如日志记录。在Spring中,切面通常是一个带有@Aspect注解的类。
  • 连接点(Joinpoint):程序执行流中一个明确的点,例如一个方法的调用或异常的抛出。
  • 通知(Advice):在切面中定义的,在特定连接点执行的动作。Spring支持五种类型的通知。
  • 切入点(Pointcut):匹配连接点的断言或谓词。它定义了通知应该在哪些连接点上执行。
  • 目标对象(Target Object):被一个或多个切面所通知的对象。Spring AOP通过代理对象来操作目标对象。
  • 代理(Proxy):AOP框架创建的对象,用于实现切面的功能。Spring AOP默认使用JDK动态代理,如果目标对象没有实现接口,则使用CGLIB代理。

通知类型:

  • 前置通知(Before Advice):在连接点之前执行。
  • 后置通知(After Advice):在连接点退出时执行,无论结果是正常返回还是异常退出。
  • 返回后通知(After Returning Advice):在连接点正常返回后执行。
  • 环绕通知(Around Advice):包围一个连接点,可以在方法调用前后执行自定义逻辑,并决定是否继续执行目标方法。
  • 异常后通知(After Throwing Advice):在方法抛出异常后执行。

Spring AOP可以通过注解或XML配置两种方式实现。下面我们将分别介绍这两种方式。

一、基于注解的实现

使用注解配置AOP,通常需要两个步骤:

  1. 在配置类上启用AOP自动代理功能。
  2. 创建一个切面类,并使用AOP注解定义切点和通知。

首先,我们创建一个Spring配置类:

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

@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
    // 此处可以定义其他Bean
}

然后,我们创建一个切面类LoggingAspect,用于记录服务方法的执行日志:

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class LoggingAspect {

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

    // 定义一个切入点,匹配com.example.service包下所有类的所有方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceMethod() {}

    // 前置通知
    @Before("serviceMethod()")
    public void logBefore(JoinPoint joinPoint) {
        logger.info("开始执行方法: {}", joinPoint.getSignature().getName());
    }

    // 后置通知
    @After("serviceMethod()")
    public void logAfter(JoinPoint joinPoint) {
        logger.info("方法执行结束: {}", joinPoint.getSignature().getName());
    }

    // 返回后通知
    @AfterReturning(pointcut = "serviceMethod()", returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        logger.info("方法 {} 执行成功,返回值: {}", joinPoint.getSignature().getName(), result);
    }

    // 异常后通知
    @AfterThrowing(pointcut = "serviceMethod()", throwing = "ex")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) {
        logger.error("方法 {} 执行异常: {}", joinPoint.getSignature().getName(), ex.getMessage());
    }

    // 环绕通知
    @Around("serviceMethod()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        try {
            Object result = joinPoint.proceed(); // 执行目标方法
            long endTime = System.currentTimeMillis();
            logger.info("方法 {} 执行耗时: {} ms", joinPoint.getSignature().getName(), (endTime - startTime));
            return result;
        } catch (Throwable e) {
            long endTime = System.currentTimeMillis();
            logger.error("方法 {} 执行异常,耗时: {} ms,异常信息: {}", joinPoint.getSignature().getName(), (endTime - startTime), e.getMessage());
            throw e; // 重新抛出异常,确保异常继续传播
        }
    }
}

接下来,我们定义一个简单的服务类OrderService

import org.springframework.stereotype.Service;

@Service
public class OrderService {

    public String createOrder(String orderId) {
        System.out.println("正在创建订单: " + orderId);
        return "订单 " + orderId + " 创建成功";
    }

    public void cancelOrder(String orderId) {
        System.out.println("正在取消订单: " + orderId);
        // 模拟异常
        throw new RuntimeException("订单 " + orderId + " 取消失败");
    }
}

最后,我们编写一个测试类来验证AOP功能:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AopDemo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
        OrderService orderService = context.getBean(OrderService.class);

        System.out.println("--- 测试正常方法 ---");
        String result = orderService.createOrder("ORD-001");
        System.out.println("创建结果: " + result);

        System.out.println("\n--- 测试异常方法 ---");
        try {
            orderService.cancelOrder("ORD-002");
        } catch (Exception e) {
            System.out.println("捕获到异常: " + e.getMessage());
        }

        context.close();
    }
}

二、基于XML的实现

如果偏好使用XML配置,我们可以在Spring的XML配置文件中定义切面、切入点和通知。

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

    <!-- 定义服务Bean -->
    <bean id="orderService" class="com.example.service.OrderService"/>

    <!-- 定义切面Bean -->
    <bean id="loggingAspect" class="com.example.aop.LoggingAspect"/>

    <!-- AOP配置 -->
    <aop:config>
        <!-- 声明一个切面,并引用切面Bean -->
        <aop:aspect id="logging" ref="loggingAspect">
            <!-- 定义切入点 -->
            <aop:pointcut id="serviceMethod" expression="execution(* com.example.service.*.*(..))"/>
            
            <!-- 前置通知 -->
            <aop:before pointcut-ref="serviceMethod" method="logBefore"/>
            
            <!-- 后置通知 -->
            <aop:after pointcut-ref="serviceMethod" method="logAfter"/>
            
            <!-- 返回后通知 -->
            <aop:after-returning pointcut-ref="serviceMethod" returning="result" method="logAfterReturning"/>
            
            <!-- 异常后通知 -->
            <aop:after-throwing pointcut-ref="serviceMethod" throwing="ex" method="logAfterThrowing"/>
            
            <!-- 环绕通知 -->
            <aop:around pointcut-ref="serviceMethod" method="logAround"/>
        </aop:aspect>
    </aop:config>

</beans>

切入点表达式

切入点表达式用于精确定位连接点。最常用的表达式是execution,其语法如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

  • modifiers-pattern?:方法修饰符(可选)。
  • ret-type-pattern:返回值类型。
  • declaring-type-pattern?:方法所在的类或接口(可选)。
  • name-pattern:方法名。
  • param-pattern:方法参数。
  • throws-pattern?:方法抛出的异常(可选)。

一些常见的execution表达式示例:

  • execution(* com.example.service.*.*(..)):匹配com.example.service包及其子包下所有类的所有方法。
  • execution(public * com.example.service.OrderService.create*(..)):匹配OrderService类中以create开头且为public的所有方法。
  • execution(* com.example.service.OrderService.createOrder(String)):匹配OrderService类中名为createOrder且参数为String类型的方法。

通过合理使用AOP,我们可以有效地将横切关注点与核心业务逻辑分离,使代码更加清晰和易于维护。

标签: Spring AOP

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。