qinfengge

qinfengge

醉后不知天在水,满船清梦压星河
github
email
telegram

Javaの非同期と戻り値

最近の仕事で、いくつかの知識を学びましたが、非常に役立つものでしたので、いくつかのブログを書いて記録することにしました。

先週、新しい要件があり、プログラムのエラーを取得し、責任者に通知を送信する必要がありました。実現自体は難しくありませんが、優雅に実装する方法が難しいです。ちょうど 2 つの知識を見たばかりで、それを組み合わせることで完璧に要件を実現できます。

グローバル例外処理#

グローバル例外処理は非常に簡単で、2 つのアノテーションが必要です。@RestControllerAdvice@ExceptionHandler です。

まず、@RestControllerAdvice の説明は @RestControllerAdvice は、@ControllerAdvice と @ResponseBody からなるコンビネーションアノテーションであり、@ControllerAdvice は @Component を継承しているため、@RestControllerAdvice は本質的に Component です。

次に、@ExceptionHandler は例外インターセプターであり、@ExceptionHandler({Exception.class}) *//どの例外クラスをキャッチするかを宣言* を使用できます。

これら 2 つのアノテーションを組み合わせることで、グローバルな例外インターセプターが作成されます。

@RestControllerAdvice
public class GlobalController {

    @ExceptionHandler
    public void notifyWeChat(Exception e) throws 
        log.error(e)
    }

ボット通知#

さて、グローバルな例外インターセプターができたので、プログラムがエラーを出すと、エラーメッセージをキャッチできます。しかし、責任者に通知を送信する必要があります。優雅な通知方法はありますか?

もちろんあります。叮鸽 は、優雅なメッセージ通知ミドルウェアで、Spring Boot を使用して、DingTalk / 企業 WeChat/Feishu グループボットを統合してメッセージ通知を実現します。

こちら が公式の開発文書です。

まず、依存関係を追加します。

<dependency>
    <groupId>com.github.answerail</groupId>
    <artifactId>dinger-spring-boot-starter</artifactId>
    <version>${dinger.version}</version>
</dependency>

企業 WeChat ボットを例に、設定ファイルは次のようになります。

#Dinger
spring.dinger.project-id=sa-token
#WeChatボットのトークン
spring.dinger.dingers.wetalk.token-id=xxx
#@メンバーの電話番号を設定
wetalk.notify.phones = 17633*****,17633*****

次に、インターフェースを定義する必要があります。

public interface DingerConfig {
    @DingerText(value = "注文番号${orderNum}の注文が成功しました。注文金額は${amt}です。")
    DingerResponse orderSuccess(
            @DingerPhone List<String> phones,
            @Parameter("orderNum") String orderNo,
            @Parameter("amt") BigDecimal amt
    );

    @DingerMarkdown(
            value = "#### 方法エラー\n - リクエスト時間: ${requestTime}\n - リクエストパス: ${requestPath}\n - リクエストパラメータ: ${queryString}\n - エラーメッセージ: ${exceptionMessage}",
            title = "エラー詳細"
    )
    DingerResponse exceptionNotify(@DingerPhone List<String> phones, String requestTime, String requestPath, String queryString, String exceptionMessage);

@DingerText はテキストタイプのメッセージを送信し、@DingerMarkdown は Markdown 形式のメッセージを送信します。注意点として、テキストメッセージのみがユーザーを正しく @できます。

次に、起動クラスでスキャンパッケージのパスを追加します。

@DingerScan(basePackages = "xyz.qinfengge.satokendemo.dinger")

@指定された電話番号のユーザーを設定するには、コンポーネントを追加するだけです。

設定ファイルから情報を読み取り、必要な形式に変換します。

@Component
public class NotifyPhones {

    @Value("${wetalk.notify.phones}")
    private String phones;

    public List<String> handlePhones() {
        return Arrays.asList(phones.split(","));
    }

}

最後に使用します。

List<String> phones = notifyPhones.handlePhones();
        String requestDate = DateUtil.format(new Date(), "yyyy-MM-dd--HH:mm:ss");
        dingerConfig.exceptionNotify(phones, requestDate, request.getServletPath(), parameters, e.getMessage());

非同期処理#

現在、例外をキャッチしてメッセージを送信できるようになりましたが、インターフェースにアクセスする際にエラーが発生すると、速度が明らかに遅くなります。では、どうやって最適化するのでしょうか?

一般的に、リアルタイム性がそれほど求められない操作に対しては、非同期処理を使用できます。

非同期処理は非常に簡単で、メソッドに @Async アノテーションを追加するだけで、これは非同期メソッドであることを示します。その後、起動クラスに @EnableAsync を追加して非同期処理を有効にします。

非同期処理を追加すると、次のようになります。

@RestControllerAdvice
public class GlobalController {

    @Resource
    private NotifyPhones notifyPhones;

    @Resource
    private DingerConfig dingerConfig;

    @ExceptionHandler
    @Async
    public void handleException(Exception e) {
        List<String> phones = notifyPhones.handlePhones();
        String requestDate = DateUtil.format(new Date(), "yyyy-MM-dd--HH:mm:ss");
        dingerConfig.exceptionNotify(phones, e.getMessage());
    }

}

しかし、別の問題が発生しました。非同期処理を追加すると、メソッドの戻り値は void または CompletableFuture のみになります。インターフェースでエラーが発生した場合、戻り値がなくなりますので、正常なインターフェース構造を返すように再構築する必要があります。

最終的な構造は次のようになります。

HttpServletRequest を追加してリクエストパスやリクエストパラメータを取得し、リクエストの IP アドレスなど、他の情報も追加できます。

@Slf4j
@RestControllerAdvice
public class GlobalController {

    @Resource
    private NotifyPhones notifyPhones;

    @Resource
    private DingerConfig dingerConfig;

    @ExceptionHandler
    public Result<Object> notifyWeChat(HttpServletRequest request, Exception e) throws ExecutionException, InterruptedException {
        return Result.fail(this.handleException(request, e).get());
    }

    /**
     * グローバル例外インターセプト
     *
     * @param e 例外
     * @return 例外情報
     */
    @Async
    public CompletableFuture<Result<Object>> handleException(HttpServletRequest request, Exception e) {
        // 非同期メソッド内で非同期コンテキストを取得
        AsyncContext asyncContext = request.startAsync();
        List<String> phones = notifyPhones.handlePhones();
        String requestDate = DateUtil.format(new Date(), "yyyy-MM-dd--HH:mm:ss");
        Map<String, String[]> parameterMap = request.getParameterMap();
        String parameters = JSON.toJSONString(parameterMap);
        dingerConfig.exceptionNotify(phones, requestDate, request.getServletPath(), parameters, e.getMessage());
        log.error(MessageFormat.format("リクエスト {0} でエラーが発生しました。リクエストパラメータ{1}、エラーメッセージ:{2}", request.getServletPath(), parameters, e.getMessage()));
        // 非同期メソッドの実行が完了した後、completeメソッドを呼び出してコンテナに非同期呼び出しの終了を通知
        // リクエストを回収
        asyncContext.complete();
        // 結果を返す。非同期メソッドはCompletableFutureまたはvoidのみを返すことができるため、最初にCompletableFutureを返し、その後メソッドを呼び出して内部の値を取得する必要があります。
        return CompletableFuture.supplyAsync(() -> Result.fail(MessageFormat.format("リクエスト {0} でエラーが発生しました。リクエストパラメータ{1}、エラーメッセージ:{2}", request.getServletPath(), parameters, e.getMessage())));
//        return CompletableFuture.completedFuture(Result.fail(MessageFormat.format("リクエスト {0} でエラーが発生しました。リクエストパラメータ{1}、エラーメッセージ:{2}", request.getServletPath(), parameters, e.getMessage())));
    }

}

最終的な結果は次のようになります。

image

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。