Spring MVC Model i SessionAttributes

Przyjrzyjmy się poniższemu prostemu kontrolerowi:

@Controller
@Slf4j
@RequestMapping("/person")
public class PersonController {
  
    @GetMapping
    public ModelAndView showPersonForm() {
        ModelAndView modelAndView = new ModelAndView("person-form");
        modelAndView.addObject("person", getPerson());
        return modelAndView;
    }

    public Person getPerson() {
        Person person = Person.builder()
                .firstName("John")
                .lastName("Smith")
                .id(UUID.randomUUID().toString())
                .build();
        return person;
    }
}

Metoda showPersonForm przekazuje do widoku person-form utworzony uprzednio obiekt person.

Formularz widoku wygląda następująco:

<p th:text="${'Id ' + person.getId()}">35c67611-fbe9-46cf-aef3-00f8aaa3b354</p>

<form th:object="${person}" th:action="@{'/person/update'}" method="post">
    <div class="form-group">
        <label for="firstName">Imię</label>
        <input type="text" class="form-control" id="firstName" th:field="*{firstName}"/>
    </div>
    <div class="form-group">
        <label for="lastName">Nazwisko</label>
        <input type="text" class="form-control" th:classappend="${#fields.hasErrors('lastName')} ? 'is-invalid' : ''"
               id="lastName" th:field="*{lastName}"/>
        <span class="invalid-feedback" th:if="${#fields.hasErrors('lastName')}">
                        <ul>
                            <li th:each="err : ${#fields.errors('lastName')}" th:text="${err}"></li>
                        </ul>
                    </span>
    </div>
    <button type="submit" class="btn btn-primary">Zapisz</button>
</form>

Jak widać formularz umożliwia edycję pól firstNameoraz lastName, pole id jest tylko wyświetlane.

Metoda obsługując żądanie post wygląda następująco:

@PostMapping("/update")
public String updatePerson(@Valid @ModelAttribute("person") Person person, BindingResult bindingResult) {
  return "person-form";
}

Jak już wspominałem, pole id nie jest wypełniane na formularzu, nie zostanie więc przesłane wraz z obiektem person do metody updatePerson kontrolera.

Rozwiązaniem problemu jest użycie adnotacji @SessionAttributes i wskazanie, które obiekty modelu mają być współdzielone w ramach sesji.

@SessionAttributes("person")
public class PersonController { ... }

Obiekt person, utworzony w metodzie showPersonForm, w momencie odesłania przez kontroler będzie uzupełniony polami znajdującymi się w formularzu. Adnotację tę można udostępnić w innym kontrolerze i uzyskać w nim dostęp do współdzielonych obiektów modelu.

Można również zdefiniować metody w kontrolerze udostępniające obiekty modelu, np:

@ModelAttribute("visitorInfo")
public VisitorInfo getVisitorInfo(){
    return new VisitorInfo(LocalDateTime.now());
}

gdzie klasa VisitorInfo zdefiniowana w następująco:

@Getter
@Setter
@AllArgsConstructor
public class VisitorInfo {
    private LocalDateTime lastUpdate;
}

Po podaniu obiektu do sesji @SessionAttributes({"person", "visitorInfo"}) możemy zmodyfikować metodę updatePerson tak aby aktualizowała przechowywany w obiekcie znacznik czasu.

@PostMapping("/update")
public String updatePerson(@ModelAttribute("visitorInfo") VisitorInfo visitorInfo,
                           @Valid @ModelAttribute("person") Person person, BindingResult bindingResult) {
    visitorInfo.setLastUpdate(LocalDateTime.now());
    return "person";
}

Gdybyśmy nie dodali obiektu visitorInfo do atrybutów sesji, to każdorazowe odwołanie poprzez @ModelAttribute("visitorInfo") powodowałoby wywołanie metody getVisitorInfo()