Введение в Spring MVC

В этой статье напишем небольшое веб-приложение (без Ajax). В нем пользователь добавляет в корзину животных и делает заказ в зоомагазине. Животные могут иметь любые названия, которые пользователь вводит с клавиатуры. При переходе между страницами корзина с животными сохраняется в бине с областью видимости Session. При окончательном заказе она сохраняется в базе.

Корзина животных (на главной странице)
Корзина животных (на главной странице). У каждого пользователя своя.
Заказы
Заказы. Тут отображаются все заказы от всех пользователей, а не только свои заказы.

Maven-зависимости

Чтобы написать веб-приложение, необходимо в POM-файл добавить зависимость:

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
</dependency>

spring-security добавлять, как ни странно, не надо. Сессии поддерживаются благодаря серверу Apache Tomcat. Они выделяются автоматически любому новому пользователю без всякой авторизации.

Добавим еще шаблонизатор Thymeleaf (можно другой, но рекомендуется этот):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Он соединяет html-шаблон с данными.

Сущности базы Animal и Order

В базе будут сохраняться две сущности: Order (заказ) и Animal (животное). С точки зрения функционала только Order, но Order содержит набор Animal:

@NoArgsConstructor
@Data
@Entity
public class Animal {
    @Id
    @GeneratedValue(strategy= GenerationType.SEQUENCE)
    private long id;
    private String name;
}

Обратите внимание, что мы используем аннотации Lombok: @NoArgsConstructor и @Data. Они генерируют перед компиляцией пустой конструктор и геттеры/сеттеры соответственно.

Заказ Order содержит дату, телефон и список заказанных животных:

@NoArgsConstructor
@Data
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy= GenerationType.SEQUENCE)
    private long id;
    private String phone;
    private Date date=new Date();
    @OneToMany(
            cascade = CascadeType.ALL,
            orphanRemoval = true)
    private Set<Animal> animals=new HashSet<>();
}

@SessionScope бин AnimalCart

И есть еще корзина с животными, которая сущностью базы не является, но используется для временного хранения животных в корзине:

@Data
@NoArgsConstructor
@Component
@SessionScope
public class AnimalCart {
    private Set<Animal> animals=new HashSet<>();

    public void add(Animal animal){
        animals.add(animal);
    }

    public void clear(){
        animals.clear();
    }
}

По сути она содержит только Set с животными и методы для добавления животного в корзину и очистки корзины.

@SessionScope означает, что этот бин имеет область видимости Session, то есть  каждый раз для новой сессии создается новый экземпляр AnimalCart. То есть существует своя корзина для каждого пользователя (хоть пользователя у нас анонимные). Этот бин не удаляется, пока сессия пользователя не истечет.

Контроллеры

Перейдем к контроллерам. Они обрабатывают запросы и обычно возвращают названия шаблонов.

AnimalController

AnimalController содержит два метода:

  • вывода животных из корзины @GetMapping(«/»)
  • и добавления животного в корзину @PostMapping(«/»)
Аннотация показывает название метода (Get/Post) и url (/) по которому идет запрос данным методом.

С базой AnimalController не работает, только с корзиной, так что внедряем в него AnimalCart:

@AllArgsConstructor
@Controller
public class AnimalController {
    private final AnimalCart animalCart;

    @GetMapping("/")
    public String get(Model model) {
        Set<Animal> animals = animalCart.getAnimals();

        model.addAttribute("animals", animals);
        model.addAttribute("animal", new Animal());

        return "index";
    }
    @PostMapping("/")
    public String add(Animal animal) {
        animalCart.add(animal);
        return  "redirect:/";
    }
}
Аннотация @Autowire у animalCart не стоит, но @AllArgsConstructor создает конструктор со всеми final-полями, так что animalCart считается внедренной через конструктор.

Каждый запрос попадает в какой-то один метод контроллера, делает то, что в нем прописано и сопоставляется с нужным шаблоном. Контроллер обычно возвращает название этого шаблона. Например, метод get() возвращает index, это значит, что он сопоставляется с шаблоном  index.html, который лежит в специальной папке templates в ресурсах нашего приложения.

Также в методе контроллера заполняется модель Model — по сути это HashMap. Атрибут модели — ключ, данные — значение. В шаблоне index.html названия атрибутов animals и animal будут доступны как переменные, из которых мы можем вывести добавленные в контроллере данные.

Но не всегда контроллер возвращает название шаблона. Например,

return "redirect:/"

в методе add() означает перенаправление по адресу /. То есть после добавления животного мы делаем редирект на ту же страницу (а мы на главной странице), чтобы обновить список животных в корзине.

OrderController

OrderController построен аналогично, только работает с базой, так что внедряем в этот контроллер репозиторий:

@Repository
public interface OrderRepository  extends JpaRepository<Order, Long> {
}

Итак, OrderController:

@AllArgsConstructor
@Controller
public class OrderController {
    private final OrderRepository orderRepository;
    private final AnimalCart productCart;

    @GetMapping("/order")
    public String get(Model model) {
        List<Order> orders=orderRepository.findAll();
        model.addAttribute("orders", orders);
        return "order";
    }

    @PostMapping("/order")
    public String order(@RequestParam String phone) {

        Order order=new Order();
        order.setPhone(phone);
        order.setAnimals(productCart.getAnimals());

        orderRepository.save(order);

        // после заказа очищаем корзину, чтобы можно было набирать ее снова
        productCart.clear();

        return  "redirect:/order";
    }
}

 

Таким образом, заказы выводятся из базы, и список заказов един для всех пользователей.

Корзина же для каждого пользователя своя, она хранится как объект с областью видимости Session до тех пор, пока сессия не истечет. Если бы мы убрали аннотацию @SessionScope с AnimalCart, то AnimalCart стал бы синглтоном, создавался единожды при запуске приложения, и в корзине всех пользователей было бы одно и то же. Но, к счастью, это не так — AnimalCart создается в начале каждой сессии. Можно проверить это, заполнив корзину с двух разных браузеров — у каждого пользователя будет своя корзина. Причем AnimalCart сохраняется, если перейти из нее / на страницу /order и обратно, потому что живет до тех пор, пока сессия не истечет.

Обратите внимание, что мы не добавляли AnimalCart в атрибут сессии в методах контроллера, а просто пометили ее аннотацией @SessionScope. Этого достаточно.

Thymeleaf-шаблоны

Шаблоны хранятся в папке /templates в ресурсах:

Представления
Представления

У нас их два.

Названия

Методы контроллеров, возвращающие «index», выводят пользователю шаблон index.html, а методы, возвращающие «order»order.html.

Есть еще методы, возвращающие «redirect:/» и «redirect:/order» — они сначала возвращают пользователю статус 302 и так перебрасывают в методы вывода списков get(), а уж оттуда пользователю возвращается заполненный шаблон.

index.html и статические ресурсы

index.html выглядит так (оставлены значимые части):

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">

<head>
  ...
    <link rel="stylesheet" type="text/css" href="style.css">
...
</head>

<body>
...
<form id="add-form" th:action="@{/}" th:object="${animal}" th:method="post">
    <div class="row">
       ...
        <input type="text" th:field="*{name}"></input>
    </div>
    <div class="row">
        <button id="add" type="submit">Добавить в корзину</button>
    </div>
</form>

<table>
   ...
    <tbody th:each="animal, iStat : ${animals}">
    <tr th:style="${iStat.odd}? 'font-weight: bold;'">
        <td th:text="${iStat.index}+1"></td>
        <td th:text="${animal.name}"></td>
    </tr>
    </tbody>
</table>
...
</body>
</html>

Обратите внимание, что файл стиля style.css подключен так

href="style.css"

как будто он лежит в корне сайта.

На самом деле он лежит в папке /static в ресурсах (см. картинку выше). Все, что в корне /static (либо в /public — на выбор), попадает в корень сайта.

Атрибуты модели доступны в представлении

Теперь вернемся к контроллеру, в котором мы добавляли атрибуты в модель:

model.addAttribute("animals", animals);//список животных для вывода
model.addAttribute("animal", new Animal()); //новое пустое животное, 
//которое будет заполнено значением в шаблоне и попадет в параметр метода add() контроллера

Эти атрибуты доступны в представлении по именам ${animal} и ${animals} — см. код index.html выше.

Более подробно о синтаксисе Thymeleaf-шаблона в следующей статье.

Код

Код примера доступен на GitHub.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *