はじめに
先日はまった箇所なので個人的メモとして作成します。
間違っている箇所があればご指摘いただければと思います。
エラー内容
Spring SecurityのSecurityFilterChainなどのサーブレットフィルター内で@RequestScopeなどのスコープを持つBeanをDIしたい場合、下記のようなエラーが発生してしまいます。
Caused by: java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
前提として、リクエスト時にサーブレットフィルターは DispatcherServletの外で動作します。
よってサーブレットフィルターでRequestScopeを持つBeanを使おうとした場合、DispatcherServletによるHTTPリクエスト(RequestContextHolder)がまだ作成されていないため必要な依存性が足りない旨のエラーが発生しているのです。
解決法
解決法としては以下の3パターンがあります。
使い分けについては下記の方針とするとよいそうです。
- 特別な要件がない限り、proxyModeを使用することを推奨
- パフォーマンスが重要な場合はObjectProviderを検討
- 上記では不都合な要件等がある場合のみ3のRequestContextHolderを使用(エラーログに記載のある方法)
→ Springの内部実装に最も依存しているため
1. proxyModeを指定する
この方法を用いた場合、SpringはCGLIBを使用して、元のbeanのプロキシクラスを作成します。フィルター内で注入されるのは実際のbeanではなく、このプロキシオブジェクトとなります。リクエスト時に実際のRequestScopeBeanのインスタンスが生成された際には、プロキシオブジェクトのメソッド呼び出しが実インスタンスに委譲されます
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Data
public class RequestScopeBean {
private String message = "Hello, World!";
}
@Component
public class RequestScopeBeanLoggingFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(RequestScopeBeanLoggingFilter.class);
private final RequestScopeBean requestScopeBean;
// アプリケーション起動時はプロキシオブジェクトが注入される
public RequestScopeBeanLoggingFilter(RequestScopeBean requestScopeBean) {
this.requestScopeBean = requestScopeBean;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
log.info("RequestScopeBean message: {}", requestScopeBean.getMessage());
filterChain.doFilter(request, response);
}
}
2. RequestContextHolderを使用して手動でリクエストスコープのBeanを取得する
リクエスト時に、RequestContextHolderを介してRequestScopeBeanを取得します。
1. RequestContextHolderを使用して現在のリクエストのServletRequestAttributesを取得します
2. ServletRequestAttributesからHttpServletRequestを取得し、そこからServletContextを取得します
3. WebApplicationContextUtilsを使用してApplicationContext経由でRequestScopeBeanを取得します
@Component
public class RequestScopeBeanLoggingFilter extends OncePerRequestFilter {
// (略)
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// RequestContextHolderを使用し、リクエスト時にBean取得する
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.currentRequestAttributes();
RequestScopeBean requestScopeBean = WebApplicationContextUtils
.getRequiredWebApplicationContext(attributes.getRequest().getServletContext())
.getBean(RequestScopeBean.class);
log.info("RequestScopeBean message: {}", requestScopeBean.getMessage());
filterChain.doFilter(request, response);
}
}
3. ObjectProviderを使用する
ObjectProviderはBeanの取得を遅延させるためのインターフェースであり、コンストラクタインジェクション時には実際のBeanではなくObjectProviderが注入されます。実際のBeanはgetObject()メソッドを呼び出した時点で取得されます。
@Component
public class RequestScopeBeanLoggingFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(RequestScopeBeanLoggingFilter.class);
private final ObjectProvider<RequestScopeBean> requestScopeBeanProvider;
// ObjectProviderによるBeanの遅延取得を行う。
public RequestScopeBeanLoggingFilter(ObjectProvider<RequestScopeBean> requestScopeBeanProvider) {
this.requestScopeBeanProvider = requestScopeBeanProvider;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
RequestScopeBean requestScopeBean = requestScopeBeanProvider.getObject();
log.info("RequestScopeBean message: {}", requestScopeBean.getMessage());
filterChain.doFilter(request, response);
}
}