Spring はカスタム アノテーションを組み合わせて AOP アスペクト関数を実装します
- Spring AOP アノテーションの概要
- @Aspect クイックスタート
- 実行ポイントカット式は、指定されたクラスのメソッドをインターセプトします
- @Pointcut("@annotation(xx)") は、指定されたアノテーションを持つメソッドをインターセプトします
- 共通の注釈
- JoinPoint と ProceedingJoinPoint の概念とメソッドの説明
- プログラミング(実行順序)の優先順位
- ターゲットメソッドの仮パラメータにアクセスします
- エントリーポイントの使用
- 場合
Spring AOP アノテーションの概要
1. Spring の AOP 機能の設定ファイルには、エントリ ポイント、式、通知などの多くの設定を行うことに加えて、アノテーションを使用する方が便利で高速です。特に Spring Boot の登場以降、オリジナルのBeans.xmlやその他の設定ファイルは利用できなくなりましたが、アノテーションプログラミングが推奨されます。
注釈 | 関数 |
---|---|
@側面 | アスペクト宣言。クラス、インターフェース (注釈型を含む)、または列挙型に注釈が付けられます。 |
@ポイントカット | ポイントカット宣言、つまりカットされるターゲット クラスのターゲット メソッド。実行ポイントカット式またはアノテーションを使用して、指定されたアノテーションを持つメソッドのインターセプトを指定できます。value 属性はポイントカット式を指定します。デフォルトは "" で、これは通知アノテーションによって参照されるために使用されるため、通知アノテーションは次のことのみを行う必要があります。このポイントカットに関連付けられます。宣言するだけで、ポイントカット式を繰り返し記述する必要はありません |
@前に | 事前通知は対象のメソッド(ポイントカット)が実行される前に実行されます。value 属性は、バインディング通知のポイントカット式です。ポイントカット ステートメントに関連付けることも、ポイントカット式を直接設定することもできます。注意: このコールバック メソッドで例外がスローされた場合、ターゲット メソッドは実行されなくなり、続行されます。ポストスクリプトを実行するには、「通知」→「例外通知」を選択します。 |
@後 | ポスト通知。ターゲット メソッド (エントリ ポイント) の実行後に実行されます。 |
@AfterReturning | ターゲット メソッド (ポイントカット) が結果を返した後に実行される通知を返します。ポイントカット プロパティ バインディング通知のポイントカット式の優先順位は値より高く、デフォルトは "" です。 |
@AfterThrowing | メソッドが例外をスローした後に実行される例外通知は、リターン通知ポイントカット属性バインディング通知ポイントカット式をスキップすることを意味します。優先度は値より高く、デフォルトは「」です。 注: ターゲット メソッド自体が例外をトライキャッチする場合、引き続きスローされない場合、このコールバック関数には入りません。 |
@その周り | サラウンド通知: ターゲット メソッドの実行の前後に、インターセプターと同様に、ターゲット メソッドが実行を継続するかどうかを制御できるコードを実行します。通常、時間のかかる統計手法、パラメータ検証、その他の操作に使用されます。 |
通常処理:[サラウンド通知 - 前] -> [前通知] -> [戻り通知] -> [後通知] -> [サラウンド通知 - 後]。
2. 上記の AOP アノテーションはすべて、以下に示すように、aspectjweaver 依存関係にあります。
3. Spring ファミリーのバケット プログラミングに慣れている人にとっては、aspectjweaver 依存関係を直接導入する必要はありません。コンポーネントはデフォルトで既に含まれており、AOP 機能を実装するために aspectjweaver を参照しました。言い換えれば、Spring の AOP 関数は、aspectjweaver に依存しているということです。
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
4. AOP の最下層は Spring が提供するダイナミックプロキシ技術によって実装されており、プロキシオブジェクトは実行時に動的に生成され、プロキシオブジェクトのメソッドが実行されると拡張機能が介在し、対象オブジェクトのメソッドが実行されます。機能拡張を完了するために呼び出されます。主に JDK ダイナミック プロキシと Cglib ダイナミック プロキシを使用します。
5. したがって、対象クラスが Spring コンポーネントでない場合はインターセプトできず、クラス名とメソッド名で呼び出された場合もインターセプトできません。
@Aspect クイックスタート
1. @Aspect は、ログ記録、集中例外処理、権限検証、Web パラメータ検証、トランザクション処理などに一般的に使用されます。
2. クラスをアスペクト クラスに変換するには、次の 3 つの手順だけが必要です。
- クラスに @Aspect アノテーションを使用して、それをアスペクト クラスにします
@Service
アスペクト クラスは Spring コンテナによって管理される必要があるため、クラスには、@Repository
、@Controller
などの@Component
アノテーションも必要です。- 通知を受信するようにアスペクト クラスのメソッドをカスタマイズする
3. AOP の意味についてはここでは繰り返しませんが、例を次に示します。
/**
* 切面注解 Aspect 使用入门
* 1、@Aspect:声明本类为切面类
* 2、@Component:将本类交由 Spring 容器管理
* 3、@Order:指定切入执行顺序,数值越小,切面执行顺序越靠前,默认为 Integer.MAX_VALUE
*
* @author wangMaoXiong
* @version 1.0
* @date 2020/8/20 19:22
*/
@Aspect
@Order(value = 999)
@Component
public class AspectHelloWorld {
private static final Logger LOG = LoggerFactory.getLogger(AspectHelloWorld.class);
/**
* @Pointcut :切入点声明,即切入到哪些目标方法。value 属性指定切入点表达式,默认为 ""。
* 用于被下面的通知注解引用,这样通知注解只需要关联此切入点声明即可,无需再重复写切入点表达式
* <p>
* 切入点表达式常用格式举例如下:
* - * com.wmx.aspect.EmpService.*(..)):表示 com.wmx.aspect.EmpService 类中的任意方法
* - * com.wmx.aspect.*.*(..)):表示 com.wmx.aspect 包(不含子包)下任意类中的任意方法
* - * com.wmx.aspect..*.*(..)):表示 com.wmx.aspect 包及其子包下任意类中的任意方法
* </p>
* value 的 execution 可以有多个,使用 || 隔开.
*/
@Pointcut(value =
"execution(* com.wmx.hb.controller.DeptController.*(..)) " +
"|| execution(* com.wmx.hb.controller.EmpController.*(..))")
private void aspectPointcut() {
}
/**
* 前置通知:目标方法执行之前执行以下方法体的内容。
* value:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
* <br/>
* * @param joinPoint:提供对连接点处可用状态和有关它的静态信息的反射访问<br/> <p>
* * * Object[] getArgs():返回此连接点处(目标方法)的参数,目标方法无参数时,返回空数组
* * * Signature getSignature():返回连接点处的签名。
* * * Object getTarget():返回目标对象
* * * Object getThis():返回当前正在执行的对象
* * * StaticPart getStaticPart():返回一个封装此连接点的静态部分的对象。
* * * SourceLocation getSourceLocation():返回与连接点对应的源位置
* * * String toLongString():返回连接点的扩展字符串表示形式。
* * * String toShortString():返回连接点的缩写字符串表示形式。
* * * String getKind():返回表示连接点类型的字符串
* * * </p>
*/
@Before(value = "aspectPointcut()")
public void aspectBefore(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
Signature signature = joinPoint.getSignature();
Object target = joinPoint.getTarget();
Object aThis = joinPoint.getThis();
JoinPoint.StaticPart staticPart = joinPoint.getStaticPart();
SourceLocation sourceLocation = joinPoint.getSourceLocation();
String longString = joinPoint.toLongString();
String shortString = joinPoint.toShortString();
LOG.debug("【前置通知】" +
"args={},signature={},target={},aThis={},staticPart={}," +
"sourceLocation={},longString={},shortString={}"
, Arrays.asList(args), signature, target, aThis, staticPart, sourceLocation, longString, shortString);
}
/**
* 后置通知:目标方法执行之后执行以下方法体的内容,不管目标方法是否发生异常。
* value:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
*/
@After(value = "aspectPointcut()")
public void aspectAfter(JoinPoint joinPoint) {
LOG.debug("【后置通知】kind={}", joinPoint.getKind());
}
/**
* 返回通知:目标方法返回后执行以下代码
* value 属性:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
* pointcut 属性:绑定通知的切入点表达式,优先级高于 value,默认为 ""
* returning 属性:通知签名中要将返回值绑定到的参数的名称,默认为 ""
*
* @param joinPoint :提供对连接点处可用状态和有关它的静态信息的反射访问
* @param result :目标方法返回的值,参数名称与 returning 属性值一致。无返回值时,这里 result 会为 null.
*/
@AfterReturning(pointcut = "aspectPointcut()", returning = "result")
public void aspectAfterReturning(JoinPoint joinPoint, Object result) {
LOG.debug("【返回通知】,shortString={},result=", joinPoint.toShortString(), result);
}
/**
* 异常通知:目标方法发生异常的时候执行以下代码,此时返回通知不会再触发
* value 属性:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
* pointcut 属性:绑定通知的切入点表达式,优先级高于 value,默认为 ""
* throwing 属性:与方法中的异常参数名称一致,
*
* @param ex:捕获的异常对象,名称与 throwing 属性值一致
*/
@AfterThrowing(pointcut = "aspectPointcut()", throwing = "ex")
public void aspectAfterThrowing(JoinPoint jp, Exception ex) {
String methodName = jp.getSignature().getName();
if (ex instanceof ArithmeticException) {
LOG.error("【异常通知】" + methodName + "方法算术异常(ArithmeticException):" + ex.getMessage());
} else {
LOG.error("【异常通知】" + methodName + "方法异常:" + ex.getMessage());
}
}
/**
* 环绕通知
* 1、@Around 的 value 属性:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
* 2、Object ProceedingJoinPoint.proceed(Object[] args) 方法:继续下一个通知或目标方法调用,返回处理结果,如果目标方法发生异常,则 proceed 会抛异常.
* 3、假如目标方法是控制层接口,则本方法的异常捕获与否都不会影响目标方法的事务回滚
* 4、假如目标方法是控制层接口,本方法 try-catch 了异常后没有继续往外抛,则全局异常处理 @RestControllerAdvice 中不会再触发
*
* @param joinPoint
* @return
* @throws Throwable
*/
@Around(value = "aspectPointcut()")
public Object handleControllerMethod(ProceedingJoinPoint joinPoint) throws Throwable {
this.checkRequestParam(joinPoint);
StopWatch stopWatch = StopWatch.createStarted();
LOG.debug("【环绕通知】执行接口开始,方法={},参数={} ", joinPoint.getSignature(), Arrays.asList(joinPoint.getArgs()).toString());
//继续下一个通知或目标方法调用,返回处理结果,如果目标方法发生异常,则 proceed 会抛异常.
//如果在调用目标方法或者下一个切面通知前抛出异常,则不会再继续往后走.
Object proceed = joinPoint.proceed(joinPoint.getArgs());
stopWatch.stop();
long watchTime = stopWatch.getTime();
LOG.debug("【环绕通知】执行接口结束,方法={}, 返回值={},耗时={} (毫秒)", joinPoint.getSignature(), proceed, watchTime);
return proceed;
}
/**
* 参数校验,防止 SQL 注入
*
* @param joinPoint
*/
private void checkRequestParam(ProceedingJoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
if (args == null || args.length <= 0) {
return;
}
String params = Arrays.toString(joinPoint.getArgs()).toUpperCase();
String[] keywords = {
"DELETE ", "UPDATE ", "SELECT ", "INSERT ", "SET ", "SUBSTR(", "COUNT(", "DROP ",
"TRUNCATE ", "INTO ", "DECLARE ", "EXEC ", "EXECUTE ", " AND ", " OR ", "--"};
for (String keyword : keywords) {
if (params.contains(keyword)) {
LOG.warn("参数存在SQL注入风险,其中包含非法字符 {}.", keyword);
throw new RuntimeException("参数存在SQL注入风险:params=" + params);
}
}
}
}
このように、元のビジネス層のコードを変更することなく、AOP機能を利用することで、実行前後や例外発生時に対象のメソッドをキャプチャして実行することができます。
実行ポイントカット式は、指定されたクラスのメソッドをインターセプトします
1. @Pointcut ポイントカット宣言アノテーションとすべての通知アノテーションは、value 属性または pointcut 属性を通じてポイントカット式を指定できます。
2. ポイントカット式は、実行関数を通じて接続ポイントと一致します 構文:execution([メソッド修飾子] 戻り値の型 パッケージ名.クラス名.メソッド名 (パラメーターの型) [例外の種類])
- アクセス修飾子は省略できます。
- 戻り値の型、パッケージ名、クラス名、メソッド名はアスタリスク「*」で表すことができます。
- パッケージ名とクラス名の間の 1 つのドットは現在のパッケージの下のクラスを表し、2 つのドットは現在のパッケージとそのサブパッケージの下のクラスを表します。
- パラメータ リストでは、2 つのドットを使用して、任意の数および任意のタイプのパラメータ リストを表すことができます。3.
ポイントカット式の記述は、より柔軟です。たとえば、* は任意の 1 を表し、... は任意の数値を表し、&& を表します。 | |, ! は論理演算を行いますが、実際の開発ではそれほど多くの追加機能は必要なく、基本的には以下の機能をマスターすれば十分です。
4. 特記事項: カットインクラスを明示的に指定する場合は、そのクラスが存在する必要があり、存在しない場合は起動時にエラーが報告されますが、このときクラス名の前後に * を追加することであいまい包含を示すことができます。
ポイントカット式の一般的な例
タイトル | コンテンツ |
---|---|
実行(* com.wmx.aspect.EmpServiceImpl.findEmpById(Integer)) | com.wmx.aspect.EmpService クラスの findEmpById メソッドと一致し、Integer 型パラメータを受け取ります。 |
実行(* com.wmx.aspect.EmpServiceImpl.findEmpById(*)) | com.wmx.aspect.EmpService クラスの findEmpById メソッドを任意の型のパラメーターと照合します。 |
実行(* com.wmx.aspect.EmpServiceImpl.findEmpById(…)) | パラメータに制限はなく、com.wmx.aspect.EmpService クラスの findEmpById メソッドと一致します。 |
実行(* grp.basic3.se.service.SEBasAgencyService3.editAgencyInfo(…)) || 実行(*grp.basic3.se.service.SEBasAgencyService3.adjustAgencyInfo(…)) | editAgencyInfo メソッドまたはAdjustAgencyInfo メソッドと一致します。 |
@Pointcut(“(execution(* grp.basic3…コントローラ. (…)) && !execution( grp.basic3.BaseExceptionController*.*(…))”) | 一致する grp.basic3 パッケージとそのサブパッケージの名前には、「Controller」クラスのすべてのメソッドが含まれますが、BaseExceptionController で始まるクラスは除外されます。 |
実行(* com.wmx.aspect.EmpService.*(…)) | com.wmx.aspect.EmpService クラスの任意のメソッドと一致します |
実行(* com.wmx.aspect. . (…)) | com.wmx.aspect パッケージ内の任意のクラスの任意のメソッドと一致します (サブパッケージを除く) |
実行(* com.wmx.aspect… . (…)) | com.wmx.aspect パッケージおよびそのサブパッケージ内の任意のクラスの任意のメソッドと一致します。 |
実行(* grp.pm…コントローラー。 (…)) | grp.pm パッケージの下の子孫パッケージ内の「Controller」で終わるクラスのすべてのメソッドと一致します。 |
* com.wmx…コントローラー.*(…)) | com.wmx パッケージおよびそのサブパッケージの下にある、クラス名に「Controller」が含まれるクラス内のメソッド |
*com.wmx。。コントローラ。.*(…)) | 第 1 層と第 2 層のパッケージには com.wmx という名前が付けられ、第 3 層のパッケージには any という名前が付けられ、第 4 層のパッケージにはコントローラ以下の任意のクラスの任意のメソッドという名前が付けられます。 |
@Pointcut(“@annotation(xx)”) は、指定されたアノテーションを持つメソッドをインターセプトします
/**
* @Pointcut :切入点声明,即切入到哪些目标方法。
* execution:可以用于指定具体类中的具体方法
* annotation:匹配拥有指定注解的方法; 只匹配实现类中有注解的方法,不会匹配接口中的注解方法; 如果注解是在类上,而不是方法上,并不会匹配类中的全部方法.
* 用于被下面的通知注解引用,这样通知注解只需要关联此切入点声明即可,无需再重复写切入点表达式
* @annotation 中的路径表示拦截特定注解
*/
@Pointcut("@annotation(com.wmx.annotation.RedisLock)")
public void redisLockPC() {
}
共通の注釈
1.@Before: ポイントカットメソッドの前に実行されます。
- 事前アドバイス: メソッドが実行される前に実行されるアドバイス
- 拡張メソッドの@Before("実行(* パッケージ名. . (…))")
- 上記の式ではポイントカットまたはカットイン式を使用できますが、効果は同じであり、後で繰り返すことはありません。
- ポイントカット メソッドには戻り値に関係する形状がありません。
サンプルコード
@Aspect
public class AuthAspect {
//定义切点
@Pointcut("execution(* com.cnblogs.hellxz.service.*.*(..))")
public void pointCut() {
}
//前置处理
@Before("pointCut()")
public void auth() {
System.out.println("模拟权限检查……");
}
}
2.@After: ポイントカットメソッドの後に実行されます。
- 事後通知: 事後通知は、接続ポイントが完了した後、つまり接続ポイントが結果を返すか例外をスローしたときに実行されます。
- 使い方は@Beforeと同じです
3.@Around: ポイントカットメソッドの周りを実行します。
-
サラウンド通知はすべての通知タイプの中で最も強力で、参加ポイントや参加ポイントを実行するかどうかを完全に制御できます。
サラウンド アドバイスの場合、ジョイン ポイント パラメーターのタイプは ProceedingJoinPoint である必要があります。これは、ジョインポイントをいつ実行するか、実行するかどうかを制御できるようにする JoinPoint のサブインターフェイスです。 -
周囲のアドバイスでは、ProceedingJoinPoint の continue() メソッドを明示的に呼び出して、委任されたメソッドを実行する必要があります。これを忘れると通知は実行されますが、対象のメソッドは実行されません。
-
知らせ注:周囲の通知メソッドは対象メソッド実行後の結果、つまりjoinPoint.proceed();呼び出しの戻り値を返す必要があり、それ以外の場合はnullポインタ例外が発生します。
-
@Around("execution(* パッケージ名.*(...))") または拡張メソッドで pointcut @Around("pointcut()") を使用する
-
受け取ったパラメータのタイプは ProceedingJoinPoint です。このパラメータはアスペクト メソッドの最初の入力パラメータである必要があります。
-
戻り値はオブジェクトです
-
ProceedingJoinPoint
オブジェクトのメソッドを実行する必要がありますproceed
。このメソッドの前後でラップアラウンド処理を行う、いつ実行するかを決定し、メソッドの実行を完全に防ぐことができます -
continueメソッドの戻り値を返す
-
@Around相当于@Before和@AfterReturning功能的总和
-
可以改变方法参数,在proceed方法执行的时候可以传入Object[]对象作为参数,作为目标方法的实参使用。
-
如果传入Object[]参数与方法入参数量不同或类型不同,会抛出异常
-
通过改变proceed()的返回值来修改目标方法的返回值
示例代码
@Aspect
public class TxAspect {
//环绕处理
@Around("execution(* com.cnblogs.hellxz.service.*.*(..))")
Object auth(ProceedingJoinPoint point) {
Object object = null;
try {
System.out.println("事务开启……");
//放行
object = point.proceed();
System.out.println("事务关闭……");
} catch (Throwable e) {
e.printStackTrace();
}
return object;
}
}
4.@AfterRetruning: 在方法返回之前,获取返回值并进行记录操作
- 返回通知:无论连接点是正常返回还是抛出异常,后置通知都会执行。如果只想在连接点返回的时候记录日志,应使用返回通知代替后置通知。
- 和上边的方法不同的地方是该注解除了切点,还有一个返回值的对象名
- 不同的两个注解参数:returning与pointcut,其中pointcut参数可以为切面表达式,也可为切点
- returning定义的参数名作为切面方法的入参名,类型可以指定。如果切面方法入参类型指定Object则无限制,如果为其它类型,- 则当且仅当目标方法返回相同类型时才会进入切面方法,否则不会
- 还有一个默认的value参数,如果指定了pointcut则会覆盖value的值
- 与@After类似,但@AfterReturning只有方法成功完成才会被织入,而@After不管结果如何都会被织入
- 虽然可以拿到返回值,但无法改变返回值
- 在返回通知中访问连接点的返回值
- 在返回通知中,只要将returning属性添加到@AfterReturning注解中,就可以访问连接点的返回值。该属性的值即为用来传入 返回值的参数名称
- 必须在通知方法的签名中添加一个同名参数。在运行时Spring AOP会通过这个参数传递返回值
- 原始的切点表达式需要出现在pointcut属性中
示例代码
@Aspect
public class AfterReturningAspect {
@AfterReturning(returning="rvt",
pointcut = "execution(* com.cnblogs.hellxz.service.*.*(..))")
//声明rvt时指定的类型会限定目标方法的返回值类型,必须返回指定类型或者没有返回值
//rvt类型为Object则是不对返回值做限制
public void log(Object rvt) {
System.out.println("获取目标返回值:"+ rvt);
System.out.println("假装在记录日志……");
}
/**
* 这个方法可以看出如果目标方法的返回值类型与切面入参的类型相同才会执行此切面方法
* @param itr
*/
@AfterReturning(returning="itr",
pointcut="execution(* com.cnblogs.hellxz.service.*.*(..))")
public void test(Integer itr) {
System.out.println("故意捣乱……:"+ itr);
}
}
5.@AfterThrowing: 在异常抛出前进行处理,比如记录错误日志
- 异常通知:只在连接点抛出异常时才执行异常通知
- 将throwing属性添加到@AfterThrowing注解中,也可以访问连接点抛出的异常。Throwable是所有错误和异常类的顶级父类,所 以在异常通知方法可以捕获到任何错误和异常。
- 如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型。然后通知就只在抛出这个类型及其子类的异常时才被执行
- 与@AfterReturning类似,同样有一个切点和一个定义参数名的参数——throwing
- 同样可以通过切面方法的入参进行限制切面方法的执行,e.g. 只打印IOException类型的异常, 完全不限制可以使用Throwable类型
- pointcut使用同@AfterReturning
- 还有一个默认的value参数,如果指定了pointcut则会覆盖value的值
- 如果目标方法中的异常被try catch块捕获,此时异常完全被catch块处理,如果没有另外抛出异常,那么还是会正常运行,不会进入AfterThrowing切面方法
示例代码
@Aspect
public class AfterThrowingAspect {
@Pointcut("execution(* com.cnblogs.hellxz.test.*.*(..))")
public void pointcut() {
}
/**
* 如果抛出异常在切面中的几个异常类型都满足,那么这几个切面方法都会执行
*/
@AfterThrowing(throwing="ex1",
pointcut="pointcut()")
//无论异常还是错误都会记录
//不捕捉错误可以使用Exception
public void throwing(Throwable ex1) {
System.out.println("出现异常:"+ex1);
}
@AfterThrowing(throwing="ex",
pointcut="pointcut()")
//只管IOException的抛出
public void throwing2(IOException ex) {
System.out.println("出现IO异常: "+ex);
}
}
pointcut定义的切点方法在@Before/@After/@Around需要写在双引号中,e.g. @Before(“pointCut()”)
JoinPoint和ProceedingJoinPoint的概念与方法说明
JoinPoint的概念与方法说明
概念
- 顾名思义,连接点,织入增强处理的连接点
- 程序运行时的目标方法的信息都会封装到这个连接点对象中
- 此连接点只读
方法说明 - Object[] getArgs():返回执行目标方法时的参数
- Signature getSignature():返回被增强方法的相关信息,e.g 方法名 etc
- Object getTarget():返回被织入增强处理的目标对象
- Object getThis():返回AOP框架目标对象生成的代理对象
使用 - 在@Before/@After/@AfterReturning/@AfterThrowing所修饰的切面方法的参数列表中加入JoinPoint对象,可以使用这个对象获得整个增强处理中的所有细节
- 此方法不适用于@Around, 其可用ProceedingJoinPoint作为连接点
ProceedingJoinPoint的概念与方法说明
概念
- 是JoinPoint的子类
- 与JoinPoint概念基本相同,区别在于是可修改的
- 使用@Around时,第一个入参必须为ProceedingJoinPoint类型
- 在@Around方法内时需要执行proceed()或proceed(Object[] args)方法使方法继续,否则会一直处于阻滞状态
方法说明 - ProceedingJoinPoint是JoinPoint的子类,包含其所有方法外,还有两个公有方法
- Object proceed():执行此方法才会执行目标方法
- Object proceed(Object[] args):执行此方法才会执行目标方法,而且会使用Object数组参数去代替实参,如果传入Object[]参数与方法入参数量不同或类型不同,会抛出异常
通过修改proceed方法的返回值来修改目标方法的返回值
编入(执行顺序)的优先级
优先级最高的会最先被织入,在退出连接点的时候,具有最高的优先级的最后被织入
当不同切面中两个增强处理切入同一连接点的时候,Spring AOP 会使用随机织入的方式
如果想要指定优先级,那么有两种方案:
- 让切面类实现 org.springframework.core.Ordered接口,实现getOrder方法,返回要指定的优先级
- 切面类使用@Order修饰,指定一个优先级的值,值越小,优先级越高
示例代码
HelloService接口
package zhl.service;
public interface HelloService {
int add();
}
HelloServiceImpl接口
package zhl.service;
import org.springframework.stereotype.Service;
@Service("helloService")
public class HelloServiceImpl implements HelloService{
@Override
public int add() {
System.out.println("运行的代码");
// int i = 1/0;
return 111;
}
LogAspect.java如下
package zhl.service;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
@Aspect
public class LogAspect {
// @Before("bean(helloService)")
// @Before("within(zhl.*)") zhl包下的子类
// @Before("within(zhl..*)") zhl包下的所有子孙类
// @Before("execution(public void zhl..*.*(..))") public 可以省略
@Pointcut("execution(* zhl..*.*(..))")
public void pointcut() {
}
@Before("pointcut()")
public void before(JoinPoint jointPoint){
Object[] args = jointPoint.getArgs();
String methodName = jointPoint.getSignature().getName();
Class<?> targetClass = jointPoint.getTarget().getClass();
System.out.println("[普通前置日志]:方法名称 "+methodName+" 目标对象的类型 "+
targetClass+" 参数 "+ Arrays.toString(args));
}
@AfterReturning(value = "pointcut()",returning = "result")
public void afterReturn(JoinPoint joinPoint,Object result){
String methodName = joinPoint.getSignature().getName();
System.out.println("[普通返回日志]:方法调用完成 方法名:"+methodName+"返回值信息:"+result);
}
@AfterThrowing(value = "pointcut()",throwing = "throwable")
public void throwable(JoinPoint jointPoint,Throwable throwable){
String methodName = jointPoint.getSignature().getName();
System.out.println("[普通异常日志] 方法调用异常 方法名:"+ methodName+"异常信息:"+throwable);
}
@After("pointcut()")
public void after() {
System.out.println("普通后置通知");
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint){
Object result = null;
try {
System.out.println("环绕前置通知");//类似于前置通知
result = proceedingJoinPoint.proceed();
System.out.println("环绕返回通知");//类似于返回通知
}catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕异常通知"+throwable.getMessage());//类似于异常通知
}finally {
System.out.println("环绕后置通知");//类似于后置通知
}
return result;
}
}
无异常存在的执行顺序:环绕前置–> 普通前置–> 目标方法执行–> 普通返回 --> 普通后置–> 环绕返回 -->环绕后置
以下为代码运行结果
环绕前置通知
[普通前置日志]:方法名称 add 目标对象的类型 class zhl.service.HelloServiceImpl 参数 []
运行的代码
[普通返回日志]:方法调用完成 方法名:add返回值信息:111
普通后置通知
环绕返回通知
环绕后置通知
有异常存在的执行顺序:环绕前置–> 普通前置 --> 目标方法执行 -->普通异常 --> 普通后置
(1)目标方法有无输出与输出语句在异常语句上还是下决定,若一开始就出现异常则不会输出“运行的代码”
(2)出现异常由普通异常通知捕捉对其进行处理,异常通知与返回通知是互斥的,有异常无返回,有返回无异常
(3)因为异常已经被普通异常捕捉,环绕异常通知便不在执行
环绕前置通知
[普通前置日志]:方法名称 add 目标对象的类型 class zhl.service.HelloServiceImpl 参数 []
运行的代码
[普通异常日志] 方法调用异常 方法名:add异常信息:java.lang.ArithmeticException: / by zero
普通后置通知
访问目标方法的形参
除了使用JoinPoint或ProceedingJoinPoint来获取目标方法的相关信息外(包括形参),如果只是简单访问形参,那么还有一种方法可以实现
在pointcut的execution表达式之后加入&& args(arg0,arg1)这种方式
@Aspect
public class AccessInputArgs {
@Before("execution(* com.cnblogs.hellxz.test.*.*(..)) && args(arg0, arg1)")
public void access(String arg0, String arg1){
System.out.println("接收到的参数为arg0="+arg0+",arg1="+arg1);
}
}
注意:通过这种方式会只匹配到方法只有指定形参数量的方法,并且,在切面方法中指定的类型会限制目标方法,不符合条件的不会进行织入增强
切入点的使用
定义切入点
通过定义切入点,我们可以复用切点,减少重复定义切点表达式等
切入点定义包含两个部分:
- 切入点表达式
- 包含名字和任意参数的方法签名
使用@Pointcut注解进行标记一个无参无返回值的方法,加上切点表达式
@Pointcut("execution(* com.cnblogs.hellxz.test.*.*(..))")
public void pointcut(){
}
切入点指示符
Spring AOP 支持10种切点指示符:execution、within、this、target、args、@target、@args、@within、@annotation、bean下面做下简记(没有写@Pointcut(),请注意):
-
execution: 用来匹配执行方法的连接点的指示符。
用法相对复杂,格式如下:execution(权限访问符 返回值类型 方法所属的类名包路径.方法名(形参类型) 异常类型)
e.g. execution(public String com.cnblogs.hellxz.test.Test.access(String,String))
权限修饰符和异常类型可省略,返回类型支持通配符,类名、方法名支持*通配,方法形参支持…通配 -
within: 用来限定连接点属于某个确定类型的类。
within(com.cnblogs.hellxz.test.Test)
within(com.cnblogs.hellxz.test.) //包下类
within(com.cnblogs.hellxz.test…) //包下及子包下 -
this和target: this用于没有实现接口的Cglib代理类型,target用于实现了接口的JDK代理目标类型
举例:this(com.cnblogs.hellxz.test.Foo) //Foo没有实现接口,使用Cglib代理,用this
实现了个接口public class Foo implements Bar{…}
target(com.cnblogs.hellxz.test.Test) //Foo实现了接口的情况 -
args: 对连接点的参数类型进行限制,要求参数类型是指定类型的实例。
args(Long) -
@target: 用于匹配类头有指定注解的连接点
@target(org.springframework.stereotype.Repository) -
@args: 用来匹配连接点的参数的,@args指出连接点在运行时传过来的参数的类必须要有指定的注解
@Pointcut("@args(org.springframework.web.bind.annotation.RequestBody)")
public void methodsAcceptingEntities() {
}
-
@within: 指定匹配必须包括某个注解的的类里的所有连接点
@within(org.springframework.stereotype.Repository) -
@annotation: 匹配那些有指定注解的连接点
@annotation(org.springframework.stereotype.Repository) -
bean: 用于匹配指定Bean实例内的连接点,传入bean的id或name,支持使用*通配符
切点表达式组合
使用&&、||、!、三种运算符来组合切点表达式,表示与或非的关系execution(* com.cnblogs.hellxz.test..(…)) && args(arg0, arg1)
案例
1:环绕通知 实现开关目标方法
1、比如某个方法只有管理员才有权限执行,而普通用户是没有权限
2、比如不符合条件的时候,需要终止(跳过)目标方法的执行
3、比如一个组件(Component)专门用于做校验,里面的方法是否校验可以配置在数据库中,当配置为启用时,则继续校验,否则不校验。
/**
* 环绕通知
* 1、@Around 的 value 属性:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
* 2、Object ProceedingJoinPoint.proceed(Object[] args) 方法:继续下一个通知或目标方法调用,返回处理结果,如果目标方法发生异常,则 proceed() 会抛异常.
* 3、假如目标方法是控制层接口,则本方法的异常捕获与否都不会影响业务层方法的事务回滚
* 4、假如目标方法是控制层接口,本方法 try-catch 了异常后没有继续往外抛,则全局异常处理 @RestControllerAdvice 中不会再触发
*/
@Around(value = "aspectPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Signature signature = joinPoint.getSignature();
Object target = joinPoint.getTarget();
System.out.println("环绕通知=" + signature);
System.out.println("环绕通知=" + target);
// 是否继续校验
boolean validation = true;
if (validation) {
// 校验通过后执行目标方法
// 继续下一个切面通知或目标方法调用,返回处理结果,如果目标方法发生异常,则 proceed 会抛异常.
// 如果在调用目标方法或者下一个切面通知前抛出异常,则不会再继续往后走
return joinPoint.proceed(joinPoint.getArgs());
} else {
// 校验未通过时,不继续往后走,直接返回。
// 可以返回提示信息,但是必须保证返回的参数类型与目标方法的返回值类型一致,否则类型转换异常。
// 也可以直接抛异常。
return null;
}
}
2:自定义注解+切面实现统一日志处理
2.1 自定义日志注解
/**
* 自定义操作日志注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OptLog {
/**
* 业务
* @return
*/
String business();
/**
* 操作类型,增删改查
* @return
*/
OptType optType();
}
2.2 声明日志切面组件
import com.alibaba.fastjson.JSONObject;
import com.example.demo.annotation.OptLog;
import com.example.demo.annotation.OptType;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Component
public class OptLogAspect {
private static final Logger LOG = LoggerFactory.getLogger(OptLogAspect.class);
/**
* 声明切入点,凡是使用该注解都经过拦截
*/
@Pointcut("@annotation(com.example.demo.annotation.OptLog)")
public void OptLog() {
}
@Before("OptLog()")
public void doOptLogBefore(JoinPoint proceedingJoinPoint) {
LOG.info("前置通知, 在方法执行之前执行...");
}
@After("OptLog()")
public void doOptLogAfter(JoinPoint proceedingJoinPoint) {
LOG.info("后置通知, 在方法执行之后执行...");
}
@AfterReturning("OptLog()")
public void doOptLogAfterReturning(JoinPoint proceedingJoinPoint) {
LOG.info("返回通知, 在方法返回结果之后执行...");
}
@AfterThrowing("OptLog()")
public void doOptLogAfterThrowing(JoinPoint proceedingJoinPoint) {
LOG.info("异常通知, 在方法抛出异常之后执行...");
}
/**
* 设置环绕通知,围绕着方法执行
*
* @param proceedingJoinPoint
* @return
*/
@Around("OptLog()")
public Object optLogAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
if (method == null) {
return null;
}
// 获取方法名称
String methodName = proceedingJoinPoint.getSignature().getName();
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
// 请求参数名称
String[] parameterNames = discoverer.getParameterNames(method);
// 请求参数值
Object[] paramValues = proceedingJoinPoint.getArgs();
OptLog optLog = method.getAnnotation(OptLog.class);
this.handle(optLog.optType(), optLog.business(), methodName, parameterNames, paramValues);
return proceedingJoinPoint.proceed();
}
/**
* 日志处理
*
* @param optType
* @param business
* @param methodName
* @param parameterNames
* @param paramValues
*/
public void handle(OptType optType, String business, String methodName,
String[] parameterNames, Object[] paramValues) {
JSONObject jsonObject = new JSONObject();
if (parameterNames != null && parameterNames.length > 0) {
for (int i = 0; i < parameterNames.length; i++) {
jsonObject.put(parameterNames[i], paramValues[i]);
}
}
LOG.info("optType:" + optType + ",business:" + business + ", methodName:" + methodName + ", params:" + jsonObject);
}
}
2.3 控制层运行结果
@RestController
@RequestMapping("/user/")
public class UserController {
@OptLog(optType = OptType.CREATE,business = "用户信息")
@RequestMapping("create")
public String createUser(String userName,int age,String address) {
System.out.println("方法执行中...");
return "success";
}
}
2.4 运行结果
15:32:49.494 [http-nio-8080-exec-2] INFO c.e.d.a.OptLogAspect - [handle,91] - optType:CREATE,business:用户信息, methodName:createUser, params:{
"address":"广州市","userName":"阿杰","age":18}
15:32:49.494 [http-nio-8080-exec-2] INFO c.e.d.a.OptLogAspect - [doOptLogBefore,32] - 前置通知, 在方法执行之前执行...
方法执行中...
15:32:49.495 [http-nio-8080-exec-2] INFO c.e.d.a.OptLogAspect - [doOptLogAfterReturning,42] - 返回通知, 在方法返回结果之后执行...
15:32:49.495 [http-nio-8080-exec-2] INFO c.e.d.a.OptLogAspect - [doOptLogAfter,37] - 后置通知, 在方法执行之后执行...
3:自定义注解与切面类
3.1 创建自定义注解
import java.lang.annotation.*;
@Target({
ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TestAnnotation {
String name() default "默认值"; // 允许注解有参数
String age() default "15"; // 允许多个参数
}
3.2 创建一个类,定义方法后使用自定义注解
import com.yh.annotation.OperateLogAnnotation;
import com.yh.annotation.TestAnnotation;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestAOPController {
@RequestMapping("/show3")
@ResponseBody
@TestAnnotation(name = "我把值传进去", age = "24") // 加上自定义注解
public String getById() {
return "hello";
}
}
3.3 定义切面类进行,扫描自定义注解,并对切入点进行处理
import com.yh.annotation.TestAnnotation;
import com.yh.annotation.TestAnnotation;
//import javassist.bytecode.SignatureAttribute;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect // FOR AOP
@Order(-99) // 控制多个Aspect的执行顺序,越小越先执行, 当然也可以不写这注解, 对于写和不写@order的两个切面, 有@order的优先于无@order的执行; 都有@order时, 越小越执先执行
@Component
public class TestAspect {
// 可以参考若依的自定义注解。自定义注解一般使用@annotation
// @Before可以有两种写法, @annotation(形参test),
@Before("@annotation(test)")// 拦截被TestAnnotation注解的方法;如果你需要拦截指定package指定规则名称的方法,可以使用表达式execution(...)
public void beforeTest(JoinPoint point, TestAnnotation test) throws Throwable {
System.out.println("beforeTest:" + test.name()); // 直接获取注解参数
//test.name()和test.age()
}
@After("@annotation(test)")
public void afterTest(JoinPoint point, TestAnnotation test) {
System.out.println("afterTest:" + test.name()); // 直接获取注解参数
}
// 可以控制方法运行, 同时修改入参和返回值
@Around("@annotation(test)") // test表示aroundTest方法中的test入参
public Object aroundTest(ProceedingJoinPoint pjp, TestAnnotation test) throws Throwable {
System.out.println("aroundTest:" + test.value());
// 获取入参并修改
Object[] args = pjp.getArgs();
args[0] = "";
// 传入修改后的参数, 并继续执行
Object res = pjp.proceed(args);
// 修改返回值
return res.toString() + res.toString();
}
/*
// 指定切面
@Pointcut("@annotation(com.yh.annotation.TestAnnotation)")
public void annotationPointCut() {
}
// @Before可以有两者写法, @annotation(函数名annotationPointCut)
@Before("annotationPointCut()")
public void before(JoinPoint joinPoint) {
MethodSignature sign = (MethodSignature) joinPoint.getSignature();
Method method = sign.getMethod();
TestAnnotation annotation = method.getAnnotation(TestAnnotation.class); // 获取指定注解实例
System.out.println("打印:" + annotation.name() + " 前置日志1"); // 获取注解实例的参数
}
@After("annotationPointCut()")
public void afterTTT(JoinPoint point) {
MethodSignature sign = (MethodSignature) point.getSignature();
Method method = sign.getMethod();
TestAnnotation annotation = method.getAnnotation(TestAnnotation.class); // 获取指定注解实例
System.out.println("打印自带参数:" + annotation.age() + " 后置日志1"); // 获取注解实例的参数
}
*/
}
4. After应用实例
4.1 使用After增强处理
Spring还提供了一个After增强处理,它与AfterReturning优点类似,但也有区别:
-
AfterReturning增强处理只有在目标方法正确完成后才会被织入
-
After增强处理不管目标方法如何结束(正确还是异常),它都会被织入
正是因为这个特点,因此After增强处理必须准备处理正常返回和异常返回两种情况,这种增强处理通常用于释放资源。使用@After注解标注一个方法,即可将该方法转换为After增强处理。使用@After注解是需要指定一个value属性,用于指定该增强处理的切入点,既可以是一个已有的切入点,也可以直接定义切入点表达式。
在com.abc.advice包下面增加AfterAdviceTest,这个类定义了一个After增强处理:
@Aspect
public class AfterAdviceTest {
@After(value="execution(* com.abc.servie.impl.*.afterAdvice*(..))")
public void releaseResource() {
System.out.println("模拟释放数据库连接");
}
}
并在AdviceManager类中增加以下内容:
//将被AfterAdvice的releaseResource方法匹配
public void afterAdvice() {
System.out.println("方法: afterAdvice");
}
上面定义了一个After增强处理,不管切入点的目标方法如何结束,该增强处理都会被织入。
4.2 使用Around增强处理
-
@Around注解用于标注Around增强处理,它近似等于Before增强处理和AfterReturning增强处理的总和,Around增强处理既可以在执行目标方法前织入增强动作,也可以在目标方法之后织入增强动作。
-
与@Before和@AfterReturning不同的是,@Around甚至可以决定目标方法在什么时候执行,如何执行,甚至可以完全阻止目标方法的执行。@Around可以修改目标方法的参数值,也可以修改目标方法的返回值。
-
@Around的功能虽然强大,但通常需要在线程安全的环境下使用,因此,如果使用普通的@Before和@AfterReturning就能解决的问题,就没有必要使用@Around了。如果需要目标方法执行之前和执行之后共享某种数据状态,则应该考虑使用@Around;尤其是需要使用增强处理阻止目标方法的执行,或者需要改变目标方法的参数和执行后的返回值时,就只能使用@Around了。
-
可以想象,使用@Around时,也需要指定一个value属性,这个属性依然是用于指定切入点。另外,当定义一个Around增强处理时,该方法的第一个形参必须是ProceedingJoinPoint类型(就是说至少包含一个形参),在增强处理方法体内,调用ProceedingJoinPoint的proceed()方法才会执行目标方法——这就是Around增强处理可以完全控制目标方法的执行时机、如何执行的关键,如果增强处理的方法体内没有调用这个proceed()方法,则目标方法不会执行。
-
调用proceed()方法时,还可以传入一个Object[]对象,该数组中的值将被传入目标方法作为执行方法的实参。因此我们可以通过这个参数,修改方法的参数值。
在com.abc.advice包下面增加AroundAdviceTest,这个类定义了一个Around增强处理:
package com.abc.advice;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class AroundAdviceTest {
@Around(value="execution(* com.abc.service.*.around*(..))")
public Object process(ProceedingJoinPoint point) throws Throwable {
System.out.println("模拟执行目标方法前的增强处理:事务开始...");
//修改目标方法的参数
String[] params = new String[]{
"param1"};
//执行目标方法,并保存目标方法执行后的返回值
Object returnValue = point.proceed(params);
System.out.println("模拟执行目标方法后的增强处理:事务结束...");
//返回修改后的返回值
return "方法实际返回值:" + returnValue + ",这是返回值的后缀";
}
}
上面定义了一个AroundAdviceTest切面,该切面包含了一个Around增强处理:process()方法,该方法中第一行代码用于模拟调用目标方法之前的处理,第二行修改了目标方法的第一个参数,接下来调用目标方法,后面模拟调用目标方法之后的处理和对返回值的修改。正如前面说的,通过这个process方法,我们可以增加类似于@Before和@AfterReturning的增强处理,可以决定什么时候执行目标方法,可以修改目标方法的参数值,还可以修改目标方法的返回值,真是想做什么就做什么啊!
在AdviceManager类中增加以下内容:
//将被AroundAdvice的process方法匹配
public String aroundAdvice(String param1) {
System.out.println("方法: aroundAdvice");
return param1;
}
メソッド呼び出しを com.abc.main.AOPTest に追加して、ポイントカットをトリガーします。
String result = manager.aroundAdvice("param1");
System.out.println("返回值:" + result);
ProceedingJoinPoint の continue() メソッドを呼び出すとき、渡された Object[] パラメータの値がターゲット メソッドのパラメータとして使用されることに注意してください。この配列の長さがターゲットのパラメータの数と等しくない場合は、対象のメソッドのパラメータの型が一致しない場合、プログラム内で例外が発生します。