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