Que faisons-nous de mal avec le printemps

Dans cet article, je veux partager des observations sur certains antipatterns trouvés dans le code des applications exécutées sur Spring. Tous d'une manière ou d'une autre ont fait surface dans le code en direct: soit je les ai rencontrés dans des cours existants, soit je les ai pris en lisant le travail de collègues.


J'espère que ce sera intéressant pour vous, et si après lecture vous reconnaissez vos «péchés» et vous engagez sur le chemin de la correction, je serai doublement satisfait. Je vous invite également à partager vos propres exemples dans le commentaire, nous ajouterons les plus curieux et insolites à l'article.


Autowired


Le grand et terrible @Autowired est une ère entière au printemps. Vous ne pouvez toujours pas vous en passer lors de l'écriture de tests, mais dans le code principal, il (PMSM) est clairement superflu. Dans plusieurs de mes projets récents, il ne l'était pas du tout. Pendant longtemps, nous avons écrit comme ceci:


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

Les raisons pour lesquelles l'injection de dépendances via les champs et les setters sont critiquées ont déjà été décrites en détail, notamment ici . Une alternative est l'implémentation via le constructeur. En suivant le lien, un exemple est décrit:


 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; } 

Cela a l'air plus ou moins décent, mais imaginez que nous avons 10 dépendances (oui, oui, je sais que dans ce cas, elles doivent être regroupées en classes distinctes, mais maintenant ce n'est pas à ce sujet). L'image n'est plus si attrayante:


 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; } 

Le code franchement, il a l'air monstrueux.


Et ici, beaucoup oublient qu'ici aussi violoniste @Autowired pas nécessaire! Si une classe n'a qu'un seul constructeur, Spring (> = 4) comprendra que les dépendances doivent être implémentées via ce constructeur. Nous pouvons donc le jeter, en le remplaçant par le Lombok @AllArgsContructor . Ou encore mieux - sur @RequiredArgsContructor , sans oublier de déclarer tous les champs nécessaires final et de recevoir une initialisation sûre de l'objet dans un environnement multi-thread (à condition que toutes les dépendances soient également initialisées en toute sécurité):


 @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; } 

Méthodes statiques dans les classes d'utilité et les fonctions d'énumération


Bloody E a souvent pour tâche de convertir des objets porteurs de données d'une couche d'application en objets similaires d'une autre couche. Pour cela, des classes utilitaires avec des méthodes statiques comme celle-ci sont toujours utilisées (rappel, en 2019):


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

Les utilisateurs plus avancés qui lisent des livres intelligents sont conscients des propriétés magiques des énumérations:


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

Certes, dans ce cas, l'appel se produit toujours vers un seul objet et non vers un composant contrôlé par Spring.
Les gars (et les filles) les plus avancés connaissent MapStruct , qui vous permet de tout décrire dans une seule interface:


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

Maintenant, nous obtenons la composante ressort. Cela ressemble à une victoire. Mais le diable est dans les détails, et il arrive que la victoire devienne écrasante. Premièrement, les noms de champ doivent être les mêmes (sinon les hémorroïdes commencent), ce qui n'est pas toujours pratique, et deuxièmement, s'il y a des transformations de champ complexes des objets traités, des difficultés supplémentaires se posent. Eh bien, mapstruct lui-même doit être ajouté en fonction.


Et peu de gens se souviennent de la manière ancienne, mais néanmoins simple et fonctionnelle d'obtenir un convertisseur à ressort:


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

L'avantage ici est que dans une autre classe, j'ai juste besoin d'écrire


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

et Spring résoudra tout par lui-même!


Qualificateur de déchets


Cas de vie: l'application fonctionne avec deux bases de données. En conséquence, il existe deux sources de données ( java.sql.DataSource ), deux gestionnaires de transactions, deux groupes de référentiels, etc. Tout cela est commodément décrit dans deux paramètres distincts. C'est pour 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; } } 

Et c'est pour 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; } } 

Puisque j'ai deux bases de données, pour les tests, je veux rouler deux DDL / DML distincts sur elles. Étant donné que les deux configurations sont chargées en même temps lorsque l'application est en cours, si je @Qualifier , Spring perdra sa désignation cible et, au mieux, échouera. Il s'avère que les @Qualifier encombrants et sujets aux rayures, et sans eux cela ne fonctionne pas. Pour sortir de l'impasse, vous devez vous rendre compte que la dépendance peut être obtenue non seulement comme argument, mais aussi comme valeur de retour, en réécrivant le code comme ceci:


 @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


Comment obtenir un bean avec une portée prototype? Je suis souvent tombé sur ce


 @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(); } } 

Il semblerait que tout va bien, le code fonctionne. Cependant, dans ce baril de miel, il y a une mouche dans la pommade. Nous devons faire glisser une autre javax.inject:javax.inject:1 , qui a été ajoutée à Maven Central il y a exactement 10 ans et n'a jamais été mise à jour depuis.


Mais Spring a longtemps pu faire de même sans addictions de tiers! Remplacez simplement javax.inject.Provider::get par org.springframework.beans.factory.ObjectFactory::getObject et tout fonctionne de la même manière.


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

Maintenant, nous pouvons, en toute conscience, couper javax.inject de la liste des dépendances.


Utilisation de chaînes au lieu de classes dans les paramètres


Un exemple courant de connexion de référentiels Spring Data à un projet:


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

Ici, nous prescrivons explicitement le package qui sera consulté par Spring. Si nous autorisons un peu de nommage supplémentaire, l'application se bloquera au démarrage. Je voudrais détecter de telles erreurs stupides dans les premiers stades, dans la limite - juste lors de l'édition du code. Le framework va vers nous, donc le code ci-dessus peut être réécrit:


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

Ici, AuditRepository est l'un des référentiels de packages que nous allons consulter. Puisque nous avons indiqué la classe, nous devrons connecter cette classe à notre configuration, et maintenant les fautes de frappe seront détectées directement dans l'éditeur ou, au pire, lors de la construction du projet.


Cette approche peut être appliquée dans de nombreux cas similaires, par exemple:


 @ComponentScan(basePackages = "com.smth") 

se transforme en


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

Si nous devons ajouter une classe à un dictionnaire de la forme Map<String, Object> , cela peut être fait comme ceci:


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

mais il vaut mieux utiliser un type explicite:


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

Et quand il y a quelque chose comme


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

il est à noter que la méthode packages() est surchargée et utilise les classes:


Ne mettez pas tous les haricots dans un seul paquet


Je pense que dans de nombreux projets sur Spring / Spring Booth, vous avez vu une structure similaire:


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

Ici Application.java est la classe racine de l'application:


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

Il s'agit du code de microservice classique: les composants sont disposés dans des dossiers en fonction de l'objectif, la classe avec les paramètres est à la racine. Alors que le projet est petit, tout va bien. À mesure que le projet se développe, de gros packages apparaissent avec des dizaines de référentiels / services. Et si le projet reste un monolithe, alors D.ieu avec eux. Mais si la tâche se pose de diviser l'application irrégulière en plusieurs parties, les questions commencent. Ayant ressenti cette douleur une fois, j'ai décidé d'adopter une approche différente, à savoir de regrouper les cours par leur domaine. Le résultat est quelque chose comme


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

Ici, le package user comprend des sous-packages avec des classes responsables de la logique utilisateur:


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

Maintenant, dans UserConfig vous pouvez décrire tous les paramètres associés à cette fonctionnalité:


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

L'avantage de cette approche est que, si nécessaire, les modules peuvent être plus facilement alloués à des services / applications séparés. Il est également utile si vous avez l'intention de modulariser votre projet en ajoutant module-info.java , en cachant les classes utilitaires du monde extérieur.


C'est tout, j'espère, mon travail vous a été utile. Décrivez vos antipatterns dans les commentaires, nous les trierons ensemble :)

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


All Articles