Was machen wir falsch mit dem Frühling?

In diesem Artikel möchte ich Beobachtungen über einige Antimuster teilen, die im Code von Anwendungen enthalten sind, die auf Spring ausgeführt werden. Alle tauchten auf die eine oder andere Weise im Live-Code auf: Entweder bin ich in bestehenden Klassen auf sie gestoßen, oder ich habe sie beim Lesen der Arbeit von Kollegen erwischt.


Ich hoffe, es wird für Sie interessant sein, und wenn Sie nach dem Lesen Ihre „Sünden“ anerkennen und sich auf den Weg der Korrektur begeben, werde ich mich doppelt freuen. Ich fordere Sie außerdem auf, Ihre eigenen Beispiele im Kommentar mitzuteilen. Wir werden dem Beitrag die neugierigsten und ungewöhnlichsten hinzufügen.


Autodraht


Das große und schreckliche @Autowired ist eine ganze Ära im Frühling. Sie können beim Schreiben von Tests immer noch nicht darauf verzichten, aber im Hauptcode (PMSM) ist dies eindeutig überflüssig. In einigen meiner letzten Projekte war er überhaupt nicht. Wir haben lange so geschrieben:


 @Component public class MyService { @Autowired private ServiceDependency; @Autowired private AnotherServiceDependency; //... } 

Die Gründe, warum die Abhängigkeitsinjektion durch Felder und Setter kritisiert wird, wurden insbesondere hier bereits ausführlich beschrieben. Eine Alternative ist die Implementierung durch den Konstruktor. Nach dem Link wird ein Beispiel beschrieben:


 private DependencyA dependencyA; private DependencyB dependencyB; private DependencyC dependencyC; @Autowired public DI(DependencyA dependencyA, DependencyB dependencyB, DependencyC dependencyC) { this.dependencyA = dependencyA; this.dependencyB = dependencyB; this.dependencyC = dependencyC; } 

Es sieht mehr oder weniger anständig aus, aber stellen Sie sich vor, wir haben 10 Abhängigkeiten (ja, ja, ich weiß, dass sie in diesem Fall in separate Klassen eingeteilt werden müssen, aber jetzt geht es nicht darum). Das Bild ist nicht mehr so ​​attraktiv:


 private DependencyA dependencyA; private DependencyB dependencyB; private DependencyC dependencyC; private DependencyD dependencyD; private DependencyE dependencyE; private DependencyF dependencyF; private DependencyG dependencyG; private DependencyH dependencyH; private DependencyI dependencyI; private DependencyJ dependencyJ; @Autowired public DI(/* ... */) { this.dependencyA = dependencyA; this.dependencyB = dependencyB; this.dependencyC = dependencyC; this.dependencyD = dependencyD; this.dependencyE = dependencyE; this.dependencyF = dependencyF; this.dependencyG = dependencyG; this.dependencyH = dependencyH; this.dependencyI = dependencyI; this.dependencyJ = dependencyJ; } 

Der Code sieht ehrlich gesagt monströs aus.


Und das vergessen viele auch hier Geiger @Autowired nicht benötigt! Wenn eine Klasse nur einen Konstruktor hat, versteht Spring (> = 4), dass Abhängigkeiten über diesen Konstruktor implementiert werden müssen. Also können wir es wegwerfen und durch den Lombok @AllArgsContructor . Oder noch besser - auf @RequiredArgsContructor , ohne zu vergessen, alle erforderlichen Felder als final zu deklarieren und eine sichere Initialisierung des Objekts in einer Multithread-Umgebung zu erhalten (vorausgesetzt, alle Abhängigkeiten werden auch sicher initialisiert):


 @RequiredArgsConstructor public class DI { private final DependencyA dependencyA; private final DependencyB dependencyB; private final DependencyC dependencyC; private final DependencyD dependencyD; private final DependencyE dependencyE; private final DependencyF dependencyF; private final DependencyG dependencyG; private final DependencyH dependencyH; private final DependencyI dependencyI; private final DependencyJ dependencyJ; } 

Statische Methoden in Utility-Klassen und Enum-Funktionen


Bloody E hat häufig die Aufgabe, Datenträgerobjekte von einer Anwendungsschicht in ähnliche Objekte einer anderen Schicht zu konvertieren. Hierzu werden weiterhin Utility-Klassen mit solchen statischen Methoden verwendet (Rückruf im Jahr 2019):


 @UtilityClass public class Utils { public static UserDto entityToDto(UserEntity user) { //... } } 

Fortgeschrittene Benutzer, die intelligente Bücher lesen, sind sich der magischen Eigenschaften von Aufzählungen bewusst:


 enum Function implements Function<UserEntity, UserDto> { INST; @Override public UserDto apply(UserEntity user) { //... } } 

In diesem Fall erfolgt der Aufruf weiterhin für ein einzelnes Objekt und nicht für eine von Spring gesteuerte Komponente.
Noch fortgeschrittenere Männer (und Mädchen) kennen MapStruct , mit dem Sie alles in einer einzigen Oberfläche beschreiben können:


 @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR) public interface CriminalRecommendationMapper { UserDto map(UserEntity user); } 

Jetzt bekommen wir die Federkomponente. Es scheint ein Sieg zu sein. Aber der Teufel steckt im Detail und es kommt vor, dass der Sieg überwältigend wird. Erstens sollten die Feldnamen gleich sein (andernfalls beginnen Hämorrhoiden), was nicht immer zweckmäßig ist, und zweitens treten bei komplexen Feldtransformationen der verarbeiteten Objekte zusätzliche Schwierigkeiten auf. Nun, mapstruct selbst muss abhängig hinzugefügt werden.


Und nur wenige Menschen erinnern sich an die altmodische, aber dennoch einfache und funktionierende Art, einen federgetriebenen Konverter zu bekommen:


 import org.springframework.core.convert.converter.Converter; @Component public class UserEntityToDto implements Converter<UserEntity, UserDto> { @Override public UserDto convert(UserEntity user) { //... } } 

Der Vorteil hier ist, dass ich in einer anderen Klasse nur schreiben muss


 @Component @RequiredArgsConstructor public class DI { private final Converter<UserEntity, UserDto> userEnityToDto; } 

und der Frühling wird alles von selbst lösen!


Abfallqualifizierer


Lebensfall: Die Anwendung arbeitet mit zwei Datenbanken. Dementsprechend gibt es zwei Datenquellen ( java.sql.DataSource ), zwei Transaktionsmanager, zwei Gruppen von Repositorys usw. All dies wird bequem in zwei separaten Einstellungen beschrieben. Dies ist für Postgre:


 @Configuration public class PsqlDatasourceConfig { @Bean @Primary @ConfigurationProperties(prefix = "spring.datasource.psql") public DataSource psqlDataSource() { return DataSourceBuilder.create().build(); } @Bean public SpringLiquibase primaryLiquibase( ProfileChecker checker, @Qualifier("psqlDataSource") DataSource dataSource ) { boolean isTest = checker.isTest(); SpringLiquibase liquibase = new SpringLiquibase(); liquibase.setDataSource(dataSource); liquibase.setChangeLog("classpath:liquibase/schema-postgre.xml"); liquibase.setShouldRun(isTest); return liquibase; } } 

Und das ist für DB2:


 @Configuration public class Db2DatasourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.db2") public DataSource db2DataSource() { return DataSourceBuilder.create().build(); } @Bean public SpringLiquibase liquibase( ProfileChecker checker, @Qualifier("db2DataSource") DataSource dataSource ) { boolean isTest = checker.isTest(); SpringLiquibase liquibase = new SpringLiquibase(); liquibase.setDataSource(dataSource); liquibase.setChangeLog("classpath:liquibase/schema-db2.xml"); liquibase.setShouldRun(isTest); return liquibase; } } 

Da ich zwei Datenbanken habe, möchte ich für die Tests zwei separate DDL / DML auf ihnen rollen. Da beide Konfigurationen gleichzeitig geladen werden, wenn die Anwendung aktiv ist, verliert Spring beim @Qualifier seine Zielbezeichnung und @Qualifier bestenfalls fehl. Es stellt sich heraus, dass die @Qualifier umständlich und anfällig für Kratzer sind, und ohne sie funktioniert es nicht. Um den Deadlock zu überwinden, müssen Sie erkennen, dass die Abhängigkeit nicht nur als Argument, sondern auch als Rückgabewert erhalten werden kann, indem Sie den Code wie folgt umschreiben:


 @Configuration public class PsqlDatasourceConfig { @Bean @Primary @ConfigurationProperties(prefix = "spring.datasource.psql") public DataSource psqlDataSource() { return DataSourceBuilder.create().build(); } @Bean public SpringLiquibase primaryLiquibase(ProfileChecker checker) { boolean isTest = checker.isTest(); SpringLiquibase liquibase = new SpringLiquibase(); liquibase.setDataSource(psqlDataSource()); // <----- liquibase.setChangeLog("classpath:liquibase/schema-postgre.xml"); liquibase.setShouldRun(isTest); return liquibase; } } //... @Configuration public class Db2DatasourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.db2") public DataSource db2DataSource() { return DataSourceBuilder.create().build(); } @Bean public SpringLiquibase liquibase(ProfileChecker checker) { boolean isTest = checker.isTest(); SpringLiquibase liquibase = new SpringLiquibase(); liquibase.setDataSource(db2DataSource()); // <----- liquibase.setChangeLog("classpath:liquibase/schema-db2.xml"); liquibase.setShouldRun(isTest); return liquibase; } } 

javax.inject.Provider


Wie bekomme ich eine Bohne mit Prototyp-Umfang? Ich bin oft darauf gestoßen


 @Component @Scope(SCOPE_PROTOTYPE) @RequiredArgsConstructor public class ProjectBuilder { private final ProjectFileConverter converter; private final ProjectRepository projectRepository; //... } @Component @RequiredArgsConstructor public class PrototypeUtilizer { private final Provider<ProjectBuilder> projectBuilderProvider; void method() { ProjectBuilder freshBuilder = projectBuilderProvider.get(); } } 

Es scheint, dass alles in Ordnung ist, der Code funktioniert. In diesem Fass Honig befindet sich jedoch eine Fliege in der Salbe. Wir müssen noch eine javax.inject:javax.inject:1 Abhängigkeit ziehen, die vor genau 10 Jahren zu Maven Central hinzugefügt wurde und seitdem nie mehr aktualisiert wurde.


Aber Spring ist seit langem in der Lage, dasselbe ohne Sucht durch Dritte zu tun! Ersetzen javax.inject.Provider::get einfach javax.inject.Provider::get durch org.springframework.beans.factory.ObjectFactory::getObject und alles funktioniert auf die gleiche Weise.


 @Component @RequiredArgsConstructor public class PrototypeUtilizer { private final ObjectFactory<ProjectBuilder> projectBuilderFactory; void method() { ProjectBuilder freshBuilder = projectBuilderFactory.getObject(); } } 

Jetzt können wir mit gutem Gewissen javax.inject aus der Liste der Abhängigkeiten streichen.


Verwenden von Zeichenfolgen anstelle von Klassen in Einstellungen


Ein häufiges Beispiel für das Verbinden von Spring Data-Repositorys mit einem Projekt:


 @Configuration @EnableJpaRepositories("com.smth.repository") public class JpaConfig { //... } 

Hier schreiben wir ausdrücklich das Paket vor, das von Spring angezeigt wird. Wenn wir eine zusätzliche Benennung zulassen, stürzt die Anwendung beim Start ab. Ich möchte solche dummen Fehler in den frühen Stadien, im Limit - direkt während der Bearbeitung des Codes - erkennen. Das Framework geht auf uns zu, sodass der obige Code neu geschrieben werden kann:


 @Configuration @EnableJpaRepositories(basePackageClasses = AuditRepository.class) public class JpaConfig { //... } 

Hier ist AuditRepository eines der Paket-Repositorys, die wir AuditRepository werden. Da wir die Klasse angegeben haben, müssen wir diese Klasse mit unserer Konfiguration verbinden. Tippfehler werden jetzt direkt im Editor oder im schlimmsten Fall beim Erstellen des Projekts erkannt.


Dieser Ansatz kann in vielen ähnlichen Fällen angewendet werden, zum Beispiel:


 @ComponentScan(basePackages = "com.smth") 

verwandelt sich in


 import com.smth.Smth; @ComponentScan(basePackageClasses = Smth.class) 

Wenn wir einem Wörterbuch der Form Map<String, Object> eine Klasse hinzufügen müssen, können Sie dies folgendermaßen tun:


 void config(LocalContainerEntityManagerFactoryBean bean) { String property = "hibernate.session_factory.session_scoped_interceptor"; bean.getJpaPropertyMap().put(property, "com.smth.interceptor.AuditInterceptor"); } 

Es ist jedoch besser, einen expliziten Typ zu verwenden:


 import com.smth.interceptor.AuditInterceptor; void config(LocalContainerEntityManagerFactoryBean bean) { String property = "hibernate.session_factory.session_scoped_interceptor"; bean.getJpaPropertyMap().put(property, AuditInterceptor.class); } 

Und wenn es so etwas gibt


 LocalContainerEntityManagerFactoryBean bean = builder .dataSource(dataSource) .packages( //...     ) .persistenceUnit("psql") .build(); 

Es ist erwähnenswert, dass die packages() -Methode überladen ist und die Klassen verwendet:


Legen Sie nicht alle Bohnen in eine Packung


Ich denke, in vielen Projekten auf Spring / Spring Booth haben Sie eine ähnliche Struktur gesehen:


 root-package | \ repository/ entity/ service/ Application.java 

Hier ist Application.java die Stammklasse der Anwendung:


 @SpringBootApplication @EnableJpaRepositories(basePackageClasses = SomeRepository.class) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } 

Dies ist der klassische Microservice-Code: Die Komponenten sind je nach Zweck in Ordnern angeordnet, die Klasse mit den Einstellungen befindet sich im Stammverzeichnis. Während das Projekt klein ist, ist alles in Ordnung. Während das Projekt wächst, erscheinen Fettpakete mit Dutzenden von Repositories / Services. Und wenn das Projekt ein Monolith bleibt, dann G-tt mit ihnen. Wenn sich jedoch die Aufgabe ergibt, die zerlumpte Anwendung in Teile zu unterteilen, beginnen die Fragen. Nachdem ich diesen Schmerz einmal erlebt hatte, entschied ich mich für einen anderen Ansatz, nämlich Klassen nach ihrer Domäne zu gruppieren. Das Ergebnis ist so etwas wie


 root-package/ | \ user/ | \ repository/ domain/ service/ controller/ UserConfig.java billing/ | \ repository/ domain/ service/ BillingConfig.java //... Application.java 

Hier enthält das user Unterpakete mit Klassen, die für die Benutzerlogik verantwortlich sind:


 user/ | \ repository/ UserRepository.java domain/ UserEntity.java service/ UserService.java controller/ UserController.java UserConfig.java 

Jetzt UserConfig Sie in UserConfig alle Einstellungen beschreiben, die mit dieser Funktionalität verbunden sind:


 @Configuration @ComponentScan(basePackageClasses = UserServiceImpl.class) @EnableJpaRepositories(basePackageClasses = UserRepository.class) class UserConfig { } 

Der Vorteil dieses Ansatzes besteht darin, dass Module bei Bedarf einfacher separaten Diensten / Anwendungen zugeordnet werden können. module-info.java ist auch nützlich, wenn Sie Ihr Projekt modularisieren module-info.java , indem Sie module-info.java hinzufügen und Dienstprogrammklassen vor der Außenwelt verbergen.


Das ist alles, ich hoffe, meine Arbeit hat Ihnen geholfen. Beschreibe deine Antimuster in den Kommentaren, wir werden es gemeinsam klären :)

Source: https://habr.com/ru/post/de472994/


All Articles