среда, 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, а так же добавлю в демонстрационное приложение аутентификацию и авторизацию.

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

Разработка приложений со Spring Boot. Часть 2: Работа с базами данных

В своём предыдущем посте я описал начало разработки приложения с использованем Spring Boot. В этом посте я расскажу о работе с реляционными базами данных.

Spring Boot и реляционные БД

Для работы с реляционным базами данных в Spring Boot есть стартер spring-boot-starter-data-jpa:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Из названия следует, что стандартный способ работы с реляционными базами данных реализован посредством Spring Data JPA. Основываясь на своём опыте, могу сказать, что возможности этого фреймворка и JPA полностью покрывают потребности практически любого проекта. Исключения составляют сложные запросы с вложенными запросами и кучей джоинов, но и они решаются при помощи хранимых процедур и представлений.
Теперь нашему проекту нужно добавить связь с базой данных. Для демонстрационного проекта вполне хватит и локальной БД вроде HSQLDB или H2:
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

В application.properties нужно добавить два свойства, что бы схема базы данных создавалась при запуске приложения:
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create-drop

Теперь всё готово для написания классов-сущностей и логики работы с базой данных.

Классы сущностей и репозитории

В простейшем хелпдеске есть как минимум две сущности: заявка с описанием проблемы и её обсуждение в виде комментариев.
Для заявки создадим класс Ticket:
@Entity
public class Ticket implements Serializable {

    private static final long serialVersionUID = 20160403085401L;

    /**
     * Идентификатор, генерируется автоматически при создании новой записи.
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**
     * Краткое описание проблемы.
     */
    private String issue;

    /**
     * Подробное описание проблемы.
     */
    @Column(columnDefinition = "TEXT")
    private String description;

    /**
     * Дата создания заявки.
     */
    @Temporal(javax.persistence.TemporalType.TIMESTAMP)
    private Date createDate;

    /**
     * Дата закрытия заявки.
     */
    @Temporal(javax.persistence.TemporalType.TIMESTAMP)
    private Date resolveDate;

    // getters, setters, hashCode, equals, toString
}


Для комментария к заявке создадим класс TicketComment:
@Entity
public class TicketComment implements Serializable {

    private static final long serialVersionUID = 20160403085801L;

    /**
     * Идентификатор комментария. Генерируется автоматически при создании нового комментария.
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**
     * Комментарий.
     */
    private String comment;

    /**
     * Дата создания комментария
     */
    @Temporal(javax.persistence.TemporalType.TIMESTAMP)
    private Date createDate;

    /**
     * Заявка, к которой привязан комментарий.
     */
    @ManyToOne
    private Ticket ticket;

    // getters, setters, hashCode, equals, toString
} 

При помощи JPA-аннотаций можно более подробно сконфигурировать классы сущностей и их маппинг в таблицы базы данных.

Теперь нам нужно определить два интерфейса для взаимодействия с базой данных. Для работы с заявками это будет TicketRepository:

@Repository
public interface TicketRepository extends CrudRepository<Ticket, Long> {

    /**
     * Поиск всех незакрытых заявок.
     */
    List<Ticket> findByResolveDateIsNull(Sort sort);

    /**
     * Получение полного списка заявок с возможностью сортировки.
     */
    List<Ticket> findAll(Sort sort);

    /**
     * Закрытие заявки.
     */
    @Transactional
    @Modifying
    @Query("update Ticket set resolveDate = now() where id = :id")
    int resolveTicket(@Param("id") long id);

    /**
     * Повторное открытие заявки.
     */
    @Transactional
    @Modifying
    @Query("update Ticket set resolveDate = null where id = :id")
    int reopenTicket(@Param("id") long id);
}

Рассмотрим код описанного интерфейса более подробно:
  • Аннотация @Repository указывает, что данный класс или интерфейс является репозиторием и объект этого класса будет использоваться при внедрении зависимостей.
  • Описанный интерфейс расширяет CrudRepository, в котором уже определены основные CRUD-операции. Так же можно расширить интерфейс JpaRepository. Дженерики указывают, что данный репозиторий работает с сущностью Ticket, а классом первичных ключей во всех операциях является Long.
  • Метод findByResolveDateIsNull производит поиск всех записей сущности Ticket, у которых свойство resolveDate не задано.
  • Методы resolveTicket и reopenTicket обновляют запись, делая заявку закрытой или открытой. Для методов, которые модифицируют данные в БД необходимы аннотации @Transactional и @Modifying.
Реализацию данного интерфейса писать не нужно, за нас это сделает Spring Data JPA. Но если со стандартными методами, а так же с методами, с аннотацией @Query всё понятно, то как фреймворк определит поведение метода findByResolveDateIsNull? Всё просто, поведение метода реализуется на основании его названия:
  • Начало названия метода определяет, что нужно сделать в запросе. Например, findBy вернёт записи, соотвествующие критерии выборки (SELECT), deleteBy - удалит (DELETE).
  • Дальше указываются условия запроса. В приведённом примере условием является ResolveDateIsNull, что будет транслировано в resolve_date is null (на SQL). Если бы мы хотели просто произвести поиск по полному соответствию resolveDate, то сигнатура метода выглядела бы следующим образом: List<Ticket> findByResolveDate(Date date). Условия выборки можно объединять при помощи And или Or в зависимости от ситуации.
Методам репозитория так же могут передаваться объекты классов, определяющих дополнительные манипуляции при поиске. В указанном выше методе таким объектом является sort, добавляющий возможность изменения сортировки получаемых записей.
Метод, возвращающий несколько записей из БД может возвращать Iterable, Collection или даже Stream, здесь каждый решит за себя, что ему будет удобнее использовать.

Возможности Spring Data JPA серьёзно ускоряют и упрощают процесс разработки, позволяя меньше отвлекаться на написание кода работы с БД и сконцентрироваться на написании бизнес-логики. Но, что если запрос к БД имеет больше 2-3 условий, или условия выборки несколько сложнее? В этом случае удобным будет использование аннотации @Query, которая позволяет описывать запросы в JPQL или SQL (при установленном свойстве native). Рассмотрим на примере репозитория комментариев, TicketCommentRepository:

@Repository
public interface TicketCommentRepository extends CrudRepository<TicketComment, Long> {

    List<TicketComment> findByTicket(Ticket ticket);

    @Query(value = "select tc from TicketComment tc join ticket t where t.id = :id")
    List<TicketComment> findByTicketId(@Param("ticketId") long id);
}

В свойстве value мы описали простой JPQL-запрос, в котором производится выборка всех TicketComment, у которых id вложенного объекта ticket соответствует искомому. Имена методов, у которых есть аннотация @Query, могут иметь свободную форму, Spring Data JPA реализует поведение этих методом на основании запроса.

По сути, оба описанных выше метода имеют одну и ту же функциональность - они возвращают список комментариев к указанной заявке.

На этом этапе у нас есть классы сущностей и репозитории с реализацией основных действий с БД. В следующем посте я опишу работу со Spring WebMVC на примере контроллеров и представлений с использованием Thymeleaf.

А что если...

  • Нужна нормальная СУБД, а не H2? 
    • Добавляем в зависимости нужный драйвер:
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>

    • Описываем свойства подключения в application.properties:
spring.datasource.platform=postgresql
spring.datasource.url=jdbc:postgresql://localhost:5432/postgresql
spring.datasource.username=postgresql
spring.datasource.password=postgresql
spring.datasource.driver-class-name=org.postgresql.Driver

  • Нужна нестандартная реализация источника данных?
    • Добавляем в зависимости библиотеку с желаемой реализацией источника данных:
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
</dependency>

    • В классе конфигурации создаём метод с аннотацией @Bean, в котором создаём объект источника данных:
    @Value("${spring.datasource.type}")
    private String dataSourceClassName;

    @Value("${spring.datasource.url}")
    private String dataSourceUrl;

    @Value("${spring.datasource.username}")
    private String dataSourceUsername;

    @Value("${spring.datasource.password}")
    private String dataSourcePassword;

    @Bean
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDataSourceClassName(dataSourceClassName);
        dataSource.setDataSourceProperties(getDataSourceProperties());

        return dataSource;
    }

    private Properties getDataSourceProperties() {
        Properties properties = new Properties();
        properties.setProperty("url", dataSourceUrl);
        properties.setProperty("user", dataSourceUsername);
        properties.setProperty("password", dataSourcePassword);

        return properties;
    }

четверг, 24 марта 2016 г.

Разработка приложений со Spring Boot. Часть 1: Основы разработки приложений со Spring Boot.

Этим постом я хочу начать серию статей о Spring Boot, в которой расскажу и покажу на наглядных примерах, как разрабатывать приложения используя Spring Boot и Spring Framework. За год активного использования этого фреймворка накопилось немало опыта, информации и нетривиальных моментов, которые приходилось решать, что пора бы и поделиться этим всем.


Что такое Spring Boot?


Spring Boot - это фреймворк для быстрой разработки приложений на основе Spring Framework и его компонентов, входящих в Spring Data, Spring Security и другие подпроекты. Spring Boot предоставляет огромное количество сконфигурированных компонентов, что позволяет сократить время, затрачиваемое на конфигурирование приложения и состредоточиться непосредственно на разработке, а так же упрощает работу с зависимостями. Ну и конечно Spring Boot позволяет легко и просто разрабатывать bootiful-приложения (так разработчики Spring называют standalone-приложения, основанные на Spring Boot). Но это всё лишь поверхностно, на самом деле возможности Spring Boot значительно мощнее. В целом, Spring Boot является идеальным инструментом для разработки микросервисов.


О демонстрационном проекте


В рамках цикла статей я буду демонстрировать примеры использования Spring Boot на примере разработки достаточно простого, но в то же время наглядного веб-приложения - сервисдеска/хелпдеска.

Для проекта потребуется JDK 1.8, Maven и любая среда разработки (в моём случае - NetBeans).


Подготовка проекта


Создадим новый maven-проект с упаковкой в WAR. В нашем случае это обусловлено тем, что в рамках статей будет продемонстрировано использование JSP для построения представлений, а так же будет продемонстрировано развёртывание приложения в сервере приложений. Если вам этого не нужно, то с лихвой хватит и JAR-упаковки. В любом случае, и JAR, и WAR являются исполняемыми при использовании Spring Boot.
Первое, что нужно сделать - добавить управление зависимостями, предоставляемое Spring Boot.
Это можно сделать двумя способами:

1. Указать в качестве родительского проекта spring-boot-starter-parent, если у проекта нет родительского:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.3.RELEASE</version>
    </parent>

2. Указать dependencyManagement:

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>1.3.3.RELEASE</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>

Это решит все возможные проблемы с версиями зависимостей, которые описаны в Spring Boot. Хоть этот шаг и необязателен, я рекомендую его проделывать при разработке приложений, так как разные стартеры одной версии Spring Boot могут ссылаться на разные версии одной и той же зависимости, что может привести к неожиданным и неочевидным ошибкам.


Второй шаг при подготовке проекта - указание maven-плагина, предоставляемого Spring Boot для сборки проекта:

1. При использовании родительского проекта:
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

2. При использовании dependencyManagement:
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

Этот плагин найдёт класс, содержащий метод public static void main, пометит его главным и соберёт исполняемый JAR или WAR-файл, а так же скопирует в него все зависимости.

Ну и последний шаг, что бы получить работающее приложение - добавление в зависимости как минимум одного стартера (spring-boot-starter). В нашем случае понадобится spring-boot-starter-thymeleaf:
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

Spring Boot предоставляет большое количество стартеров практически на все случаи жизни. Стартер - это зависимость, содержащая все зависимости необходимые для реализации какой-либо функциональности в рамках разрабатывамого приложения. Так, например, что бы добавить приложению веб-функциональность, понадобится spring-boot-starter-web, который позволяет разрабатывать как стандартные веб-приложения, основанные на Spring WebMVC, так и REST-сервисы. Если же есть необходимость добавить приложению управление доступом, то можно добавить spring-boot-starter-security. Указанный мной стартер spring-boot-starter-thymeleaf содержит все зависимости, необходимые для разработки веб-приложения с использованием Thymeleaf в качестве фреймворка для построения представлений.

Теперь у нас всё готово для непосредственной разработки приложения на Spring Boot.

Разработка приложения

Самый простой пример - отображение представления в браузере без использования контроллера.

Первым делом создадим, класс, с которого будет начинаться работа нашего приложения:
package name.alexkosarev.bootdesk;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;

@SpringBootApplication
public class Application extends SpringBootServletInitializer {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args)
                .registerShutdownHook();
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(Application.class);
    }
}

Вкратце пробегусь по написанному коду:
  • Аннтоация @SpringBootApplication объединяет аннотации @Configuration, @EnableAutoConfiguration и @ComponentScan, объявляет Application классом-конфигурацией, включает автоматическую конфигурацию приложения и включает автоматический поиск компонентов в пакете name.alexkosarev.bootdesk и во всех вложенных.
  • SpringApplication.run(Application.class, args) запускает приложение при запуске при помощи java -jar
  • Класс Application расширяет класс SpringBootServletInitializer и переопределяет метод SpringApplicationBuilder configure(SpringApplicationBuilder builder) для запуска приложения при развёртывании в сервере приложении.

Кстати, на данном этапе приложение уже можно запустить. Но мы не добавили в приложение ничего, что можно было бы увидеть.
Создадим класс WebConfig в name.alexkosarev.bootdesk.config, в котором сконфигурируем перенаправление с / на /site/index и добавим отображение главной страницы при переходе на /site/index:
package name.alexkosarev.bootdesk.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addRedirectViewController("/", "/site/index");

        registry.addViewController("/site/index")
                .setViewName("site/index");
    }

}

Осталось добавить представление. Spring WebMVC по умолчанию в Spring Boot ищет представления в директории templates. Создадим html-файл в директории templates/site (как мы указали в WebConfig). Обратите внимание, что Thymeleaf в режиме HTML5, который выставлен по умолчанию, ожидает XML-валидный HTML (по сути XHTML). Если же вы привыкли писать простой HTML, то нужно проделать следующие два шага:
  1. Добавить в файл свойств application.properties, расположенном в ресурсах проекта, строку spring.thymeleaf.mode=LEGACYHTML5
  2. Добавить в зависимости проекта nekohtml из группы nekohtml:
            <dependency>
                <groupId>nekohtml</groupId>
                <artifactId>nekohtml</artifactId>
                <version>1.9.6.2</version>
            </dependency>
    

NekoHTML будет превращать обычный HTML-код в XML-валидный.

На данном этапе приложение имеет минимальную функциональность и готово к первому запуску.

Сборка и запуск приложения


Для начала соберём приложение стандартным способом: командой mvn package или при помощи IDE. В директории target окажется два варианта WAR-архива: bootiful, с именем файла, заканчивающимся на .war, и обычный, заканчивающийся на .war.orginal. Обычный WAR-файл не является исполняемым, не содержит provided-зависимости, но может быть развёрнут в сервере приложений. Bootiful-вариант содержит все необходимые для работы зависимости, не может быть развёрнут в сервере приложений (попытка развернуть его в сервере приложений приведёт к ошибке), но может быть запущен как самостоятельное приложение при помощи команды java -jar bootdesk-1.0.0.war.

Кстати, если вы разрабатываете приложение на основе Spring Boot с упаковкой в JAR, то после сборки получите так же два варианта: bootiful, содержащий все необходимые для работы зависимости, и обычный вариант, зависимости для запуска которого нужно будет указывать при помощи -classpath.

Запустив приложение при помощи команды java -jar bootdesk-1.0.0.war мы увидим вывод нашего приложения.
Если мы откроем адрес http://localhost:8080, приложение сначала нас перенаправит на http://localhost:8080/site/index, а затем покажет нам содержимое index.html:


Spring Boot и JSP


По умолчанию ни один стартер из предоставляемых Spring Boot не предоставляет возможности работать с JSP. Но это решается достаточно просто:
1. В зависимости проекта нужно добавить tomcat-embed-jasper и jstl:
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

Обратите внимание на scope=provided для tomcat-embed-jasper.

2. В WebConfig сконфигурировать ViewResolver:
    @Bean
    public ViewResolver viewResolver() {
        UrlBasedViewResolver urlBasedViewResolver = new UrlBasedViewResolver();
        urlBasedViewResolver.setViewClass(JstlView.class);
        urlBasedViewResolver.setPrefix("/WEB-INF/templates/");
        urlBasedViewResolver.setSuffix(".jspx");
        
        return urlBasedViewResolver;
    }

После этого можно будет использовать JSP-представления, а так же использовать Apache Tiles, если в этом будет необходимость.

В следующем посте я добавлю приложению немного функцональности, добавив взаимодействие с базой данных посредством Spring Data JPA. Так же я постараюсь в ближайшие несколько дней выложить проект в GitHub и опубликую видеоподкаст.

среда, 9 сентября 2015 г.

Spring Security и кастомная аутентификация


Краткая предыстория

Один из моих проектов представлялет собой, по сути, набор REST-сервисов на Spring WebMVC с AngularJS-фронтендом. В качестве механизма аутентификации в REST-бекенде я выбрал token-аутентификацию. При этом клиент сначала авторизуется при помощи BASIC или Digest-авторизации, получает в ответ токен аутентификации и уже с ним обращается к REST-сервисам. Такая идея пришла в голову после прочтения https://spring.io/blog/2015/01/12/spring-and-angular-js-a-secure-single-page-application и нескольких постов на хабре. Код аутентификации по сути был эдакой сборной солянкой из обрывков кода из найденных постов. Красотой особой он не отличался, но работал как надо и на первое время сгодился. Большего от него и не требовалось.

Однако это было, когда бекенд был обычным веб-приложением и жил себе спокойно в томкате. Но всё изменилось, когда я решил его перевести на Spring Boot. Процесс аутентификации просто напросто зацикливался и я в ответ получал StackOverflowException. Варианта решения этой проблемы для меня было три: отказаться от Spring Boot, вылечить моего Франкенштейна или же наконец-то разобраться с Spring Security и реализовать нормальный механизм аутентификации. В итоге я выбрал самый сложный и самый интересный вариант - последний.

Компоненты Spring Security
Процесс аутентификации в Spring Security проходит через следующие компоненты:
- Фильтр
Задача фильра - определить, удовлетворяет ли запрос условиям для запуска процесса аутентификации, и, если запрос будет им удовлетворять, создать объект аутентификации, попробовать его аутентифицировать при помощи менеджера аутентификации и, в случае успеха, положить объект аутентификации в контекст. Если же не получилось аутентифицировать пользователя по полученным данным, то можно либо продолжить цепочку фильтров в надежде аутентифцировать пользователя каким-то другим способом, либо вызвать метод commence точки входа, который сообщит пользователю, что данный метод аутентификации обязателен.
- Объект аутентификации (Authentication)
В объекте аутентификации хранится информация, полученная фильтром из запроса. По этому объекту менеджеры и провайдеры будут пытаться аутентифицировать пользователя в системе.
- Менеджер аутентификации (AuthenticationManager)
По сути, задача менеджера - обойти все известные ему провайдеры аутентификации, найти подходящий и попробовать аутентифицировать полученный от фильтра объект аутентификации. Менеджер аутентификации можно использовать стандартный, в него нужно будет только добавить нашего провайдера аутентификации.
- Провайдер аутентификации (AuthenticationProvider)
Провайдер проверяет, поддерживает ли он объект аутентификации указанного класса, и, если подерживает, пытается аутентифицировать пользователя. В стандартном подходе провайдер обращается к методу loadUserByUsername объекта класса UserDetailsService и, найдя его, проверяет предоставленные данные на соответствие полученным из БД (например, проверяет корректность введённого пароля). Если у провайдера получается аутентифицировать пользователя, то он возвращает объект авторизации с пометкой об успешной аутентификации и пользовательскими данными.
- Сервис пользовательских данных (UserDetailsService)
Этот сервис просто вытаскивает пользователя по полученным данным из хранилища (БД, памяти, properties-файла, нужное подчеркнуть).
- Пользовательские данные (UserDetails)
В пользовательских данных хранится необходимая информация об аутентифицированном пользователе - имя пользователя, пароль и список выданных ролей. Данный класс удобно расширять для хранения каких-то собственных данных о пользователе, которые понадобятся при обработке запроса.

Реализация аутентификации

В моём варианте аутентификация будет производиться по токену. Токен может передаваться в параметрах запроса, либо в заголовках. Мне лично больше нравится вариант с заголовком, который я назову X-Auth-Token.
Так же в моём варианте не предусмотрен какой-либо другой вариант аутентификации, а это значит, что в случае отсутствия заголовка в запросе, приложение должно ответить клиенту, что оно ожидает увидеть этот заголовок.
В рассматриваемом примере система должна аутентифицировать пользователя по токену, переданному в заголовке. Токен для наглядности будет иметь формат username:password, но в реальной жизни он будет являться уникальным ключом.

Нам потребуются следующие зависимости:
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>1.2.5.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
        <version>1.2.5.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>18.0</version>
        <type>jar</type>
    </dependency>
</dependencies>

Собственно, фильтр:
public class TokenAuthenticationFilter extends GenericFilterBean {

    private final AuthenticationManager authenticationManager;

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    @Value("${application.tokenAuthentication.header}")
    private String header;

    @Value("${application.tokenAuthentication.ignoreFault}")
    private boolean ignoreFault;

    public TokenAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;

        try {
            String headerValue = httpServletRequest.getHeader(header);
            if (Strings.isNullOrEmpty(headerValue)) {
                throw new TokenAuthenticationHeaderNotFound("Header " + header + " is not found.", null);
            }

            Authentication authentication = authenticationManager.authenticate(new TokenAuthentication(headerValue));

            SecurityContextHolder.getContext().setAuthentication(authentication);

            filterChain.doFilter(servletRequest, servletResponse);
        } catch (AuthenticationException authenticationException) {
            if (!ignoreFault) {
                authenticationEntryPoint.commence(httpServletRequest, httpServletResponse, authenticationException);
            } else {
                filterChain.doFilter(servletRequest, servletResponse);
            }
        }
    }
} 


Класс аутентификации:
public class TokenAuthentication extends AbstractAuthenticationToken {

    private final String token;

    public TokenAuthentication(String token) {
        super(null);
        this.token = token;
    }

    public TokenAuthentication(String token, Collection authorities) {
        super(authorities);
        this.token = token;
    }

    @Override
    public Object getCredentials() {
        return token.split(":")[1];
    }

    @Override
    public Object getPrincipal() {
        return token.split(":")[0];
    }

}

Точка входа аутентификации:
public class TokenAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        if (authException instanceof TokenAuthenticationHeaderNotFound) {
            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED, authException.getMessage());
        } else {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
        }
    }

}

Провайдер аутентификации:
public class TokenAuthenticationProvider implements AuthenticationProvider {

    private static final Logger LOGGER = LoggerFactory.getLogger(TokenAuthenticationProvider.class);

    private final UserDetailsService userDetailsService;

    public TokenAuthenticationProvider(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        TokenAuthentication tokenAuthentication = (TokenAuthentication) authentication;

        String token = (String) tokenAuthentication.getPrincipal();
        UserDetails userDetails = userDetailsService.loadUserByUsername(token.split(":")[0]);
        if (userDetails == null) {
            throw new UsernameNotFoundException("Unknown token");
        }

        tokenAuthentication.setAuthenticated(true);
        tokenAuthentication.setDetails(userDetails);

        return authentication;
    }

    @Override
    public boolean supports(Class authentication) {
        return authentication == TokenAuthentication.class;
    }

}

Конфигурация:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests().anyRequest().authenticated()
                .and()
                .httpBasic().disable()
                .formLogin().disable()
                .csrf().disable()
                .addFilterBefore(tokenAuthenticationFilter(authenticationManager()), BasicAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user").password("password").roles("USER")
                .and()
                .withUser("admin").password("adminPassword").roles("USER", "ADMIN");
        auth.authenticationProvider(tokenAuthenticationProvider(auth.getDefaultUserDetailsService()));
    }

    @Bean
    public AuthenticationEntryPoint tokenAuthenticationEntryPoint() {
        return new TokenAuthenticationEntryPoint();
    }

    @Bean
    public AuthenticationProvider tokenAuthenticationProvider(UserDetailsService userDetailsService) {
        return new TokenAuthenticationProvider(userDetailsService);
    }

    @Bean
    public Filter tokenAuthenticationFilter(AuthenticationManager authenticationManager) {
        return new TokenAuthenticationFilter(authenticationManager);
    }
}

Поскольку мне интересно видеть в проекте только собственную схему аутентификации, другие схемы отключены (BASIC, form-based), так же используются сессии без сохранения состояния, что вполне логично для REST-бекенда.

Теперь при отправке HTTP-запроса с заголовком X-Auth-Token: user:password будет происходить аутентификация пользователя в системе. Для пущей кастомизации можно (а в большинстве случаев нужно) написать свои реализации UserDetails и UserDetailsService. Хотя в случае с передачей токена, являющегося уникальным ключом, по которому можно получить пользователя, нужно будет реализовывать свой сервис, аналогичный UserDetailsService, но загружающий пользователя по токену, а не по имени пользователя.

А в случае, когда токен является зашифрованной строкой, содержащей логин и пароль пользователя, можно даже обойтись без собственной реализации Authentication - достаточно будет использовать стандартную реализацию UsernamePasswordAuthenticationToken.

В следующий раз попробую написать пример многофакторной авторизации на основе Spring Security.

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

четверг, 30 июля 2015 г.

Spring Boot на примере простого REST-сервиса

Последние пару месяцев я активно изучаю Spring Boot и перевожу некоторые из своих проектов на эту архитектуру. В этом посте я в кратце опишу создание простого приложения на Spring Boot.

Стартеры и зависимости

Разработчики Spring Boot постарались по максимуму уменьшить время, уходящее на конфигурирование проекта, добавив в Maven артефакты spring-boot-starter-* из группы org.springframework.boot. По сути это зависимости, которые тянут за собой остальные зависимости, необходимые для разработки приложения нужного типа. Подробнее со списком доступных стартеров можно ознакомиться в официальной документации по Spring Boot.

Хотя, если вы хотите самостоятельно указывать используемые зависимости, можно не использовать стартеры, а указать в зависимостях артефакт spring-boot-autoconfigure из org.springframework.boot. Но в целом стартеры очень удобны.

Приложение

В данном примере я буду использовать единственную зависимость:
<dependencies>
    <dependency>
        <groupid>org.springframework.boot</groupid>
        <artifactid>spring-boot-starter-web</artifactid>
        <version>1.2.5.RELEASE</version>
    </dependency>
</dependencies> 
Она за собой потянет всё необходимое для написания простого веб-приложения.

Так как это standalone-приложение, нам потребуется стадартый класс с методом main:
package name.alexkosarev.sandbox.springboot.rest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args).registerShutdownHook();
    }
}
В данном случае аннотация @SpringBootApplication заменяет собой аннотации @Configuration, @EnableAutoConfiguration и @ComponentScan.

В моём примере REST-сервис будет возвращать объект простого класса, в котором будет текстовое сообщение:

package name.alexkosarev.sandbox.springboot.rest.wrappers;

public class MessageWrapper {

    private String message;

    public MessageWrapper() {
    }

    public MessageWrapper(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

}

Теперь нужно добавить контроллер, который будет обрабатывать запрос и возвращать ответ:
package name.alexkosarev.sandbox.springboot.rest.services;

import name.alexkosarev.sandbox.springboot.rest.wrappers.MessageWrapper;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/example")
public class ExampleService {

    @RequestMapping
    public MessageWrapper index() {
        return new MessageWrapper("Hello world!");
    }
}

Аннотация @RestController комбинирует в себе @Controller и @ResponseBody, указывая, что данный контроллер является REST-контроллером и возвращаемые значения методов данного класса, помеченных аннотацией @RequestMapping, будут ответами на HTTP-запросы.

Теперь можно запустить приложение и открыть адрес http://localhost:8080/example. В ответ будет выведено {"message":"Hello world!"}.

МегаJAR

Это конечно, круто, скажите вы, но теперь у нас есть JAR с приложением и горстка JAR с зависимостями. Можно ли как-то с этим бороться? Да, можно. Разработчики Spring предлагают 2 варианта решения данной проблемы:
  1. Поместить все JAR зависимостей в директорию lib внутри JAR проекта, что не очень правильно
  2. Добавить в Maven-плагин spring-boot-maven-plugin из группы org.springframework.boot с целью repackage, в результате чего в директории target будет лежать 2 JAR: обычный и толстый, в который будут скопировано содержимое всех JAR зависимостей.
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <version>1.2.5.RELEASE</version>
    <executions>
        <execution>
   <goals>
       <goal>repackage</goal>
   </goals>
        </execution>
    </executions>
</plugin>

Микросервисы глазами простого разработчика

Последние два-три года стало модно говорить о микросервисах. В твиттере и на профильных ресурсах слово "microservice" мелькает уж очень часто. Но...

В моём представлении микросервис по сути представляет собой самостоятельный компонент большой системы, выполняющий узкий круг задач. Кто-то превращает большие монолитные проекты в набор микросервисов, кто-то создаёт проекты сразу изначально в виде набора микросервисов. Наиболее важная на мой взгляд выгода в использовании микросервисов - достижение слабой связанности компонентов проекта. Плюс можно разделить микросервисы между командами разработчиков, что наверняка увеличит производительность труда.

Интересно, что микросервисы как подход к разработке можно было применять и пять лет назад и десять - этому ничего не мешало, просто не было такого тренда. Никто не мешал дробить крупные Java EE-проекты на большое количество маленьких WAR и JAR, разворачивать фермы Glassfish или JBoss и добиваться той же слабой связанности. Так же никто не мешал разрабатывать микросервисы в виде standalone-приложений. В целом этому способствовало и существование OSGi-контейнеров, которые, впрочем, в народе не особо прижились. Наиболее важной причиной увеличения популярности микросервисов на мой взгляд является популярность облачных сервисов: PaaS, IaaS и прочих XaaS.

Standalone или контейнеры?

В этом вопросе, на мой взгляд, удобнее standalone-микросервисы. Во-первых при обновлении библиотек любой контейнер прийдётся перезапускать вместе со всеми запущенными в нём приложениями. Это решается запуском нескольких инстансов контейнеров и фэйловер-конфигурацией, но... Во-вторых лучше много мелких инстансов JVM, чем несколько раздутых. Однако, если в микросервисах используется HTTP-контейнер, то для каждого инстанса понадобится свой порт, что не очень удобно.

Spring Boot и WildFly Swarm

На волне популярности микросервисов начали появляться фреймворки и проекты, упрощающие разработку микросервисов: к вышеупомянутым OSGi-контейнерам добавился Spring Boot, а так же ведётся разработка WildFly Swarm. Оба проекта предоставляют возможность разрабатывать микросервисы в виде standalone-приложений, минимизируя затрачиваемое время на конфигурирование компонента.

Стоит отметить, что Spring Boot уже является стабильным и достаточно успешным фреймворком, с помощью которого можно без лишней головной боли разрабатывать standalone-приложения, основанные на Spring Framework. Хотя далеко не все рекомендуют это делать, особенно приверженцы Java EE.

А вот с WildFly Swarm всё несколько хуже. Идея проекта заключается в интеграции Java EE в standalone-приложения. Смысл в этом есть, но сам проект ещё находится в разработке и до стабильного релиза, судя по всему, ещё очень далеко. Если при разработке простенького микросервиса с REST в случае со Spring Boot в итоге получается компактный jar на 5-10Мб, то в случае WildFly Swarm у вас в зависимостях будет добрая половина сервера приложений WildFly. Модульность Spring Framework взяла своё в данном случае.

Слово "микросервис" использовано в данном посте 20 раз.

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

Spring Framework, Hibernate, JPA: Аннотации или XML?

Споры на данную тему в стане приверженцев Spring Framework существуют с момента релиза третьей версии данного фреймворка. И хотя в версии 2.5 уже можно было частично заменить конфигурирование приложения аннотациями, полностью отказаться от XML было нереально.

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

Стоит отметить забавный момент, что среди разработчиков фреймворка во время Spring Framework 3 не было единого мнения по этому поводу - в книге Pro Spring 3 использовались XML-конфигурации, а в книге Pro Spring MVC: with Web Flow - аннотации.

XML

С XML-конфигурациями в Spring Framework я работаю ещё с версии 2.5. На тот момент альтернатив не было. Зато можно было хотя бы использовать аннотацию @Autowired для внедрения зависимостей, чему я предпочитал непосредственное указание зависимостей в XML через сеттеры и конструкторы. С тех пор я унаследовал привычку конфигурировать практически всё при помощи XML, в том числе JPA и Hibernate. Для меня главным аргументом в пользу XML была возможность изменять настройки приложения на лету без необходимости собирать проект заново. Такая необходимость возникала, но нечасто. Хотя объективности ради стоит заметить, что именяющиеся компоненты Spring-приложения вполне можно конфигурировать properties-файлами или через JNDI.

Против XML главным аргументом всегда являлся тот факт, что в больших проектах XML-конфигурацию очень сложно читать ввиду большого количества конфигурируемых компонентов и нередко большого количества файлов конфигурации. В качестве примера можно привести проекты CMDBuild и OpenOLAT.

Актуальность XML-конфигураций теряется ещё и в том случае, когда разработчик не имеет физического доступа к развёрнутому приложению, как это зачастую бывает в серьёзных организациях. Именно это и стало для меня решающим фактором в пользу перехода на использование аннотаций.


Аннотации

В пользу аннотаций можно однозначно записать простоту. Конфигурирование приложения посредством аннотаций происходит проще, чем при использовании XML. Всё делается тем же Java-кодом. Разработчику в данном случае даже не обязательно знать XML, хотя я сильно сомневаюсь, что в природе существуют Java-разработчики без базовых знаний XML.

Отдельно стоит отметить тот факт, что в мире Java EE всё давно конфигурируется при помощи аннтоаций: EJB, CDI, JPA и т.д. и Spring Framework совместим со всеми аннотациями Java EE.

Ну а минус, соответственно, заключается в отсутствии возможности конфигурировать проект на лету.

Groovy

Отдельно стоит упомянуть возможность конфигурировать Spring-приложения при помощи Groovy-классов. Если классы конфигурации не компилировать, а оставлять скриптами, то приложение можно будет конфигурировать на лету. И при этом сам процесс конфигурирования будет значительно более гибким, чем даже при использовании XML. Но это если в проекте используется Groovy.

Вывод

Вывод прост - нужно стремиться к использованию аннотаций, раз это действительно проще, правильнее и рекомендуется разработчиками Spring Framework. Тем более, что в Java EE это стандарт, а на горизонте маячит Spring Boot, хотя никто не запрещает и там использовать XML.

Однако в плане маппингов JPA и Hibernate я так и остался при мнении, что использование XML значительно удобнее аннотаций, особенно в случаях, когда POJO-классы находятся в отдельной от бизнес-логики библиотеке. В этом случае клиентским Java-приложениям при использовании тех же API не нужно будет тянуть ненужные зависимости.