Olá pessoal. Hoje compartilhamos a primeira parte do artigo, cuja tradução foi preparada especificamente para os alunos do curso "Developer on the Spring Framework" . Vamos começar!
A primavera é talvez uma das plataformas de desenvolvimento Java mais populares. Esta é uma ferramenta poderosa, mas bastante difícil de aprender. Seus conceitos básicos são bastante fáceis de entender e aprender, mas são necessários tempo e esforço para se tornar um desenvolvedor experiente do Spring.
Neste artigo, examinaremos alguns dos erros mais comuns cometidos ao trabalhar no Spring e relacionados, em particular, ao desenvolvimento de aplicativos da Web e ao uso da plataforma Spring Boot. Conforme observado no
site do Spring Boot , o Spring Boot adota uma abordagem
padronizada para criar aplicativos prontos para uso, e este artigo seguirá essa abordagem. Ele fornecerá várias recomendações que podem ser efetivamente usadas no desenvolvimento de aplicativos Web padrão baseados no Spring Boot.
Caso você não esteja familiarizado com a plataforma Spring Boot, mas queira experimentar os exemplos deste artigo, criei um
repositório GitHub com materiais adicionais para este artigo . Se em algum momento você estiver um pouco confuso ao ler este artigo, aconselho a criar um clone deste repositório e experimentar o código no seu computador.
Erro comum nº 1: programação em nível muito baixo
Nós sucumbimos facilmente a esse erro generalizado, já que a
"síndrome de rejeição do desenvolvimento de outra pessoa" é bastante típica para o ambiente de programação. Um de seus sintomas é a constante reescrita de partes do código comumente usado, e esse é um sintoma observado por muitos programadores.
Estudar a estrutura interna e os recursos de implementação de uma biblioteca específica geralmente é um processo útil, necessário e interessante, mas se você escrever constantemente o mesmo tipo de código ao executar a implementação de funções de baixo nível, isso pode ser prejudicial para você como desenvolvedor de software. É por isso que abstrações e plataformas como o Spring existem e são usadas - elas evitam o trabalho manual repetitivo e permitem que você se concentre nos objetos da sua área de assunto e na lógica do código do programa em um nível superior.
Portanto, abstrações não devem ser evitadas. Da próxima vez que você precisar resolver um problema específico, faça uma pesquisa rápida e descubra se a biblioteca que resolve esse problema já está incorporada no Spring - você provavelmente encontrará uma solução pronta para uso. Uma dessas bibliotecas úteis é o
Projeto Lombok , cujas anotações serão usadas nos exemplos deste artigo. O Lombok é usado como um gerador de código de modelo; portanto, o desenvolvedor preguiçoso que mora em cada um de nós ficará muito feliz em se familiarizar com esta biblioteca. Veja, por exemplo, como o
componente JavaBean padrão se parece no Lombok:
@Getter @Setter @NoArgsConstructor public class Bean implements Serializable { int firstBeanProperty; String secondBeanProperty; }
Como você já deve ter entendido, o código acima é convertido no seguinte formato:
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 que você provavelmente precisará instalar o plug-in apropriado se desejar usar o Lombok em seu ambiente de desenvolvimento integrado. A versão deste plug-in para o IntelliJ IDEA pode ser encontrada
aqui .
Erro comum número 2. “Vazamento” de estruturas internas
Dar acesso a estruturas internas nunca foi uma boa ideia, pois prejudica a flexibilidade do modelo de serviço e, como resultado, contribui para a formação de um estilo de programação ruim. O "vazamento" de estruturas internas se manifesta no fato de que a estrutura do banco de dados se torna acessível a partir de determinados pontos de extremidade da API. Por exemplo, suponha que o seguinte “bom e velho objeto Java” (POJO) 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 um objeto
TopTalentEntity
. Retornar instâncias do
TopTalentEntity
parece uma ideia tentadora, mas uma solução mais flexível seria criar uma nova classe que represente os dados do TopTalentEntity para o terminal da API:
@AllArgsConstructor @NoArgsConstructor @Getter public class TopTalentData { private String name; }
Portanto, fazer alterações nos componentes internos do banco de dados não exigirá alterações adicionais no nível do serviço. Vamos ver o que acontece se o campo de senha for adicionado à classe
TopTalentEntity
para armazenar hashes de senha de usuário no banco de dados: se não houver conector, como
TopTalentData
, e o desenvolvedor esquecer de alterar a parte da interface do serviço, isso poderá levar à divulgação indesejada de informações secretas!
Erro comum número 3. Combinar funções que seriam melhores para distribuir no código
Organizar o código do aplicativo à medida que cresce se torna uma tarefa cada vez mais importante. Curiosamente, a maioria dos princípios de programação eficaz para de funcionar quando uma certa escala de desenvolvimento é atingida, especialmente se a arquitetura do aplicativo não foi bem pensada. E um dos erros cometidos com mais frequência é a combinação de funções no código que são mais razoáveis para implementar separadamente.
O motivo da violação do princípio da
separação de responsabilidades é geralmente a adição de novas funcionalidades às classes existentes. Talvez essa seja uma boa solução momentânea (em particular, requer escrever uma quantidade menor de código), mas no futuro inevitavelmente se tornará um problema, inclusive nos estágios de teste e manutenção do código e entre eles. Considere o seguinte controlador que retorna
TopTalentData
do 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()); } }
À primeira vista, parece que não há erros óbvios nesse fragmento de código. Ele fornece uma lista de objetos
TopTalentData
, extraídos de instâncias da classe
TopTalentEntity
. Mas se você observar o código mais de perto, veremos que, na realidade, o
TopTalentController
executa várias ações aqui, a saber, correlaciona solicitações com um endpoint específico, recupera dados do repositório e converte objetos recebidos do
TopTalentRepository
em um formato diferente. Uma solução mais limpa deve separar essas funções em classes separadas. Por exemplo, pode ser 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 entender onde uma função está localizada simplesmente observando o nome da classe. Posteriormente, durante o teste, podemos substituir facilmente o código de qualquer uma dessas classes por código substituto, se necessário.
Número de erro comum 4. Código uniforme e tratamento inadequado de erros
O tópico uniformidade de código não é exclusivo do Spring (ou Java em geral), mas, no entanto, é um aspecto importante que deve ser considerado ao trabalhar com projetos no Spring. A escolha de um estilo de programação específico pode ser um tópico de discussão (e geralmente é consistente na equipe de desenvolvimento ou em toda a empresa), mas, em qualquer caso, a presença de um padrão comum para escrever código contribui para um aumento na eficiência do trabalho. Isto é especialmente verdade se várias pessoas trabalham no código. O código uniforme pode ser transmitido de desenvolvedor para desenvolvedor sem gastar muito esforço em mantê-lo ou em longas explicações sobre por que essas ou aquelas classes são necessárias.
Imagine que existe um projeto Spring no qual existem vários arquivos de configuração, serviços e controladores. Seguindo a uniformidade semântica em nomeá-los, criamos uma estrutura pela qual você pode pesquisar facilmente e na qual qualquer desenvolvedor pode entender facilmente o código. Por exemplo, você pode adicionar o sufixo Config aos nomes das classes de configuração, o sufixo Service aos nomes do serviço e o sufixo Controller aos nomes do controlador.
O tratamento de erros no servidor está intimamente relacionado à uniformidade do código e merece atenção especial. Se você já lidou com exceções provenientes de uma API mal escrita, provavelmente sabe como é difícil entender o significado dessas exceções corretamente e ainda mais difícil determinar por que elas realmente ocorreram.
Como desenvolvedor de API, idealmente, você gostaria de cobrir todos os pontos de extremidade com os quais o usuário está trabalhando e levá-los a usar um único formato de mensagem de erro. Geralmente, isso significa que você precisa usar códigos de erro padrão e sua descrição e abandonar decisões duvidosas, como fornecer ao usuário uma mensagem "500 Internal Server Error" ou os resultados de um rastreamento de pilha (a última opção, a propósito, deve ser evitada por todos os meios, pois você está revelando dados internos, além disso, é difícil processar esses resultados no lado do cliente).
Aqui, por exemplo, como pode ser o formato geral da mensagem de erro:
@Value public class ErrorResponse { private Integer errorCode; private String errorMessage; }
Um formato semelhante a este geralmente é encontrado nas APIs mais populares e, por regra, funciona muito bem porque pode ser documentado de maneira fácil e sistemática. Você pode converter uma exceção nesse formato adicionando a anotação
@ExceptionHandler
ao método (para um exemplo da anotação, consulte a seção "Erro comum nº 6").
Erro comum número 5. Trabalho incorreto com multithreading
A implementação de multithreading pode ser uma tarefa difícil, independentemente de ser usada em aplicativos de desktop ou web, em projetos Spring ou em outras plataformas. Os problemas causados pela execução paralela de programas são difíceis de rastrear, e lidar com eles com um depurador geralmente é muito difícil. Se você entender que está lidando com um erro relacionado à execução paralela, provavelmente terá que abandonar o depurador e examinar o código manualmente até encontrar a causa raiz do erro. Infelizmente, não existe uma maneira padrão de resolver esses problemas. Em cada caso, é necessário avaliar a situação e "atacar" o problema com o método que parece melhor nas condições especificadas.
Idealmente, é claro, você gostaria de evitar completamente os erros associados ao multithreading. E, embora não exista uma receita universal aqui, ainda posso oferecer algumas recomendações práticas.
Evite usar estados globais
Primeiro, lembre-se sempre do problema dos "estados globais". Se você estiver desenvolvendo um aplicativo multithread, precisará monitorar cuidadosamente absolutamente todas as variáveis globalmente modificáveis e, se possível, livrar-se delas completamente. Se você ainda tiver um motivo pelo qual a variável global deve ser modificável,
sincronize e monitore adequadamente o desempenho do seu aplicativo - verifique se ele não diminui a velocidade devido a períodos de espera adicionais.
Evite objetos mutáveis
Essa idéia vem diretamente dos princípios da
programação funcional e, sendo adaptada aos princípios da OOP, afirma que classes mutáveis e estados mutáveis devem ser evitados. Em resumo, isso significa que você deve abster-se de definir métodos (setters) e ter campos particulares com o modificador final em todas as classes de modelo. O único momento em que seus valores mudam é quando o objeto é criado. Nesse caso, você pode ter certeza de que não há problemas associados à competição por recursos e, ao acessar as propriedades do objeto, sempre serão obtidos os valores corretos.
Registrar dados importantes
Avalie onde os problemas podem ocorrer no aplicativo e configure um log de todos os dados importantes com antecedência. Se ocorrer um erro, você ficará feliz em ter informações sobre quais solicitações foram recebidas e poderá entender melhor os motivos do mau funcionamento do seu aplicativo. No entanto, lembre-se de que o log envolve E / S de arquivo adicional e não deve ser abusado, pois isso pode afetar adversamente o desempenho do aplicativo.
Use implementações de threads prontas
Quando você precisar gerar seus encadeamentos (por exemplo, para criar solicitações assíncronas para vários serviços), use implementações de encadeamentos seguros prontas em vez de criar seus próprios. Na maioria dos casos, você pode usar as abstrações
ExecutorService e as espetaculares abstrações funcionais
CompletableFuture para Java 8. A plataforma Spring também permite lidar com solicitações assíncronas usando a classe
DeferredResult .
O fim da primeira parte.
Leia a segunda parte.