Сервисы (service) – это классы в проекте Spring, которые отвечают за управление объектами сущностей. Сервис – это бизнес-логика. В совокупности, сервисы представляют собой прослойку между контроллерами и репозиториями, обеспечивая выполнение правил и операций над данными.
Напишем парочку сервисов!
Создание сервиса
Для создания сервиса вам нужно просто создать класс и пометить его аннотацией @Service. Как правило, название сервиса складывают из названия класса сущности и слова Service. Например:
1 2 3 4 |
@Service public class UserService { } |
Цепочка такая. Репозиторий оперирует информацией напрямую из БД, сервис эксплуатирует репозиторий, а контроллер эксплуатирует сервис:
При этом контроллер никак не взаимодействует, например, с репозиторием. И, в свою очередь, сервис не владеет непосредственным доступом к БД. Это позволяет более-менее абстрагироваться от конкретной реализации каждого из них. Они взаимодействуют через слои. Каждый слой может быть изменён, но взаимодействие останется таким, каким и было. Такова структура Spring-проекта.
Для того, чтобы использовать репозиторий в сервисе, нужно поместить в сервис переменную репозитория. Но управлять тем, чтобы создать репозиторий и поместить в эту переменную, мы с вами не будем. Этим займётся Spring. Для того, чтобы он знал, что делать, есть несколько способов:
Самый простой способ – пометить нужное поле аннотацией @Autowired. Spring сам разберётся с тем, чтобы подставить в это поле объект репозитория.
1 2 3 4 5 6 |
@Service public class UserService { @Autowired private UserRepository userRepository; ... } |
Следующий способ – создать конструктор, принимающий аргументом поле, которое нужно заполнить. При этом аннотация @Autowired необязательна, так как Spring автоматически ищет подходящий компонент для внедрения. Но лучше всё-таки обозначать её для большей явности.
1 2 3 4 5 6 7 8 9 10 11 |
@Service public class UserService { private UserRepository userRepository; @Autowired public UserService(UserRepository userRepository) { this.userRepository = userRepository; } ... } |
Ещё вы можете создать сеттер для репозитория и пометить его аннотацией @Autowired. Spring также автоматически внедрит зависимость.
1 2 3 4 5 6 7 8 9 10 11 |
@Service public class UserService { private UserRepository userRepository; @Autowired public setUserRepository(UserRepository userRepository) { this.userRepository = userRepository; } ... } |
Теперь мы готовы к тому, чтобы написать методы сервиса с использованием репозитория.
Простой сервис
В небольших и относительно несложных проектах слой сервисов зачастую просто повторяет функциональность слоя репозиториев. Это происходит потому, что сервис просто делегирует запросы к репозиторию без какой-либо дополнительной бизнес-логики. Но даже в маленьких проектах так случается не всегда и не во всём. А в проектах побольше – очень редко.
Простой сервис по управлению статьями:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@Service public class ArticleService { @Autowired private final ArticleRepository articleRepository; public List<Article> getAllArticles() { return articleRepository.findAll(); } public Optional<Article> getArticleById(Long id) { return articleRepository.findById(id); } public Article saveArticle(Article article) { return articleRepository.save(article); } public void deleteArticle(Long id) { articleRepository.deleteById(id); } } |
Как видно, ArticleService просто является оболочкой для той же самой функциональности из ArticleRepository. Но чаще сервисы всё-таки привносят что-то от себя. Давайте узнаем, каким образом.
Усложнённые сервисы
Посмотрим, как сервисы могут предоставлять дополнительную бизнес-логику, не копируя функционал репозитория.
Использование данных из нескольких репозиториев
Например, сервис может содержать методы, которые используют данные из нескольких репозиториев. Можно даже указать, чтобы методы сервиса проходили в рамках одной транзакции. Либо все операции должны быть успешно завершены, либо ни одна из них не должна быть применена.
Когда метод или класс помечается аннотацией @Transactional, Spring автоматически управляет транзакциями вокруг этих методов или всего класса. Если метод вызывает другие методы, помеченные аннотацией @Transactional, то Spring создаёт вложенные транзакции, которые могут быть приняты или откатаны независимо друг от друга.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Service @Transactional public class TransactionalService { @Autowired private UserRepository userRepository; @Autowired private OrderRepository orderRepository; public void saveUserAndOrder(User user, Order order) { userRepository.save(user); orderRepository.save(order); } } |
Метод этого сервиса saveUserAndOrder() либо сохранит и пользователя, и заказ, либо не сохранит ничего – если что-то пойдёт не так, транзакция будет отменена целиком.
Фильтрация и сортировка данных
Сервис может содержать методы, которые фильтруют и сортируют данные, полученные из репозитория, в соответствии с определёнными правилами. Например, сортировка продуктов по цене по возрастанию:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@Service public class FilteringService { @Autowired private ProductRepository productRepository; public List<Product> getProductsByCategorySortedByPriceAscending(String category) { List<Product> products = productRepository.findByCategory(category); products = sortByPriceAscending(products); return products; } private List<Product> sortByPriceAscending(List<Product> products) { products.sort(Comparator.comparing(Product::getPrice)); return products; } } |
Вычисления и операции
Сервис может содержать методы, которые выполняют сложные вычисления или анализ данных, полученных из репозитория. Например, расчёт средней цены продуктов:
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 |
@Service public class CalculationService { @Autowired private OrderRepository orderRepository; public double calculateAverageOrderValue() { List<Order> orders = orderRepository.findAll(); double totalOrderValue = calculateTotalOrderValue(orders); if (orders.size() > 0) { return totalOrderValue / orders.size(); } else { return 0.0; } } public double calculateTotalOrderValue(List<Order> orders) { double totalOrderValue = 0.0; for (Order order : orders) { totalOrderValue += order.getTotalPrice(); } return totalOrderValue; } } |
Обработка исключений
Сервис может содержать методы для обработки определённых исключений, возникающих во время выполнения операций с данными.
1 2 3 4 5 6 7 8 9 10 |
@Service public class ExceptionHandlingService { @Autowired private UserRepository userRepository; public User getUserById(Long id) { return userRepository.findById(id) .orElseThrow(() -> new UserNotFoundException("User with id " + id + " not found")); } } |
Пример с User и Post
Продолжим наш пример с созданными ранее двумя таблицами в БД: users и posts. Мы создали для них классы-сущности, а также репозитории. Время создать сервисы.
Сервисы будут простыми. Они будут практически копировать функциональность репозиториев. Обойдёмся без усложнений на первых порах. Я буду использовать библиотеку Lombok для автоматического создания конструктора со всеми final-полями с помощью аннотации @RequiredArgsConstructor. Spring увидит этот единственный конструктор и самостоятельно положит туда репозитории.
Сервис для сущности User:
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 |
@Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; public List<User> getAll() { return userRepository.findAll(); } public User getUserById(Long userId) { return userRepository.findById(userId).orElse(null); } public User getUserByEmail(String email) { return userRepository.findByEmail(email); } public User saveUser(User user) { return userRepository.save(user); } public void deleteUserById(Long userId) { userRepository.deleteById(userId); } } |
Сервис для сущности Post:
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 |
@Service @RequiredArgsConstructor public class PostService { private final PostRepository postRepository; public List<Post> getAllPosts() { return postRepository.findAll(); } public List<Post> getPostsByUserId(Long userId) { return postRepository.findByUserId(userId); } public Post getPostById(Long postId) { return postRepository.findById(postId).orElse(null); } public Post savePost(Post post) { return postRepository.save(post); } public void deletePostById(Long postId) { postRepository.deleteById(postId); } } |
Итого, все базовые операции CRUD реализованы, и даже чуть больше. Для пользователей, к примеру – дополнительно поиск по email-у. Для записей – поиск по id пользователя, который эти записи создавал. Что логично.
Так как метод findById() у репозиториев возвращает не сам объект сущности, а Optional, то приходится с ним соответствующим образом обращаться. Обёртка Optional означает, что объект может там лежать, а может и не лежать. Поэтому используем orElse(null), чтобы в случае отсутствия объекта возвращало null. Обойдёмся без исключений.
Вот мы и разобрались с очередным слоем Spring-приложения. Отличная работа!