KUSAMAKURA

智に働けば角が立つ。情に棹させば流される。意地を通せば窮屈だ。とかくに人の世は住みにくい。

Spring の @Scope のデフォルト挙動

Spring の DI では、デフォルト「Singleton」が設定されます。Controller であってもデフォルト Singleton と言うのは、直感的な動作とは異なるため、それぞれの Scope 設定でどのように動作するのかを調べてみました。

スコープの説明

まずは、それぞれのスコープの説明。

検証

Service の Scope を prototype で固定し、Controller の Scope を変化させて、どのように出力されるかを検証する。
よく事故るのは、Service の Scope 変えていたのに勝手に書き換わっていたってパターンだと思う。

SampleService クラス

値を保持してるだけ。@Scopeは、"prototype"のため、意図としては、リクエスト毎に違う値を保持したい。

@Data
@Scope("prototype")
@Service
public class SampleService {

  private String name;


}

SampleController クラス

SampleServiceをDI。値の設定後3秒待ってAfterのログを出力。この Controller の @Scope を変更し、SampleServiceの値がどうなるかを検証する。

@RestController
@Scope("singleton")
@RequestMapping(value = "/sample")
public class SampleController {

  @Autowired
  private SampleService sampleService;



  @RequestMapping(method = RequestMethod.GET)
  public ResponseEntity<String> changeSettings(@RequestParam("name") String name, @RequestParam("locale") String locale) {
    System.out.println(String.format("From [%s], Data is [%s].", locale, name));
    System.out.println(String.format("Before[%s]:%s", locale, this.sampleService.getName()));

    this.sampleService.setName(name);

    try {
      Thread.sleep(3000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }

    System.out.println(String.format("After[%s]:%s", locale, this.sampleService.getName()));

    return new ResponseEntity<String>(HttpStatus.OK);
  }


}

RunThread クラス

テスト用のスレッドクラス。

@Data
@AllArgsConstructor
public class RunThread extends Thread {


  private final String val;
  private final String locale;
  private final MockMvc mockMvc;

  @Override
  public void run() {
    String responseContent = null;
    try {
      responseContent =
        this.mockMvc.perform(get(String.format("/sample?name=%s&locale=%s", this.val, this.locale)))
          .andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
    } catch (UnsupportedEncodingException e) {
      e.printStackTrace();
    } catch (Exception e) {
      e.printStackTrace();
    }

    System.out.println(responseContent);
  }

}

SampleControllerTest クラス

テストクラス。別スレッドで2回リクエストを実施している。

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@SpringApplicationConfiguration(classes = Application.class)
public class SampleControllerTest {
  private MockMvc mockMvc;

  @Autowired
  private WebApplicationContext context;


  @Before
  public void setup() {
    MockitoAnnotations.initMocks(this);
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
  }



  @Test
  public void sample() throws UnsupportedEncodingException, Exception {
    RunThread thread1 = new RunThread("hoge", "1st", this.mockMvc);
    RunThread thread2 = new RunThread("moge", "2nd", this.mockMvc);

    thread1.start();
    thread2.start();

    Thread.sleep(3050);

  }

}

検証結果

Controller の @Scope の値毎にログをまとめてみた。
デフォルトでは Controller も Singleton で動作するため、Service に prototype を設定しても意図した挙動となっていないことがわかる。
多くの場合、Controller に対して、 request のスコープを与える事で、意図した動作となるのではないだろうか。

singleton

From [1st], Data is [hoge].
From [2nd], Data is [moge].
Before[1st]:null
Before[2nd]:null
After[1st]:moge
After[2nd]:moge

prototype

From [2nd], Data is [moge].
From [1st], Data is [hoge].
Before[2nd]:null
Before[1st]:null
After[2nd]:moge
After[1st]:hoge

request

From [2nd], Data is [moge].
From [1st], Data is [hoge].
Before[2nd]:null
Before[1st]:null
After[2nd]:moge
After[1st]:hoge

session

From [1st], Data is [hoge].
From [2nd], Data is [moge].
Before[1st]:null
Before[2nd]:null
After[2nd]:moge
After[1st]:hoge

default

From [1st], Data is [hoge].
From [2nd], Data is [moge].
Before[1st]:null
Before[2nd]:null
After[1st]:moge
After[2nd]:moge