Совсем недавно на Хабре мелькнула статья коллеги, который описал довольно интересный подход к совмещению Generics и возможностей Spring. Мне она напомнила один подход, который я использую для написания микросервисов, и именно им я решил поделиться с читателями.
![](https://habrastorage.org/r/w780q1/webt/_h/d_/ir/_hd_ir-s7t2xkca2yy2iir0am_s.jpeg)
На выходе мы получаем транспортную систему, для добавления в которую новой сущности нужно будет ограничиться инициализацией одного бина в каждом элементе связки репозиторий-сервис-контроллер.
Сразу ресурсы.
Ветка, как я не делаю: standart_version.
Подход, о котором рассказывается в статье, в ветке abstract_version.
Я собрал проект через Spring Initializr, добавив фреймворки JPA, Web и H2. Gradle, Spring Boot 2.0.5. Этого будет вполне достаточно.
![](https://habrastorage.org/r/w780q1/webt/-s/wq/4s/-swq4s7cdtzea0mo4oajhw3stf8.jpeg)
Для начала, рассмотрим классический вариант транспорта от контроллера до репозитория и обратно, лишённый всякой дополнительной логики. Если же Вы хотите перейти к сути подхода, проматывайте вниз до абстрактного варианта. Но, всё же, рекомендую прочитать статью целиком.
В ресурсах примера представлены несколько сущностей и методов для них, но в статье пусть у нас будет только одна сущность User и только один метод save(), который мы протащим от репозитория через сервис до контроллера. В ресурсах же их 7, а вообще Spring CRUD / JPA Repository позволяют использовать около дюжины методов сохранения / получения / удаления плюс Вы можете пользоваться, к примеру, какими-то своими универсальными. Также, мы не будем отвлекаться на такие нужные вещи, как валидацию, мапинг dto и прочее. Это Вы сможете дописать сами или изучить в других статьях Хабра.
У нас получился некий набор зависимых классов, которые помогут нам оперировать сущностью User на уровне CRUD. Это в нашем примере по одному методу, в ресурсах их больше. Этот нисколько не абстрактный вариант написания слоёв представлен в ветке standart_version.
Допустим, нам нужно добавить ещё одну сущность, скажем, Car. Мапить на уровне сущностей мы их друг к другу не будем (если есть желание, можете замапить).
Для начала, создаём сущность.
Потом создаём репозиторий.
Потом сервис…
Потом имплементация сервиса…… Контроллер………
Да, можно просто скопипастить те же методы (они же у нас универсальные) из класса User, потом поменять User на Car, потом проделать то же самое с имплементацией, с контроллером, далее на очереди очередная сущность, а там уже выглядывают ещё и ещё… Обычно устаёшь уже на второй, создание же служебной архитектуры для пары десятков сущностей (копипастинг, замена имени сущности, где-то ошибся, где-то опечатался...) приводит к мукам, которые вызывает любая монотонная работа. Попробуйте как-нибудь на досуге прописать двадцать сущностей и Вы поймёте, о чём я.
И вот, в один момент, когда я как раз увлекался дженериками и типовыми параметрами, меня осенило, что процесс можно сделать гораздо менее рутинным.
Смысл данного подхода заключается в том, чтобы вынести всю логику в абстракцию, абстракцию привязать к типовым параметрам интерфейса, а в бины инжектить другие бины. И всё. Никакой логики в бинах. Только инжект других бинов. Этот подход предполагает написать архитектуру и логику один раз и не дублировать её при добавлении новых сущностей.
Начнём с краеугольного камня нашей абстракции — абстрактной сущности. Именно с неё начнётся цепочка абстрактных зависимостей, которая послужит каркасом сервиса.
У всех сущностей есть как минимум одно общее поле (обычно больше). Это ID. Вынесем это поле в отдельную абстрактную сущность и унаследуем от неё User и Car.
Не забудьте пометить абстракцию аннотацией @MappedSuperclass — Hibernate тоже должен узнать, что это абстракция.
С Car, соответственно, то же самое.
В каждом слое у нас, помимо бинов, будет один интерфейс с типовыми параметрами и один абстрактный класс с логикой. Кроме репозитория — благодаря специфике Spring Data JPA, здесь всё будет намного проще.
Первое, что нам потребуется в репозитории — общий репозиторий.
В этом репозитории мы задаём общие правила для всей цепочки: все сущности, участвующие в ней, будут наследоваться от абстрактной. Далее, для каждой сущности мы должны написать свой репозиторий-интерфейс, в котором обозначим, с какой именно сущностью будет работать эта цепочка репозиторий-сервис-контроллер.
На этом, благодаря особенностям Spring Data JPA, настройка репозитория заканчивается — всё будет работать и так. Далее следует сервис. Мы должны создать общий интерфейс, абстракцию и бин.
Здесь мы переопределяем все методы, а также, создаём параметризированный конструктор для будущего репозитория, который мы переопределим в бине. Таким образом, мы уже используем репозиторий, который мы ещё не определили. Мы пока не знаем, какая сущность будет обработана в этой абстракции и какой репозиторий нам потребуется.
В бине мы делаем заключительную вещь — явно определяем нужный нам репозиторий, который потом вызовется в конструкторе абстракции. И всё.
При помощи интерфейса и абстракции мы создали магистраль, по которой будем гонять все сущности. В бине же мы подводим к магистрали развязку, по которой будем выводить нужную нам сущность на магистраль.
Контроллер строится по тому же принципу: интерфейс, абстракция, бин.
Это вся структура. Она пишется один раз.
И вот теперь давайте представим, что у нас появилась новая сущность, которую мы уже унаследовали от AbstractEntity, и нам нужно прописать для неё такую же цепочку. На это у нас уйдёт минута. И никаких копипаст и исправлений.
Возьмём уже унаследованный от AbstractEntity Car.
Как мы видим, копирование одинаковой логики состоит в простом добавлении бина. Не надо заново писать логику в каждом бине с изменением параметров и сигнатур. Они написаны один раз и работают в каждом последующем случае.
Конечно, в примере описана этакая сферическая ситуация, в которой CRUD для каждой сущности имеет одинаковую логику. Так не бывает — какие-то методы Вам всё равно придётся переопределять в бине или добавлять новые. Но это будет происходить от конкретных потребностей обработки сущности. Хорошо, если процентов 60 от общего количества методов CRUD будет оставаться в абстракции. И это будет хорошим результатом, потому что чем больше мы генерим лишнего кода вручную, тем больше времени мы тратим на монотонную работу и тем выше риск ошибки или опечатки.
Надеюсь, статья была полезна, спасибо за внимание.
![](https://habrastorage.org/webt/_h/d_/ir/_hd_ir-s7t2xkca2yy2iir0am_s.jpeg)
На выходе мы получаем транспортную систему, для добавления в которую новой сущности нужно будет ограничиться инициализацией одного бина в каждом элементе связки репозиторий-сервис-контроллер.
Сразу ресурсы.
Ветка, как я не делаю: standart_version.
Подход, о котором рассказывается в статье, в ветке abstract_version.
Я собрал проект через Spring Initializr, добавив фреймворки JPA, Web и H2. Gradle, Spring Boot 2.0.5. Этого будет вполне достаточно.
![](https://habrastorage.org/webt/-s/wq/4s/-swq4s7cdtzea0mo4oajhw3stf8.jpeg)
Для начала, рассмотрим классический вариант транспорта от контроллера до репозитория и обратно, лишённый всякой дополнительной логики. Если же Вы хотите перейти к сути подхода, проматывайте вниз до абстрактного варианта. Но, всё же, рекомендую прочитать статью целиком.
Классический вариант.
В ресурсах примера представлены несколько сущностей и методов для них, но в статье пусть у нас будет только одна сущность User и только один метод save(), который мы протащим от репозитория через сервис до контроллера. В ресурсах же их 7, а вообще Spring CRUD / JPA Repository позволяют использовать около дюжины методов сохранения / получения / удаления плюс Вы можете пользоваться, к примеру, какими-то своими универсальными. Также, мы не будем отвлекаться на такие нужные вещи, как валидацию, мапинг dto и прочее. Это Вы сможете дописать сами или изучить в других статьях Хабра.
Domain:
@Entity
public class User implements Serializable {
private Long id;
private String name;
private String phone;
@Id
@GeneratedValue
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Column(nullable = false)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Column
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
//equals, hashcode, toString
}
Repository:
@Repository
public interface UserRepository extends CrudRepository<User, Long> {
}
Service:
public interface UserService {
Optional<User> save(User user);
}
Service (имплементация):
@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Autowired
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public Optional<User> save(User user) {
return Optional.of(userRepository.save(user));
}
}
Controller:
@RestController
@RequestMapping("/user")
public class UserController {
private final UserService service;
@Autowired
public UserController(UserService service) {
this.service = service;
}
@PostMapping
public ResponseEntity<User> save(@RequestBody User user) {
return service.save(user).map(u -> new ResponseEntity<>(u, HttpStatus.OK))
.orElseThrow(() -> new UserException(
String.format(ErrorType.USER_NOT_SAVED.getDescription(), user.toString())
));
}
}
У нас получился некий набор зависимых классов, которые помогут нам оперировать сущностью User на уровне CRUD. Это в нашем примере по одному методу, в ресурсах их больше. Этот нисколько не абстрактный вариант написания слоёв представлен в ветке standart_version.
Допустим, нам нужно добавить ещё одну сущность, скажем, Car. Мапить на уровне сущностей мы их друг к другу не будем (если есть желание, можете замапить).
Для начала, создаём сущность.
@Entity
public class Car implements Serializable {
private Long id;
private String brand;
private String model;
@Id
@GeneratedValue
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
//геттеры, сеттеры, equals, hashcode, toString
}
Потом создаём репозиторий.
public interface CarRepository extends CrudRepository<Car, Long> {
}
Потом сервис…
public interface CarService {
Optional<Car> save(Car car);
List<Car> saveAll(List<Car> cars);
Optional<Car> update(Car car);
Optional<Car> get(Long id);
List<Car> getAll();
Boolean deleteById(Long id);
Boolean deleteAll();
}
Потом имплементация сервиса…… Контроллер………
Да, можно просто скопипастить те же методы (они же у нас универсальные) из класса User, потом поменять User на Car, потом проделать то же самое с имплементацией, с контроллером, далее на очереди очередная сущность, а там уже выглядывают ещё и ещё… Обычно устаёшь уже на второй, создание же служебной архитектуры для пары десятков сущностей (копипастинг, замена имени сущности, где-то ошибся, где-то опечатался...) приводит к мукам, которые вызывает любая монотонная работа. Попробуйте как-нибудь на досуге прописать двадцать сущностей и Вы поймёте, о чём я.
И вот, в один момент, когда я как раз увлекался дженериками и типовыми параметрами, меня осенило, что процесс можно сделать гораздо менее рутинным.
Итак, абстракции на основе типовых параметров.
Смысл данного подхода заключается в том, чтобы вынести всю логику в абстракцию, абстракцию привязать к типовым параметрам интерфейса, а в бины инжектить другие бины. И всё. Никакой логики в бинах. Только инжект других бинов. Этот подход предполагает написать архитектуру и логику один раз и не дублировать её при добавлении новых сущностей.
Начнём с краеугольного камня нашей абстракции — абстрактной сущности. Именно с неё начнётся цепочка абстрактных зависимостей, которая послужит каркасом сервиса.
У всех сущностей есть как минимум одно общее поле (обычно больше). Это ID. Вынесем это поле в отдельную абстрактную сущность и унаследуем от неё User и Car.
AbstractEntity:
@MappedSuperclass
public abstract class AbstractEntity implements Serializable {
private Long id;
@Id
@GeneratedValue
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
Не забудьте пометить абстракцию аннотацией @MappedSuperclass — Hibernate тоже должен узнать, что это абстракция.
User:
@Entity
public class User extends AbstractEntity {
private String name;
private String phone;
//...
}
С Car, соответственно, то же самое.
В каждом слое у нас, помимо бинов, будет один интерфейс с типовыми параметрами и один абстрактный класс с логикой. Кроме репозитория — благодаря специфике Spring Data JPA, здесь всё будет намного проще.
Первое, что нам потребуется в репозитории — общий репозиторий.
CommonRepository:
@NoRepositoryBean
public interface CommonRepository<E extends AbstractEntity> extends CrudRepository<E, Long> {
}
В этом репозитории мы задаём общие правила для всей цепочки: все сущности, участвующие в ней, будут наследоваться от абстрактной. Далее, для каждой сущности мы должны написать свой репозиторий-интерфейс, в котором обозначим, с какой именно сущностью будет работать эта цепочка репозиторий-сервис-контроллер.
UserRepository:
@Repository
public interface UserRepository extends CommonRepository<User> {
}
На этом, благодаря особенностям Spring Data JPA, настройка репозитория заканчивается — всё будет работать и так. Далее следует сервис. Мы должны создать общий интерфейс, абстракцию и бин.
CommonService:
public interface CommonService<E extends AbstractEntity> { {
Optional<E> save(E entity);
//какое-то количество нужных нам методов
}
AbstractService:
public abstract class AbstractService<E extends AbstractEntity, R extends CommonRepository<E>>
implements CommonService<E> {
protected final R repository;
@Autowired
public AbstractService(R repository) {
this.repository = repository;
}
//другие методы, переопределённые из интерфейса
}
Здесь мы переопределяем все методы, а также, создаём параметризированный конструктор для будущего репозитория, который мы переопределим в бине. Таким образом, мы уже используем репозиторий, который мы ещё не определили. Мы пока не знаем, какая сущность будет обработана в этой абстракции и какой репозиторий нам потребуется.
UserService:
@Service
public class UserService extends AbstractService<User, UserRepository> {
public UserService(UserRepository repository) {
super(repository);
}
}
В бине мы делаем заключительную вещь — явно определяем нужный нам репозиторий, который потом вызовется в конструкторе абстракции. И всё.
При помощи интерфейса и абстракции мы создали магистраль, по которой будем гонять все сущности. В бине же мы подводим к магистрали развязку, по которой будем выводить нужную нам сущность на магистраль.
Контроллер строится по тому же принципу: интерфейс, абстракция, бин.
CommonController:
public interface CommonController<E extends AbstractEntity> {
@PostMapping
ResponseEntity<E> save(@RequestBody E entity);
//остальные методы
}
AbstractController:
public abstract class AbstractController<E extends AbstractEntity, S extends CommonService<E>>
implements CommonController<E> {
private final S service;
@Autowired
protected AbstractController(S service) {
this.service = service;
}
@Override
public ResponseEntity<E> save(@RequestBody E entity) {
return service.save(entity).map(ResponseEntity::ok)
.orElseThrow(() -> new SampleException(
String.format(ErrorType.ENTITY_NOT_SAVED.getDescription(), entity.toString())
));
}
//другие методы
}
UserController:
@RestController
@RequestMapping("/user")
public class UserController extends AbstractController<User, UserService> {
public UserController(UserService service) {
super(service);
}
}
Это вся структура. Она пишется один раз.
Что дальше?
И вот теперь давайте представим, что у нас появилась новая сущность, которую мы уже унаследовали от AbstractEntity, и нам нужно прописать для неё такую же цепочку. На это у нас уйдёт минута. И никаких копипаст и исправлений.
Возьмём уже унаследованный от AbstractEntity Car.
CarRepository:
@Repository
public interface CarRepository extends CommonRepository<Car> {
}
CarService:
@Service
public class CarService extends AbstractService<Car, CarRepository> {
public CarService(CarRepository repository) {
super(repository);
}
}
CarController:
@RestController
@RequestMapping("/car")
public class CarController extends AbstractController<Car, CarService> {
public CarController(CarService service) {
super(service);
}
}
Как мы видим, копирование одинаковой логики состоит в простом добавлении бина. Не надо заново писать логику в каждом бине с изменением параметров и сигнатур. Они написаны один раз и работают в каждом последующем случае.
Заключение
Конечно, в примере описана этакая сферическая ситуация, в которой CRUD для каждой сущности имеет одинаковую логику. Так не бывает — какие-то методы Вам всё равно придётся переопределять в бине или добавлять новые. Но это будет происходить от конкретных потребностей обработки сущности. Хорошо, если процентов 60 от общего количества методов CRUD будет оставаться в абстракции. И это будет хорошим результатом, потому что чем больше мы генерим лишнего кода вручную, тем больше времени мы тратим на монотонную работу и тем выше риск ошибки или опечатки.
Надеюсь, статья была полезна, спасибо за внимание.