Spring在做什么错

在本文中,我想分享有关在Spring上运行的应用程序代码中发现的一些反模式的观察。 所有这些都以一种或另一种方式出现在实时代码中:要么是我在现有的课程中遇到了它们,要么是我在阅读同事的工作时发现了它们。


我希望这对您来说很有趣,如果您在阅读后承认自己的“罪过”并踏上了改正之路,我会倍加高兴。 我还敦促您在评论中分享您自己的示例,我们将在帖子中添加最好奇和最不寻常的内容。


自动接线


伟大而可怕的@Autowired是春天的整个时代。 在编写测试时,您仍然不能没有它,但是在主要代码中,它(PMSM)显然是多余的。 在我最近的几个项目中,他一点都没有。 很长一段时间,我们这样写:


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

批评了通过字段和setter注入依赖项的原因,已经在此进行了详细描述。 另一种选择是通过构造函数实现。 在链接之后,描述了一个示例:


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

它看起来或多或少是不错的,但是假设我们有10个依赖项(是的,我知道在这种情况下需要将它们分组到单独的类中,但是现在不关乎此事了)。 图片不再那么吸引人了:


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

坦率地说,代码看起来很可怕。


在这里,许多人也忘记了在这里 小提琴家 不需要@Autowired ! 如果一个类只有一个构造函数,那么Spring(> = 4)将理解需要通过此构造函数实现依赖关系。 因此,我们可以将其丢弃,以Lombok @AllArgsContructor 。 甚至更好-在@RequiredArgsContructor ,不要忘记将所有必需字段声明为final并在多线程环境中接受对象的安全初始化(假设所有依赖项也已安全初始化):


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

实用程序类和枚举函数中的静态方法


Bloody E通常具有将数据载体对象从一个应用程序层转换为另一层的相似对象的任务。 为此,仍然使用具有此类静态方法的实用程序类(回想一下,在2019年):


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

阅读智能书的更高级的用户意识到枚举的神奇特性:


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

的确,在这种情况下,调用仍然发生在单个对象上,而不是对Spring控制的组件。
甚至更高级的人(和女孩)都了解MapStruct ,它使您可以在单个界面中描述所有内容:


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

现在我们得到了弹簧组件。 好像是胜利。 但是魔鬼在细节上,而胜利恰恰是压倒一切的。 首先,字段名称必须相同(否则,会以痔疮开头),这并不总是很方便;其次,如果处理对象存在复杂的字段转换,则会产生其他困难。 好吧,mapstruct本身需要添加依赖。


很少有人回想起老式但简单易行的获得弹簧驱动转换器的方法:


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

这样做的好处是,在另一个类中,我只需要编写


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

Spring将自行解决所有问题!


废物预选赛


生活案例:该应用程序可用于两个数据库。 因此,有两个数据源( java.sql.DataSource ),两个事务管理器,两组存储库等。 所有这一切都在两个单独的设置中方便地描述。 这是给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; } } 

这是针对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; } } 

由于我有两个数据库,因此对于测试,我想在它们上滚动两个单独的DDL / DML。 由于两种配置是在应用程序启动时同时加载的,因此,如果我@Qualifier ,那么Spring将失去其目标名称,充其量将失败。 事实证明,@ @Qualifier繁琐且容易划痕,没有它们,它将不起作用。 要打破僵局,您需要认识到,通过重写如下代码,不仅可以作为参数获得依赖,还可以作为返回值获得依赖:


 @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


如何获得具有原型范围的bean? 我经常碰到这个


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

看起来一切都很好,代码可以正常工作。 但是,在这桶蜂蜜中,药膏中有苍蝇。 我们需要再拖一个javax.inject:javax.inject:1依赖项,它完全是10年前添加到Maven Central的,此后从未更新过。


但是,Spring早已能够做到这一点而无需第三方上瘾! 只需将org.springframework.beans.factory.ObjectFactory::getObject替换为javax.inject.Provider::get ,一切都将以相同的方式进行。


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

现在,我们可以清楚地从依赖列表中删除javax.inject


在设置中使用字符串而不是类


将Spring Data存储库连接到项目的常见示例:


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

在这里,我们明确规定了Spring将要查看的包。 如果我们允许一些额外的命名,那么应用程序将在启动时崩溃。 我想尽早在极限内-在代码编辑期间检测到此类愚蠢的错误。 该框架适用于我们,因此上面的代码可以重写:


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

在这里, AuditRepository是我们将要查看的软件包存储库之一。 既然我们指示了该类,我们将需要将该类连接到我们的配置,并且现在可以在编辑器中直接检测到错别字,或者最糟糕的是,在构建项目时。


此方法可以在许多类似情况下应用,例如:


 @ComponentScan(basePackages = "com.smth") 

变成


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

如果我们需要将一些类添加到Map<String, Object>形式的字典中,则可以这样进行:


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

但最好使用显式类型:


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

当有类似的东西


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

值得注意的是, packages()方法已重载并使用以下类:


不要将所有豆子都放在一个包装中


我认为在Spring / Spring Booth的许多项目中,您看到了类似的结构:


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

这里Application.javaApplication.java的根类:


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

这是经典的微服务代码:组件根据用途排列在文件夹中,带有设置的类位于根目录。 虽然项目很小,但是一切都很好。 随着项目的发展,胖包中出现了数十个存储库/服务。 如果该项目仍然是一个整体,那么Gd就会与他们合作。 但是,如果出现了将破烂的应用程序分成几部分的任务,那么问题就开始了。 经历过一次痛苦之后,我决定采用一种不同的方法,即按其领域对类进行分组。 结果是像


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

在这里, user包包括带有负责用户逻辑的类的子包:


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

现在,在UserConfig您可以描述与此功能关联的所有设置:


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

这种方法的优点是,如有必要,可以更轻松地将模块分配给单独的服务/应用程序。 如果您打算通过添加module-info.java来对项目进行模块化,从而对外部世界隐藏实用程序类,则也很有用。


希望如此,我的工作对您有所帮助。 在评论中描述您的反模式,我们将一起整理:)

Source: https://habr.com/ru/post/zh-CN472994/


All Articles