среда, 6 апреля 2016 г.

Разработка приложений со Spring Boot. Часть 3: Разработка веб-приложений со Spring WebMVC и Thymeleaf

В первом посте я уже описал начальную настройку веб-приложения. Логика работы с БД уже готова, теперь мы займёмся описанием представлений и контроллеров.

Контроллеры

Для начала создадим пару контроллеров, в которых будет реализована вся бизнес-логика для работы с заявками и комментариями к ним. TicketController:
@Controller
@RequestMapping("/ticket")
public class TicketController {

    @Autowired
    private TicketRepository ticketRepository;

    @Autowired
    private TicketCommentRepository ticketCommentRepository;

    /**
     * Список заявок.
     *
     * @param all Отображать все заявки
     * @return
     */
    @RequestMapping({"", "/", "/index"})
    public ModelAndView index(@RequestParam(required = false, defaultValue = "false") boolean all) {
        return new ModelAndView("ticket/index")
                .addObject("all", all)
                .addObject("tickets", all
                        ? ticketRepository.findAll(new Sort(Sort.Direction.DESC, "createDate"))
                        : ticketRepository.findByResolveDateIsNull(new Sort(Sort.Direction.DESC, "createDate")));
    }

    /**
     * Создание новой заявки.
     *
     * @param ticket Заявка
     * @return
     */
    @RequestMapping(method = RequestMethod.POST)
    public ModelAndView create(@ModelAttribute Ticket ticket) {
        ticket.setCreateDate(new Date());
        ticket = ticketRepository.save(ticket);

        return new ModelAndView("redirect:/ticket/" + ticket.getId() + "/view");
    }

    /**
     * Просмотр заявки.
     *
     * @param id Идентификатор запрашиваемой заявки
     * @return
     */
    @RequestMapping("/{id:\\d+}/view")
    public ModelAndView view(@PathVariable long id) {
        Ticket ticket = ticketRepository.findOne(id);

        return new ModelAndView("ticket/view")
                .addObject("ticket", ticket)
                .addObject("ticketComments", ticketCommentRepository.findByTicketId(id));
    }

    /**
     * Закрытие заявки.
     *
     * @param id Идентификатор запрашиваемой заявки
     * @return
     */
    @RequestMapping("/{id:\\d+}/resolve")
    public ModelAndView resolve(@PathVariable long id) {
        ticketRepository.resolveTicket(id);

        return new ModelAndView("redirect:/ticket/" + id + "/view");
    }

    /**
     * Повторное открытие заявки.
     *
     * @param id Идентификатор запрашиваемой заявки
     * @return
     */
    @RequestMapping("/{id:\\d+}/reopen")
    public ModelAndView reopen(@PathVariable long id) {
        ticketRepository.reopenTicket(id);

        return new ModelAndView("redirect:/ticket/" + id + "/view");
    }
}

Человек, знакомый с Spring WebMVC, ничего нового не увидит в этом контроллере.

Аналогичным образом выглядит и контроллер для работы с комментариями к заявкам, TicketCommentController:

@Controller
@RequestMapping("/ticket/{ticketId:\\d+}/ticketComment")
public class TicketCommentController {

    @Autowired
    private TicketCommentRepository ticketCommentRepository;

    @Autowired
    private TicketRepository ticketRepository;

    @RequestMapping(method = RequestMethod.POST)
    public ModelAndView create(@PathVariable long ticketId, @ModelAttribute TicketComment ticketComment) {
        ticketComment.setTicket(ticketRepository.findOne(ticketId));
        ticketComment.setCreateDate(new Date());
        ticketCommentRepository.save(ticketComment);

        return new ModelAndView("redirect:/ticket/" + ticketId + "/view");
    }
}

Отображения с использованием Thymeleaf

Как уже было сказано, по умолчанию Thymeleaf в связке с Spring Boot будет искать шаблоны в classpath:/templates. Создадим поддиректорию ticket, в которой будут находиться отображения для заявок.

Страница со списком заявок:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Bootdesk :: Tickets</title>
        <meta charset="UTF-8"/>
        <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"/>
    </head>
    <body>
        <div class="container">
            <div class="row">
                <div class="col-lg-4" th:include="ticket/create :: #_ticket_form"></div>
                <div class="col-lg-8">
                    <h1>Tickets list
                        <small>
                            <a th:if="!${all}" th:href="@{/ticket(all=true)}">All</a>
                            <a th:if="${all}" th:href="@{/ticket}">Not resolved</a>
                        </small>
                    </h1>
                    <table class="table table-striped">
                        <thead>
                            <tr>
                                <th>
                                    ID
                                </th>
                                <th>
                                    Issue
                                </th>
                                <th>
                                    Opened
                                </th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr th:each="ticket : ${tickets}">
                                <td th:text="${ticket.id}">ID</td>
                                <td>
                                    <a th:style="${ticket.resolveDate != null}?'text-decoration:line-through;':''" th:href="@{/ticket/{id}/view(id=${ticket.id})}" th:text="${ticket.issue}"></a>
                                </td>
                                <td th:text="${ticket.createDate}">Opened</td>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </body>
</html>


В качестве небольшого украшения дизайна я использовал Twitter Bootstrap. В глаза сразу бросаются Thymeleaf-аттрибуты в HTML-тэгах. Пробежимся по ним вкратце:
  • th:include вкладывает в текущий шаблон часть другого. В качестве значения указывается путь к вкладываемому файлу отображения. Если нужно вложить не весь файл, а лишь его часть, то после имени добавляется :: и CSS-селектор вкладываемого компонента. В данном случае будет вложен элемент с id="_ticket_form".
  • th:if - условный аттрибут. В качестве значения используется SPEL-выражение. Если выражение возвращает true, то элемент будет отображён.
  • th:href - аттрибут, генерирующий значение аттрибута href у ссылки. С тонкостями этого аттрибута лучше ознакомиться в официальной документации Thymeleaf.
  • th:text - определяет тектовое содержимое элемента.
  • th:style - настройки CSS-стиля элемента.
Thymeleaf-аттрибуты могут работать с SPEL-выражениями, что показано на примере зачёркивания закрытой заявки.

Вкладываемое отображение выглядит следующим образом:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Bootdesk :: Create New Ticket</title>
        <meta charset="UTF-8"/>
        <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    </head>
    <body>
        <div id="_ticket_form">
            <form method="post" th:action="@{/ticket}">
                <div class="form-group">
                    <label for="_ticket_issue">Issue</label>
                    <input type="text" class="form-control" name="issue" placeholder="Type your problem" id="_ticket_issue"/>
                </div>
                <div class="form-group">
                    <label for="_ticket_description">Description</label>
                    <textarea placeholder="Input a detailed description of your problem here ..." id="_ticket_description" name="description" class="form-control"></textarea>
                </div>
                <button class="btn btn-primary btn-block"><i class="glyphicon glyphicon-send"></i> Submit</button>
            </form>
        </div>
    </body>
</html> 

Thymeleaf в связке с Spring Boot ищет вкладываемые отображения в той же самой директории - classpath:/templates.

В этом отображении используется аттибут th:action у тэга form, он задаёт URL, который будет указан в аттрибуте action. Его поведение аналогично th:href у ссылок.

Осталось отображение заявки с её обсуждением:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Bootdesk :: Open Tickets</title>
        <meta charset="UTF-8"/>
        <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"/>
    </head>
    <body>
        <div class="container">
            <div class="row">
                <div class="col-lg-4" th:include="ticket/create :: #_ticket_form"></div>
                <div class="col-lg-8">
                    <a th:href="@{/ticket}">← All tickets</a>
                    <h1 th:style="${ticket.resolveDate != null}?'text-decoration:line-through;':''">Ticket #<span th:text="${ticket.id}"></span>: <span th:text="${ticket.issue}"></span></h1>
                    <div>Created: <span th:text="${ticket.createDate}"></span></div>
                    <p th:text="${ticket.description}"></p>
                    <br/>
                    <div>
                        <a th:if="${ticket.resolveDate == null}" th:href="@{/ticket/{ticketId}/resolve(ticketId=${ticket.id})}">Resolve</a>
                        <a th:if="${ticket.resolveDate != null}" th:href="@{/ticket/{ticketId}/reopen(ticketId=${ticket.id})}">Re-open</a>
                    </div>
                    <hr/>
                    <h3>Discussion</h3>
                    <div th:each="ticketComment : ${ticketComments}" class="well well-sm">
                        <strong th:text="${ticketComment.createDate}"></strong>
                        <p th:text="${ticketComment.comment}"></p>
                    </div>
                    <form th:action="@{/ticket/{ticketId}/ticketComment(ticketId=${ticket.id})}" method="post">
                        <div class="form-group">
                            <label>Your comment:</label>
                            <textarea name="comment" placeholder="Input your comment here..." class="form-control"></textarea>
                        </div>
                        <button class="btn btn-primary pull-right"><i class="glyphicon glyphicon-comment"></i> Comment</button>
                    </form>
                </div>
            </div>
        </div>
    </body>
</html>  

В следующем посте я опишу работу со Spring Security в связке со Spring Boot, а так же добавлю в демонстрационное приложение аутентификацию и авторизацию.

Полезные ссылки

3 комментария: