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());
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;
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(
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 :)