O que estamos fazendo de errado com o Spring

Neste artigo, quero compartilhar observações sobre alguns antipadrões encontrados no código de aplicativos em execução no Spring. Todos eles, de uma maneira ou de outra, vieram à tona no código ao vivo: ou eu os encontrei nas classes existentes ou os peguei enquanto lia o trabalho dos colegas.


Espero que você esteja interessado, mas se depois de ler você reconhecer seus "pecados" e seguir o caminho da correção, ficarei duplamente satisfeito. Peço também que você compartilhe seus próprios exemplos no comentário; adicionaremos os mais curiosos e incomuns ao post.


Ligado automaticamente


O grande e terrível @Autowired é uma época inteira na primavera. Você ainda não pode ficar sem isso ao escrever testes, mas no código principal (PMSM) é claramente supérfluo. Em vários dos meus projetos recentes, ele não estava. Por um longo tempo, escrevemos assim:


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

As razões pelas quais a injeção de dependência através de campos e setters são criticadas já foram descritas em detalhes, em particular aqui . Uma alternativa é a implementação por meio do construtor. Seguindo o link, um exemplo é descrito:


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

Parece mais ou menos decente, mas imagine que tenhamos 10 dependências (sim, sim, eu sei que nesse caso elas precisam ser agrupadas em classes separadas, mas agora não é sobre isso). A imagem não é mais tão atraente:


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

O código, francamente, parece monstruoso.


E aqui, muitos esquecem que aqui também violinista @Autowired não @Autowired necessário! Se uma classe tiver apenas um construtor, o Spring (> = 4) entenderá que as dependências precisam ser implementadas por meio desse construtor. Assim, podemos jogá-lo fora, substituindo-o pelo Lombok @AllArgsContructor . Ou melhor ainda - no @RequiredArgsContructor , sem esquecer de declarar todos os campos necessários como final e receber uma inicialização segura do objeto em um ambiente multithread (desde que todas as dependências também sejam inicializadas com segurança):


 @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étodos estáticos em classes utilitárias e funções enum


O Bloody E geralmente tem a tarefa de converter objetos do suporte de dados de uma camada de aplicativo em objetos semelhantes de outra camada. Para isso, classes de utilidade com métodos estáticos ainda são usados ​​(lembre-se, no ano de 2019):


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

Usuários mais avançados que lêem livros inteligentes conhecem as propriedades mágicas das enumerações:


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

É verdade que, nesse caso, a chamada ainda ocorre para um único objeto e não para um componente controlado pelo Spring.
Garotos (e garotas) ainda mais avançados conhecem o MapStruct , que permite descrever tudo em uma única interface:


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

Agora temos o componente de mola. Parece uma vitória. Mas o diabo está nos detalhes, e acontece que a vitória se torna esmagadora. Em primeiro lugar, os nomes dos campos devem ser os mesmos (caso contrário as hemorróidas começam), o que nem sempre é conveniente e, em segundo lugar, se houver alguma transformação complexa nos objetos processados, dificuldades adicionais surgirão. Bem, o próprio mapstruct precisa ser adicionado dependendo.


E poucas pessoas se lembram da maneira antiquada, porém simples e funcional de obter um conversor acionado por mola:


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

A vantagem aqui é que, em outra aula, eu só preciso escrever


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

e a Primavera resolverá tudo sozinha!


Qualificador de resíduos


Caso de vida: o aplicativo funciona com dois bancos de dados. Conseqüentemente, existem duas fontes de dados ( java.sql.DataSource ), dois gerenciadores de transações, dois grupos de repositórios etc. Tudo isso é convenientemente descrito em duas configurações separadas. Isto é para o 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; } } 

E isto é para o 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; } } 

Desde que eu tenho dois bancos de dados, para os testes eu quero rolar dois DDL / DML separados neles. Como as duas configurações são carregadas ao mesmo tempo em que o aplicativo é @Qualifier , se eu @Qualifier , o Spring perderá a designação de destino e, na melhor das hipóteses, falhará. Acontece que os @Qualifier pesados ​​e propensos a arranhões e, sem eles, não funcionam. Para quebrar o impasse, você precisa perceber que a dependência pode ser obtida não apenas como argumento, mas também como valor de retorno, reescrevendo o código da seguinte maneira:


 @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


Como obter um bean com escopo de protótipo? Muitas vezes me deparei com isso


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

Parece que está tudo bem, o código funciona. No entanto, neste barril de mel há uma mosca na pomada. Precisamos arrastar mais uma javax.inject:javax.inject:1 , que foi adicionada ao Maven Central exatamente há 10 anos e nunca foi atualizada desde então.


Mas a primavera tem sido capaz de fazer o mesmo sem vícios de terceiros! Apenas substitua javax.inject.Provider::get por org.springframework.beans.factory.ObjectFactory::getObject e tudo funciona da mesma maneira.


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

Agora podemos, com a consciência limpa, cortar javax.inject da lista de dependências.


Usando cadeias em vez de classes nas configurações


Um exemplo comum de conexão de repositórios do Spring Data a um projeto:


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

Aqui, prescrevemos explicitamente o pacote que será visualizado pelo Spring. Se permitirmos um pouco de nomeação extra, o aplicativo falhará na inicialização. Eu gostaria de detectar esses erros estúpidos nos estágios iniciais, no limite - durante a edição do código. A estrutura segue em nossa direção, portanto, o código acima pode ser reescrito:


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

Aqui o AuditRepository é um dos repositórios de pacotes que iremos visualizar. Como indicamos a classe, precisaremos conectar essa classe à nossa configuração e agora os erros de digitação serão detectados diretamente no editor ou, na pior das hipóteses, na criação do projeto.


Essa abordagem pode ser aplicada em muitos casos semelhantes, por exemplo:


 @ComponentScan(basePackages = "com.smth") 

se transforma em


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

Se precisarmos adicionar alguma classe a um dicionário no formato Map<String, Object> , isso poderá ser feito assim:


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

mas é melhor usar um tipo explícito:


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

E quando há algo como


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

vale ressaltar que o método packages() está sobrecarregado e usa as classes:


Não coloque todos os grãos em um pacote


Eu acho que em muitos projetos no Spring / Spring Booth você viu uma estrutura semelhante:


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

Aqui Application.java é a classe raiz do aplicativo:


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

Este é o código clássico de microsserviço: os componentes são organizados em pastas de acordo com a finalidade, a classe com as configurações está na raiz. Enquanto o projeto é pequeno, tudo está bem. À medida que o projeto cresce, pacotes gordos aparecem com dezenas de repositórios / serviços. E se o projeto continua sendo um monólito, então D'us com eles. Mas se surgir a tarefa de dividir o aplicativo em partes, as perguntas começarão. Tendo experimentado essa dor uma vez, decidi adotar uma abordagem diferente, ou seja, agrupar as classes por seu domínio. O resultado é algo como


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

Aqui, o pacote do user inclui subpacotes com classes responsáveis ​​pela lógica do usuário:


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

Agora, no UserConfig você pode descrever todas as configurações associadas a esta funcionalidade:


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

A vantagem dessa abordagem é que, se necessário, os módulos podem ser mais facilmente alocados para serviços / aplicativos separados. Também é útil se você pretende modularizar seu projeto adicionando module-info.java , ocultando classes de utilitários do mundo exterior.


É tudo, espero, que meu trabalho tenha sido útil para você. Descreva seus antipadrões nos comentários, vamos resolver juntos :)

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


All Articles