
本文为 【Spring】面向切面编程 相关知识,下边将对AOP概述(包含什么是AOP、AOP相关术语、Spring AOP通知类型),Spring AOP能力和目标,AOP代理,@AspectJ风格的支持(包含对于 @AspectJ的支持、声明一个切面、声明一个切入点、声明通知、引入Introduction、Advice Ordering、AOP的例子),基于schema的AOP支持,AOP声明风格,以编程方式创建@AspectJ代理等进行详尽介绍~
📌博主主页:小新要变强 的主页
👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~
👉Java微服务开源项目可参考:企业级Java微服务开源项目(开源框架,用于学习、毕设、公司项目、私活等,减少开发工作,让您只关注业务!)
↩️本文上接:最新最全面的Spring详解(三)——Resources,验证、数据绑定和类型转换与Spring表达式语言(SpEL)


Spring和AspectJ
@AspectJ是将【切面】声明为带有注解的常规Java类的一种风格。 @AspectJ风格是由AspectJ项目作为AspectJ 5发行版的一部分引入的。 Spring与AspectJ 5有相同的注解, 但是,AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ编译器或编织器。
要在Spring配置中使用@AspectJ注解,您需要启用Spring支持,以便基于@AspectJ注解配置Spring AOP,如果Spring确定一个bean被一个或多个切面通知,它将自动为该bean生成一个代理,以拦截方法调用,并确保通知在需要时运行。
@AspectJ支持可以通过XML或java的配置来启用。 在这两种情况下,你还需要确保【AspectJ的’ aspectjweaver.jar ‘库】在你的应用程序的类路径上(1.8或更高版本)。 这个库可以在AspectJ发行版的’ lib '目录中或Maven中央存储库中获得。
🍀使用Java配置启用@AspectJ支持
要使用Java的【@Configuration】启用@AspectJ支持,请添加【@EnableAspectJAutoProxy】注解,如下面的示例所示:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {}
🍀使用XML配置启用@AspectJ支持,请使用元素,如下例所示:
当然,我们需要引入aop的命名空间。
启用@AspectJ支持后,在应用程序上下文中定义的任何带有@AspectJ注解类的bean都会被Spring自动检测并用于配置Spring AOP。
两个示例中的第一个展示了应用程序上下文中的常规beanDifination,它指向一个具有“@Aspect”注解的bean类:
两个示例中的第二个展示了’ NotVeryUsefulAspect ‘类定义,它是用’ org.aspectj.lang.annotation '标注的。 方面的注解;
package org.xyz;
import org.aspectj.lang.annotation.Aspect;@Aspect
public class NotVeryUsefulAspect {}
用“@Aspect”标注的类可以有方法和字段,与任何其他类一样。 它们还可以包含切入点、通知和引入(类型间)声明。
通过组件扫描自动检测切面你可以在Spring XML配置中通过“@Configuration”类中的“@Bean”方法将切面类注册为常规bean,或者让Spring通过类路径扫描自动检测它们——就像任何其他Spring管理的bean一样。 但是,请注意,“@Aspect”注解不足以实现类路径中的自动检测。 为了达到这个目的,您需要添加一个单独的【@Component】注解。
在Spring AOP中,切面本身不能成为来自其他通知的目标。 类上的“@Aspect”注解将其标记为一个切面类,因此会将其排除在自动代理之外。
【切入点确定感兴趣的连接点】,从而使我们能够控制通知何时运行。
切入点声明由两部分组成:包含【名称和方法签名】,以及确定我们感兴趣的方法执行的【切入点表达式】。
怎么确定一个方法:public void com.ydlclass.service.impl.*(…)
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature
🍀支持切入点指示器
Spring AOP支持以下在切入点表达式中使用的AspectJ切入点指示器(PCD):
execution:(常用)用于匹配方法执行的连接点,这是在使用Spring AOP时使用的主要切入点指示符。(匹配方法)
within: 用于匹配指定类型内的方法执行。(匹配整个类)
this: 用于匹配当前【AOP代理对象】类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能【包括引入接口】也进行类型匹配。(配置整个类)
target: 用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就【不包括引入接口】也进行类型匹配。(配置整个类)
args: 限制匹配连接点(使用Spring AOP时的方法执行),其中参数是给定类型的实例。 (参数类型匹配)
@target: 用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解 。(类上的注解)
@args: 用于匹配当前执行的方法传入的参数持有指定注解的执行。(参数上的注解)
@within: 用于匹配所有持有指定注解类型内的方法。(类上的注解)
@annotation: (常用)于匹配当前执行方法持有指定注解的方法。(方法上的注解)
bean:使用“bean(Bean id或名字通配符)”匹配特定名称的Bean对象的执行方法;Spring ASP扩展的,在AspectJ中无相应概念。
🍀切入点表达式运算
可以使用’ &&’ || ‘和’ ! '组合切入点表达式。 您还可以通过名称引用切入点表达式。 下面的例子展示了三个切入点表达式:
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} @Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {} @Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
🍀共享公共切入点定义
在使用企业应用程序时,开发人员经常希望从几个切面引用应用程序的模块和特定的操作集。 我们建议定义一个【CommonPointcut】切面来捕获通用的切入点表达式。 这样一个方面典型地类似于以下示例:
package com.xyz.myapp;import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;@Aspect
public class CommonPointcuts {/*** A join point is in the web layer if the method is defined* in a type in the com.xyz.myapp.web package or any sub-package* under that.*/@Pointcut("within(com.xyz.myapp.web..*)")public void inWebLayer() {}/*** A join point is in the service layer if the method is defined* in a type in the com.xyz.myapp.service package or any sub-package* under that.*/@Pointcut("within(com.xyz.myapp.service..*)")public void inServiceLayer() {}/*** A join point is in the data access layer if the method is defined* in a type in the com.xyz.myapp.dao package or any sub-package* under that.*/@Pointcut("within(com.xyz.myapp.dao..*)")public void inDataAccessLayer() {}/*** A business service is the execution of any method defined on a service* interface. This definition assumes that interfaces are placed in the* "service" package, and that implementation types are in sub-packages.** If you group service interfaces by functional area (for example,* in packages com.xyz.myapp.abc.service and com.xyz.myapp.def.service) then* the pointcut expression "execution(* com.xyz.myapp..service.*.*(..))"* could be used instead.** Alternatively, you can write the expression using the 'bean'* PCD, like so "bean(*Service)". (This assumes that you have* named your Spring service beans in a consistent fashion.)*/@Pointcut("execution(* com.xyz.myapp..service.*.*(..))")public void businessService() {}/*** A data access operation is the execution of any method defined on a* dao interface. This definition assumes that interfaces are placed in the* "dao" package, and that implementation types are in sub-packages.*/@Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")public void dataAccessOperation() {}}
您可以在任何需要切入点表达式的地方引用在这样一个切面中定义的切入点。 例如,要使服务层成为事务性的,可以这样写:
通知与切入点表达式相关联,并在切入点匹配的方法执行之前、之后或前后运行。 切入点表达式可以是对指定切入点的【简单引用】,也可以是适当声明的切入点表达式。
🍀(1)(Before advice)前置通知
你可以使用【@Before】注解在方面中声明before通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;@Aspect
public class BeforeExample {@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")public void doAccessCheck() {// ...}
}
如果使用切入点表达式,可以将前面的示例重写为以下示例:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;@Aspect
public class BeforeExample {@Before("execution(* com.xyz.myapp.dao.*.*(..))")public void doAccessCheck() {// ...}
}
🍀(2)(After returning advice)返回通知
当匹配的方法执行正常返回时,返回通知运行。 你可以通过使用【@AfterReturning】注解声明它:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;@Aspect
public class AfterReturningExample {@AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")public void doAccessCheck() {// ...}
}
有时,您需要在通知主体中访问返回的实际值。 你可以使用’ @afterreturn '的形式绑定返回值以获得访问,如下例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;@Aspect
public class AfterReturningExample {@AfterReturning(pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",returning="retVal")public void doAccessCheck(Object retVal) {// ...}
}
🍀(3)(After throwing advice)抛出异常后通知
抛出通知后,当匹配的方法执行通过抛出异常退出时运行。 你可以通过使用【 @AfterThrowing】注解来声明它,如下面的例子所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;@Aspect
public class AfterThrowingExample {@AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")public void doRecoveryActions() {// ...}
}
通常,您如果希望通知仅在【抛出给定类型】的异常时运行,而且您还经常需要访问通知主体中抛出的异常。 你可以使用’ thrown ‘属性来限制匹配(如果需要,则使用’ Throwable '作为异常类型),并将抛出的异常绑定到一个advice参数。 下面的例子展示了如何做到这一点:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;@Aspect
public class AfterThrowingExample {@AfterThrowing(pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",throwing="ex")public void doRecoveryActions(DataAccessException ex) {// ...}
}
【throwing】属性中使用的【名称必须与通知方法中的参数名称】相对应。 当一个方法执行通过抛出异常而退出时,异常将作为相应的参数值传递给advice方法。
🍀(4)After (Finally) 最终通知
After (finally)通知在匹配的方法执行退出时运行。 它是通过使用【@After 】注解声明的。 After advice必须准备好处理正常和异常返回条件,它通常用于释放资源以及类似的目的。 下面的例子展示了如何使用after finally通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;@Aspect
public class AfterFinallyExample {@After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")public void doReleaseLock() {// ...}
}
更多值得注意的地方,AspectJ中的’ @After '通知被定义为【after finally】,类似于try-catch语句中的finally块。 它将对任何结果,其中包括【正常返回】或【从连接点抛出异常】都会进行调用,而【 @ afterreturn】只适用于成功的正常返回。
🍀(5)Around通知
【Around advice】环绕匹配的方法执行。 它有机会在方法运行之前和之后进行工作,并确定方法何时、如何运行,甚至是否真正运行。 如果您需要在方法执行之前和之后以线程安全的方式共享状态(例如,启动和停止计时器),经常使用Around通知。 我们推荐,总是使用最弱的通知形式,以满足你的要求(也就是说,不要使用环绕通知,如果前置通知也可以完成需求)。
Around通知是通过使用【@Around】注解声明的。 advice方法的第一个参数必须是【ProceedingJoinPoint】类型。 在通知体中,在【ProceedingJoinPoint】上调用【proceed()】会导致底层方法运行。 【proceed】方法也可以传入【Object[] 】。 当方法执行时,数组中的值被用作方法执行的参数。
下面的例子展示了如何使用around advice:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;@Aspect
public class AroundExample {@Around("com.xyz.myapp.CommonPointcuts.businessService()")public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {// 我们可以在前边做一些工作,比如启动计时器// 这里是真正的方法调用的地方Object retVal = pjp.proceed();// 我们可以在后边做一些工作,比如停止计时器,搜集方法的执行时间return retVal;}
}
注意: around通知返回的值是【方法调用者看到的返回值】。 例如,一个简单的缓存切面可以从缓存返回一个值(如果它有一个值),如果没有,则调用’ proceed() '。 注意,【proceed方法】你可以只调用一次,也可以调用多次,也可以根本不去调用, 这都是可以的。
🍀(6)通知的参数
Spring提供了完整类型的通知,这意味着您可以在【通知签名】中声明【所需的参数】(就像我们前面在返回和抛出示例中看到的那样)。
访问当前 JoinPoint
任何通知方法都可以声明一个类型为【org.aspectj.lang.JoinPoint】的参数作为它的【第一个参数】(注意,around通知需要声明类型为’ ProceedingJoinPoint )的第一个参数,它是【oinPoint】的一个子类。 【JoinPoint】接口提供了许多有用的方法:
getArgs(): 返回方法参数。getThis(): 返回代理对象。getTarget(): 返回目标对象。getSignature(): 返回被通知的方法的签名。toString(): 打印被建议的方法的有用描述。@Before("beforePointcut()")
private void beforeAdvice(JoinPoint jp) throws InvocationTargetException, IllegalAccessException {MethodSignature signature = (MethodSignature)jp.getSignature();// 能拿到方法,能不能拿到方法的注解Method method = signature.getMethod();// 调用方法的过程method.invoke(jp.getTarget(), jp.getArgs());System.out.println("this is before advice");
}

将参数传递给Advice
我们已经看到了如何绑定【返回值或异常值】。 要使参数值对通知主体可用,可以使用【args 】的绑定形式。 如果在args表达式中使用【参数名】代替类型名,则在【调用通知】时将传递相应值作为参数值。
举个例子应该能更清楚地说明这一点:
@Override
public String order(Integer money) {try {logger.info("这是order的方法");return "inner try";} finally {logger.info("finally");//return "finally";}
}@Before("execution(* com.ydlclass.service.impl.OrderService.*(..)) && args(money,..)")
public void validateAccount(Integer money) {System.out.println("before--------" + money);
}
切入点表达式的’ args(account,…) '部分有两个目的
另一种方式是【编写方法】声明一个切入点,该切入点在匹配连接点时“提供”‘Account’对象值,然后从通知中引用指定的切入点。 这看起来如下:
@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {// ...
}
引入使切面能够声明被通知的对象【实现给定的接口】,也就是让代理对象实现新的接口。
@DeclareParents(value="com.ydlclass.service.impl.OrderService",defaultImpl= ActivityService.class)
public static IActivityService activityService;
要实现的接口由注解字段的类型决定。 @DeclareParents注解的【value】属性是一个AspectJ类型类。 任何与之匹配的类型的bean都将实现【UsageTracked】接口。 注意,在前面示例的before通知中,服务bean可以直接用作【UsageTracked】接口的实现。 如果以编程方式访问bean,您将编写以下代码:
IActivityService bean = ctx.getBean(IActivityService.class);
bean.sendGif();
搞过debug看到了,生成的代理实现了两个接口:

业务代码的执行有时会由于【并发性问题】而失败(例如,死锁而导致的失败)。 如果再次尝试该操作,很可能在下一次尝试时成功。 对于适合在这种条件下重试的业务服务,我们希望进行透明地重试操作。 这是一个明显跨越service层中的多个服务的需求,因此是通过切面实现的理想需求。
因为我们想要重试操作,所以我们需要使用around通知,以便我们可以多次调用’ proceed '。 下面的例子显示了基本方面的实现:
@Aspect
public class ConcurrentOperationExecutor implements Ordered {private static final int DEFAULT_MAX_RETRIES = 2;private int maxRetries = DEFAULT_MAX_RETRIES;private int order = 1;public void setMaxRetries(int maxRetries) {this.maxRetries = maxRetries;}public int getOrder() {return this.order;}public void setOrder(int order) {this.order = order;}@Around("com.xyz.myapp.CommonPointcuts.businessService()")public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {int numAttempts = 0;PessimisticLockingFailureException lockFailureException;do {numAttempts++;try {return pjp.proceed();}catch(PessimisticLockingFailureException ex) {lockFailureException = ex;}} while(numAttempts <= this.maxRetries);throw lockFailureException;}
}
注意,切面实现了’ Ordered '接口,因此我们可以将【该切面的优先级】设置得高于【事务通知】,我们希望每次重试时都有一个新的事务。 ’ maxRetries ‘和’ order '属性都是可以由Spring配置注入的。
对应的Spring配置如下:
如果您喜欢基于xml的格式,Spring还提供了使用【aop命名空间】标记定义切面的支持。 它支持与使用@AspectJ样式时完全相同的切入点表达式和通知类型。
要使用本节中描述的aop命名空间标记,您需要导入’ spring-aop '模块。
在Spring配置中,所有【切面和通知】元素都必须放在一个 元素中(在应用程序上下文配置中可以有多个 元素)。 一个 元素可以包含切入点、通知和切面元素(注意这些元素必须按照这个顺序声明)。
🍀配置切面,切点表达式,通知的方法如下
🍀Introduction
🍀AOP示例
public class ConcurrentOperationExecutor implements Ordered {private static final int DEFAULT_MAX_RETRIES = 2;private int maxRetries = DEFAULT_MAX_RETRIES;private int order = 1;public void setMaxRetries(int maxRetries) {this.maxRetries = maxRetries;}public int getOrder() {return this.order;}public void setOrder(int order) {this.order = order;}public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {int numAttempts = 0;PessimisticLockingFailureException lockFailureException;do {numAttempts++;try {return pjp.proceed();}catch(PessimisticLockingFailureException ex) {lockFailureException = ex;}} while(numAttempts <= this.maxRetries);throw lockFailureException;}
}
对应的Spring配置如下:
一旦您确定使用aop是实现给定需求的最佳方法,您如何决定是使用Spring AOP还是Aspect?是使用@AspectJ注解风格还是Spring XML风格?
如果您选择使用Spring AOP,那么您可以选择【@AspectJ或XML】样式。
XML样式可能是现有Spring用户最熟悉的,并且它是由真正的【pojo支持】(侵入性很低)的。 当使用AOP作为配置企业服务的工具时,XML可能是一个很好的选择(一个很好的理由是您【是否认为切入点表达式】 是需要【独立更改】的一部分配置)。使用XML样式,可以从配置中更清楚地看出系统中存在哪些切面。
XML样式有两个缺点。 首先,它没有将它所处理的需求的实现完全封装在一个地方。 其次,与@AspectJ风格相比,XML风格在它能表达的内容上稍微受到一些限制,不可能在XML中声明的命名切入点进行组合。 例如,在@AspectJ风格中,你可以写如下内容:
@Pointcut("execution(* get*())")
public void propertyAccess() {}@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}
在XML样式中,可以声明前两个切入点:
XML方法的缺点是不能通过组合这些定义来定义“accountPropertyAccess”切入点。
@AspectJ还有一个优点,即@AspectJ切面可以被Spring AOP和AspectJ理解(从而被使用)。 因此,如果您以后决定需要AspectJ的功能来实现额外的需求,您可以轻松地迁移到经典的AspectJ当中。
总的来说,Spring团队更喜欢自定义切面的@AspectJ风格,而不是简单的企业服务配置。
除了通过使用或在配置中声明方面之外,还可以通过编程方式创建通知目标对象的代理。
代码如下,这只是一个小例子,用来看一下spring是怎么封装代理的:
public static void main(String[] args) {AspectJProxyFactory aspectJProxyFactory = new AspectJProxyFactory(new OrderService());aspectJProxyFactory.addAspect(MyAspect.class);IOrderService proxy = (IOrderService)aspectJProxyFactory.getProxy();proxy.order(111);}

👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~
上一篇:C语言文件操作