本章节主要介绍,Spring的面向方面编程的内容。
面向方面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP)。OOP中模块化的关键单位是类,而AOP中模块化的单位是方面。方面支持跨越多种类型和对象的关注点的模块化(比如事务管理)。(在AOP文献中,这样的关注点通常被称为“横切”关注点。)
Spring的关键组件之一是AOP框架。虽然Spring IoC容器不依赖于AOP(意味着如果不想使用AOP,就不需要使用AOP),但是AOP补充了Spring IoC,提供了一个非常强大的中间件解决方案。
AOP在Spring框架中用于:
- 提供声明式企业服务。最重要的服务是声明式事务管理.
- 让用户实现自定义方面,用AOP补充他们对OOP的使用。
使用AspectJ切入点的Spring AOP
Spring通过使用基于模式的方法或@AspectJ
注释样式,提供了编写自定义方面的简单而强大的方法。这两种风格都提供了完全类型的通知和AspectJ切入点语言的使用,同时仍然使用Spring AOP进行编织。
让我们从定义一些核心的AOP概念和术语开始。这些术语不是Spring特有的。不幸的是,AOP术语不是特别直观。但是,如果Spring使用自己的术语,那就更令人困惑了。
@Aspect
注释。Spring AOP包括以下类型的通知:
通知是最普通的通知。因为Spring AOP和AspectJ一样,提供了一系列完整的通知类型,所以我们建议您使用功能最弱的通知类型来实现所需的行为。例如,如果您只需要用方法的返回值更新缓存,那么您最好在返回通知后实现,而不是在通知后实现,尽管通知可以完成同样的事情。使用最具体的通知类型可以提供更简单的编程模型,减少出错的可能性。例如,您不需要调用proceed()
上的方法JoinPoint用于环绕通知,因此,您不能不调用它。
所有通知参数都是静态类型的,这样你就可以使用适当类型的通知参数(例如,方法执行返回值的类型),而不是Object数组。
由切入点匹配的连接点的概念是AOP的关键,这将它与仅提供拦截的旧技术区别开来。切入点使通知能够独立于面向对象的层次结构。例如,您可以将提供声明性事务管理的环绕建议应用于一组跨多个对象的方法(例如服务层中的所有业务操作)。
Spring AOP是用纯Java实现的。不需要特殊的编译过程。Spring AOP不需要控制类加载器的层次结构,因此适合在servlet容器或应用服务器中使用。Spring AOP目前只支持方法执行连接点(通知Spring beans上方法的执行)。没有实现字段拦截,尽管可以在不破坏核心Spring AOP APIs的情况下添加对字段拦截的支持。
Spring AOP目的不是提供最完整的AOP实现(尽管Spring AOP相当有能力)。相反,目标是在AOP实现和Spring IoC之间提供一个紧密的集成,以帮助解决企业应用程序中的常见问题。例如,Spring框架的AOP功能通常与Spring IoC容器结合使用。方面是通过使用普通的bean定义语法来配置的(尽管这允许强大的“自动代理”功能)。这是与其他AOP实现的重要区别。使用Spring AOP,您无法轻松有效地完成一些事情,比如通知非常细粒度的对象(通常是域对象)。在这种情况下,AspectJ是最好的选择。
Spring AOP和AspectJ框架,它们是互补的,而不是竞争的。Spring无缝地将Spring AOP和IoC与AspectJ集成在一起,在一个一致的基于Spring的应用程序架构中支持AOP的所有使用。
Spring框架的核心原则之一是非侵入性。然而,在某些地方,Spring框架确实给了您将特定于Spring框架的依赖项引入代码库的选项。理由是因为,在某些场景中,以这种方式阅读或编写某些特定的功能可能更容易。Spring框架(几乎)总是为您提供选择:您可以自由地做出明智的决定,选择最适合您的特定用例或场景的选项。
Spring AOP默认使用AOP代理的标准JDK动态代理。这使得任何接口(或一组接口)都可以被代理。
Spring AOP也可以使用CGLIB代理。这对于代理类而不是接口是必要的。默认情况下,如果业务对象没有实现接口,则使用CGLIB。由于对接口而不是对类编程是一个好习惯,业务类通常实现一个或多个业务接口。有可能强制使用CGLIB,在那些(希望很少)需要通知一个没有在接口上声明的方法,或者需要将代理对象作为具体类型传递给方法的情况下。
重要的是要理解Spring AOP是基于代理的这一事实。
@AspectJ
注释是将常规的Java类声明为方面。@AspectJ
样式是由AspectJ项目作为AspectJ 5发行版的一部分引入的。Spring解释与AspectJ 5相同的注释,使用AspectJ提供的切入点解析和匹配库。不过,AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ编译器或编织器。
您需要启用Spring支持,可以通过XML或Java风格的配置来启用@AspectJ
支持。以便基于@AspectJ
方面配置Spring AOP,并基于beans是否被这些方面建议来自动代理它们。所谓自动代理,我们的意思是,如果Spring确定一个bean被一个或多个方面通知,它会自动为该bean生成一个代理来拦截方法调用,并确保通知按需运行。
用Java启用@AspectJ
支持@Configuration
,添加@EnableAspectJAutoProxy
注释,如下例所示:
@Configuration
@EnableAspectJAutoProxy
public class Config {
}
你可以将注释设置为true,启用CGLIB代理
@EnableAspectJAutoProxy(proxyTargetClass = true)
要在基于XML的配置中启用@AspectJ
支持,请使用
元素(需要引入aop描述),如下例所示:
<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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:aspectj-autoproxy>aop:aspectj-autoproxy>
beans>
启用@AspectJ
支持后,任何在应用程序上下文中定义的bean,只要有一个类是@AspectJ
方面(具有@Aspect
注释)由Spring自动检测,并用于配置Spring AOP。
我们先定义一个bean,class属性指向方面的类。
<beans>
<bean id="myAspect" class="com.example.MyAspect">bean>
beans>
public class MyAspect {
}
当然你也可以使用@Aspect
注释,在此之前,先引入Spring AOP的依赖。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
<version>3.0.6version>
dependency>
或
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.9.19version>
dependency>
方面(用@Aspect
)可以有方法和字段,和其他类一样。它们还可以包含切入点、通知和引入(内部类型)声明。
@Aspect
public class MyAspect {
}
但是,请注意@Aspect
批注不足以在类路径中进行自动检测。为此,您需要添加一个单独的@Component
注释(或者,根据Spring组件扫描器的规则,一个合格的定制原型注释)。
@Aspect
@Component
public class MyAspect {
}
切入点决定了感兴趣的连接点,从而使我们能够控制通知何时运行。Spring AOP只支持Spring bean的方法执行连接点,所以您可以将切入点看作是匹配Spring bean上方法的执行。
切入点声明有两个部分:一个包含名称和任何参数的签名,以及一个确切决定我们对哪个方法执行感兴趣的切入点表达式。
<beans>
<aop:aspectj-autoproxy>aop:aspectj-autoproxy>
<bean id="myAspect" class="com.example.MyAspect">bean>
<aop:config>
<aop:aspect ref="myAspect">
<aop:pointcut id="anyOldTransfer" expression="execution(* transfer(..))">aop:pointcut>
aop:aspect>
aop:config>
beans>
在AOP的@AspectJ
注释风格中,切入点签名是由一个常规方法定义提供的,切入点表达式是通过使用@Pointcut
注释(作为切入点签名的方法必须有一个void返回类型)。
@Aspect
@Component
public class MyAspect {
@Pointcut("execution(* transfer(..))") //切入点表达式
private void anyOldTransfer() {} //切入点签名
}
Spring AOP用户很可能使用execution
切入点指示符。执行表达式的格式如下:
execution([模式修饰符] [返回类型模式] [名称模式][([参数模式])][抛出模式])
除了返回类型模式,名称模式和参数模式是可选的。
返回类型模式决定了方法的返回类型必须是什么,以使连接点匹配。*
最常用作返回类型模式。它匹配任何返回类型。只有当方法返回给定类型时,完全限定的类型名才匹配。名称模式与方法名称相匹配。您可以使用*
通配符作为名称模式的全部或一部分。如果指定声明类型模式,请在.
将其连接到名称模式组件。参数模式稍微复杂一些:()
匹配不带参数的方法,而(..)
匹配任意数量(零个或多个)的参数。这(*)
模式匹配接受任何类型的一个参数的方法。
通配符有两种:*
和..
举例说明:
execution(public int com.example.MyAspect.add(int, int))
*
:表示模糊匹配。匹配任意返回类型,第一个参数为int,第二个参数为任意类型的两个参数
execution(public * com.example.MyAspect.add(int, *))
匹配任意返回类型、任意方法.*.*(表示任意方法)
的无参方法
execution(public * com.example.MyAspect. *. *())
匹配名称以set
开头的任何方法
execution(* set*(…))
..
:表示可变参数(匹配任意数量、任意类型的参数)。匹配任意返回类型,任意数量、任意类型的参数
execution(public * com.example.MyAspect.add( . . ))
Spring AOP支持在切入点表达式中使用以下AspectJ切入点指示符(PCD ):
execution
:用于匹配方法执行连接点。这是使用Spring AOP时使用的主要切入点指示符。execution(public int com.example.Test.a(String, int))
示例代码如下:
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
bean.a("a",1);
}
public void a(String a,int b){}
}
within
:将匹配限制在特定包内的任何连接点(使用Spring AOP时,执行匹配类型内声明的方法)。within(com.example. .*)
示例代码如下:
package com.example;
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
bean.a("a",1);
}
public void a(String a,int b){}
}
this
:匹配指定对象类型下的执行方法(注意this
中使用的表达式必须是类型全限定名,不支持通配符)。this(com.example.Test)
示例代码如下:
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
bean.a();
}
public void a(){ }
}
target
:匹配当前目标对象类型的执行方法(注意target
中使用的表达式必须是类型全限定名,不支持通配符)。target(com.example.Test)
当匹配规则为接口的实现类时,this
和target
区别再于,默认JDK代理时,this
指向Proxy类,接口会出现匹配不上的情况。
args
:匹配参数是给定类型的方法,(参数必须是类型全限定名,可以使用通配符)。args(java.lang.String,*)
示例代码如下:
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
bean.a("a",1);
}
public void a(String a,int b){}
}
@target
:匹配具有指定类型注释的类中所有方法(声明在类上,注解类型也必须是全限定类型名)。@target(org.springframework.transaction.annotation.Transactional)
示例代码如下:
@Component
@Transactional
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
bean.a();
}
public void a(){}
}
@args
:匹配具有指定类型的注释的类,当作引用类型参数传递的方法(声明在类上,注解类型也必须是全限定类型名)。@args(javax.validation.constraints.NotNull)
示例代码如下:
@NotNull
public class A{
private String name;
}
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
A a = new A();
bean.a(a);
}
public void a(A a){}
}
@within
:匹配具有指定类型注释的类中所有方法(声明在类上,注解类型也必须是全限定类型名)。@within(org.springframework.transaction.annotation.Transactional)
示例代码如下:
@Component
@Transactional
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
bean.a();
}
public void a(){}
}
@annotation
:匹配具有指定注释的方法(声明在方法上,注解类型也必须是全限定类型名)。@annotation(org.springframework.transaction.annotation.Transactional)
示例代码如下:
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
bean.a();
}
@Transactional
public void a(){ }
}
AspectJ切入点语言还有其他切入点指示符:call
,get
, set
等等,在Spring AOP切入点表达式中使用这些切入点指示符会导致IllegalArgumentException,Spring AOP支持的切入点指示器集可能会在未来的版本中得到扩展,以支持更多的AspectJ切入点指示器。
Spring AOP还支持一个名为bean。这个PCD允许您将连接点的匹配限制到一个特定的命名Spring bean或一组命名Spring bean(当使用通配符时)。
bean(test)
示例代码如下:
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
bean.a();
}
public void a(){ }
}
这bean PCD仅在Spring AOP中受支持,在原生AspectJ编织中不受支持。
您可以使用&&
,||
和!
组合切入点表达式。
比如,拦截Test类中的所有方法或使用@Transactional
注解的类的所有方法示例代码如下:
@Aspect
@Component
public class MyAspect {
@Pointcut("target(com.example.Test) || @target(org.springframework.transaction.annotation.Transactional)")
private void anyOldTransfer() {}
}
你可能会认为组合切入点表达式太长了,你可以将切入点表达式变成一个通用的表达式,通过引用方法名进行访问,示例代码如下:
@Aspect
@Component
public class MyAspect {
@Pointcut("target(com.example.Test)")
private void targetExp(){}
@Pointcut("@target(org.springframework.transaction.annotation.Transactional)")
private void targetAnnotationExp(){}
@Pointcut("targetExp() || targetAnnotationExp()")
private void anyOldTransfer() {}
}
XML方法如下:
<beans>
<aop:aspectj-autoproxy>aop:aspectj-autoproxy>
<bean id="myAspect" class="com.example.MyAspect">bean>
<bean id="test" class="com.example.Test">bean>
<bean id="testService" class="com.example.service.impl.TestServiceImpl">bean>
<aop:config>
<aop:aspect ref="myAspect">
<aop:pointcut id="targetExp" expression="target(com.example.Test)">aop:pointcut>
<aop:pointcut id="targetAnnotationExp" expression="@target(org.springframework.transaction.annotation.Transactional)">aop:pointcut>
<aop:pointcut id="anyOldTransfer" expression="targetExp() || targetAnnotationExp())">aop:pointcut>
aop:aspect>
aop:config>
beans>
注意:spring5.3+aspectjweaver1.9.6,版本不一致,有可能执行时报 :
error at ::0 can’t find referenced pointcut *
最佳实践是从较小的切入点构建更复杂的切入点表达式命名切入点。当使用企业应用程序时,开发人员通常需要从几个方面引用应用程序的模块和特定的操作集。我们建议定义一个专门的方面来捕获常用的命名切入点用于此目的的表达。
@Aspect
public class CommonPointcuts {
@Pointcut("within(com.example.web..*)")
public void inWebLayer() {}
@Pointcut("within(com.example.service..*)")
public void inServiceLayer() {}
}
检查代码并确定每个连接点是否匹配(静态或动态)给定的切入点是一个代价很高的过程。(动态匹配意味着不能从静态分析中完全确定匹配,并且在代码中放置一个测试来确定代码运行时是否存在实际匹配)。在第一次遇到切入点声明时,AspectJ将其重写为匹配过程的最佳形式。基本上,切入点在DNF(析取范式)中被重写,切入点的组件被排序,使得那些评估成本较低的组件被首先检查。这意味着您不必担心理解各种切入点指示符的性能,并且可以在切入点声明中以任何顺序提供它们。
为了获得最佳的匹配性能,您应该考虑您要达到的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示器自然分为三类:种类、范围和上下文:
种类指示器选择特定种类的连接点:execution
、get
、 set
、 call
、以及handler
。
范围指示器选择一组感兴趣的连接点(可能有很多种):within
和withincode
。
上下文指示符基于上下文匹配(并可选地绑定):this
、 target
、以及@annotation
。
一个写得好的切入点应该至少包括前两种类型(种类和范围)。您可以根据连接点上下文来包含要匹配的上下文指示器,或者绑定该上下文以便在通知中使用。只提供一个种类指示符或上下文指示符是可行的,但由于额外的处理和分析,可能会影响编织性能(所用的时间和内存)。范围指示器匹配起来非常快,使用它们意味着AspectJ可以非常快地消除不应进一步处理的连接点组。如果可能的话,一个好的切入点应该总是包含一个。
通知与一个切入点表达式相关联,并在切入点匹配的方法执行之前、之后或前后运行。切入点表达式可以是内嵌切入点或者是对命名切入点。
方法在通知之前声明@Before
注释。
@Aspect
@Component
public class MyAspect {
@Before("execution(* com.example.Test.a())")
public void before(){
System.out.println("before");
}
}
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
bean.a();
/** Output:
* before
* a
*/
}
public void a(){
System.out.println("a");
}
}
XML方式如下:
<beans>
<aop:aspectj-autoproxy>aop:aspectj-autoproxy>
<bean id="myAspect" class="com.example.MyAspect">bean>
<bean id="test" class="com.example.Test">bean>
<aop:config>
<aop:aspect ref="myAspect">
<aop:before method="before" pointcut="execution(* com.example.Test.a())">aop:before>
aop:aspect>
aop:config>
beans>
通过ClassPathXmlApplicationContext访问bean。
当匹配的方法执行正常返回时,返回后通知运行。您可以使用@AfterReturning
注释。
@Aspect
@Component
public class MyAspect {
@AfterReturning(value = "execution(* com.example.Test.a())",returning = "obj")
public void afterReturning(Object obj){
System.out.println(obj);
}
}
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
bean.a();
/** Output:
* a
* s
*/
}
public String a(){
System.out.println("a");
return "s";
}
}
有时,您需要在通知体中访问返回的实际值。使用@AfterReturning
注释中returning
属性必须对应于通知方法中的参数名。当方法执行返回时,返回值作为相应的参数值传递给通知方法。
XML方式如下:
<beans>
<aop:aspectj-autoproxy>aop:aspectj-autoproxy>
<bean id="myAspect" class="com.example.MyAspect">bean>
<bean id="test" class="com.example.Test">bean>
<aop:config>
<aop:aspect ref="myAspect">
<aop:after-returning method="before" pointcut="execution(* com.example.Test.a())" returning="obj">aop:after-returning>
aop:aspect>
aop:config>
beans>
抛出后,当匹配的方法执行因抛出异常而退出时,建议运行。您可以使用@AfterThrowing
注释。
@Aspect
@Component
public class MyAspect {
@AfterThrowing(value = "execution(* com.example.Test.a())",throwing = "ex")
public void afterThrowing(RuntimeException ex){
System.out.println("AfterThrowing");
}
}
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
bean.a();
/** Output:
* a
* AfterThrowing
* Exception in thread "main" java.lang.RuntimeException
*/
}
public void a(){
System.out.println("a");
throw new RuntimeException();
}
}
通常,您希望通知只在抛出给定类型的异常时运行,并且您还经常需要访问通知体中抛出的异常。您可以使用throwing
属性来限制匹配。throwing
属性必须对应于通知方法中的参数名。
XML方式如下:
<beans>
<aop:aspectj-autoproxy>aop:aspectj-autoproxy>
<bean id="myAspect" class="com.example.MyAspect">bean>
<bean id="test" class="com.example.Test">bean>
<aop:config>
<aop:aspect ref="myAspect">
<aop:after-throwing method="before" pointcut="execution(* com.example.Test.a())" throwing="ex">aop:after-throwing>
aop:aspect>
aop:config>
beans>
finally事后通知在匹配的方法执行退出时运行。它是通过@After
注释声明的。事后通知必须准备好处理正常和异常返回条件。它通常用于释放资源和类似目的。
@Aspect
@Component
public class MyAspect {
@After(value = "execution(* com.example.Test.a())")
public void after(){
System.out.println("after");
}
}
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
bean.a();
/** Output:
* a
* after
*/
}
public void a(){
System.out.println("a");
}
}
XML方式如下:
<beans>
<aop:aspectj-autoproxy>aop:aspectj-autoproxy>
<bean id="myAspect" class="com.example.MyAspect">bean>
<bean id="test" class="com.example.Test">bean>
<aop:config>
<aop:aspect ref="myAspect">
<aop:after method="after" pointcut="execution(* com.example.Test.a())">aop:after>
aop:aspect>
aop:config>
beans>
注意,AspectJ中的
@After
通知被定义为“after finally advice”,类似于try-catch语句中的finally块。对于任何结果、正常返回或从连接点抛出的异常(用户声明的目标方法)都会调用它,而@AfterReturning
只适用于成功的正常返回。
最后一种是环绕通知。环绕通知“绕过”匹配方法的执行。它有机会在方法运行之前和之后完成工作,并确定何时、如何,甚至方法是否实际运行。如果您需要以线程安全的方式在方法执行前后共享状态(例如,启动和停止计时器),则通常使用环绕通知。环绕通知是通过用@Around
注释。
@Aspect
@Component
public class MyAspect {
@Around(value = "execution(* com.example.Test.a())")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("before");
proceedingJoinPoint.proceed();
System.out.println("after");
}
}
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
bean.a();
/** Output:
* before
* a
* after
*/
}
public void a(){
System.out.println("a");
}
}
XML方式如下:
<beans>
<aop:aspectj-autoproxy>aop:aspectj-autoproxy>
<bean id="myAspect" class="com.example.MyAspect">bean>
<bean id="test" class="com.example.Test">bean>
<aop:config>
<aop:aspect ref="myAspect">
<aop:around method="around" pointcut="execution(* com.example.Test.a())">aop:around>
aop:aspect>
aop:config>
beans>
总是使用最不有力的通知来满足你的需求。
例如,如果before
的通知足以满足你的需要,就不要使用around
的通知。
方法应该声明Object作为它的返回类型,并且方法的第一个参数必须是ProceedingJoinPoint类型。在通知方法的主体中,必须调用ProceedingJoinPoint上的proceed()
才能运行底层方法。调用不带参数的proceed()
调用方的原始参数在调用底层方法时被提供给底层方法。
@Aspect
@Component
public class MyAspect {
@Around(value = "execution(* com.example.Test.a(String,String))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object proceed = proceedingJoinPoint.proceed();
System.out.println("return:"+proceed);
return proceed;
}
}
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
String str = bean.a("1", "2");
System.out.println("最终结果:"+str);
/** Output:
* 1,2
* return:a
* 最终结果:a
*/
}
public String a(String a,String b){
System.out.println(a+","+b);
return "a";
}
}
环绕通知返回的值是方法调用方看到的返回值。如果您将环绕通知方法的返回类型声明为void,则将始终向调用者返回null,从而有效地忽略任何调用proceed()
的结果(也就是说环绕通知可以覆盖调用者的返回值)。因此,建议通知方法声明Object的返回类型。通知方法通常应该返回调用proceed()
返回的值,即使底层方法具有空返回类型。
@Aspect
@Component
public class MyAspect {
@Around(value = "execution(* com.example.Test.a(String,String))")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object proceed = proceedingJoinPoint.proceed();
System.out.println("return:"+proceed);
}
}
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
String str = bean.a("1", "2");
System.out.println("最终结果:"+str);
/** Output:
* 1,2
* return:a
* 最终结果:null
*/
}
public String a(String a,String b){
System.out.println(a+","+b);
return "a";
}
}
对于高级用例,有一个proceed()
方法的重载变体,它接受一个参数数组(Object[])。当调用基础方法时,数组中的值将用作其参数(也就是说会覆盖原始参数)。
@Aspect
@Component
public class MyAspect {
@Around(value = "execution(* com.example.Test.a(String,String))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object[] objects = new Object[]{"a","b"};
Object proceed = proceedingJoinPoint.proceed(objects);
System.out.println("return:"+proceed);
return proceed;
}
}
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
String str = bean.a("1", "2");
System.out.println("最终结果:"+str);
/** Output:
* a,b
* return:a
* 最终结果:a
*/
}
public String a(String a,String b){
System.out.println(a+","+b);
return "a";
}
}
传统AspectJ语言编写的环绕通知,传递给
proceed()
方法的参数数量必须与传递给环绕通知的参数数量相匹配(而不是底层连接点接受的参数数量),并且在给定参数位置传递给proceed()
方法的值将替换该值绑定到的实体的连接点上的原始值。
任何通知方法都可以将类型的参数声明为其第一个参数org.aspectj.lang.JoinPoint
。请注意,环绕通知需要使用ProceedingJoinPoint来声明类型的第一个参数,它是JoinPoint的子类。
@Aspect
@Component
public class MyAspect {
@Before(value = "execution(* com.example.Test.a(String))")
public void before(JoinPoint joinPoint) throws Throwable {
String string = joinPoint.toString();
System.out.println(string);//Output:execution(String com.example.Test.a(String))
Object[] args = joinPoint.getArgs();
for (Object o:args) {
System.out.println(o);//Output:1
}
String kind = joinPoint.getKind();
System.out.println(kind);//Output:method-execution
Signature signature = joinPoint.getSignature();
System.out.println(signature);//Output:String com.example.Test.a(String)
SourceLocation sourceLocation = joinPoint.getSourceLocation();
System.out.println(sourceLocation);//Output:org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint$SourceLocationImpl@8ad6665
Object target = joinPoint.getTarget();
System.out.println(target);//Output:com.example.Test@30af5b6b
Object aThis = joinPoint.getThis();
System.out.println(aThis);//Output:com.example.Test@30af5b6b
String string1 = joinPoint.toLongString();
System.out.println(string1);//Output:execution(public java.lang.String com.example.Test.a(java.lang.String))
String string2 = joinPoint.toShortString();
System.out.println(string2);//Output:execution(Test.a(..))
}
}
getArgs()
:返回方法参数。getThis()
:返回代理对象。getTarget()
:返回目标对象。getSignature()
:返回所建议方法的描述。toString()
:打印建议方法的有用描述。我们已经看到了如何绑定返回值或异常值(使用返回后和抛出建议后)。要使参数值对通知主体可用,可以使用args
。如果使用参数名代替args
表达式中,调用通知时,相应参数的值作为参数值传递。
假设您想建议执行采用String对象作为第一个参数,并且您需要访问通知主体中的信息,示例代码如下:
@Aspect
@Component
public class MyAspect {
@Before(value = "execution(* com.example.Test.a(String)) && args(str)")
public void before(String str) throws Throwable {
System.out.println("before:"+str);
}
}
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
String str = bean.a("1");
/** Output:
* before:1
*/
}
public String a(String a){
return "a";
}
}
XML方式基本相同,这里就不再做讲解了。
另一种编写方式是声明一个切入点来“提供”对象值,然后从通知中引用命名的切入点。
@Aspect
@Component
public class MyAspect {
@Pointcut(value = "execution(* com.example.Test.a(String)) && args(str)")
private void pointcut(String str){}
@Before(value = "pointcut(str)")
public void before(String str) throws Throwable {
System.out.println("before:"+str);
}
}
代理对象(this
),目标对象(target
),以及注释(@within
, @target
, @annotation
,以及@args
)都可以以类似的方式绑定。下一组示例展示了如何匹配用@Auditable
注释并提取审计代码:
@Aspect
@Component
public class MyAspect {
@Before(value = "execution(* com.example.Test.a(..)) && @annotation(auditable)")
public void before(Auditable auditable) {
String name = auditable.name();
System.out.println(name);
}
}
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
bean.a("1");
/** Output:
* test
*/
}
@Auditable(name = "test")
public String a(String a){
return "a";
}
}
Spring AOP可以处理类声明和方法参数中使用的泛型。假设您有一个如下所示的泛型类型:
public class A{
private String name;
}
public interface Base<T> {
void method(T t);
}
@Component
public class B<T> implements Base<T>{
@Override
public void method(T t) {}
}
通过将通知参数绑定到您要为其拦截方法的参数类型,您可以将方法类型的拦截限制到某些参数类型:
@Aspect
@Component
public class MyAspect {
@Before(value = "execution(* com.example.Base+.method(*)) && args(param)")
public void before(A param) {
System.out.println(param.getName());
}
}
@AspectJ
通知和切入点注释有一个可选的argNames属性,该属性可用于指定带批注的方法的参数名称。
@Aspect
@Component
public class MyAspect {
@Before(value = "execution(* com.example.Test.a(..)) && args(param) && @annotation(auditable)"
,argNames = "param,auditable")
public void before(String param,Auditable auditable) {
System.out.println(param);
System.out.println(auditable.name());
}
}
如果
@AspectJ
方面已经被AspectJ编译器(ajc)编译,即使没有调试信息,你也不需要添加argNames属性,因为编译器保留了所需的信息。
类似地,如果@AspectJ
方面是用javac使用**-parameters标志编译的,则不需要添加argNames**属性,因为编译器会保留所需的信息。
当多条建议都想在同一个连接点运行时会发生什么?Spring AOP遵循与AspectJ相同的优先规则来确定通知执行的顺序。优先级最高的通知首先运行(因此,给定两个before通知,优先级最高的通知首先运行)。从连接点“出去”时,优先级最高的通知最后运行(因此,给定两个after通知,优先级最高的通知将第二个运行)。
当在不同方面定义的两条通知都需要在同一个连接点上运行时,除非您另外指定,否则执行的顺序是不确定的。您可以通过指定优先级来控制执行顺序。这是以正常的Spring方式完成的,方法是实现org.springframework.core.Ordered
接口,或者用@Order
注释。给定两个方面,从Ordered.getOrder()
(或注释值)具有较高的优先级。
特定方面的每种不同的通知类型在概念上都意味着直接应用于连接点。因此,一个
@AfterThrowing
通知方法不应该从伴随的
@After
/@AfterReturning
方法。
从Spring Framework 5.2.7开始,在相同的
@Aspect
需要在同一个连接点上运行的类根据它们的通知类型按以下顺序被分配优先级,从最高优先级到最低优先级:@Around
,@Before
,@After
,@AfterReturning
,@AfterThrowing
。但是请注意,@After
通知方法将在同一方面的任何@AfterReturning
或@AfterThrow
通知方法之后有效地调用,遵循AspectJ的@After
的“after finally advice”语义。
当两条相同类型的建议(例如,两条
@After
通知方法)中定义的相同@Aspect
类都需要在同一个连接点上运行,顺序是未定义的(因为没有办法通过反射为javac编译的类检索源代码声明顺序)。考虑将这样的通知方法合并到每个连接点的一个通知方法中@Aspect
类,或者将这些建议重构为单独的@Aspect
您可以通过以下方式在方面级别订购的类Ordered
或者@Order
。
引入(在AspectJ中称为类型间声明)使方面能够声明被通知的对象实现了给定的接口,并代表这些对象提供了该接口的实现。你可以用@DeclareParents
注释。该注释用于声明匹配类型有一个新的父类型(因此得名)。
public interface OneService {
void oneMethod();
}
@Component
public class OneServiceImpl implements OneService {
@Override
public void oneMethod() {}
}
public interface TwoService {
void twoMethod();
}
@Component
public class TwoServiceImpl implements TwoService {
@Override
public void twoMethod() {}
}
@Aspect
@Component
public class MyAspect {
@DeclareParents(value = "com.example.OneService+",defaultImpl = TwoServiceImpl.class)
public TwoService twoService;
}
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
OneService bean = applicationContext.getBean(OneService.class);
bean.oneMethod();
TwoService twoService = (TwoService) bean;
twoService.twoMethod();
}
}
XML方式如下:
<beans>
<aop:aspectj-autoproxy>aop:aspectj-autoproxy>
<bean id="myAspect" class="com.example.MyAspect">bean>
<bean id="test" class="com.example.Test">bean>
<aop:config>
<aop:aspect ref="myAspect">
<aop:declare-parents types-matching="com.example.OneService+"
implement-interface="com.example.TwoService"
default-impl="com.example.TwoServiceImpl">aop:declare-parents>
aop:aspect>
aop:config>
beans>
默认情况下,应用程序上下文中的每个方面都有一个实例。AspectJ称之为单例实例化模型。Spring支持AspectJ的singleton
、perthis
、pertarget
实例化模型(目前不支持percflow
、percflowbelow
和pertypewithin
)。
@Aspect("perthis(execution(* com.example.Test.a(..)))")
@Scope("prototype")//一定要声明否则报bean范围和aop范围不匹配
@Component
public class MyAspect {
@Before(value = "execution(* com.example.Test.a(..))")
public void before() {
System.out.println("before");
}
}
@Component
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example");
Test bean = applicationContext.getBean(Test.class);
bean.a();
/** Output:
* before
*/
}
public String a(){
return "a";
}
}
perthis
子句每个切入点表达式匹配的连接点对应的AOP对象都会创建一个新方面实例。方面实例是在第一次对服务对象调用方法时创建的。当服务对象超出范围时,方面也超出范围。在创建方面实例之前,其中的任何通知都不会运行。一旦创建了方面实例,在其中声明的通知就在匹配的连接点上运行,但是只有当服务对象是与这个方面相关联的对象时才运行。
pertarget
实例化模型为匹配连接点上的每个唯一目标对象创建一个方面实例。
XML定义方面唯一支持的实例化模型是单例模型。未来的版本可能会支持其他实例化模型。
“顾问”的概念来自Spring中定义的AOP支持,在AspectJ中没有直接的对等物。顾问就像一个小型的独立方面,只有一条建议。Spring通过
元素。您最常看到它与事务性通知结合使用,后者在Spring中也有自己的名称空间支持。
元素,示例代码如下:
<aop:config>
<aop:advisor advice-ref="" pointcut-ref="" pointcut="" order="">aop:advisor>
aop:config>
advice-ref
属性用于指定一个org.aopalliance.aop.Advice
实现;pointcut-ref
属性用于指定一个通过已经存在的表达式,你也可以通过pointcut
属性直接定义表达式。若要定义顾问的优先级,以便建议可以参与排序,请使用order
属性。
下面演示标签用法,示例代码如下:
public class AdvisorTest implements MethodBeforeAdvice, MethodInterceptor, AfterReturningAdvice {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("before");
Object invoke = invocation.getMethod().invoke(invocation.getThis());
System.out.println("after");
return invoke;
}
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("after");
}
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("before");
}
}
我们实现了MethodBeforeAdvice(前置通知)、MethodInterceptor(环绕通知)、AfterReturningAdvice(返回后置通知) 三个接口进行扩展,ThrowsAdvice、AfterAdvice是一个空接口,要实现AfterThrowing、After 通知可以通过实现接口自定义内容。
<beans>
<aop:aspectj-autoproxy>aop:aspectj-autoproxy>
<bean id="test" class="com.example.Test">bean>
<bean id="advisorTest" class="com.example.AdvisorTest">bean>
<aop:config>
<aop:pointcut id="pointcut" expression="execution(* com.example.Test.a(..))">aop:pointcut>
<aop:advisor advice-ref="advisorTest" pointcut-ref="pointcut">aop:advisor>
aop:config>
beans>
执行如下:
@Component
public class Test {
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
Test bean = applicationContext.getBean(Test.class);
bean.a();
/** Output:
* before
* 方法调用
* after
*/
}
public String a(){
System.out.println("方法调用");
return "a";
}
}
我们再来回顾
元素,示例代码如下:
<aop:config>
<aop:aspect ref="myAspect">
<aop:before method="" pointcut-ref=""/>
aop:aspect>
aop:config>
我们发现
元素和
元素功能上都很相似,
元素针对不同的通知需要实现不同的接口,而
元素只需要一个注释或一个标签就能实现,
元素似乎更加的灵活。
一旦决定了某个方面是实现给定需求的最佳方法,那么如何决定是使用Spring AOP还是AspectJ,以及是使用方面语言(代码)风格、@AspectJ
注释风格还是Spring XML风格呢?这些决策受到许多因素的影响,包括应用程序需求、开发工具和团队对AOP的熟悉程度。
用最简单的能用的东西。Spring AOP比使用完整的AspectJ更简单,因为不需要在开发和构建过程中引入AspectJ编译器/ weaver。如果您只需要建议在Spring beans上执行操作,Spring AOP是正确的选择。如果需要通知不受Spring容器管理的对象(通常是域对象),就需要使用AspectJ。
如果您选择了使用Spring AOP,那么您可以选择@AspectJ
或XML样式。XML风格可能是现有Spring用户最熟悉的,它由真正的POJOs支持。使用XML风格,可以从您的配置中更清楚地看出系统中存在哪些方面。与@AspectJ
风格相比,XML风格在所能表达的内容方面稍有限制:只支持“singleton”方面实例化模型,并且不可能组合XML中声明的命名切入点。@AspectJ
风格支持额外的实例化模型和更丰富的切入点组合。它的优点是将方面保持为模块化单元。如果您后来决定需要AspectJ的功能来实现额外的需求,您可以很容易地迁移到经典的AspectJ设置。
org.springframework.aop.Pointcut
接口是中央接口,用于将通知指向特定的类和方法。
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
将Pointcut接口分成两个部分,允许重用类和方法匹配部分以及细粒度的组合操作(例如与另一个方法匹配器执行“联合”)。
ClassFilter接口用于将切入点限制为一组给定的目标类。如果matches()
方法总是返回true,则匹配所有目标类。
public interface ClassFilter {
boolean matches(Class clazz);
}
MethodMatcher接口通常更重要。
public interface MethodMatcher {
boolean matches(Method m, Class<?> targetClass);
boolean isRuntime();
boolean matches(Method m, Class<?> targetClass, Object... args);
}
matches(Method, Class)
方法用于测试该切入点是否与目标类上的给定方法匹配。这种评估可以在创建AOP代理时执行,以避免对每个方法调用进行测试。如果对于给定的方法,双参数匹配方法返回true,并且MethodMatcher的isRuntime()
方法返回true,则在每个方法调用时都会调用三参数匹配方法。这使得切入点可以在目标通知开始之前查看传递给方法调用的参数。
大多数MethodMatcher实现都是静态的,这意味着它们的isRuntime()
方法返回false。在这种情况下,永远不会调用三个参数匹配方法。
如果可能的话,尽量使切入点成为静态的,允许AOP框架在创建AOP代理时缓存切入点评估的结果。
Spring提供了有用的切入点超类来帮助你实现你自己的切入点。因为静态切入点是最有用的,所以您可能应该子类化StaticMethodMatcherPointcut。这要求只实现一个抽象方法(尽管您可以重写其他方法来定制行为)。
public abstract class StaticMethodMatcherPointcut extends StaticMethodMatcher implements Pointcut {
private ClassFilter classFilter;
public StaticMethodMatcherPointcut() {
this.classFilter = ClassFilter.TRUE;
}
public void setClassFilter(ClassFilter classFilter) {
this.classFilter = classFilter;
}
public ClassFilter getClassFilter() {
return this.classFilter;
}
public final MethodMatcher getMethodMatcher() {
return this;
}
}
class TestStaticPointcut extends StaticMethodMatcherPointcut {
public boolean matches(Method m, Class targetClass) {
// return true if custom criteria match
}
}
尽管Java不允许用它的类型系统来表达空安全,但是Spring框架现在在org.springframework.lang
让您声明API和字段的可空性的包:
@Nullable
:表示特定参数、返回值或字段可以被null。
@NonNull
:批注,指示特定的参数、返回值或字段不能null(参数/返回值和字段不需要,其中@NonNullApi
和@NonNullFields
分别适用)。
@NonNullApi
:包级别的批注,它将非null声明为参数和返回值的默认语义。
@NonNullFields
:包级别的注释,它将非null声明为字段的默认语义。
除了为Spring Framework API空性提供显式声明之外,IDE(如IDEA或Eclipse)还可以使用这些注释来提供与空安全相关的有用警告,以避免NullPointerException在运行时。没有必要也不推荐向项目类路径添加JSR-305依赖项来利用Spring空安全API。
Java NIO提供了ByteBuffer但是许多库在其上构建了自己的字节缓冲API,特别是对于重用缓冲区和/或使用直接缓冲区有利于提高性能的网络操作。例如,Netty拥有ByteBuf层次结构,Undertow使用XNIO,Jetty使用池化的字节缓冲区,带有一个要释放的回调,等等。这spring-core模块提供了一组抽象来处理各种字节缓冲API,如下所示:
DataBufferFactory用于以两种方式之一创建数据缓冲区:
(1)分配一个新的数据缓冲区,可以选择预先指定容量(如果知道的话),这样效率更高,即使DataBuffer可以按需增长和收缩。
(2)包装现有的byte[]或者java.nio.ByteBuffer
,它用一个DataBuffer这不涉及分配。
请注意,WebFlux应用程序不会创建DataBufferFactory直接访问,而是通过ServerHttpResponse或者ClientHttpRequest在客户端。工厂的类型取决于底层客户端或服务器,例如NettyDataBufferFactory对于反应器Netty,DefaultDataBufferFactory对其他人来说。
DataBuffer接口提供了类似于java.nio.ByteBuffer
但也带来了一些额外的好处,其中一些是受Netty的启发ByteBuf。以下是部分优势列表:
(1)用独立的位置读写,即不需要调用flip()在读和写之间交替。
(2)容量按需扩展,与java.lang.StringBuilder
。
(3)混合缓冲液和参考计数通过PooledDataBuffer。
(4)将缓冲区视为java.nio.ByteBuffer,InputStream,或者OutputStream。
确定给定字节的索引或最后一个索引。
DataBufferUtils提供了许多对数据缓冲区进行操作的实用方法:
(1)如果底层字节缓冲API支持,可以通过复合缓冲区将一个数据缓冲区流加入到一个零拷贝的缓冲区中。
(2)转动InputStream或者NIOChannel到…里面Flux
反之亦然Publisher
到…里面OutputStream或者NIOChannel。
(3)方法来释放或保留DataBuffer如果缓冲区是的实例PooledDataBuffer。
从一个字节流中跳过或取出一个特定的字节数。
org.springframework.core.codec
包提供了以下策略接口:
(1)Encoder编码Publisher转换成数据缓冲区流。
(2)Decoder解码Publisher转换成更高级别的对象流。
这spring-core模块提供byte[], ByteBuffer, DataBuffer, Resource,以及String编码器和解码器实现。这spring-web模块增加了Jackson JSON、Jackson Smile、JAXB2、协议缓冲区等编码器和解码器。看见编解码器在WebFlux部分。