Os 10 erros mais comuns do Spring Framework

Publicados: 2022-03-11

O Spring é indiscutivelmente um dos frameworks Java mais populares e também uma fera poderosa para domar. Embora seus conceitos básicos sejam bastante fáceis de entender, tornar-se um desenvolvedor forte do Spring requer algum tempo e esforço.

Neste artigo, abordaremos alguns dos erros mais comuns no Spring, especificamente voltados para aplicativos da Web e Spring Boot. Como afirma o site do Spring Boot, o Spring Boot tem uma visão opinativa sobre como os aplicativos prontos para produção devem ser construídos, portanto, este artigo tentará imitar essa visão e fornecer uma visão geral de algumas dicas que serão incorporadas bem ao desenvolvimento de aplicativos da Web Spring Boot padrão.

Caso você não esteja muito familiarizado com o Spring Boot, mas ainda gostaria de experimentar algumas das coisas mencionadas, criei um repositório GitHub que acompanha este artigo. Se você se sentir perdido a qualquer momento durante o artigo, recomendo clonar o repositório e brincar com o código em sua máquina local.

Erro comum nº 1: ir muito baixo

Estamos nos acertando com esse erro comum porque a síndrome do “não inventado aqui” é bastante comum no mundo do desenvolvimento de software. Os sintomas incluem reescrever regularmente partes de código comumente usado e muitos desenvolvedores parecem sofrer com isso.

Embora entender os componentes internos de uma biblioteca específica e sua implementação seja na maioria das vezes bom e necessário (e também pode ser um ótimo processo de aprendizado), é prejudicial ao seu desenvolvimento como engenheiro de software estar constantemente abordando a mesma implementação de baixo nível detalhes. Há uma razão pela qual abstrações e frameworks como Spring existem, que é precisamente para separá-lo do trabalho manual repetitivo e permitir que você se concentre em detalhes de nível superior – seus objetos de domínio e lógica de negócios.

Portanto, abrace as abstrações - da próxima vez que você se deparar com um problema específico, faça uma pesquisa rápida primeiro e determine se uma biblioteca que resolve esse problema já está integrada ao Spring; hoje em dia, é provável que você encontre uma solução existente adequada. Como exemplo de uma biblioteca útil, usarei as anotações do Project Lombok em exemplos para o restante deste artigo. O Lombok é usado como um gerador de código clichê e o desenvolvedor preguiçoso dentro de você esperançosamente não deve ter problemas para se familiarizar com a biblioteca. Como exemplo, confira como é um “bean Java padrão” com o Lombok:

 @Getter @Setter @NoArgsConstructor public class Bean implements Serializable { int firstBeanProperty; String secondBeanProperty; }

Como você pode imaginar, o código acima compila para:

 public class Bean implements Serializable { private int firstBeanProperty; private String secondBeanProperty; public int getFirstBeanProperty() { return this.firstBeanProperty; } public String getSecondBeanProperty() { return this.secondBeanProperty; } public void setFirstBeanProperty(int firstBeanProperty) { this.firstBeanProperty = firstBeanProperty; } public void setSecondBeanProperty(String secondBeanProperty) { this.secondBeanProperty = secondBeanProperty; } public Bean() { } }

Observe, no entanto, que você provavelmente precisará instalar um plug-in caso pretenda usar o Lombok com seu IDE. A versão do plugin do IntelliJ IDEA pode ser encontrada aqui.

Erro comum nº 2: Internos 'vazando'

Expor sua estrutura interna nunca é uma boa ideia, pois cria inflexibilidade no design de serviço e, consequentemente, promove más práticas de codificação. Internos de 'vazamento' são manifestados tornando a estrutura do banco de dados acessível a partir de determinados pontos de extremidade da API. Como exemplo, digamos que o seguinte POJO (“Plain Old Java Object”) representa uma tabela em seu banco de dados:

 @Entity @NoArgsConstructor @Getter public class TopTalentEntity { @Id @GeneratedValue private Integer id; @Column private String name; public TopTalentEntity(String name) { this.name = name; } }

Digamos que exista um endpoint que precise acessar os dados do TopTalentEntity . Por mais tentador que seja retornar instâncias de TopTalentEntity , uma solução mais flexível seria criar uma nova classe para representar os dados de TopTalentEntity no endpoint da API:

 @AllArgsConstructor @NoArgsConstructor @Getter public class TopTalentData { private String name; }

Dessa forma, fazer alterações no back-end do banco de dados não exigirá alterações adicionais na camada de serviço. Considere o que aconteceria no caso de adicionar um campo 'senha' ao TopTalentEntity para armazenar os hashes de senha de seus usuários no banco de dados - sem um conector como TopTalentData , esquecer de alterar o front-end do serviço exporia acidentalmente algumas informações secretas muito indesejáveis !

Erro comum nº 3: falta de separação de interesses

À medida que seu aplicativo cresce, a organização do código começa a se tornar uma questão cada vez mais importante. Ironicamente, a maioria dos bons princípios de engenharia de software começa a desmoronar em escala – especialmente nos casos em que pouca atenção foi dada ao design da arquitetura do aplicativo. Um dos erros mais comuns aos quais os desenvolvedores tendem a sucumbir é misturar preocupações de código, e é extremamente fácil de fazer!

O que geralmente quebra a separação de interesses é apenas 'despejar' novas funcionalidades em classes existentes. Esta é, obviamente, uma ótima solução de curto prazo (para iniciantes, requer menos digitação), mas inevitavelmente se torna um problema mais adiante, seja durante testes, manutenção ou em algum lugar intermediário. Considere o seguinte controlador, que retorna TopTalentData de seu repositório:

 @RestController public class TopTalentController { private final TopTalentRepository topTalentRepository; @RequestMapping("/toptal/get") public List<TopTalentData> getTopTalent() { return topTalentRepository.findAll() .stream() .map(this::entityToData) .collect(Collectors.toList()); } private TopTalentData entityToData(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } }

A princípio, pode não parecer que haja algo particularmente errado com esse pedaço de código; ele fornece uma lista de TopTalentData que está sendo recuperada de instâncias de TopTalentEntity . Olhando mais de perto, no entanto, podemos ver que existem algumas coisas que o TopTalentController está realizando aqui; ou seja, está mapeando solicitações para um determinado endpoint, recuperando dados de um repositório e convertendo entidades recebidas do TopTalentRepository em um formato diferente. Uma solução 'mais limpa' seria separar essas preocupações em suas próprias classes. Pode parecer algo assim:

 @RestController @RequestMapping("/toptal") @AllArgsConstructor public class TopTalentController { private final TopTalentService topTalentService; @RequestMapping("/get") public List<TopTalentData> getTopTalent() { return topTalentService.getTopTalent(); } } @AllArgsConstructor @Service public class TopTalentService { private final TopTalentRepository topTalentRepository; private final TopTalentEntityConverter topTalentEntityConverter; public List<TopTalentData> getTopTalent() { return topTalentRepository.findAll() .stream() .map(topTalentEntityConverter::toResponse) .collect(Collectors.toList()); } } @Component public class TopTalentEntityConverter { public TopTalentData toResponse(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } }

Uma vantagem adicional dessa hierarquia é que ela nos permite determinar onde reside a funcionalidade apenas inspecionando o nome da classe. Além disso, durante o teste, podemos substituir facilmente qualquer uma das classes por uma implementação simulada, se necessário.

Erro comum nº 4: inconsistência e tratamento inadequado de erros

O tópico de consistência não é necessariamente exclusivo do Spring (ou Java, nesse caso), mas ainda é uma faceta importante a ser considerada ao trabalhar em projetos Spring. Embora o estilo de codificação possa ser motivo de debate (e geralmente é uma questão de acordo dentro de uma equipe ou de uma empresa inteira), ter um padrão comum acaba sendo uma grande ajuda à produtividade. Isso é especialmente verdadeiro com equipes de várias pessoas; a consistência permite que a transferência ocorra sem que muitos recursos sejam gastos em segurar a mão ou fornecer longas explicações sobre as responsabilidades de diferentes classes

Considere um projeto Spring com seus vários arquivos de configuração, serviços e controladores. Ser semanticamente consistente ao nomeá-los cria uma estrutura facilmente pesquisável onde qualquer novo desenvolvedor pode gerenciar seu caminho pelo código; anexando sufixos de configuração às suas classes de configuração, sufixos de serviço aos seus serviços e sufixos de controlador aos seus controladores, por exemplo.

Intimamente relacionado ao tópico de consistência, o tratamento de erros no lado do servidor merece uma ênfase específica. Se você já teve que lidar com respostas de exceção de uma API mal escrita, provavelmente sabe o porquê – pode ser difícil analisar as exceções corretamente e ainda mais doloroso determinar o motivo pelo qual essas exceções ocorreram em primeiro lugar.

Como desenvolvedor de API, o ideal é cobrir todos os endpoints voltados para o usuário e traduzi-los em um formato de erro comum. Isso geralmente significa ter um código de erro genérico e uma descrição em vez da solução de a) retornar uma mensagem “500 Internal Server Error” ou b) apenas despejar o rastreamento de pilha para o usuário (o que deve ser evitado a todo custo uma vez que expõe seus internos, além de ser difícil de lidar com o lado do cliente).

Um exemplo de um formato de resposta de erro comum pode ser:

 @Value public class ErrorResponse { private Integer errorCode; private String errorMessage; }

Algo semelhante a isso é comumente encontrado nas APIs mais populares e tende a funcionar bem, pois pode ser documentado de maneira fácil e sistemática. A tradução de exceções para esse formato pode ser feita fornecendo a anotação @ExceptionHandler para um método (um exemplo de anotação está no Erro comum nº 6).

Erro comum nº 5: lidar incorretamente com multithreading

Independentemente de ser encontrado em aplicativos de desktop ou web, Spring ou não Spring, o multithreading pode ser um osso duro de roer. Os problemas causados ​​pela execução paralela de programas são assustadoramente ilusórios e muitas vezes extremamente difíceis de depurar - na verdade, devido à natureza do problema, quando você perceber que está lidando com um problema de execução paralela, provavelmente tem que renunciar completamente ao depurador e inspecionar seu código “manualmente” até encontrar a causa raiz do erro. Infelizmente, não existe uma solução simples para resolver esses problemas; dependendo do seu caso específico, você terá que avaliar a situação e então atacar o problema do ângulo que achar melhor.

Idealmente, é claro que você gostaria de evitar completamente os bugs de multithreading. Novamente, uma abordagem de tamanho único não existe para fazer isso, mas aqui estão algumas considerações práticas para depuração e prevenção de erros de multithreading:

Evite o estado global

Primeiro, lembre-se sempre da questão do “estado global”. Se você estiver criando um aplicativo multithread, absolutamente tudo que for globalmente modificável deve ser monitorado de perto e, se possível, removido completamente. Se houver uma razão pela qual a variável global deve permanecer modificável, empregue cuidadosamente a sincronização e acompanhe o desempenho do seu aplicativo para confirmar que ele não está lento devido aos períodos de espera recém-introduzidos.

Evitar Mutabilidade

Este vem direto da programação funcional e, adaptado à OOP, afirma que a mutabilidade de classe e a mudança de estado devem ser evitadas. Isso, em suma, significa renunciar aos métodos setter e ter campos finais privados em todas as suas classes de modelo. A única vez que seus valores são alterados é durante a construção. Dessa forma, você pode ter certeza de que não surgem problemas de contenção e que o acesso às propriedades do objeto fornecerá os valores corretos o tempo todo.

Registrar dados cruciais

Avalie onde seu aplicativo pode causar problemas e registre preventivamente todos os dados cruciais. Se ocorrer um erro, você ficará grato por ter informações informando quais solicitações foram recebidas e ter uma melhor percepção do motivo pelo qual seu aplicativo se comportou mal. Novamente, é necessário observar que o log introduz E/S de arquivo adicional e, portanto, não deve ser abusado, pois pode afetar gravemente o desempenho do seu aplicativo.

Reutilizar implementações existentes

Sempre que você precisar gerar seus próprios threads (por exemplo, para fazer solicitações assíncronas a diferentes serviços), reutilize implementações seguras existentes em vez de criar suas próprias soluções. Isso significará, na maioria das vezes, utilizar o ExecutorServices e os CompletableFutures de estilo funcional do Java 8 para a criação de threads. O Spring também permite o processamento de solicitações assíncronas por meio da classe DeferredResult.

Erro comum nº 6: não empregar validação baseada em anotação

Vamos imaginar que nosso serviço TopTalent anterior exija um endpoint para adicionar novos Top Talents. Além disso, digamos que, por algum motivo realmente válido, todo novo nome precisa ter exatamente 10 caracteres. Uma maneira de fazer isso pode ser a seguinte:

 @RequestMapping("/put") public void addTopTalent(@RequestBody TopTalentData topTalentData) { boolean nameNonExistentOrHasInvalidLength = Optional.ofNullable(topTalentData) .map(TopTalentData::getName) .map(name -> name.length() == 10) .orElse(true); if (nameNonExistentOrInvalidLength) { // throw some exception } topTalentService.addTopTalent(topTalentData); }

No entanto, o acima (além de ser mal construído) não é realmente uma solução 'limpa'. Estamos verificando mais de um tipo de validade (ou seja, se TopTalentData não é nulo e se TopTalentData.name não é nulo e se TopTalentData.name tem 10 caracteres), além de lançar uma exceção se os dados forem inválidos .

Isso pode ser executado de forma muito mais limpa empregando o validador Hibernate com Spring. Vamos primeiro refatorar o método addTopTalent para dar suporte à validação:

 @RequestMapping("/put") public void addTopTalent(@Valid @NotNull @RequestBody TopTalentData topTalentData) { topTalentService.addTopTalent(topTalentData); } @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleInvalidTopTalentDataException(MethodArgumentNotValidException methodArgumentNotValidException) { // handle validation exception }

Além disso, teremos que indicar qual propriedade queremos validar na classe TopTalentData :

 public class TopTalentData { @Length(min = 10, max = 10) @NotNull private String name; }

Agora o Spring interceptará a solicitação e a validará antes que o método seja invocado – não há necessidade de empregar testes manuais adicionais.

Outra maneira de conseguirmos a mesma coisa é criando nossas próprias anotações. Embora você normalmente só empregue anotações personalizadas quando suas necessidades excederem o conjunto de restrições interno do Hibernate, para este exemplo vamos fingir que @Length não existe. Você faria um validador que verifica o comprimento da string criando duas classes adicionais, uma para validar e outra para anotar propriedades:

 @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint(validatedBy = { MyAnnotationValidator.class }) public @interface MyAnnotation { String message() default "String length does not match expected"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; int value(); } @Component public class MyAnnotationValidator implements ConstraintValidator<MyAnnotation, String> { private int expectedLength; @Override public void initialize(MyAnnotation myAnnotation) { this.expectedLength = myAnnotation.value(); } @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { return s == null || s.length() == this.expectedLength; } }

Observe que, nesses casos, as práticas recomendadas de separação de interesses exigem que você marque uma propriedade como válida se for nula ( s == null dentro do método isValid ) e, em seguida, use uma anotação @NotNull se esse for um requisito adicional para o propriedade:

 public class TopTalentData { @MyAnnotation(value = 10) @NotNull private String name; }

Erro comum nº 7: (ainda) usando uma configuração baseada em XML

Enquanto XML era uma necessidade para versões anteriores do Spring, hoje em dia a maior parte da configuração pode ser feita exclusivamente via código Java/anotações; As configurações XML apenas se apresentam como código clichê adicional e desnecessário.

Este artigo (assim como o repositório GitHub que o acompanha) usa anotações para configurar o Spring e o Spring sabe quais beans ele deve conectar porque o pacote raiz foi anotado com uma anotação composta @SpringBootApplication , assim:

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

A anotação composta (você pode aprender mais sobre ela na documentação do Spring simplesmente dá ao Spring uma dica sobre quais pacotes devem ser escaneados para recuperar beans. No nosso caso concreto, isso significa que o seguinte sob o pacote superior (co.kukurin) será usado para fiação:

  • @Component ( TopTalentConverter , MyAnnotationValidator )
  • @RestController ( TopTalentController )
  • @Repository ( TopTalentRepository )
  • @Service ( TopTalentService )

Se tivéssemos quaisquer classes anotadas @Configuration adicionais, elas também seriam verificadas quanto à configuração baseada em Java.

Erro comum nº 8: esquecer os perfis

Um problema frequentemente encontrado no desenvolvimento de servidor é distinguir entre diferentes tipos de configuração, geralmente suas configurações de produção e desenvolvimento. Em vez de substituir manualmente várias entradas de configuração toda vez que você alternar do teste para a implantação do aplicativo, uma maneira mais eficiente seria empregar perfis.

Considere o caso em que você está usando um banco de dados na memória para desenvolvimento local, com um banco de dados MySQL em produção. Isso significaria, em essência, que você usaria um URL diferente e (espero) credenciais diferentes para acessar cada um dos dois. Vamos ver como isso poderia ser feito dois arquivos de configuração diferentes:

arquivo application.yaml

 # set default profile to 'dev' spring.profiles.active: dev # production database details spring.datasource.url: 'jdbc:mysql://localhost:3306/toptal' spring.datasource.username: root spring.datasource.password:

arquivo application-dev.yaml

 spring.datasource.url: 'jdbc:h2:mem:' spring.datasource.platform: h2

Presumivelmente, você não gostaria de executar acidentalmente nenhuma ação em seu banco de dados de produção ao mexer no código, portanto, faz sentido definir o perfil padrão como dev. No servidor, você pode substituir manualmente o perfil de configuração fornecendo um parâmetro -Dspring.profiles.active=prod para a JVM. Alternativamente, você também pode definir a variável de ambiente do seu sistema operacional para o perfil padrão desejado.

Erro comum nº 9: não adotar a injeção de dependência

Usar corretamente a injeção de dependência com o Spring significa permitir que ele conecte todos os seus objetos, varrendo todas as classes de configuração desejadas; isso prova ser útil para desacoplar relacionamentos e também torna o teste muito mais fácil. Em vez de classes de acoplamento apertado, fazendo algo assim:

 public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController() { this.topTalentService = new TopTalentService(); } }

Estamos permitindo que o Spring faça a fiação para nós:

 public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController(TopTalentService topTalentService) { this.topTalentService = topTalentService; } }

A palestra de Misko Hevery no Google explica os 'porquês' da injeção de dependência em profundidade, então vamos ver como ela é usada na prática. Na seção sobre separação de interesses (Erros comuns nº 3), criamos uma classe de serviço e controlador. Digamos que queremos testar o controlador assumindo que o TopTalentService se comporta corretamente. Podemos inserir um objeto simulado no lugar da implementação do serviço real fornecendo uma classe de configuração separada:

 @Configuration public class SampleUnitTestConfig { @Bean public TopTalentService topTalentService() { TopTalentService topTalentService = Mockito.mock(TopTalentService.class); Mockito.when(topTalentService.getTopTalent()).thenReturn( Stream.of("Mary", "Joel").map(TopTalentData::new).collect(Collectors.toList())); return topTalentService; } }

Em seguida, podemos injetar o objeto simulado dizendo ao Spring para usar SampleUnitTestConfig como seu fornecedor de configuração:

 @ContextConfiguration(classes = { SampleUnitTestConfig.class })

Isso nos permite usar a configuração de contexto para injetar o bean personalizado em um teste de unidade.

Erro comum nº 10: falta de teste ou teste impróprio

Mesmo que a ideia de teste de unidade esteja conosco há muito tempo, muitos desenvolvedores parecem “esquecer” de fazer isso (especialmente se não for obrigatório ), ou simplesmente adicioná-lo como uma reflexão tardia. Obviamente, isso não é desejável, pois os testes devem não apenas verificar a correção do seu código, mas também servir como documentação de como a aplicação deve se comportar em diferentes situações.

Ao testar serviços da Web, você raramente está fazendo testes de unidade exclusivamente 'puros', uma vez que a comunicação por HTTP geralmente exige que você invoque o DispatcherServlet do Spring e veja o que acontece quando um HttpServletRequest real é recebido (tornando-o um teste de integração , lidando com validação, serialização , etc). REST Assured, uma DSL Java para fácil teste de serviços REST, além do MockMVC, provou ser uma solução muito elegante. Considere o seguinte trecho de código com injeção de dependência:

 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { Application.class, SampleUnitTestConfig.class }) public class RestAssuredTestDemonstration { @Autowired private TopTalentController topTalentController; @Test public void shouldGetMaryAndJoel() throws Exception { // given MockMvcRequestSpecification givenRestAssuredSpecification = RestAssuredMockMvc.given() .standaloneSetup(topTalentController); // when MockMvcResponse response = givenRestAssuredSpecification.when().get("/toptal/get"); // then response.then().statusCode(200); response.then().body("name", hasItems("Mary", "Joel")); } }

SampleUnitTestConfig uma implementação simulada de TopTalentService ao TopTalentController enquanto todas as outras classes são conectadas usando a configuração padrão inferida da verificação de pacotes enraizados no pacote da classe Application. RestAssuredMockMvc é usado simplesmente para configurar um ambiente leve e enviar uma solicitação GET para o /toptal/get .

Tornando-se um mestre da primavera

O Spring é uma estrutura poderosa que é fácil de usar, mas requer dedicação e tempo para atingir o domínio total. Reservar um tempo para se familiarizar com a estrutura definitivamente melhorará sua produtividade a longo prazo e, finalmente, ajudará você a escrever um código mais limpo e se tornar um desenvolvedor melhor.

Se você estiver procurando por mais recursos, Spring In Action é um bom livro prático que cobre muitos tópicos principais do Spring.