最近の仕事で、いくつかの知識を学びましたが、非常に役立つものでしたので、いくつかのブログを書いて記録することにしました。
先週、新しい要求があり、プログラムのエラーを取得し、責任者に通知を送信する必要がありました。実現自体は難しくありませんが、優雅に実装する方法が難しいです。ちょうど 2 つの知識を見たところで、それを組み合わせることで要求を完璧に実現できます。
グローバル例外処理#
グローバル例外処理は非常に簡単で、2 つのアノテーションが必要です。@RestControllerAdvice
と @ExceptionHandler
です。
まず、@RestControllerAdvice
の説明は @RestControllerAdvice は、@ControllerAdvice と @ResponseBody からなるコンビネーションアノテーションであり、@ControllerAdvice は @Component を継承しているため、@RestControllerAdvice は本質的にコンポーネントです。
次に、@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())));
}
}
最終的な結果は次のようになります。