Mais recentemente, um
artigo de um colega apareceu em Habré, que descreveu uma abordagem bastante interessante para combinar os recursos Genéricos e Spring. Ela me lembrou uma abordagem que eu uso para escrever microsserviços e decidi compartilhá-la com os leitores.

Na saída, obtemos um sistema de transporte, para adicionar ao qual uma nova entidade precisaremos nos limitar a inicializar um bean em cada elemento do pacote configurável repositório-serviço-controlador.
Imediatamente
recursos .
Branch, como eu não:
standart_version .
A abordagem descrita no artigo está no ramo
abstract_version .
Eu montei um projeto através do
Spring Initializr , adicionando as estruturas JPA, Web e H2. Gradle, Spring Boot 2.0.5. Isso será o bastante.

Para começar, considere a versão clássica de transporte do controlador para o repositório e vice-versa, sem nenhuma lógica adicional. Se você quiser ir para a essência da abordagem, role para baixo até a versão abstrata. No entanto, recomendo a leitura do artigo completo.
A versão clássica.
Os
recursos do exemplo fornecem várias entidades e métodos para eles, mas no artigo, temos apenas uma entidade Usuário e apenas um método save (), que arrastaremos do repositório através do serviço para o controlador. Em recursos, existem sete deles, mas, em geral, o Repositório Spring CRUD / JPA permite que você use cerca de uma dúzia de métodos de salvar / receber / excluir, além de poder usar, por exemplo,
alguns universais . Além disso, não seremos distraídos por coisas necessárias como validação, mapeamento de dto e assim por diante. Você pode adicioná-lo você mesmo ou estudar
em outros artigos de Habr .
Domínio:
@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; }
Repositório:
@Repository public interface UserRepository extends CrudRepository<User, Long> { }
Serviço:
public interface UserService { Optional<User> save(User user); }
Serviço (implementação):
@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()) )); } }
Temos um certo conjunto de classes dependentes que nos ajudarão a operar na entidade Usuário no nível CRUD. No nosso exemplo, este é um método, existem mais recursos. Esta versão não abstrata das camadas de gravação é apresentada no ramo
standart_version .
Suponha que precisamos adicionar outra entidade, digamos, Car. Não ganharemos dinheiro no nível de entidades entre si (se você quiser, pode mapeá-lo).
Primeiro, crie uma entidade.
@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; }
Em seguida, crie um repositório.
public interface CarRepository extends CrudRepository<Car, Long> { }
Então o serviço ...
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(); }
Em seguida, a implementação do serviço ....... Controller ...........
Sim, você pode simplesmente copiar e colar os mesmos métodos (eles são universais aqui) da classe User, depois mudar User para Car, fazer o mesmo com a implementação, com o controlador, depois a próxima entidade é a próxima na fila e lá eles já procuram mais e mais ... Geralmente você se cansa do segundo, a criação de uma arquitetura de serviço para algumas dezenas de entidades (copiar e colar, substituir o nome da entidade, em algum lugar errado, em algum lugar selado ...) leva ao tormento que qualquer trabalho monótono causa. Tente prescrever de alguma forma vinte entidades, e você entenderá o que quero dizer.
E assim, a certa altura, quando eu estava interessado apenas em genéricos e parâmetros típicos, percebi que o processo pode ser feito com muito menos rotina.
Então, abstrações baseadas em parâmetros típicos.
O significado dessa abordagem é levar toda a lógica para a abstração, vincular a abstração aos parâmetros típicos da interface e injetar outras caixas nas caixas. E é isso. Não há lógica nos feijões. Apenas uma injeção de outros grãos. Essa abordagem envolve escrever arquitetura e lógica uma vez e não duplicá-la ao adicionar novas entidades.
Vamos começar com a pedra angular da nossa abstração - uma entidade abstrata. É dela que começará a cadeia de dependências abstratas, que servirá como estrutura do serviço.
Todas as entidades têm pelo menos um campo comum (geralmente mais). Este é o ID. Colocamos esse campo em uma entidade abstrata separada e herdamos User e Car dele.
Resumo:
@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; } }
Lembre-se de marcar a abstração com a anotação @MappedSuperclass - o Hibernate também deve saber que é uma abstração.
Usuário:
@Entity public class User extends AbstractEntity { private String name; private String phone;
Com Car, consequentemente, o mesmo.
Em cada camada, além dos compartimentos, teremos uma interface com parâmetros típicos e uma classe abstrata com lógica. Além do repositório - graças às especificidades do Spring Data JPA, tudo será muito mais simples aqui.
A primeira coisa que precisamos no repositório é um repositório compartilhado.
CommonRepository:
@NoRepositoryBean public interface CommonRepository<E extends AbstractEntity> extends CrudRepository<E, Long> { }
Neste repositório, definimos regras gerais para toda a cadeia: todas as entidades que participam dele herdarão do resumo. Em seguida, para cada entidade, devemos escrever nossa própria interface de repositório, na qual indicamos com qual entidade essa cadeia de repositório-controlador de serviço trabalhará.
UserRepository:
@Repository public interface UserRepository extends CommonRepository<User> { }
Graças aos recursos do Spring Data JPA, a configuração do repositório termina aqui - tudo funcionará assim. Em seguida, vem o serviço. Precisamos criar uma interface comum, abstração e bean.
CommonService:
public interface CommonService<E extends AbstractEntity> { { Optional<E> save(E entity);
Serviço:
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; }
Aqui redefinimos todos os métodos e também criamos um construtor parametrizado para o repositório futuro, que redefinimos no bean. Portanto, já estamos usando um repositório que ainda não definimos. Ainda não sabemos qual entidade será processada nessa abstração e qual repositório será necessário.
UserService:
@Service public class UserService extends AbstractService<User, UserRepository> { public UserService(UserRepository repository) { super(repository); } }
Na lixeira, fazemos a coisa final - definimos explicitamente o repositório de que precisamos, que é chamado no construtor de abstração. E é isso.
Usando a interface e a abstração, criamos uma estrada através da qual conduziremos todas as entidades. No depósito, levamos o desenlace à rodovia, através da qual exibiremos a entidade que precisamos na rodovia.
O controlador é construído com o mesmo princípio: interface, abstração, bin.
Controlador Comum:
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 é toda a estrutura. Está escrito uma vez.
O que vem a seguir?
E agora vamos imaginar que temos uma nova entidade que já herdamos de AbstractEntity e precisamos escrever a mesma cadeia para ela. Vai demorar um minuto. E sem copiar e colar e correções.
Tome já herdado do 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); } }
Como podemos ver, copiar a mesma lógica consiste em simplesmente adicionar um bean. Não há necessidade de reescrever a lógica em cada compartimento com a alteração de parâmetros e assinaturas. Eles são escritos uma vez e funcionam em cada caso subseqüente.
Conclusão
Obviamente, o exemplo descreve um tipo de situação esférica na qual o CRUD de cada entidade tem a mesma lógica. Isso não acontece - você ainda precisa redefinir alguns métodos na lixeira ou adicionar novos. Mas isso virá das necessidades específicas de processamento da entidade. Bem, se 60% do número total de métodos CRUD permanecerão em abstração. E este será um bom resultado, porque quanto mais geramos código redundante manualmente, mais tempo gastamos em trabalho monótono e maior o risco de erros ou erros de digitação.
Espero que o artigo tenha sido útil, obrigado por sua atenção.
UPD
Graças à proposta,
aleksandy conseguiu obter a inicialização do bean no construtor e, assim, melhorar significativamente a abordagem. Se você ver como mais pode melhorar o exemplo, escreva nos comentários e talvez suas sugestões sejam enviadas.