Spring AOP の概念とその使用法

目次

AOPの概要

AOPとは何ですか?

Spring AOPとは何ですか?

Spring AOP クイック スタート

1.AOP依存関係を導入する

2.AOPプログラムを書く

Spring AOP のコア概念 

1. 切断ポイント

2.接続箇所

3.通知

4. セクショニング

通知タイプ

予防:

@PointCut (ポイントカットを定義)

アスペクト優先 @Order

ポイントカット式:

実行式

ポイントカット式の例

@アノテーション

1. カスタム注釈を作成する

2. @アノテーション式を使用してポイントカットを記述する


AOPの概要

        AOP は Spring フレームワークの 2 番目に大きなコアです (1 番目に大きなコアは IoC です。Spring IoC と DI の使用法 を読むことをお勧めします)。

AOPとは何ですか?

        アスペクト指向プログラミング

        アスペクト指向プログラミングとは何ですか? アスペクトは特定のタイプの問題を指します。そのため、AOP はメソッド指向プログラミングとしても理解できます。メソッド指向プログラミングとは何ですか? たとえば、「 「ログイン検証」は特定の種類の問題。ログイン検証インターセプタは、「ログイン検証」などの問題を統合して処理します ( インターセプタの使用の詳細な説明 を参照することをお勧めします)。インターセプターも AOP のアプリケーションです。AOP はアイデアであり、インターセプターは AOP アイデアの実装です。Spring フレームワークはこのアイデアを実装し、インターセプター テクノロジに関連するインターフェイスを提供します。同様に、統一されたデータ戻り形式と統一された例外処理 (読むことをお勧めします) Spring Boot Unified Data Return Format) も、AOP の考え方の実装です。簡単に言えば、 AOP は一種です。思考、つまり特定の種類のものを集中的に処理することです。

         AOP の役割:ソース コードを変更せずに既存のメソッドを強化できます (非侵襲的: 分離)

Spring AOPとは何ですか?

        AOP は考え方であり、Spring AOP、AspectJ、CGLIB など多くの実装方法があります。Spring AOP もその実装方法の 1 つです。

Spring AOP クイック スタート

1.AOP依存関係を導入する

        pom.xml ファイルに構成を追加します

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.AOPプログラムを書く

@Aspect //标识这是一个切面类
@Component  //将 AspectDemo 的对象交给 Spring 进行管理,切面类中的通知方法才能对指定的目标方法起作用
@Slf4j
public class AspectDemo {
    //环绕通知(@Around)
    //通知方法作用于目标方法执行前和执行后
    @Around("execution(* com.yulin.spring_aop_demo.Controller.*.*(..))")
    // point 参数表示目标方法
    public Object around_AOP(ProceedingJoinPoint point) throws Throwable {
        log.info("执行 around_AOP 方法,目标方法前");
        //执行目标方法,并且要用一个变量接收目标方法的值,然后再让通知方法返回
        Object result=point.proceed();
        log.info("执行 around_AOP 方法,目标方法后");
        return result;
    }
}

        上記の手順を簡単に説明すると、次のようになります。

        @Aspect: これがアスペクト クラスであることを示します

        @Component: 5 つのアノテーションの 1 つである IoC の中心的な考え方は、アスペクト クラス AspectDemo のオブジェクトが管理のために Spring に渡されることを意味します。このアノテーションは、アスペクト クラスの通知メソッドが指定されたターゲットメソッドで作業します。

        @Slf4j: lombok ツール クラスによって提供されるアノテーションは、ログの出力に使用されるログ オブジェクト ログを作成します。

        @Around: サラウンド通知, 対象メソッドの前後に実行されます. どのメソッドが強化されるかを次の式で示します. (対象メソッドを指定します)

        ProceedingJoinPoint 型のオブジェクト point はターゲット メソッドを表し、point.proceed() はターゲット メソッドの実行を表します

Spring AOP のコア概念 

1. 切断ポイント

        PointCut は、「カット ポイント」ポイントカットとも呼ばれ、グループのルールを提供し、簡単に言うと、パペットの機能を強化するようにプログラムに指示します。 。上記の式の実行(* com.yulin.spring_aop_demo.Controller.*.* (.. )) はポイントカット式です。カットオフ ポイントは、式を通じてどのターゲット メソッドの機能を強化するかを指定することです

2.接続箇所

        ポイントカット式ルールを満たすメソッドは接続ポイントです。簡単に言えば: 拡張する必要があるすべてのターゲット メソッドは接続ポイントです 、上記のコードでは、 com.yulin.spring_aop_demo.Controller パスの下にあるすべてのターゲット メソッドが接続ポイントです。

        接点と接続点の関係:接続点は、接線式を満たす要素です。接点は、多数の接続点を保存した集合とみなすことができます。簡単に言うと、 カット ポイントは強化されるすべてのターゲット メソッドを表し、 各ターゲット メソッドは接続ポイントです。

3.通知

         通知は実行すべき特定の作業であり、反復ロジック、つまり共通関数 (最終的にはメソッドとして具体化される) を指します。簡単に言えば: 私たちは、対象のメソッドに追加された新しい関数は notification です。新しい関数のコードはメソッド内に記述されます。このメソッドは notification メソッドと呼ばれます。

4. セクショニング

        アスペクト = ポイントカット + アドバイス

        アスペクトを通じて、現在の AOP プログラムがどのメソッドをターゲットにする必要があるか、どの新しい関数を追加する必要があるかを記述することができます。以下の図に示すように、完全な通知メソッドはアスペクトであり、通知メソッドが配置されているクラスはアスペクトクラス。

通知タイプ

        通知とは何かについて説明しましたが、通知の種類について学びましょう。@Around は通知の種類の 1 つで、周囲の通知を意味します。 

        Spring AOPの通知タイプは以下のとおりです。

         • @Around: サラウンド通知。このアノテーションがマークされた通知メソッドは、ターゲット メソッドの前後で実行されます。 

        • @Before: 事前通知。このアノテーションがマークされた通知メソッドはターゲット メソッドの前に実行されます。

        • @After: 通知後、このアノテーションがマークされた通知メソッドは、例外があるかどうかに関係なく、ターゲット メソッドの後に実行されます。

        • @AfterReturning: リターン後の通知。このアノテーションが付けられた通知メソッドは、ターゲット メソッドの後に実行されます。例外が発生した場合は実行されません。 

        • @AfterThrowing: 例外後の通知。このアノテーションがマークされた通知メソッドは、ターゲット メソッドで例外が発生した後に実行されます。

        次のコードを通じて、これらの通知タイプについて詳しく知ることができます。

@Aspect //标识这是一个切面类
@Component  //将 AspectDemo 的对象交给 Spring 进行管理,切面类中的通知方法才能对指定的目标方法起作用
@Slf4j
public class AspectDemo {
    //前置通知(@Before)
    //通知方法作用于目标方法执行前
    @Before("execution(* com.yulin.spring_aop_demo.Controller.*.*(..))")
    public void before_AOP(){
        log.info("执行 before_AOP 方法");
    }

    //后置通知(@After)
    //通知方法作用于目标方法执行后
    @After("execution(* com.yulin.spring_aop_demo.Controller.*.*(..))")
    public void after_AOP(){
        log.info("执行 after_AOP 方法");
    }

    //返回后通知(@AfterReturning)
    //通知方法作用于目标方法“正确”返回后
    @AfterReturning("execution(* com.yulin.spring_aop_demo.Controller.*.*(..))")
    public void afterReturning_AOP(){
        log.info("执行 afterReturning_AOP 方法");
    }

    //抛出异常后通知(@AfterThrowing)
    //通知方法作用于目标方法抛出异常后
    @AfterThrowing("execution(* com.yulin.spring_aop_demo.Controller.*.*(..))")
    public void afterThrowing_AOP(){
        log.info("执行 afterThrowing_AOP 方法");
    }

    //环绕通知(@Around)
    //通知方法作用于目标方法执行前和执行后
    @Around("execution(* com.yulin.spring_aop_demo.Controller.*.*(..))")
    // point 参数表示目标方法
    public Object around_AOP(ProceedingJoinPoint point) throws Throwable {
        log.info("执行 around_AOP 方法,目标方法前");
        //执行目标方法,并且要用一个变量接收目标方法的值,然后再让通知方法返回
        Object result=point.proceed();
        log.info("执行 around_AOP 方法,目标方法后");
        return result;
    }
}

        1. com.yulin.spring_aop_demo.Controller パス配下の対象メソッドが正常に実行された場合:

        プログラムが正常に実行されている場合、@AfterThrowing で識別される通知メソッドは実行されません。上図からわかるように、@Around で識別される通知メソッドには、「前ロジック」と「後ロジック」の 2 つの部分が含まれています。 「前ロジック」は @Before とマークされた通知メソッドの前に実行され、「事後ロジック」は @After とマークされた通知メソッドより後に実行されます。

        2. com.yulin.spring_aop_demo.Controller パス下のターゲット メソッドの実行中に例外が発生した場合:

· @AfterReturning で識別される通知メソッドは実行されませんが、@AfterThrowing で識別される通知メソッドは実行されます。

対象メソッドが正常に返らなかったため、@Around サラウンド通知のポストロジックは実行されませんでした。

予防:

        1.@Around サラウンド通知では、元のメソッドの実行を許可するために ProceedingJoinPoint.proceed() を呼び出す必要があります。他の通知では、ターゲット メソッドの実行を考慮する必要はありません。このコードより前のコードは、サラウンド通知の前ロジックです。コード 次のコードは、サラウンド通知のポストロジックです。

        2. @Around サラウンド通知メソッドの戻り値は、対象メソッドの戻り値を受け取るために Object で指定しないと、対象メソッド実行後に戻り値が取得できません。

@PointCut (ポイントカットを定義)

       上記のコードには問題があります。つまり、多数の繰り返しポイントカット式が存在します。式 それを抽出し、使用する必要があるときにエントリ ポイント式を引用するだけです。

        上記のコードは次のように変更できます。

@Aspect //标识这是一个切面类
@Component  //将 AspectDemo 的对象交给 Spring 进行管理,切面类中的通知方法才能对指定的目标方法起作用
@Slf4j
public class AspectDemo {
    //定义切点
    @Pointcut("execution(* com.yulin.spring_aop_demo.Controller.*.*(..))")
    private void pt(){}
    //前置通知(@Before)
    //通知方法作用于目标方法执行前
    @Before("pt()")
    public void before_AOP(){
        log.info("执行 before_AOP 方法");
    }

    //后置通知(@After)
    //通知方法作用于目标方法执行后
    @After("pt()")
    public void after_AOP(){
        log.info("执行 after_AOP 方法");
    }

    //返回后通知(@AfterReturning)
    //通知方法作用于目标方法“正确”返回后
    @AfterReturning("pt()")
    public void afterReturning_AOP(){
        log.info("执行 afterReturning_AOP 方法");
    }

    //抛出异常后通知(@AfterThrowing)
    //通知方法作用于目标方法抛出异常后
    @AfterThrowing("pt()")
    public void afterThrowing_AOP(){
        log.info("执行 afterThrowing_AOP 方法");
    }

    //环绕通知(@Around)
    //通知方法作用于目标方法执行前和执行后
    @Around("pt()")
    // point 参数表示目标方法
    public Object around_AOP(ProceedingJoinPoint point) throws Throwable {
        log.info("执行 around_AOP 方法,目标方法前");
        //执行目标方法,并且要用一个变量接收目标方法的值,然后再让通知方法返回
        Object result=point.proceed();
        log.info("执行 around_AOP 方法,目标方法后");
        return result;
    }
}

        注: ポイントカット定義がプライベートで変更されると、そのポイントカット定義は現在のアスペクト クラスでのみ使用できます。他のアスペクト クラスも現在のポイントカット定義を使用する場合、次のことが必要になります。 private から public に変更します。参照メソッドは次のとおりです: 完全修飾クラス名.メソッド名 ()

アスペクト優先 @Order

        プロジェクト内で複数のアスペクト クラスを定義し、これらのアスペクト クラスのエントリ ポイントがすべて同じターゲット メソッドに一致する場合、ターゲット メソッドが実行されると、これらのアスペクトのクラス内のすべての通知メソッドが実行されます。これらの通知方法は?

        このプログラムを使用して以下を検証します。

        複数のアスペクト クラスを定義します: AspectDemo1 と AspectDemo2。簡単にするために、@Before と @After の 2 つの通知のみを記述します。

アスペクトデモ1:

@Aspect
@Component
@Slf4j
public class AspectDemo1 {
    @Before("com.yulin.spring_aop_demo.Aspect.AspectDemo.pt()")
    public void before_AOP(){
        log.info("执行 AspectDemo1 -》 before_AOP 方法");
    }

    @After("com.yulin.spring_aop_demo.Aspect.AspectDemo.pt()")
    public void after_AOP(){
        log.info("执行 AspectDemo1 -》 after_AOP 方法");
    }

}

アスペクトデモ2:

@Aspect
@Component
@Slf4j
public class AspectDemo2 {
    @Before("com.yulin.spring_aop_demo.Aspect.AspectDemo.pt()")
    public void before_AOP(){
        log.info("执行 AspectDemo2 -》 before_AOP 方法");
    }

    @After("com.yulin.spring_aop_demo.Aspect.AspectDemo.pt()")
    public void after_AOP(){
        log.info("执行 AspectDemo2 -》 after_AOP 方法");
    }

}

        上記の 2 つのアスペクト クラスの通知メソッドはすべて、ターゲット メソッドの同じバッチに対して動作します。ターゲット メソッドの実行後の通知メソッドの実行順序を観察してみましょう。

        

2023-12-04 11:55:07.539  INFO 28864 --- [nio-8080-exec-1] c.y.spring_aop_demo.Aspect.AspectDemo1   : 执行 AspectDemo1 -》 before_AOP 方法
2023-12-04 11:55:07.539  INFO 28864 --- [nio-8080-exec-1] c.y.spring_aop_demo.Aspect.AspectDemo2   : 执行 AspectDemo2 -》 before_AOP 方法
2023-12-04 11:55:07.545  INFO 28864 --- [nio-8080-exec-1] c.y.spring_aop_demo.Aspect.AspectDemo2   : 执行 AspectDemo2 -》 after_AOP 方法
2023-12-04 11:55:07.545  INFO 28864 --- [nio-8080-exec-1] c.y.spring_aop_demo.Aspect.AspectDemo1   : 执行 AspectDemo1 -》 after_AOP 方法

        上記のログからわかるように:

        複数のアスペクト クラスがある場合、デフォルトの順序はアスペクト クラスのクラス名に基づきます。 

        • @Before Notice: 最も高いランクのキャラクターが最初に実行されます。

        • @通知後: 上位の単語ランキングは通知後に実行されます。

        しかし、このメソッドは管理が不便です。クラス名にはまだ特定の意味があります。このメソッドは制御できません。Spring は、これらのアスペクト通知の実行順序を制御するための新しいアノテーションを提供します。 : @Order

それの使い方:

@Aspect
@Component
@Order(2)
public class AspectDemo1 {
 //...代码省略 
}
@Aspect
@Component
@Order(1)
public class AspectDemo2 {
 //...代码省略 
}

        優先度を設定した後、ターゲット メソッドに戻って通知メソッドの実行を開始すると、結果のログは次のようになります。

2023-12-04 12:07:38.540  INFO 34560 --- [nio-8080-exec-1] c.y.spring_aop_demo.Aspect.AspectDemo2   : 执行 AspectDemo2 -》 before_AOP 方法
2023-12-04 12:07:38.541  INFO 34560 --- [nio-8080-exec-1] c.y.spring_aop_demo.Aspect.AspectDemo1   : 执行 AspectDemo1 -》 before_AOP 方法
2023-12-04 12:07:38.546  INFO 34560 --- [nio-8080-exec-1] c.y.spring_aop_demo.Aspect.AspectDemo1   : 执行 AspectDemo1 -》 after_AOP 方法
2023-12-04 12:07:38.546  INFO 34560 --- [nio-8080-exec-1] c.y.spring_aop_demo.Aspect.AspectDemo2   : 执行 AspectDemo2 -》 after_AOP 方法

        AspectDemo2 の優先度を 1、AspectDemo1 の優先度を 2 に設定すると、AspectDemo2 の優先度が AspectDemo1 よりも高くなるため、AspectDemo2 の before 通知が先に実行され、after 通知が後に実行されます。

        上記のプログラムの結果から、次の結論を導き出すことができます。 

        @Order アノテーションはアスペクト クラスを識別します。実行順序は次のとおりです。:

         • @Before notification: 数値が小さいほど優先度が高く、最初に実行されます。

         • @After notification: 数値が大きいほど優先度が低く、最初に実行されます。

ポイントカット式:

        上記のコードでは、ポイントカット式を使用してポイントカットを記述しています (ターゲット メソッドの場所を記述しています)。以下にポイントカット式の構文を紹介します。ポイントカット式には一般的に 2 つのタイプがあります。

1. メソッドのシグネチャに従って実行(......) が一致します。

2.@annotation(....) 注釈に基づいて一致します

実行式

        Execution() は最も一般的に使用されるポイントカット式であり、ターゲット メソッドと一致させるために使用されます。構文は次のとおりです。

実行(<アクセス修飾子> <戻り値の型> <パッケージ名.クラス名.メソッド(メソッドパラメータ)><例外>)、アクセス修飾子と例外は省略可能

アクセス修飾子: private、public、protected、ターゲット メソッドのアクセス修飾子と一致する

ポイントカット式はワイルドカード式をサポートしています。

1. *: 任意の文字と一致し、 1 つの 要素 (戻り値の型、パッケージ、クラス名、メソッドまたはメソッド パラメータ) のみと一致します < /span> 

        a. パッケージ名に * を使用して、任意のパッケージを示します (パッケージの 1 つの層に 1 つの * を使用します)。

        b. クラス名に * を使用して、任意のクラスを示します 

        c. 戻り値には * を使用して戻り値のタイプを示します 

        d. メソッド名に * を使用して、任意のメソッドを示します 

        e. パラメータは、任意のタイプのパラメータを表すために * を使用します

..: 複数の連続する任意の記号と一致し、パッケージの任意のレベル、または任意のタイプおよび任意の数のパラメータのワイルドカードとして使用できます。

        a. .. を使用して、このパッケージとこのパッケージ内のすべてのサブパッケージを識別するためのパッケージ名を構成します

        b. .. を使用して、任意のタイプの任意の数のパラメータに一致するようにパラメータを構成できます

ポイントカット式の例

TestController クラスでの公開変更。戻り値の型は String、メソッド名は t1、メソッドにはパラメーターがありません。

execution(public String com.example.demo.controller.TestController.t1())

コントローラー パッケージ内のすべてのクラスのすべてのメソッドと一致します。

execution(* com.example.demo.controller.*.*(..))

すべてのパッケージの TestController クラスのすべてのメソッドと一致します。

execution(* com..TestController.*(..))

com.example.demo パッケージおよび子孫パッケージにあるすべてのクラスのすべてのメソッドと一致します。

execution(* com.example.demo..*(..))

@アノテーション

        実行式はルールに適していますが、TestController の t1() や UserController の u1() など、複数の不規則なメソッドを照合したい場合はどうすればよいでしょうか。

        現時点では、実行などのポイントカット式を使用して説明するのはあまり便利ではありませんが、カスタム アノテーションと別のポイントカット式 @annotation を使用して、この種のポイントカットを説明できます。

       実装手順:

        1. カスタムアノテーションを書く

        2. @annotation 式を使用してカットポイントを記述します

        3. 接続ポイントメソッドにカスタムアノテーションを追加する

1. カスタム注釈を作成する

        アノテーション クラスを作成します (クラス ファイルの作成と同じプロセスで、[アノテーション] を選択するだけです)

@Target(ElementType.METHOD) //表示自定义注解的作用范围,即该注解可以⽤在什么地⽅.
@Retention(RetentionPolicy.RUNTIME) //表示自定义注解的生命周期,即注解被保留的时间⻓短
public @interface MyAspectAnnotation { }

        @Target アノテーションは、カスタム アノテーションのスコープ、つまりアノテーションを使用できる場所を設定するために使用されます。上記のコードは、アノテーションがメソッドで使用されることを示しています

        一般的に使用される値:

        ElementType.TYPE: クラス、インターフェース (注釈タイプを含む)、または enum 宣言を記述するために使用されます。 

        ElementType.METHOD:説明メソッド

        ElementType.PARAMETER:パラメータの説明

        ElementType.TYPE_USE: 任意のタイプをマークできます

        @Retention アノテーションでは、カスタム アノテーションのライフ サイクル、つまりアノテーションが保持される期間を設定します。上記のコードは、プログラムの実行時にアノテーションが常に存在することを意味します。実行中

        一般的に使用される値:

        1. RetentionPolicy.SOURCE: アノテーションがソース コード内にのみ存在し、バイトコードにコンパイルされた後に破棄されることを示します。これは、アノテーションの情報が実行時に取得できず、コンパイル時にのみ使用できることを意味します。例: 、@SuppressWarnings、lombok によって提供されるアノテーション @Data および @Slf4j

        2. RetentionPolicy.CLASS: コンパイル時のアノテーション. アノテーションがソース コードおよびバイトコードに存在するが、実行時に破棄されることを示します. これは、アノテーションがコンパイル時およびバイトコード情報のリフレクションを通じて取得できることを意味しますが、取得することはできません通常、一部のフレームワークやツールのアノテーションとして使用されます。

        3. RetentionPolicy.RUNTIME: ランタイム アノテーション. アノテーションがソース コード、バイトコード、およびランタイムに存在することを示します。これは、コンパイル時、バイトコード、および実際のランタイムでのリフレクションを通じて取得できることを意味します。このアノテーションの情報は、通常、次の目的で使用されます。 Spring の @Controller @ResponseBody など、実行時に処理する必要があるいくつかのアノテーション

2. @アノテーション式を使用してポイントカットを記述する

        私はこのように理解することを好みます: @annotation アノテーションを介してアスペクト クラスの通知メソッドをアノテーションにアタッチし、ターゲット メソッドでアノテーションを使用すると、次のようになります。ターゲットメソッドを実行するときに便利です。 アノテーションに添付された通知メソッドを実行します。

@Aspect
@Component
@Slf4j
public class MyAspectDemo {
    // @annotation 注解中的参数是通知方法要绑定的注解位置
    //如下代码就将 beforeAOP()通知方法绑定到了 MyAspectAnnotation 注解上
    @Before("@annotation(com.yulin.spring_aop_demo.Annotation.MyAspectAnnotation)")
    public void beforeAOP(){
        log.info("执行 MyAspectDemo -》 beforeAOP 通知方法");
    }
    @After("@annotation(com.yulin.spring_aop_demo.Annotation.MyAspectAnnotation)")
    public void afterAOP(){
        log.info("执行 MyAspectDemo -》 beforeAOP 通知方法");
    }
}

        定義されたアノテーション @MyAspectAnnotation を使用するインターフェース クラスを定義します。 

@RequestMapping("/userController")
@RestController
@Slf4j
public class userController {
    @RequestMapping("/getUserName")
    public String getUserName(){
        log.info("进入接口 getUserName ");
        return "小三";
    }
    @MyAspectAnnotation
    @RequestMapping("/getPassword")
    public String getPassword(){
        log.info("进入接口 getPassword ");
        return "123";
    }
}

        「/userController/getUserName」パスにアクセスすると、次のログが取得されます。

2023-12-04 16:33:01.873  INFO 18364 --- [nio-8080-exec-1] c.y.s.Controller.userController          : 进入接口 getUserName 

        ログから、getUserName() メソッドに @MyAspectAnnotation アノテーションが追加されていないため、インターフェイスにアクセスした後に通知メソッドが実行されないことがわかります。

         「/userController/getPassword」パスにアクセスすると、次のログが取得されます。

2023-12-04 16:35:47.123  INFO 18364 --- [nio-8080-exec-4] c.y.spring_aop_demo.Aspect.MyAspectDemo  : 执行 MyAspectDemo -》 beforeAOP 通知方法
2023-12-04 16:35:47.123  INFO 18364 --- [nio-8080-exec-4] c.y.s.Controller.userController          : 进入接口 getPassword 
2023-12-04 16:35:47.123  INFO 18364 --- [nio-8080-exec-4] c.y.spring_aop_demo.Aspect.MyAspectDemo  : 执行 MyAspectDemo -》 beforeAOP 通知方法

        ログを見ると、 getPassword() メソッドに @MyAspectAnnotation アノテーションが追加されているため、インターフェースにアクセスした後、アノテーションに付加された通知メソッドが実行されることがわかります。

        Spring AOPの実装方法(共通テスト問題) 

        1. アノテーション @Aspect を基に(上記内容を参照) 

        2. カスタム アノテーションに基づく (カスタム アノテーション @annotation セクションの内容を参照)

おすすめ

転載: blog.csdn.net/q322359/article/details/134777068