CLOVER🍀

That was when it all began.

SpringのSingletonな管理Beanに、自分より短いライフサイクルのBeanをDIしようとすると?

なんとなく結果が見えている気がするんですけど、確認という意味で試してみました的な。

SpringのBeanってデフォルトのスコープはSingletonですが、SingletonなBeanに対してそれより短いライフサイクル(例えば、RequestやSession)のものを放り込もうとするとどうなるか?という話。

簡単にSpring Boot、Spring MVCを使って試してみます。

準備

確認用のコードは、こんな感じ。

アプリケーションのエントリポイント。
src/main/java/org/littlewings/spring/scope/App.java

package org.littlewings.spring.scope;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}

Request ScopeなService。
src/main/java/org/littlewings/spring/scope/RequestScopeTimeService.java

package org.littlewings.spring.scope;

import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.springframework.web.context.WebApplicationContext;

@Service
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
public class RequestScopeTimeService {
    private LocalDateTime now;

    @PostConstruct
    public void init() {
        now = LocalDateTime.now();
    }

    public void printNow() throws InterruptedException {
        System.out.printf("[%s] %s%n", Thread.currentThread(), now);
        TimeUnit.SECONDS.sleep(10L);
    }
}

処理をしているのがどのスレッドかわかりやすいように、スレッドの名前を出力するようにしています。

Session ScopeなService。
src/main/java/org/littlewings/spring/scope/SessionScopeTimeService.java

package org.littlewings.spring.scope;

import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Service;
import org.springframework.web.context.WebApplicationContext;

@Service
@Scope(WebApplicationContext.SCOPE_SESSION)
public class SessionScopeTimeService {
    private LocalDateTime now;

    @PostConstruct
    public void init() {
        now = LocalDateTime.now();
    }

    public void printNow() throws InterruptedException {
        System.out.printf("[%s] %s%n", Thread.currentThread(), now);
        TimeUnit.SECONDS.sleep(10L);
    }
}

各ServiceをDIするRestController。
src/main/java/org/littlewings/spring/scope/TestController.java

package org.littlewings.spring.scope;

import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {
    @Autowired
    private RequestScopeTimeService requestScopeTimeService;

    @Autowired
    private SessionScopeTimeService sessionScopeTimeService;

    @RequestMapping("request")
    public String request() throws InterruptedException {
        requestScopeTimeService.printNow();

        TimeUnit.SECONDS.sleep(10L);

        requestScopeTimeService.printNow();

        return "Finish!";
    }

    @RequestMapping("session")
    public String session() throws InterruptedException {
        sessionScopeTimeService.printNow();

        TimeUnit.SECONDS.sleep(10L);

        sessionScopeTimeService.printNow();

        return "Finish!";
    }
}

これらのクラスを使って、確認してみます。

まずは起動してみる

とりあえず、アプリケーションを起動してみましょう。

$ mvn spring-boot:run

結果、失敗します。

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'testController': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private org.littlewings.spring.scope.RequestScopeTimeService org.littlewings.spring.scope.TestController.requestScopeTimeService; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestScopeTimeService': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is 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/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:334)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1210)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:537)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:476)
	at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:303)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:299)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:755)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:757)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:480)
	at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:118)

SingletonにAutowiredしようとしてるよ?ムリだから、みたいな感じですね。

まあ、DI先のBeanがSingletonなのでそりゃそうだ的な。これで動いてもらっても困りますね。

動かすには?

これで終わっても面白くないので、このコードを動かすようにするにはどうしたらいいか、ちょっと考えてみます。

RestControllerをRequest Scopeにする

最初に思いつきそうな方法。RestControllerのScopeを、Requestにします。

@RestController
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
public class TestController {

もちろん、これでも起動します。

Request/Session ScopeなBeanにProxyModeを設定する

別解。例外のメッセージにも出ていましたが、ProxyModeを変更してみます。

こんな感じです。ここでは、ScopedProxyMode.TARGET_CLASSとしています。

@Service
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopeTimeService {

Session Scopeの方も。

@Service
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionScopeTimeService {

この方法でも、動作するようになります。

今回は、動作確認してみます。2つのコンソールから、curlを使って確認しましょう。
※sleepが10秒入っているので、2つのリクエストに対する処理が時間的に重なり合うことになります。

## ひとつめ
$ curl http://localhost:8080/request

## ふたつめ
$ curl http://localhost:8080/request

結果。

[Thread[http-nio-8080-exec-1,5,org.littlewings.spring.scope.App]] 2015-10-01T23:22:32.185
[Thread[http-nio-8080-exec-2,5,org.littlewings.spring.scope.App]] 2015-10-01T23:22:34.710
[Thread[http-nio-8080-exec-1,5,org.littlewings.spring.scope.App]] 2015-10-01T23:22:32.185
[Thread[http-nio-8080-exec-2,5,org.littlewings.spring.scope.App]] 2015-10-01T23:22:34.710

それぞれのスレッドで、結果が混ざったりしていませんね。同じスレッドの名前のものだけが、同じ値になっています。

続いて、Session Scopeの場合。

## ひとつめ
$ curl -c cookie.txt -b cookie.txt http://localhost:8080/session

## ふたつめ
$ curl -c cookie.txt -b cookie.txt http://localhost:8080/session

結果。

[Thread[http-nio-8080-exec-1,5,org.littlewings.spring.scope.App]] 2015-10-01T23:26:52.594
[Thread[http-nio-8080-exec-2,5,org.littlewings.spring.scope.App]] 2015-10-01T23:26:53.974
[Thread[http-nio-8080-exec-1,5,org.littlewings.spring.scope.App]] 2015-10-01T23:26:52.594
[Thread[http-nio-8080-exec-2,5,org.littlewings.spring.scope.App]] 2015-10-01T23:26:53.974

Session Scopeなので、Cookieの値を保持していれば次にリクエストを投げても同じ結果が得られます。

ProxyModeを変えた場合には、どうなっているのか?

ScopedProxyMode.TARGET_CLASSの説明にもありますが、CGLIBによる拡張が行われています。

確認用のコード。

    public void printNow() throws InterruptedException {
        Thread.dumpStack();
        System.out.printf("[%s] %s%n", Thread.currentThread(), now);
        TimeUnit.SECONDS.sleep(10L);
    }

スタックトレースを見ると、間にCGLIBが挟まっています。

java.lang.Exception: Stack trace
	at java.lang.Thread.dumpStack(Thread.java:1329)
	at org.littlewings.spring.scope.RequestScopeTimeService.printNow(RequestScopeTimeService.java:23)
	at org.littlewings.spring.scope.RequestScopeTimeService$$FastClassBySpringCGLIB$$7d0838ae.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:717)

ScopedProxyMode.INTERFACESを選択した場合は、JDKのProxyを使用して、インターフェースに対して動的な拡張が行われるようです。

一応、くらいの気持ちで確認してみましたが、確認しておいてよかったです。