Zuletzt wurde Habré in einem
Artikel eines Kollegen vorgestellt, in dem ein ziemlich interessanter Ansatz zur Kombination von Generika- und Spring-Funktionen beschrieben wurde. Sie erinnerte mich an einen Ansatz, mit dem ich Microservices schreibe, und den ich mit den Lesern teilen wollte.

Am Ausgang erhalten wir ein Transportsystem, um eine neue EntitĂ€t hinzuzufĂŒgen, die wir auf die Initialisierung einer Bean in jedem Element des Repository-Service-Controller-Bundles beschrĂ€nken mĂŒssen.
Sofort
Ressourcen .
Verzweigen, wie ich es nicht tue:
standart_version .
Der im Artikel beschriebene Ansatz befindet sich im Zweig
abstract_version .
Ich habe ĂŒber
Spring Initializr ein Projekt zusammengestellt und die JPA-, Web- und H2-Frameworks hinzugefĂŒgt. Gradle, Spring Boot 2.0.5. Das wird völlig ausreichen.

Betrachten Sie zunÀchst die klassische Version des Transports vom Controller zum Repository und umgekehrt, ohne zusÀtzliche Logik. Wenn Sie zum Kern des Ansatzes gehen möchten, scrollen Sie nach unten zur abstrakten Version. Trotzdem empfehle ich, den ganzen Artikel zu lesen.
Die klassische Version.
Die
Ressourcen des Beispiels stellen mehrere EntitĂ€ten und Methoden fĂŒr sie bereit. In diesem Artikel haben wir jedoch nur eine BenutzerentitĂ€t und nur eine save () -Methode, die wir aus dem Repository ĂŒber den Dienst auf den Controller ziehen. In den Ressourcen gibt es 7 davon, aber im Allgemeinen können Sie mit Spring CRUD / JPA Repository etwa ein Dutzend Methoden zum Speichern / Empfangen / Löschen verwenden, und Sie können beispielsweise
einige universelle Methoden
verwenden . Wir werden auch nicht durch notwendige Dinge wie Validierung, Zuordnung von dto usw. abgelenkt. Sie können es selbst hinzufĂŒgen oder
in anderen Artikeln von Habr studieren.
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; }
Repository:
@Repository public interface UserRepository extends CrudRepository<User, Long> { }
Service:
public interface UserService { Optional<User> save(User user); }
Service (Implementierung):
@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()) )); } }
Wir haben eine Reihe von abhĂ€ngigen Klassen, die uns helfen, die BenutzerentitĂ€t auf CRUD-Ebene zu bearbeiten. In unserem Beispiel ist dies eine Methode, da mehr Ressourcen vorhanden sind. Diese ĂŒberhaupt nicht abstrakte Version von Schreibschichten wird im Zweig
standart_version dargestellt .
Angenommen, wir mĂŒssen eine weitere EntitĂ€t hinzufĂŒgen, z. B. Auto. Wir werden auf der Ebene der EntitĂ€ten kein Geld miteinander verdienen (wenn Sie möchten, können Sie es zuordnen).
Erstellen Sie zunÀchst eine EntitÀt.
@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; }
Erstellen Sie dann ein Repository.
public interface CarRepository extends CrudRepository<Car, Long> { }
Dann der Service ...
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(); }
Dann die Implementierung des Dienstes ....... Controller ...........
Ja, Sie können einfach dieselben Methoden (sie sind hier universell) aus der User-Klasse kopieren, dann User in Car Ă€ndern, dann dasselbe mit der Implementierung tun, mit dem Controller, dann steht die nĂ€chste EntitĂ€t in der Reihe, und dort sehen sie bereits mehr und mehr aus ... Normalerweise wird die zweite mĂŒde. Die Erstellung einer Servicearchitektur fĂŒr ein paar Dutzend EntitĂ€ten (Kopieren und EinfĂŒgen, Ersetzen des Namens der EntitĂ€t, irgendwo falsch, irgendwo versiegelt ...) fĂŒhrt zu der Qual, die jede monotone Arbeit verursacht. Versuchen Sie, zwanzig EntitĂ€ten in Ihrer Freizeit zu verschreiben, und Sie werden verstehen, was ich meine.
Als ich mich also nur fĂŒr Generika und typische Parameter interessierte, wurde mir klar, dass der Prozess viel weniger routinemĂ€Ăig sein kann.
Also Abstraktionen basierend auf typischen Parametern.
Die Bedeutung dieses Ansatzes besteht darin, die gesamte Logik in die Abstraktion einzubeziehen, die Abstraktion an die typischen Parameter der Schnittstelle zu binden und andere Bins in die Bins zu injizieren. Und alle. Keine Logik in den Bohnen. Nur eine Injektion anderer Bohnen. Bei diesem Ansatz werden Architektur und Logik einmal geschrieben und beim HinzufĂŒgen neuer EntitĂ€ten nicht dupliziert.
Beginnen wir mit dem Eckpfeiler unserer Abstraktion - einer abstrakten Einheit. Von ihr aus beginnt die Kette abstrakter AbhĂ€ngigkeiten, die als Rahmen fĂŒr den Dienst dienen.
Alle EntitÀten haben mindestens ein gemeinsames Feld (normalerweise mehr). Dies ist die ID. Wir nehmen dieses Feld in eine separate abstrakte EntitÀt und erben Benutzer und Auto davon.
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; } }
Denken Sie daran, die Abstraktion mit der Annotation @MappedSuperclass zu markieren. Der Ruhezustand sollte auch wissen, dass es sich um eine Abstraktion handelt.
Benutzer:
@Entity public class User extends AbstractEntity { private String name; private String phone;
Mit Auto dementsprechend das gleiche.
In jeder Schicht haben wir zusÀtzlich zu den Bins eine Schnittstelle mit typischen Parametern und eine abstrakte Klasse mit Logik. Neben dem Repository wird hier dank der Besonderheiten von Spring Data JPA alles viel einfacher.
Das erste, was wir im Repository benötigen, ist ein gemeinsam genutztes Repository.
CommonRepository:
@NoRepositoryBean public interface CommonRepository<E extends AbstractEntity> extends CrudRepository<E, Long> { }
In diesem Repository legen wir allgemeine Regeln fĂŒr die gesamte Kette fest: Alle daran teilnehmenden EntitĂ€ten erben von der Zusammenfassung. Als nĂ€chstes mĂŒssen wir fĂŒr jede EntitĂ€t eine eigene Repository-Schnittstelle schreiben, in der wir angeben, mit welcher EntitĂ€t diese Repository-Service-Controller-Kette arbeiten wird.
UserRepository:
@Repository public interface UserRepository extends CommonRepository<User> { }
Dank der Funktionen von Spring Data JPA endet das Repository-Setup hier - alles wird so funktionieren. Als nĂ€chstes kommt der Service. Wir mĂŒssen eine gemeinsame Schnittstelle, Abstraktion und Bean erstellen.
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; }
Hier definieren wir alle Methoden neu und erstellen einen parametrisierten Konstruktor fĂŒr das zukĂŒnftige Repository, den wir in der Bean neu definieren. Daher verwenden wir bereits ein Repository, das wir noch nicht definiert haben. Wir wissen noch nicht, welche EntitĂ€t in dieser Abstraktion verarbeitet wird und welches Repository wir benötigen werden.
UserService:
@Service public class UserService extends AbstractService<User, UserRepository> { public UserService(UserRepository repository) { super(repository); } }
In der Bin machen wir das Letzte - wir definieren explizit das Repository, das wir brauchen, das dann im Abstraktionskonstruktor aufgerufen wird. Und alle.
Mithilfe der Schnittstelle und der Abstraktion haben wir eine Autobahn erstellt, ĂŒber die wir alle EntitĂ€ten fahren. In der Tonne bringen wir die Auflösung auf die Autobahn, durch die wir die EntitĂ€t anzeigen, die wir auf der Autobahn benötigen.
Die Steuerung basiert auf demselben Prinzip: Schnittstelle, Abstraktion, Bin.
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); } }
Das ist die ganze Struktur. Es ist einmal geschrieben.
Was weiter?
Stellen wir uns nun vor, wir haben eine neue EntitĂ€t, die wir bereits von AbstractEntity geerbt haben, und wir mĂŒssen dieselbe Kette dafĂŒr schreiben. Es wird eine Minute dauern. Und keine Copy-Paste und Korrekturen.
Nehmen Sie bereits von AbstractEntity Car geerbt.
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); } }
Wie wir sehen können, besteht das Kopieren derselben Logik darin, einfach eine Bean hinzuzufĂŒgen. Es ist nicht erforderlich, die Logik in jedem Bin mit der Ănderung von Parametern und Signaturen neu zu schreiben. Sie werden einmal geschrieben und funktionieren in jedem nachfolgenden Fall.
Fazit
NatĂŒrlich beschreibt das Beispiel eine Art sphĂ€rische Situation, in der die CRUD fĂŒr jede EntitĂ€t dieselbe Logik hat. Es passiert nicht - Sie mĂŒssen noch einige Methoden im Bin neu definieren oder neue hinzufĂŒgen. Dies ergibt sich jedoch aus den spezifischen Anforderungen fĂŒr die Verarbeitung der EntitĂ€t. Nun, wenn 60 Prozent der Gesamtzahl der CRUD-Methoden in der Abstraktion bleiben. Dies ist ein gutes Ergebnis, denn je mehr wir redundanten Code manuell generieren, desto mehr Zeit verbringen wir mit monotoner Arbeit und desto höher ist das Risiko von Fehlern oder Tippfehlern.
Ich hoffe, der Artikel war nĂŒtzlich, danke fĂŒr Ihre Aufmerksamkeit.
UPD
Dank des
aleksandy- Vorschlags war es möglich, die Bean im Konstruktor zu initialisieren und dadurch den Ansatz erheblich zu verbessern. Wenn Sie sehen, wie Sie das Beispiel noch verbessern können, schreiben Sie in die Kommentare, und möglicherweise werden Ihre VorschlÀge eingereicht.