spring.svg

【Spring Boot】Controller Advice

SpringBoot

Controller Advice とは

Controller Advice は、エラー処理などをコントローラーの共通処理としてまとめて定義するためのものです。 MVC の場合は@ControllerAdvice、REST API の場合は@RestControllerAdviceを付与したクラスを作成します。

MVC
@ControllerAdvice
public class MyControllerAdvice {
  //...
}
REST API
@RestControllerAdvice
public class MyControllerAdvice {
  //...
}

Controller Advice では、コントローラーの処理に対して主に以下の 3 つの処理を定義します。

@InitBinder

@InitBinderは、@RequestParam@PathVariableなど、リクエストデータを Java オブジェクトにバインドする際の処理を定義します。 例えば、バインドするデータに対して両端の空白を削除するには以下のようにします。

@InitBinder
public void initBinder(WebDataBinder dataBinder) {
  dataBinder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
}

以下のコントローラーを作成して動作を確認します。

@RestController
@RequestMapping("/api/user")
public class MyController {
  @GetMapping("{id}")
  public String get(@PathVariable("id") String id) {
    return id;
  }
}

以下のようにパスパラメーターに空白を含めたリクエストを送信した時、通常であればそのまま空白を含めた値が返されます。 しかし、上記の@InitBinderを定義すれば0001と空白が除去された値が返されるようになります。

http://localhost:8080/api/user/    0001

@ModelAttribute

@ModelAttributeは、MVC において共通のデータを Model に設定したい場合に使用します。

@ModelAttribute
public void setTimestamp(Model model) {
  model.add("timestamp", System.currentTimeMillis());
}

@ExceptionHandler

@ExceptionHandlerは、コントローラーでスローされた特定の例外に対しての処理を定義します。 プロパティとして対象の例外クラスを指定します。

@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleRuntimeException(Exception e) {
  return e.getMessage();
}

既存の Controller Advice

Spring Boot には、既存の Controller Advice(Controller Advice の作成を補助するクラス)がいくつかあります。 これを継承・実装することでできることが増えます。 ここではその中でも REST API に関するものを 3 つ紹介します。

ResponseEntityExceptionHandler

@RestControllerAdvice
public class ExceptionControllerAdvice extends ResponseEntityExceptionHandler {
  /...
}

REST API に関する特定の例外に対して、どのようなレスポンスを返すかが定義されています。 詳細についてはこちらの記事で説明しているので参考にしてください。

RequestBodyAdviceAdapter

@RestControllerAdvice
public class RequestBodyAdvice extends RequestBodyAdviceAdapter {
  //...
}

リクエストボディの読み込みに関する処理を定義します。 具体的に、リクエストボディを読み込み、@RequestBodyを付与したオブジェクトにバインドする前と後の処理を定義することができます。

RequestBodyAdviceAdapterは以下の 4 つのメソッドがあり、必要に応じてこれをオーバーライドします。

メソッド必須説明
supports以下の Advice の処理を実行するかを戻り値の真偽値によって決める(true:実行)
beforeBodyRead-オブジェクトへのバインド前の処理
afterBodyRead-オブジェクトへのバインド後の処理
handleEmptyBody-オブジェクトへのバインド前かつリクエストボディが型の場合の処理

例えば、送信された JSON 情報をログ出力するには以下のようにします。

@RestControllerAdvice
public class RequestBodyAdvice extends RequestBodyAdviceAdapter {

  private static final Logger logger = LoggerFactory.getLogger(RequestBodyAdvice.class);

  @Override
  public boolean supports(MethodParameter methodParameter, Type targetType,
      Class<? extends HttpMessageConverter<?>> converterType) {
    return true;
  }

  @Override
  public Object afterBodyRead(Object body, HttpInputMessage inputMessage,
      MethodParameter parameter, Type targetType,
      Class<? extends HttpMessageConverter<?>> converterType) {
    logger.info(object.toString());
    return body;
  }
}

AbstractMappingJacksonResponseBodyAdvice

@RestControllerAdvice
public class MappingJacksonResponseBodyAdvice extends AbstractMappingJacksonResponseBodyAdvice {
  //...
}

レスポンスとして送信する JSON をレスポンスボディに書き込む前の処理を定義することができます。 例えばすべての JSON に共通の情報を設定したい場合などに使用します。

処理は、抽象メソッドであるbeforeBodyWriteInternal()を実装することで定義します。 以下は、JSON をtimestampを含めた形に変換する例です。

@RestControllerAdvice
public class MappingJacksonResponseBodyAdvice extends AbstractMappingJacksonResponseBodyAdvice {

  @Override
  protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType,
      MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {
    Object body = bodyContainer.getValue();
    Map<String, Object> map = new HashMap<>();
    map.put("body", body);
    map.put("timestamp", System.currentTimeMillis());
    bodyContainer.setValue(map);
  }
}

コントローラーで{ id: 1, name: 'Taro' }という JSON が返される場合、以下のように変換されます。

{
  "body": {
    "id": 1,
    "name": "Taro"
  },
  "timestamp": 1621400483151
}