最近の仕事で、いくつかの知識を学びましたが、非常に役立つものでしたので、いくつかのブログを書いて記録することにしました。
先週、新しい要件があり、プログラムのエラーを取得し、責任者に通知を送信する必要がありました。実現自体は難しくありませんが、優雅に実装する方法が難しいです。ちょうど 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())));
}
}
最終的な結果は次のようになります。