[Spring实战系列](17)编写切点与声明切面

简介: 版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/SunnyYoona/article/details/50655462 切点用于准确定位应该在什么地方应用切面的通知。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/SunnyYoona/article/details/50655462


切点用于准确定位应该在什么地方应用切面的通知。切点和通知是切面的最基本元素。

在Spring AOP中,需要使用AspectJ的切点表达式语言来定义切点。关于Spring AOP的AspectJ切点,最重要的一点是Spring仅支持AspectJ切点指示器的一个子集。
类型 说明
arg() 限制连接点匹配参数为指定类型的执行方法。
@args() 限制连接点匹配参数由指定注解标注的执行方法。
execution() 用于匹配的是连接点的执行方法。
this() 限制连接点匹配AOP代理的Bean引用为指定类型的类。
target() 限制连接点匹配目标对象为指定类型的类。
@target() 限制连接点匹配特定的执行对象,这些对象对应的类具备指定类型的注解。
within() 限制连接点匹配指定的类型。
@within() 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义再由指定的注解所标注的类里)。
@annotation 限制匹配带有指定注解连接点。

在Spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgumentException异常。

注意:

只有execution指示器是唯一的执行匹配,而其他的指示器都是用于限制匹配的。这说明execution指示器是我们在编写切点定义时最主要的指示器。在此基础上,我们使用其他指示器来限制所匹配的切点。

1. 编写切点

如下图所示的切点表达式表示当Singer的perform()方法执行时会触发通知。我们使用execution()指示器选择Singer的perform()方法。 方法表达式以*号开始,标示了我们不关心返回值的类型。然后,我们 指定了全限定类名和方法名。对于 方法参数列表,我们使用(..)标示切点选择任意的perform()方法,无论该方法的参数是什么。


除此之外,我们还可以对上面的匹配进行限制,可以使用 within()指示器来限制匹配。


我们使用&&操作符把execution()和within()指示器连接在一起形成and关系(切点必须匹配所有的指示器)。

2. 在XML中声明切面

Spring的AOP配置元素简化了基于POJO切面的声明:
类型 说明
<aop:advisor> 定义AOP通知器。
<aop:after> 定义AOP后置通知(不管被通知的方法是否执行成功)
<aop:after-returning> 定义AOP after-returning通知。
<aop:after-throwing> 定义 AOP after-throwing 通知。
<aop:around> 定义 AOP 环绕通知。
<aop:aspect> 定义切面。
<aop:aspectj-autoproxy> 启用@AspectJ注解驱动的切面。
<aop:before> 定义 AOP前置通知。
<aop:config> 顶层的AOP配置元素。大多数的<aop:*>元素必须包含在<aop:config>元素内。
<aop:declare-parents> 为被通知的对象引入额外的接口,并透明的实现。
<aop:pointcut> 定义切点。

为了阐述Spring AOP,我们创建一个观众类(Audience)类:
 
   
package com.sjf.bean;
/**
* 观众类
* @author sjf0115
*
*/
public class Audience {
public void takeSeats(){
System.out.println("the audience is taking their seats...");
}
public void applaud(){
System.out.println("very good, clap clap clap...");
}
public void demandRefund(){
System.out.println("very bad, We want our money back...");
}
}

Audience类并没有任何特别之处,她就是一个有几个方法的简单Java类。我们可以像其他类一样,利用XML把它注册为Spring应用上下文中的一个Bean:
 
   
<bean id = "audience" class = "com.sjf.bean.Audience">
</bean>
我们需要Spring AOP就能把它成为一个切面。
2.1 前置声明和后置声明

 
   
<?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/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<bean id = "singer" class = "com.sjf.bean.Singer">
</bean>
<bean id = "audience" class = "com.sjf.bean.Audience">
</bean>
<aop:config proxy-target-class="true">
<!-- 声明定义一个切面 -->
<aop:aspect ref = "audience">
<!-- 表演之前 -->
<aop:before method="takeSeats" pointcut="execution(* com.sjf.bean.Singer.perform(..))"/>
<!-- 表演之后 -->
<aop:after-returning method="applaud" pointcut="execution(* com.sjf.bean.Singer.perform(..))"/>
<!-- 表演失败之后 -->
<aop:after-throwing method="demandRefund" pointcut="execution(* com.sjf.bean.Singer.perform(..))"/>
</aop:aspect>
</aop:config>
</beans>
大多数的AOP配置元素必须在<aop:config>元素的上下文内使用。这条规则有几种例外场景,但是把Bean声明为一个切面时,我们总是从<aop:config>元素开始配置。

在<aop:config>元素内,我们可以声明一个或者多个通知器,切面或者切点。上述例子中,我们使用 <aop:aspect>元素声明了一个简单的切面。ref元素引用了一个Bean(Audience),该Bean实现了切面的功能。 ref元素应用的Bean提供了在切面上通知所调用的方法。

该切面应用了3个不同的通知。 <aop:before>元素定义了匹配切点的方法执行之前调用前置通知方法,audience Bean 的takeSeats()方法。 <aop:after-returning>元素定义了一个返回后(after-returning)通知,在切点所匹配的方法调用之后在执行applaud()方法。 <aop:after-throwing>元素定义了抛出异常后通知,如果所有匹配的方法执行时抛出任何异常,都将调用demandRefund()方法。

下面展示了通知逻辑如何嵌入到业务逻辑中:


在所有的通知元素中, pointcut属性定义了通知所应用的切点。pointcut属性的值是使用AspectJ切点表达式语法所定义的切点。

你或许注意到所有通知元素中的pointcut属性的值都是一样的,这是因为所有的通知都是应用到相同的切点上。这似乎违反了DRY(不要重复你自己)原则。我们做一下修改,可以 使用<aop:pointcut>元素定义一个命名切点。
 
   
<aop:config proxy-target-class="true">
<!-- 声明定义一个切面 -->
<aop:aspect ref = "audience">
<aop:pointcut id="singerPerfom" expression="execution(* com.sjf.bean.Singer.perform(..))" />
<!-- 表演之前 -->
<aop:before method="takeSeats" pointcut-ref="singerPerfom"/>
<!-- 表演之后 -->
<aop:after-returning method="applaud" pointcut-ref="singerPerfom"/>
<!-- 表演失败之后 -->
<aop:after-throwing method="demandRefund" pointcut-ref="singerPerfom"/>
</aop:aspect>
</aop:config>
<aop:pointcut>元素定义了一个id为singerPerfom的切点,同时修改所有的通知元素, 用pointcut-ref属性来引用这个命名切点。
2.2 声明环绕通知
如果不适用成员变量存储信息,那么在前置通知和后置通知之间共享信息是非常麻烦的。我们希望实现这样一个功能:希望观众一直关注演出,并报告演出的演出时长。使用前置通知和后置通知实现该功能的唯一方式是:在前置通知中记录开始时间,并在某个后置通知中报告演出的时长。但这样的话,我们必须在一个成员变量中保存开始时间。因此我们可以使用环绕通知,只需在一个方法中实现即可。
 
   
public void PerformTime(ProceedingJoinPoint joinPoint){
// 演出之前
System.out.println("the audience is taking their seats...");
try {
long start = System.currentTimeMillis();
// 执行演出操作
joinPoint.proceed();
long end = System.currentTimeMillis();
// 演出成功
System.out.println("very good, clap clap clap...");
System.out.println("该演出共需要 "+(end - start) + " milliseconds");
} catch (Throwable e) {
// 演出失败
System.out.println("very bad, We want our money back...");
e.printStackTrace();
}
}

对于这个新的通知方法,我们会注意到它使用了ProceedingJoinPoint作为方法的入参。这个对象非常重要,因为它能 让我们在通知里调用被通知 的方法。如果希望把控制转给被通知的方法时,我们可以调用 ProceedingJoinPoint的proceed()方法。如果忘记调用proceed()方法,通知将会阻止被通知方法的调用。我们还可以在通知里多次调用被通知方法,这样做的一个目的是实现重试逻辑,在被通知的方法执行失败时反复重试。

PerformTime()方法包含了之前3个通知方法的所有逻辑,并且该方法还会负责自身的异常处理。声明环绕通知与声明其他类型的通知并没有太大的区别,只需要<aop:around>元素。
 
   
<aop:config proxy-target-class="true">
<!-- 声明定义一个切面 -->
<aop:aspect ref = "audience">
<aop:pointcut id="singerPerfom" expression="execution(* com.sjf.bean.Singer.perform(..))" />
<!-- 声明环绕通知 -->
<aop:around method="performTime" pointcut-ref="singerPerfom"/>
</aop:aspect>
</aop:config>

运行结果:

the audience is taking their seats...
正在上演个人演唱会...
very good, clap clap clap...
该演出共需要 37 milliseconds  

像其他通知的XML一样,<aop:around>指定了一个切点和一个通知方法的名字。
2.3 为通知传递参数
到目前为止,我们的切面很简单,没有任何的参数。但是有时候通知并不是仅仅是对方法进行简单包装,还需要校验传递给方法的参数值,这时候为通知传递参数就非常有用了。

下面是歌手的实体类:
 
   
package com.sjf.bean;
/**
* 歌手实体类
* @author sjf0115
*
*/
public class Singer {
 
public void perform(String songName) {
System.out.println("正在上演个人演唱会,演唱曲目为 " + songName);
}
}
在这我们提供了一个Organizers(主办方)实体类,在歌手演唱之前截获歌手演唱的曲目,然后通知给大家:
 
   
package com.sjf.bean;
/**
* 主办方实体类
* @author sjf0115
*
*/
public class Organizers {
public void BeforeSong(String songName){
System.out.println("演唱会马上就开始了,演唱歌曲为 " + songName);
}
public void AfterSong(){
System.out.println("演唱曲目结束,谢谢大家...");
}
}

我们像以前一样使用<aop:aspect>,<aop:before>和<aop:after>元素。但是这次我们 通过配置实现将被通知方法的参数传递给通知。
 
   
<?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/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<bean id = "singer" class = "com.sjf.bean.Singer">
</bean>
<bean id = "Organizers" class = "com.sjf.bean.Organizers">
</bean>
<aop:config>
<!-- 声明定义一个切面 -->
<aop:aspect ref = "Organizers">
<aop:pointcut id="singerPerform" expression="execution(* com.sjf.bean.Singer.perform(String)) and args(song)" />
<aop:pointcut id="singerPerform2" expression="execution(* com.sjf.bean.Singer.perform(..))" />
<aop:before method="BeforeSong" pointcut-ref="singerPerform" arg-names="song"/>
<aop:after-returning method="AfterSong" pointcut-ref="singerPerform2"/>
</aop:aspect>
</aop:config>
</beans>

关键之处在于切点定义和<aop:before>的arg-names属性。切点标示了Singer的perform()方法,指定了String参数。然后在args参数中标示了song作为参数。同样, <aop:before>元素引用了切点中 song参数,标示该参数必须传递给Organizers的BeforeSong()方法。



运行结果:

演唱会马上就开始了,演唱歌曲为 你是我的眼泪
正在上演个人演唱会,演唱曲目为 你是我的眼泪
演唱曲目结束,谢谢大家...  


来源于:《Spring实战》


目录
相关文章
|
负载均衡 监控 Java
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
本文详细介绍了 Spring Cloud Gateway 的核心功能与实践配置。首先讲解了网关模块的创建流程,包括依赖引入(gateway、nacos 服务发现、负载均衡)、端口与服务发现配置,以及路由规则的设置(需注意路径前缀重复与优先级 order)。接着深入解析路由断言,涵盖 After、Before、Path 等 12 种内置断言的参数、作用及配置示例,并说明了自定义断言的实现方法。随后重点阐述过滤器机制,区分路由过滤器(如 AddRequestHeader、RewritePath、RequestRateLimiter 等)与全局过滤器的作用范围与配置方式,提
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
|
监控 Java API
Spring Boot 3.2 结合 Spring Cloud 微服务架构实操指南 现代分布式应用系统构建实战教程
Spring Boot 3.2 + Spring Cloud 2023.0 微服务架构实践摘要 本文基于Spring Boot 3.2.5和Spring Cloud 2023.0.1最新稳定版本,演示现代微服务架构的构建过程。主要内容包括: 技术栈选择:采用Spring Cloud Netflix Eureka 4.1.0作为服务注册中心,Resilience4j 2.1.0替代Hystrix实现熔断机制,配合OpenFeign和Gateway等组件。 核心实操步骤: 搭建Eureka注册中心服务 构建商品
1626 3
|
人工智能 Java API
Spring AI 实战|Spring AI入门之DeepSeek调用
本文介绍了Spring AI框架如何帮助Java开发者轻松集成和使用大模型API。文章从Spring AI的初探开始,探讨了其核心能力及应用场景,包括手动与自动发起请求、流式响应实现打字机效果,以及兼容不同AI服务(如DeepSeek、通义千问)的方法。同时,还详细讲解了如何在生产环境中添加监控以优化性能和成本管理。通过Spring AI,开发者可以简化大模型调用流程,降低复杂度,为企业智能应用开发提供强大支持。最后,文章展望了Spring AI在未来AI时代的重要作用,鼓励开发者积极拥抱这一技术变革。
4696 71
Spring AI 实战|Spring AI入门之DeepSeek调用
|
监控 Cloud Native Java
Spring Boot 3.x 微服务架构实战指南
🌟蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕Spring Boot 3.x与微服务架构,探索云原生、性能优化与高可用系统设计。以代码为笔,在二进制星河中谱写极客诗篇。关注我,共赴技术星辰大海!(238字)
1565 2
Spring Boot 3.x 微服务架构实战指南
|
12月前
|
XML Java 测试技术
《深入理解Spring》:IoC容器核心原理与实战
Spring IoC通过控制反转与依赖注入实现对象间的解耦,由容器统一管理Bean的生命周期与依赖关系。支持XML、注解和Java配置三种方式,结合作用域、条件化配置与循环依赖处理等机制,提升应用的可维护性与可测试性,是现代Java开发的核心基石。
|
人工智能 监控 安全
如何快速上手【Spring AOP】?核心应用实战(上篇)
哈喽大家好吖~欢迎来到Spring AOP系列教程的上篇 - 应用篇。在本篇,我们将专注于Spring AOP的实际应用,通过具体的代码示例和场景分析,帮助大家掌握AOP的使用方法和技巧。而在后续的下篇中,我们将深入探讨Spring AOP的实现原理和底层机制。 AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的核心特性之一,它能够帮助我们解决横切关注点(如日志记录、性能统计、安全控制、事务管理等)的问题,提高代码的模块化程度和复用性。
|
存储 人工智能 Java
Spring AI与DeepSeek实战四:系统API调用
在AI应用开发中,工具调用是增强大模型能力的核心技术,通过让模型与外部API或工具交互,可实现实时信息检索(如天气查询、新闻获取)、系统操作(如创建任务、发送邮件)等功能;本文结合Spring AI与大模型,演示如何通过Tool Calling实现系统API调用,同时处理多轮对话中的会话记忆。
3153 57
|
Cloud Native Java 微服务
Spring Boot 3.x 现代化应用开发实战技巧与最佳实践
本指南基于Spring Boot 3.x,融合微服务、云原生与响应式编程等前沿技术,打造现代化应用开发实践。通过构建智能电商平台案例,涵盖商品、订单、用户等核心服务,展示Spring WebFlux、OAuth 2.0认证、Spring Cloud Gateway路由、GraalVM原生编译等技术实现。同时提供Docker/Kubernetes部署方案及性能优化策略,助您掌握从开发到生产的全流程。代码示例详实,适合进阶开发者参考。
1215 2