Spring の @Scope のデフォルト挙動
Spring の DI では、デフォルト「Singleton」が設定されます。Controller であってもデフォルト Singleton と言うのは、直感的な動作とは異なるため、それぞれの Scope 設定でどのように動作するのかを調べてみました。
スコープの説明
まずは、それぞれのスコープの説明。
- singleton
コンテナに対して1つのインスタンスを定義します。(デフォルト) - prototype
Beanを取得する度に新しいインスタンスを生成します。 - request
HTTP Request単位でインスタンスを生成します。 - session
HTTP Session単位でインスタンスを生成します。
検証
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