Más recientemente, apareció un
artículo de un colega sobre Habré, quien describió un enfoque bastante interesante para combinar las capacidades genéricas y de primavera. Ella me recordó un enfoque que utilizo para escribir microservicios, y decidí compartirlo con los lectores.

En la salida, obtenemos un sistema de transporte, para agregar al cual una nueva entidad necesitaremos limitarnos a inicializar un bean en cada elemento del paquete repositorio-servicio-controlador.
Inmediatamente
recursos .
Rama, como yo no:
standart_version .
El enfoque descrito en el artículo está en la rama
abstract_version .
Arme un proyecto a través de
Spring Initializr , agregando los marcos JPA, Web y H2. Gradle, Spring Boot 2.0.5. Eso será suficiente.

Para comenzar, considere la versión clásica de transporte desde el controlador al repositorio y viceversa, sin ninguna lógica adicional. Si desea ir a la esencia del enfoque, desplácese hacia abajo hasta la versión abstracta. Pero, sin embargo, recomiendo leer el artículo completo.
La versión clásica.
Los
recursos del ejemplo proporcionan varias entidades y métodos para ellos, pero en el artículo solo tenemos una entidad de usuario y un solo método save (), que arrastraremos desde el repositorio a través del servicio al controlador. En recursos, hay 7 de ellos, pero en general Spring CRUD / JPA Repository le permite usar aproximadamente una docena de métodos de guardar / recibir / eliminar, además de que puede usar, por ejemplo,
algunos universales . Además, no nos distraeremos con cosas tan necesarias como la validación, el mapeo dto, etc. Puede agregarlo usted mismo o estudiar
en otros artículos de Habr .
Dominio:
@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; }
Repositorio:
@Repository public interface UserRepository extends CrudRepository<User, Long> { }
Servicio:
public interface UserService { Optional<User> save(User user); }
Servicio (implementación):
@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)); } }
Controlador:
@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()) )); } }
Tenemos un cierto conjunto de clases dependientes que nos ayudarán a operar en la entidad Usuario en el nivel CRUD. En nuestro ejemplo, este es un método, hay más en recursos. Esta versión en absoluto abstracta de las capas de escritura se presenta en la rama
standart_version .
Supongamos que necesitamos agregar otra entidad, por ejemplo, Car. No ganaremos dinero a nivel de entidades entre sí (si lo desea, puede asignarlo).
Primero, crea una entidad.
@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; }
Luego crea un repositorio.
public interface CarRepository extends CrudRepository<Car, Long> { }
Entonces el servicio ...
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(); }
Luego la implementación del servicio ....... Controlador ...........
Sí, puede copiar los mismos métodos (son universales aquí) de la clase Usuario, luego cambiar Usuario a Coche, luego hacer lo mismo con la implementación, con el controlador, luego la siguiente entidad es la siguiente en la fila, y allí ya se ven más y más ... Por lo general, se cansa de la segunda, la creación de una arquitectura de servicio para un par de docenas de entidades (copiar y pegar, reemplazar el nombre de la entidad, en algún lugar equivocado, en algún lugar sellado ...) conduce al tormento que causa cualquier trabajo monótono. Intenta prescribir veinte entidades de alguna manera a tu gusto y entenderás lo que quiero decir.
Y así, en un momento, cuando estaba interesado en los genéricos y los parámetros típicos, me di cuenta de que el proceso puede hacerse mucho menos rutinario.
Entonces, abstracciones basadas en parámetros típicos.
El significado de este enfoque es llevar toda la lógica a la abstracción, vincular la abstracción a los parámetros típicos de la interfaz e inyectar otros contenedores en los contenedores. Y eso es todo. No hay lógica en los frijoles. Solo una inyección de otros frijoles. Este enfoque implica escribir arquitectura y lógica una vez y no duplicarlo al agregar nuevas entidades.
Comencemos con la piedra angular de nuestra abstracción: una entidad abstracta. Es de ella que comenzará la cadena de dependencias abstractas, que servirá como marco del servicio.
Todas las entidades tienen al menos un campo común (generalmente más). Esta es la identificación. Tomamos este campo en una entidad abstracta separada y heredamos User and Car de él.
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; } }
Recuerde marcar la abstracción con la anotación @MappedSuperclass: Hibernate también debe saber que es una abstracción.
Usuario:
@Entity public class User extends AbstractEntity { private String name; private String phone;
Con Car, en consecuencia, lo mismo.
En cada capa, además de los contenedores, tendremos una interfaz con parámetros típicos y una clase abstracta con lógica. Además del repositorio, gracias a los detalles de Spring Data JPA, todo será mucho más simple aquí.
Lo primero que necesitamos en el repositorio es un repositorio compartido.
CommonRepository:
@NoRepositoryBean public interface CommonRepository<E extends AbstractEntity> extends CrudRepository<E, Long> { }
En este repositorio, establecemos reglas generales para toda la cadena: todas las entidades que participan en ella heredarán del resumen. A continuación, para cada entidad, debemos escribir nuestra propia interfaz de repositorio, en la que indicaremos con qué entidad trabajará esta cadena de controlador de servicio de repositorio.
UserRepository:
@Repository public interface UserRepository extends CommonRepository<User> { }
Gracias a las características de Spring Data JPA, la configuración del repositorio termina aquí: todo funcionará así. Luego viene el servicio. Necesitamos crear una interfaz común, abstracción y bean.
Servicio común:
public interface CommonService<E extends AbstractEntity> { { Optional<E> save(E entity);
Servicio abstracto:
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; }
Aquí redefinimos todos los métodos y también creamos un constructor parametrizado para el repositorio futuro, que redefinimos en el bean. Por lo tanto, ya estamos utilizando un repositorio que aún no hemos definido. Todavía no sabemos qué entidad se procesará en esta abstracción y qué repositorio necesitaremos.
Servicio de usuario:
@Service public class UserService extends AbstractService<User, UserRepository> { public UserService(UserRepository repository) { super(repository); } }
En el contenedor, hacemos lo último: definimos explícitamente el repositorio que necesitamos, que luego se llama en el constructor de abstracción. Y eso es todo.
Usando la interfaz y la abstracción, creamos una autopista a través de la cual manejaremos todas las entidades. En el contenedor, llevamos el desenlace a la autopista, a través del cual mostraremos la entidad que necesitamos en la autopista.
El controlador se basa en el mismo principio: interfaz, abstracción, bin.
Controlador común:
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); } }
Esta es toda la estructura. Está escrito una vez.
Que sigue
Y ahora imaginemos que tenemos una nueva entidad que ya hemos heredado de AbstractEntity, y necesitamos escribir la misma cadena para ella. Nos llevará un minuto. Y sin copiar y pegar y correcciones.
Tome ya heredado de AbstractEntity Car.
CarRepository:
@Repository public interface CarRepository extends CommonRepository<Car> { }
Servicio de coche:
@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); } }
Como podemos ver, copiar la misma lógica consiste simplemente en agregar un bean. No es necesario volver a escribir la lógica en cada contenedor con el cambio de parámetros y firmas. Se escriben una vez y funcionan en cada caso posterior.
Conclusión
Por supuesto, el ejemplo describe un tipo de situación esférica en la que el CRUD para cada entidad tiene la misma lógica. No sucede, aún debe redefinir algunos métodos en el contenedor o agregar otros nuevos. Pero esto vendrá de las necesidades específicas de procesar la entidad. Bueno, si el 60 por ciento del número total de métodos CRUD permanecerá en abstracción. Y este será un buen resultado, porque cuanto más generamos código redundante manualmente, más tiempo pasamos en trabajo monótono y mayor es el riesgo de errores o errores tipográficos.
Espero que el artículo haya sido útil, gracias por su atención.
UPD
Gracias a la propuesta,
aleksandy logró obtener la inicialización del bean en el constructor y así mejorar significativamente el enfoque. Si ve de qué otra manera puede mejorar el ejemplo, escriba los comentarios y tal vez sus sugerencias serán enviadas.