問題
ある画面で「戻る」で前の画面に遷移した後、もう一度その画面に入ると一部リストの値がクリアされずに残存する。
原因
modelAttributeはsessionAttributeを優先し、その後にJSPのリクエスト値を上書きするため、
サイズが固定ではないリストの場合、以前の値(最初入った時のセッション値など)が残ってしまう。
解決方法
InitBinder + カスタムアノテーション
@InitBinderは、リクエストパラメータが @ModelAttribute にバインド(上書き)される直前で呼ばれる。
以下の実装では、バインド直前に Model の状態を明示的にリセットできる。
カスタムannotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResetBeforeBind {
String resetMethod;
}
initbinder(@InitBinder 内でアノテーションの有無を判定)
@ControllerAdvice
public class GlobalInitBinderAdvice {
@InitBinder
public void initBinder(
WebDataBinder binder,
NativeWebRequest webRequest
) {
Object handler = webRequest.getAttribute(
HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE,
RequestAttributes.SCOPE_REQUEST
);
if (!(handler instanceof HandlerMethod)) {
return;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
ResetBeforeBind annotation =
handlerMethod.getMethodAnnotation(ResetBeforeBind.class);
if (annotation == null) {
return;
}
Object target = binder.getTarget();
if (target == null) {
return;
}
String resetMethodName = annotation.resetMethod();
//指定された resetList メソッドをリフレクションで実行
try {
Method resetMethod =
target.getClass().getMethod(resetMethodName);
resetMethod.invoke(target);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(
"Failed to invoke reset method: " + resetMethodName, e
);
}
}
}
使用例
画面遷移時の Controller メソッドにカスタムアノテーションを付与。
@GetMapping("/sample")
@ResetBeforeBind(resetMethod = "resetList")
public String showSample(
@ModelAttribute SampleForm form
) {
return "sample";
}
//やりたいRESET処理
public void resetList(){
...
}
メモ
最初は InitBinderを知らず、Interceptorでの対応を試みた。
しかし、Interceptorはバインディング処理よりも前に実行されるため、
ModelAttributeの状態を操作することができず、要件を満たせなかった。
今回の問題は、Spring MVC のライフサイクルを正しく理解していなかったことが原因だった。
今回の対応を通して、「何をしたいか」だけでなく「いつ実行されるべき処理なのか」を意識することの重要性を改めて感じた。