
В Spring Framework 5 была добавлена возможность функциональной регистрации компонентов (бинов) в контексте приложения.
Читать далее Spring Framework: Функциональная регистрация компонентовВ Spring Framework 5 была добавлена возможность функциональной регистрации компонентов (бинов) в контексте приложения.
Читать далее Spring Framework: Функциональная регистрация компонентовКомпоненты Bean Validation могут быть сконфигурированы при помощи XML-файлов. Такой подход может быть удобен, когда требуется добавить валидацию классов, недоступных для изменения, либо в тех случаях, когда хочется избежать появления сторонних зависимостей в коде. Основной XML-файл конфигурации — META-INF/validation.xml, в нём находятся основные настройки Bean Validation.
Для валидации данных в Java EE существует Bean Validation. Первая версия данного набора API была специфицирована в JSR-303 и опубликована как часть Java EE 6. Текущая версия — 2.0, является частью Java EE 8 и описана в JSR-380. Эталонной реализацией Bean Validation является Hibernate Validator. Bean Validation может использоваться не только в классических приложениях на основе Java EE, но и в приложениях на основе Spring, и даже в приложениях, не имеющих отношения к Java EE.
Сервис, реализующий бизнес-логику работы с заметками, который я описал в своей предыдущей статье, достаточно простой. В реальной жизни требуются различные проверки при выполнении CRUD-операций: проверка прав доступа, валидация полученных данных и т.д.
Очевидно, что в нашем сервисе нужна валидация получаемых данных при создании и редактировании заметок, а также проверка прав доступа к заметкам. Выделение валидации и проверки прав доступа в отдельные компоненты фактически является разделением слоя бизнес-логики на дополнительные подслои.
Читать далее Многоуровневая архитектура в проекте на Java (Часть 2)
В настоящее время в разработке ПО достаточно часто применяется многоуровневая архитектура или многослойная архитектура (n-tier architecture), в рамках которой компоненты проекта разделяются на уровни (или слои). Классическое приложение с многоуровневой архитектурой, чаще всего, состоит из 3 или 4 уровней, хотя их может быть и больше, учитывая возможность разделения некоторых уровней на подуровни. Одним из примеров многоуровневой архитектуры является предметно-ориентированное проектирование (Domain-driven Design, DDD), где основное внимание сконцентрировано на предметном уровне.
Читать далее Многоуровневая архитектура в проекте на Java (Часть 1)
Принцип подстановки [Барбары] Лисков (Liskov Substitution Principle — LSP, буква L в аббревиатуре SOLID), сформулирован Барбарой Лисков в 1987 году и звучит следующим образом:
Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.
Упрощенное описание этого принципа предложил Роберт Мартин:
Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.
Иными словами, поведение реализующих и наследующих классов не должно противоречить поведению базовых типов.
Читать далее SOLID на практике — Принцип подстановки Барбары Лисков
Принцип инверсии зависимостей (Dependency Inversion Principle — DIP, буква D в аббревиатуре SOLID), описанный Робертом Мартином, состоит из двух постулатов:
Инверсия зависимостей заключается в том, что модули разных уровней зависят не друг от друга, а от абстракций. В общих чертах принцип инверсии зависимостей сводится к следующему набору простых правил:
Читать далее SOLID на практике — принцип инверсии зависимостей
Фреймворк Spring Security позволяет реализовать авторизацию в приложении при помощи протокола OAuth 2.0. Провайдерами авторизации могут быть как общедоступные сервисы, вроде Google, Facebook или GitHub, так и персональные, реализованные, например, при помощи Apereo CAS.
Принцип единственной ответственности (Single Responsibility Principle — SRP, буква S в аббревиатуре SOLID), описанный Робертом Мартином, гласит: «Класс должен иметь только одну причину для изменения».
Обратите внимание на следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class TopicService { private final EntityManager entityManager; @Transactional public Topic update(int id, String title, String content, int userAccountId) { if (title == null || title.length() > 255) { throw new IllegalArgumentException("Title is too long"); } if (content.length() > 65535) { throw new IllegalArgumentException("Content is too long"); } return Optional.ofNullable(this.entityManager.find(Topic.class, id)) .filter(topic -> topic.getUserAccountId() == userAccountId) .map(topic -> { topic.setContent(content); topic.setTitle(title); topic.setDateLastModified(new Date()); return this.entityManager.merge(topic); }) .orElseThrow(EntityNotFoundException::new); } } |
В примере приведён метод сохранения изменений в записи типа Topic. Однако этот метод выполняет слишком много действий: валидацию полученных данных, поиск существующего объекта типа Topic, проверку прав доступа пользователя на этот объект, применение и сохранение изменений. И все эти действия могут меняться независимо друг от друга.
Следуя логике принципа единственной ответственности, нам следует выделить работу с источником данных, валидацию и проверку прав доступа, как минимум, в отдельные методы, а наиболее правильно — в отдельные классы.
В первом варианте мы разделим логику между методами класса TopicService. Кроме этого данные, получаемые методом update, логично будет инкапсулировать в одном классе:
1 2 3 4 5 6 7 8 9 |
public class UpdateTopicAction { @NotBlank @Size(max = 255) private String title; @Size(max = 65535) private String content; } |
В результате выделения логики валидации, работы с источником данных и проверки прав доступа в отдельные методы должен получиться примерно такой код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public class TopicService { private final EntityManager entityManager; private final Validator validator; private void validate(UpdateTopicAction action) { Set<ConstraintViolation<UpdateTopicAction>> violations = this.validator.validate(action); if (!violations.isEmpty()) { throw new IllegalArgumentException("Invalid data passed"); } } private Topic findOneById(int id) { return Optional.ofNullable(this.entityManager.find(Topic.class, id)) .orElseThrow(EntityNotFoundException::new); } @Transactional protected Topic save(Topic topic) { return this.entityManager.merge(topic); } private void checkAccess(Topic topic, int userAccountId) { if (topic.getUserAccountId() != userAccountId) { throw new EntityActionForbiddenException(); } } public Topic update(UpdateTopicAction action, int id, int userAccountId) { this.validate(action); Topic topic = this.findOneById(id); this.checkAccess(topic, userAccountId); topic.setContent(action.getContent()); topic.setTitle(action.getContent()); topic.setDateLastModified(new Date()); return this.save(topic); } } |
Теперь этот код на уровне методов соответствует принципу единственной ответственности, чего не скажешь о всём классе TopicService. Не смотря на то, что методы findOneById, save, checkAccess и validate скрыты от внешнего наблюдателя, класс всё равно выполняет много действий.
Однако даже при этом код уже более удобен для понимания, а так же пригоден для повторного использования.
При выделении логики валидации, проверки прав и работы с источниками данных в отдельные классы, код приложения станет ещё удобнее и понятнее.
Таким образом мы получим класс, работающий с источником данных:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class TopicRepository { private final EntityManager entityManager; public Optional<Topic> findOneById(int id) { return Optional.ofNullable(this.entityManager.find(Topic.class, id)); } @Transactional public Topic save(Topic topic) { return this.entityManager.merge(topic); } } |
Класс, выполняющий валидацию UpdateTopicAction:
1 2 3 4 5 6 7 8 |
public class UpdateTopicActionValidationService { private final Validator validator; public Set<ConstraintViolation<UpdateTopicAction>> validate(UpdateTopicAction action) { return this.validator.validate(action); } } |
Класс, проверяющий доступ пользователя к объекту Topic:
1 2 3 4 5 6 |
public class TopicAccessCheckService { public boolean hasAccess(Topic topic, int userAccountId) { return topic.getUserAccountId() == userAccountId; } } |
Итоговый вид нашего сервиса будет следующим:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class TopicService { private final TopicRepository topicRepository; private final UpdateTopicActionValidationService updateTopicActionValidationService; private final TopicAccessCheckService topicAccessCheckService; public Topic update(UpdateTopicAction action, int id, int userAccountId) { Topic topic = this.topicRepository.findOneById(id) .orElseThrow(EntityNotFoundException::new); if (!this.topicAccessCheckService.hasAccess(topic, userAccountId)) { throw new EntityActionForbiddenException(); } Set<ConstraintViolation<UpdateTopicAction>> violations = this.updateTopicActionValidationService.validate(action); if (!violations.isEmpty()) { throw new ValidationFailedException(violations); } topic.setTitle(action.getTitle()); topic.setContent(action.getContent()); topic.setDateLastModified(new Date()); return this.topicRepository.save(topic); } } |
Таким образом мы на практике применили принцип единой ответственности.
In this post, I will describe Spring Restdocs and Spring Cloud Contract integration into Cucumber tests with Spring Webflux. The main problem is that we can’t use the most of common JUnit and Spring Test annotations like @Before, @After and @Rule in Cucumber tests, so we have to set up testing environment manually. This post is a webflux adaptation of the previous post.
Читать далее Spring Restdocs and Spring Cloud Contract with Cucumber and Spring Webflux