Os 10 erros mais comuns do Spring Framework

Olá Habr! Apresento a você a tradução do artigo “Os 10 erros mais comuns do framework Spring”, de Toni Kukurin.

A primavera é provavelmente um dos frameworks Java mais populares, além de ser uma fera poderosa para domesticar. Embora seus conceitos básicos sejam bastante fáceis de entender, é preciso tempo e esforço para se tornar um desenvolvedor forte do Spring.

Neste artigo, examinaremos alguns dos erros mais comuns no Spring, especialmente aqueles relacionados a aplicativos Web e Spring Boot. Conforme declarado no site do Spring Boot , ele impõe uma idéia de como os aplicativos industriais devem ser construídos; portanto, neste artigo, tentaremos demonstrar essa idéia e fornecer uma visão geral de algumas dicas que se encaixam bem no processo padrão de desenvolvimento de aplicativos da Web do Spring Boot.
Se você não conhece muito o Spring Boot, mas ainda deseja experimentar algumas das coisas mencionadas, criei o repositório GitHub que acompanha este artigo . Se você sentir que está perdido em algum lugar do artigo, recomendo clonar o repositório no computador local e brincar com o código.

Erro comum nº 1: desça muito baixo


Encontramos esse erro comum porque a síndrome "não inventada aqui" é bastante comum no mundo do desenvolvimento de software.Os sintomas incluem a reescrita regular de fragmentos de código usado com freqüência, e muitos desenvolvedores parecem sofrer com isso.

Embora seja bom e necessário compreender o interior de uma determinada biblioteca e sua implementação em grande parte (e possa ser um excelente processo de aprendizado), resolver constantemente os mesmos detalhes de implementação de baixo nível é prejudicial ao seu desenvolvimento como engenheiro de software. Há uma razão para abstrações e estruturas como o Spring que o separam estritamente do trabalho manual repetitivo e permitem que você se concentre em detalhes de nível superior - seus objetos de domínio e lógica de negócios.

Portanto, use abstrações - na próxima vez em que encontrar um problema específico, faça uma pesquisa rápida e determine se a biblioteca que resolve esse problema está integrada ao Spring. Atualmente, é mais provável que você encontre uma solução adequada existente. Como exemplo de uma biblioteca útil, nos exemplos do restante deste artigo, usarei as anotações do projeto Lombok . O Lombok é usado como um gerador de código de modelo e o desenvolvedor preguiçoso dentro de você, espero que não deva ter problemas com a idéia desta biblioteca. Como exemplo, veja 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 é compilado em:

 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() { } } 

No entanto, observe que você provavelmente precisará instalar o plug-in se pretender usar o Lombok com seu IDE. A versão do plugin para o IntelliJ IDEA pode ser encontrada aqui .

Erro comum nº 2: vazando conteúdo interno


Revelar sua estrutura interna é sempre uma péssima idéia, pois cria inflexibilidade no design do serviço e, portanto, contribui para práticas inadequadas de codificação. Um "vazamento" de conteúdo interno se manifesta no fato de que a estrutura do banco de dados é acessível a partir de determinados pontos de extremidade da API. Como exemplo, suponha que o seguinte POJO ("Objeto Java Antigo Simples") represente uma tabela no 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; } } 

Suponha que exista um terminal que precise acessar os dados de TopTalentEntity. Não importa o quão tentador seja retornar instâncias TopTalentEntity, uma solução mais flexível seria criar uma nova classe para exibir os dados TopTalentEntity no terminal da API:

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

Portanto, fazer alterações no back-end do banco de dados não exigirá alterações adicionais na camada de serviço. Pense no que acontece se você adicionar o campo de senha ao TopTalentEntity para armazenar hashes de senha de usuário no banco de dados - sem um conector como o TopTalentData, se você esquecer de alterar o serviço, o front-end exibirá acidentalmente algumas informações secretas indesejáveis!

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


À medida que o aplicativo cresce, a organização do código se torna um problema cada vez mais importante. Ironicamente, a maioria dos bons princípios de desenvolvimento de software está começando a ser violada em todos os lugares - especialmente nos casos em que pouca atenção é dada ao design da arquitetura do aplicativo. Um dos erros mais comuns enfrentados pelos desenvolvedores é a mistura de responsabilidades de código, e é muito fácil de fazer!

O que geralmente viola o princípio da separação de tarefas é simplesmente "adicionar" novas funcionalidades às classes existentes. Essa, é claro, é uma excelente solução de curto prazo (para iniciantes, requer menos digitação), mas inevitavelmente se tornará um problema no futuro, 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, não é perceptível que algo esteja errado com esse trecho de código. Ele fornece uma lista TopTalentData que é recuperada das instâncias TopTalentEntity. No entanto, se você observar de perto, veremos que, de fato, o TopTalentController faz algumas coisas aqui. A saber: ele mapeia solicitações para um terminal específico, extrai dados do repositório e converte entidades obtidas do TopTalentRepository em outro formato. Uma solução "mais limpa" seria dividir essas responsabilidades em suas próprias classes. Pode ser algo como isto:

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

Um benefício adicional dessa hierarquia é que ela nos permite determinar onde a funcionalidade está localizada, basta verificar 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 consistência não é necessariamente exclusivo do Spring (ou Java, por sinal), mas ainda é um aspecto importante a ser considerado ao trabalhar em projetos do Spring. Embora o estilo de escrever código possa ser objeto de discussão (e geralmente é uma questão de acordo entre a equipe ou a empresa), a presença de um padrão comum é de grande ajuda no desempenho. Isto é especialmente verdade para equipes de várias pessoas. A consistência permite que o código seja transmitido sem o gasto de recursos para manutenção ou o fornecimento de explicações detalhadas sobre as responsabilidades de várias classes.

Considere um projeto Spring com vários arquivos de configuração, serviços e controladores. Sendo semanticamente consistente em nomeá-los, é criada uma estrutura facilmente pesquisável na qual qualquer novo desenvolvedor pode controlar como trabalhar com o código: por exemplo, o sufixo Config é adicionado às classes de configuração, Service sufixo aos serviços e Controller sufixo aos controladores.

Intimamente relacionado ao tópico consistência, o tratamento de erros do lado do servidor merece atenção especial. Se você já teve que lidar com respostas de exceção de uma API mal escrita, provavelmente sabe por que pode ser doloroso analisar exceções e é ainda mais difícil determinar o motivo pelo qual essas exceções ocorreram originalmente.

Como desenvolvedor de API, você idealmente deseja cobrir todos os pontos de extremidade do usuário e convertê-los em um formato de erro comum. Isso geralmente significa que você tem um código de erro e uma descrição comuns, e não apenas uma desculpa na forma de: a) retornar a mensagem “500 Internal Server Error” ou b) apenas redefinir o rastreamento da pilha para o usuário (o que deve ser evitado a todo custo, pois mostra suas informações internas) além da complexidade do processamento no 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 é geralmente encontrado nas APIs mais populares e geralmente funciona bem, pois pode ser fácil e sistematicamente documentado. Você pode converter exceções nesse formato fornecendo ao método a anotação @ExceptionHandler (um exemplo da anotação é fornecido no Erro comum nº 6).

Erro comum nº 5: multithreading incorreto


Independentemente de ser encontrado em aplicativos de desktop ou da Web, no Spring ou não no Spring, o multithreading pode ser uma tarefa assustadora. Os problemas causados ​​pela execução de programas paralelos são esquivos e muitas vezes extremamente difíceis de depurar - de fato, devido à natureza do problema, depois que você entende que está lidando com o problema de execução paralela, provavelmente deve abandonar completamente o depurador e iniciar verifique seu código manualmente até encontrar a causa do erro. Infelizmente, para resolver esses problemas, não há solução de modelo. Dependendo do caso específico, você terá que avaliar a situação e atacar o problema de um ângulo que você considera o melhor.

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

Evitar status global


Primeiro, lembre-se sempre do problema do "estado global". Se você estiver criando um aplicativo multithread, absolutamente tudo o que pode ser alterado globalmente deve ser cuidadosamente monitorado e, se possível, completamente removido. Se houver um motivo para a variável global permanecer mutável, use com cuidado a sincronização e monitore o desempenho do seu aplicativo para confirmar que não está diminuindo a velocidade devido a novos períodos de espera.

Evitar Mutabilidade


Isso segue diretamente da programação funcional e, de acordo com a OOP, afirma que a volatilidade de classe e a mudança de estado devem ser evitadas. Em resumo, o que precede significa a presença de levantadores e campos finais privados em todas as classes do modelo. Seus valores mudam apenas durante a construção. Assim, você pode ter certeza de que não haverá problemas na corrida por recursos e que o acesso às propriedades do objeto sempre fornecerá os valores corretos.

Registrar dados críticos


Avalie onde seu aplicativo pode causar problemas e pré-registre todos os dados importantes. Se ocorrer um erro, você será grato por informações sobre quais solicitações foram recebidas e poderá entender melhor por que seu aplicativo está se comportando mal. Novamente, observe que o registro aumenta a E / S do arquivo, portanto você não deve abusar dele, pois isso pode afetar seriamente o desempenho do seu aplicativo.

Reutilizar implementações existentes


Sempre que você precisar criar seus próprios encadeamentos (por exemplo, para fazer solicitações assíncronas para vários serviços), reutilize implementações seguras existentes, em vez de criar suas próprias soluções. Na maioria das vezes, isso significaria usar ExecutorServices e CompletableFutures no elegante estilo funcional do Java 8 para criar encadeamentos. O Spring também permite o processamento de solicitações assíncronas através da classe DeferredResult .

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


Vamos imaginar que nosso serviço TopTalent, mencionado acima, precise de um endpoint para adicionar novos super talentos. Além disso, suponha que, por algum motivo realmente bom, cada novo nome tenha 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 exposto (além de ser mal projetado) não é realmente uma solução "limpa". Verificamos mais de um tipo de validade (ou seja, que TopTalentData não é nulo e que TopTalentData.name não é nulo e que TopTalentData.name possui 10 caracteres) e também gera uma exceção se os dados forem inválidos.

Isso pode ser feito de maneira muito mais limpa usando o validador Hibernate com Spring. Primeiro, reescrevemos 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, devemos indicar qual propriedade queremos verificar na classe TopTalentData:

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

O Spring agora interceptará a solicitação e a verificará antes de chamar o método - não há necessidade de usar testes manuais adicionais.

Outra maneira de conseguirmos o mesmo é criar nossas próprias anotações. Embora as anotações personalizadas geralmente sejam usadas apenas quando suas necessidades excederem o conjunto de constantes internas do Hibernate , neste exemplo, vamos imaginar que as anotações Comprimento não existam. Você deve criar um validador que verifique o comprimento de uma sequência criando duas classes adicionais, uma para verificação e outra para propriedades de anotação:

 @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 para separação de tarefas exigem que você marque uma propriedade como válida se for nula (s == null no método isValid) e, em seguida, use a anotação NotNull se esse for um requisito adicional para a propriedade:

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

Erro comum nº 7: usando a configuração XML (estática)


Embora o XML fosse necessário para versões anteriores do Spring, atualmente a maior parte da configuração pode ser feita exclusivamente com código / anotações Java. As configurações XML simplesmente representam um clichê adicional e desnecessário.
Este artigo (e o repositório do GitHub que o acompanha) usa anotações para configurar o Spring e o Spring sabe quais beans ele deve se conectar porque o pacote raiz foi anotado usando a anotação composta @SpringBootApplication, por exemplo:

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

Essa anotação composta (você pode aprender mais sobre isso na documentação do Spring ) apenas fornece ao Spring uma dica sobre quais pacotes devem ser verificados para extrair os beans. No nosso caso específico, isso significa que as seguintes classes serão usadas para conectar os beans, começando com o pacote de nível superior (co.kukurin):

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

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

Erro comum número 8: esquecer perfis


O problema geralmente encontrado ao desenvolver servidores é a diferença entre diferentes tipos de configurações, geralmente configurações industriais e de desenvolvimento. Em vez de alterar manualmente os vários parâmetros de configuração cada vez que você alterna do teste para a implantação de aplicativos, uma maneira mais eficiente seria usar perfis.

Considere o caso ao usar o banco de dados na memória para desenvolvimento local e o banco de dados MySQL no PROM. Em essência, isso significa que você usará URLs diferentes e (espero) credenciais diferentes para acessar cada um deles. Vamos ver como isso pode ser feito com dois arquivos de configuração diferentes:

FILE 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: 

FILE APPLICATION-DEV.YAML


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

Aparentemente, você não deseja executar acidentalmente nenhuma ação no seu banco de dados industrial enquanto mexe no código; portanto, faz sentido definir o perfil padrão em dev. Em seguida, no servidor, você pode substituir manualmente o perfil de configuração especificando o parâmetro -Dspring.profiles.active = prod para a JVM. Além disso, você também pode definir a variável de ambiente do SO para o perfil padrão desejado.

Erro comum nº 9: incapacidade de aceitar injeção de dependência


O uso adequado da injeção de dependência no Spring significa que ele permite vincular todos os seus objetos, varrendo todas as classes de configuração necessárias; isso é útil para dissociar relacionamentos e também facilita muito os testes. Em vez de vincular classes, fazendo algo como isto:

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


Deixamos a Spring fazer a ligação para nós:

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

Misko Hevery, do Google talk, explica detalhadamente os "motivos" da injeção de dependência, então vamos ver como isso é usado na prática. Na divisão de responsabilidades (Common Mistakes # 3), criamos classes de serviço e controlador. Suponha que desejamos testar um controlador sob a suposição de que TopTalentService está se comportando corretamente. Podemos inserir um objeto simulado em vez da implementação real do serviço, 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 incorporar o objeto simulado dizendo ao Spring para usar o SampleUnitTestConfig como o provedor de configuração:

 @ContextConfiguration(classes = { SampleUnitTestConfig.class }) 

Isso nos permitirá usar a configuração de contexto para incorporar o bean personalizado no teste de unidade.

Erro comum nº 10: falta de teste ou teste incorreto


Apesar do fato de que a idéia de teste de unidade está conosco há muito tempo, muitos desenvolvedores parecem "esquecer" de fazer isso (especialmente se isso não for necessário), ou simplesmente deixá-lo para mais tarde. Obviamente, isso é indesejável, pois os testes não devem apenas verificar a correção do seu código, mas também servir como documentação sobre como o aplicativo deve se comportar em diferentes situações.

Ao testar serviços da Web, você raramente realiza testes de unidade excepcionalmente "limpos", já que a interação por HTTP geralmente exige chamar DispatcherServlet Spring e verificar o que acontece quando o HttpServletRequest real é recebido (o que o torna um teste de integração, com usando validação, serialização etc.). REST Assured - Java DSL para testar facilmente os serviços REST sobre o MockMVC provou ser uma solução muito elegante. Considere o seguinte fragmento 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")); } } 

O SampleUnitTestConfig habilita a implementação simulada TopTalentService no TopTalentController, enquanto todas as outras classes são conectadas usando a configuração padrão obtida pela verificação de pacotes com raízes no pacote da classe Application. RestAssuredMockMvc é simplesmente usado para criar um ambiente leve e enviar uma solicitação GET para o terminal / toptal / get.

Torne-se um mestre da primavera


A primavera é uma estrutura poderosa e fácil de começar, mas que exige dedicação e tempo para alcançar o domínio total. Se você gastar tempo conhecendo a estrutura, certamente aumentará 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 recursos adicionais, o Spring In Action é um livro de boas práticas que abrange muitos dos principais tópicos do Spring.

TAGS
Java SpringFramework

Comentários


Timothy Schimandle
Em # 2, acho que é preferível retornar um objeto de domínio na maioria dos casos. Seu exemplo de objeto personalizado é uma das várias classes que possuem campos que queremos ocultar. Mas a grande maioria dos objetos com os quais trabalhei não possui essa restrição, e adicionar a classe dto é apenas um código desnecessário.
Em suma, um bom artigo. Bom trabalho

Apaixonado por Timothy Schimandle,
eu concordo totalmente. Parece que uma camada extra desnecessária de código foi adicionada, acho que o @JsonIgnore ajudará a ignorar os campos (embora com falhas nas estratégias de detecção de repositório padrão), mas no geral, este é um ótimo post no blog. Orgulhoso de tropeçar ...

Arokiadoss Asirvatham
Cara, outro erro comum dos iniciantes é: 1) Dependência cíclica e 2) não conformidade com doutrinas básicas de declaração da classe Singleton, como o uso de uma variável de instância em beans com escopo singleton.

Hlodowig
Em relação ao número 8, acredito que as abordagens aos perfis são muito insatisfatórias. Vamos ver:

  • Segurança: algumas pessoas dizem: se o seu repositório fosse público, haveria chaves / senhas secretas? Muito provavelmente, será assim, seguindo esta abordagem. A menos, é claro, que você adicione arquivos de configuração ao .gitignore, mas essa não é uma opção séria.
  • Duplicação: toda vez que tenho configurações diferentes, preciso criar um novo arquivo de propriedades, o que é bastante irritante.
  • Portabilidade: Eu sei que esse é apenas um argumento da JVM, mas zero é melhor que um. Infinitamente menos propenso a erros.

Tentei encontrar uma maneira de usar variáveis ​​de ambiente nos meus arquivos de configuração, em vez de "codificar" os valores, mas até agora não consegui, acho que preciso fazer mais pesquisas.

Ótimo artigo Tony, mantenha o bom trabalho!

Tradução concluída: tele.gg/middle_java

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


All Articles