1. 概要

Webアプリケーションを開発するとき、多くの場合、複数のビューで同じ属性を参照する必要があります。 たとえば、複数のページに表示する必要のあるショッピングカートのコンテンツがある場合があります。

これらの属性を保存するのに適した場所は、ユーザーのセッションです。

このチュートリアルでは、簡単な例に焦点を当て、セッション属性を操作するための2つの異なる戦略を検討します。

  • スコーププロキシの使用
  • @ SessionAttributesアノテーションを使用する

2. Mavenのセットアップ

Spring Bootスターターを使用してプロジェクトをブートストラップし、必要なすべての依存関係を取り込みます。

セットアップには、親宣言、Webスターター、およびthymeleafスターターが必要です。

また、springテストスターターを含めて、単体テストにいくつかの追加ユーティリティを提供します。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.1</version>
    <relativePath/>
</parent>
 
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
     </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

これらの依存関係の最新バージョンは、MavenCentralにあります。

3. ユースケースの例

この例では、単純な「TODO」アプリケーションを実装します。 TodoItem のインスタンスを作成するためのフォームと、すべてのTodoItemを表示するリストビューがあります。

フォームを使用してTodoItemを作成すると、フォームへの後続のアクセスには、最後に追加されたTodoItemの値が事前に入力されます。 t の機能を使用して、セッションスコープに格納されているフォーム値を「記憶」する方法を示します。

2つのモデルクラスは、単純なPOJOとして実装されています。

public class TodoItem {

    private String description;
    private LocalDateTime createDate;

    // getters and setters
}
public class TodoList extends ArrayDeque<TodoItem>{

}

TodoListクラスはArrayDequeを拡張し、peekLastメソッドを介して最近追加されたアイテムに簡単にアクセスできるようにします。

2つのコントローラークラスが必要です。これから説明する戦略ごとに1つです。 それらには微妙な違いがありますが、コア機能は両方で表されます。 それぞれに3つの@RequestMappingがあります。

  • @GetMapping(“ / form”) –このメソッドは、フォームの初期化とフォームビューのレンダリングを担当します。 TodoList が空でない場合、メソッドは最後に追加されたTodoItemをフォームに事前入力します。
  • @PostMapping(“ / form”) –このメソッドは、送信されたTodoItemTodoListに追加し、リストURLにリダイレクトします。
  • @GetMapping(“ /todos.html”)– このメソッドは、TodoListModelに追加して表示し、リストビューをレンダリングします。

4. スコーププロキシの使用

4.1. 設定

この設定では、 TodoList は、プロキシによってサポートされるセッションスコープの@Beanとして構成されています。 @Bean がプロキシであるという事実は、シングルトンスコープの@Controllerにそれを注入できることを意味します。

コンテキストが初期化されるときにセッションがないため、Springは TodoList のプロキシを作成して、依存関係として挿入します。 TodoList のターゲットインスタンスは、リクエストで必要になったときに必要に応じてインスタンス化されます。

SpringでのBeanスコープの詳細については、トピックに関するの記事を参照してください。

まず、@Configurationクラス内でBeanを定義します。

@Bean
@Scope(
  value = WebApplicationContext.SCOPE_SESSION, 
  proxyMode = ScopedProxyMode.TARGET_CLASS)
public TodoList todos() {
    return new TodoList();
}

次に、Beanを @Controller の依存関係として宣言し、他の依存関係と同じように挿入します。

@Controller
@RequestMapping("/scopedproxy")
public class TodoControllerWithScopedProxy {

    private TodoList todos;

    // constructor and request mappings
}

最後に、リクエストでBeanを使用するには、そのメソッドを呼び出すだけです。

@GetMapping("/form")
public String showForm(Model model) {
    if (!todos.isEmpty()) {
        model.addAttribute("todo", todos.peekLast());
    } else {
        model.addAttribute("todo", new TodoItem());
    }
    return "scopedproxyform";
}

4.2. ユニットテスト

スコーププロキシを使用して実装をテストするには、 まず、SimpleThreadScopeを構成しますこれにより、単体テストで、テストしているコードの実行時の状態を正確にシミュレートできます。

まず、TestConfigCustomScopeConfigurerを定義します。

@Configuration
public class TestConfig {

    @Bean
    public CustomScopeConfigurer customScopeConfigurer() {
        CustomScopeConfigurer configurer = new CustomScopeConfigurer();
        configurer.addScope("session", new SimpleThreadScope());
        return configurer;
    }
}

これで、フォームの最初のリクエストに初期化されていない TodoItem:が含まれていることをテストすることから始めることができます。

@RunWith(SpringRunner.class) 
@SpringBootTest
@AutoConfigureMockMvc
@Import(TestConfig.class) 
public class TodoControllerWithScopedProxyIntegrationTest {

    // ...

    @Test
    public void whenFirstRequest_thenContainsUnintializedTodo() throws Exception {
        MvcResult result = mockMvc.perform(get("/scopedproxy/form"))
          .andExpect(status().isOk())
          .andExpect(model().attributeExists("todo"))
          .andReturn();

        TodoItem item = (TodoItem) result.getModelAndView().getModel().get("todo");
 
        assertTrue(StringUtils.isEmpty(item.getDescription()));
    }
}

また、送信によってリダイレクトが発行され、後続のフォームリクエストに新しく追加されたTodoItemが事前入力されていることを確認できます。

@Test
public void whenSubmit_thenSubsequentFormRequestContainsMostRecentTodo() throws Exception {
    mockMvc.perform(post("/scopedproxy/form")
      .param("description", "newtodo"))
      .andExpect(status().is3xxRedirection())
      .andReturn();

    MvcResult result = mockMvc.perform(get("/scopedproxy/form"))
      .andExpect(status().isOk())
      .andExpect(model().attributeExists("todo"))
      .andReturn();
    TodoItem item = (TodoItem) result.getModelAndView().getModel().get("todo");
 
    assertEquals("newtodo", item.getDescription());
}

4.3. 討論

スコーププロキシ戦略を使用する主な機能は、 リクエストマッピングメソッドのシグネチャには影響しません。 これにより、読みやすさが @SessionAttributes ストラテジー。

コントローラにはデフォルトでシングルトンスコープがあることを思い出してください。

これが、プロキシされていないセッションスコープのBeanを単に注入するのではなく、プロキシを使用する必要がある理由です。 スコープの小さいbeanをスコープの大きいbeanに注入することはできません。

この場合、そうしようとすると、次のメッセージを含む例外がトリガーされます。スコープ’セッション’は現在のスレッドに対してアクティブではありません。

セッションスコープを使用してコントローラーを定義する場合は、proxyModeの指定を回避できます。 これには、特にコントローラーインスタンスをユーザーセッションごとに作成する必要があるため、コントローラーの作成にコストがかかる場合に不利になる可能性があります。

TodoList は、他のコンポーネントでインジェクションに使用できることに注意してください。 これは、ユースケースに応じてメリットまたはデメリットになる可能性があります。 beanをアプリケーション全体で使用できるようにすることが問題になる場合は、次の例で示すように、 @SessionAttributes を使用する代わりに、インスタンスをコントローラーにスコープできます。

5. @SessionAttributesアノテーションの使用

5.1. 設定

この設定では、TodoListをSpring管理の@Beanとして定義していません。 代わりに、それを@ModelAttributeとして宣言し、@ SessionAttributesアノテーションを指定して、コントローラーのセッションにスコープを設定します。

コントローラに初めてアクセスすると、Springはインスタンスをインスタンス化し、Modelに配置します。 Beanも@SessionAttributesで宣言するため、Springはインスタンスを保存します。

Springの@ModelAttributeの詳細については、トピックに関するの記事を参照してください。

まず、コントローラーにメソッドを提供してBeanを宣言し、そのメソッドに@ModelAttributeというアノテーションを付けます。

@ModelAttribute("todos")
public TodoList todos() {
    return new TodoList();
}

次に、 @SessionAttributes を使用して、TodoListをセッションスコープとして扱うようにコントローラーに通知します。

@Controller
@RequestMapping("/sessionattributes")
@SessionAttributes("todos")
public class TodoControllerWithSessionAttributes {
    // ... other methods
}

最後に、リクエスト内でBeanを使用するために、@RequestMappingのメソッドシグネチャでBeanへの参照を提供します。

@GetMapping("/form")
public String showForm(
  Model model,
  @ModelAttribute("todos") TodoList todos) {
 
    if (!todos.isEmpty()) {
        model.addAttribute("todo", todos.peekLast());
    } else {
        model.addAttribute("todo", new TodoItem());
    }
    return "sessionattributesform";
}

@PostMapping メソッドでは、 RedirectAttributes を挿入し、addFlashAttributeを呼び出してからRedirectViewを返します。 これは、最初の例と比較した実装の重要な違いです。

@PostMapping("/form")
public RedirectView create(
  @ModelAttribute TodoItem todo, 
  @ModelAttribute("todos") TodoList todos, 
  RedirectAttributes attributes) {
    todo.setCreateDate(LocalDateTime.now());
    todos.add(todo);
    attributes.addFlashAttribute("todos", todos);
    return new RedirectView("/sessionattributes/todos.html");
}

Springは、URLパラメータのエンコードをサポートするリダイレクトシナリオに、特殊なRedirectAttributes実装のModelを使用します。 リダイレクト中、 Model に保存されている属性は、通常、URLに含まれている場合にのみフレームワークで使用できます。

addFlashAttributeを使用することで、TodoListがURLにエンコードせずにリダイレクトを存続させたいことをフレームワークに伝えています。

5.2. ユニットテスト

フォームビューコントローラーメソッドの単体テストは、最初の例で見たテストと同じです。 ただし、 @PostMapping のテストは、動作を確認するためにフラッシュ属性にアクセスする必要があるため、少し異なります。

@Test
public void whenTodoExists_thenSubsequentFormRequestContainsesMostRecentTodo() throws Exception {
    FlashMap flashMap = mockMvc.perform(post("/sessionattributes/form")
      .param("description", "newtodo"))
      .andExpect(status().is3xxRedirection())
      .andReturn().getFlashMap();

    MvcResult result = mockMvc.perform(get("/sessionattributes/form")
      .sessionAttrs(flashMap))
      .andExpect(status().isOk())
      .andExpect(model().attributeExists("todo"))
      .andReturn();
    TodoItem item = (TodoItem) result.getModelAndView().getModel().get("todo");
 
    assertEquals("newtodo", item.getDescription());
}

5.3. 討論

セッションに属性を格納するための@ModelAttributeおよび@SessionAttributes戦略は、が追加のコンテキスト構成またはSpring管理の@Beansを必要としない単純なソリューションです。

最初の例とは異なり、@RequestMappingメソッドTodoListを挿入する必要があります。

さらに、リダイレクトシナリオにはフラッシュ属性を使用する必要があります。

6. 結論

この記事では、SpringMVCでセッション属性を操作するための2つの戦略としてスコーププロキシと@SessionAttributesを使用する方法について説明しました。 この単純な例では、セッションに保存されている属性は、セッションの存続期間中のみ存続することに注意してください。

サーバーの再起動またはセッションのタイムアウトの間に属性を保持する必要がある場合は、SpringSessionを使用して情報の保存を透過的に処理することを検討できます。 詳細については、SpringSessionの記事をご覧ください。

いつものように、この記事で使用されているすべてのコードは、GitHubから入手できます。