Gerenciamento de Exceções em Java e Spring Boot: Melhores Práticas

Andrey Nithack
5 min readSep 6, 2023

--

Introdução

Gerenciar exceções de forma eficaz é crucial para o desenvolvimento de aplicações Java robustas e escaláveis, especialmente quando se utiliza frameworks avançados como o Spring Boot. Este guia se concentra em desmistificar as melhores práticas para o manejo de exceções, fornecendo uma abordagem estruturada que vai desde os fundamentos do Java até as especificidades do Spring Boot. A importância deste tema não pode ser subestimada, pois um manejo inadequado de exceções pode levar a falhas de sistema, comportamento imprevisível e problemas de segurança. Portanto, este guia visa ser um recurso completo para desenvolvedores, engenheiros e arquitetos que desejam aprimorar suas habilidades e conhecimentos em um dos aspectos mais críticos do desenvolvimento de software Java.

Tipos de Exceções e Quando Usá-las

Exceções Verificadas (Checked Exceptions)

O que são

Estas são exceções que exigem um tratamento ou declaração obrigatórios. Elas herdam da classe Exception e são usadas para situações em que é possível recuperar-se do erro.

Quando Usar

Use exceções verificadas quando você espera que o chamador possa recuperar-se da exceção. Por exemplo, ao tentar ler um arquivo que não existe, o chamador pode optar por criar o arquivo ou selecionar um novo arquivo para leitura.

Exemplo

public void readFile(String filePath) throws FileNotFoundException {
File file = new File(filePath);
if (!file.exists()) {
throw new FileNotFoundException("Arquivo não encontrado");
}
// Código para leitura do arquivo
}

Exceções Não Verificadas (Unchecked Exceptions)

O que são

Estas são exceções que não precisam ser declaradas ou tratadas. Elas herdam da classe RuntimeException e são geralmente usadas para falhas de programação.

Quando Usar

Use-as quando o erro representa uma falha irrecuperável, como um bug. Por exemplo, tentar dividir por zero em um cálculo.

Exemplo

public void divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("Divisão por zero não permitida");
}
int result = a / b;
}

Uso em Camadas de Arquitetura

Camada de Domínio

O que é

Esta é a camada onde as regras e lógicas de negócio são implementadas. A consistência e a integridade dos dados devem ser mantidas aqui.

Quando Levantar Exceções

É prudente lançar exceções quando uma regra de negócio é violada. As exceções devem ser específicas para o domínio para que sejam significativas e úteis.

Exemplo

public void setAge(int age) {
if (age < 0) {
throw new InvalidAgeException("Idade não pode ser negativa");
}
this.age = age;
}

Camada de Repositório

O que é

Responsável pelo armazenamento e recuperação de dados, esta camada atua como uma ponte entre o banco de dados e a camada de domínio.

Quando Levantar Exceções

Se uma operação de CRUD falha, é apropriado lançar uma exceção personalizada para que o chamador saiba o que deu errado e possa tomar ações corretivas.

Exemplo

public User findUserById(String id) {
User user = userRepository.findById(id).orElseThrow(() -> new UserNotFoundException("Usuário não encontrado"));
return user;
}

Camada de Serviços

O que é

Coordena as atividades de negócios e serve como uma ponte entre a camada de domínio e a camada de aplicação.

Quando Levantar Exceções

Quando uma lógica de negócios ou uma regra de validação é violada, deve-se lançar uma exceção para evitar estados inconsistentes.

Exemplo Prático

public void processPayment(PaymentData data) {
if (data.getAmount() <= 0) {
throw new InvalidPaymentException("O valor do pagamento deve ser positivo");
}
// Código para processar o pagamento
}

Boas Práticas de Código Limpo em Exceções

Nomeação Clara e Específica

Nomear exceções de forma clara e específica facilita a compreensão e manutenção do código.

Exemplo

throw new InsufficientFundsException("Saldo insuficiente na conta");

Mensagens de Exceção Descritivas

Inclua mensagens descritivas nas exceções para fornecer contexto sobre o erro. Isso ajudará no diagnóstico e na correção do problema.

Exemplo

throw new InvalidOrderException("A quantidade de itens no pedido excede o limite máximo");

Propagação Cautelosa de Exceções

Evite a propagação de exceções desnecessárias que possam revelar informações sensíveis ou detalhes de implementação. Em vez disso, envolva-as em exceções personalizadas mais genéricas.

Exemplo

try {
// lógica de negócios
} catch (SQLException e) {
throw new DatabaseException("Falha ao acessar os dados", e);
}

Utilização de Códigos de Erro

Para exceções de domínio, considere usar códigos de erro específicos juntamente com a mensagem de exceção. Isso pode ser útil para localização ou para fornecer mais informações ao cliente da API.

Exemplo

public class CustomException extends Exception {
private final ErrorCode errorCode;
public CustomException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
}

Logging Adequado

Registrar adequadamente as exceções permite um diagnóstico mais fácil. No entanto, cuidado com o registro excessivo de informações sensíveis.

Exemplo

catch (Exception e) {
logger.error("Falha ao processar o pedido", e);
}

Separação de Exceções Técnicas e de Negócio

Manter exceções técnicas (falhas de IO, SQL etc.) separadas das exceções de negócio (validação de dados, regras de negócios etc.) torna o sistema mais organizado e fácil de gerenciar.

Exemplo

catch (SQLException e) {
throw new TechnicalException("Erro de SQL", e);
}
catch (InsufficientFundsException e) {
throw new BusinessException("Saldo insuficiente", e);
}

Uso do Finally para Limpeza de Recursos

Use blocos finally para garantir que recursos como streams, conexões e outros sejam liberados independentemente de uma exceção ser lançada ou não.

Exemplo

InputStream is = null;
try {
is = new FileInputStream("file.txt");
// Processamento
} catch (IOException e) {
// Manipulação de exceções
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// Log do erro
}
}
}

Não Capturar Exceções Irrecuperáveis

Exceções como OutOfMemoryError ou SystemExitError são irrecuperáveis e não devem ser capturadas, a menos que seja para registro ou para lançar uma nova exceção que encerre a aplicação.

Exemplo

try {
// Código
} catch (OutOfMemoryError e) {
logger.fatal("Sem memória", e);
System.exit(1);
}

Essas práticas ajudarão a tornar seu manejo de exceções mais robusto, mantendo o código claro e fácil de manter.

Considerações Especiais para Spring Boot

@ControllerAdvice

Esta anotação permite que você crie um manipulador de exceções global para toda a sua aplicação Spring Boot.

Exemplo

@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<?> handleUserNotFound(UserNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
}

@ResponseStatus

Esta anotação é usada para indicar qual será o status HTTP quando uma exceção específica é lançada.

Exemplo

@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}

Conclusão

O manejo de exceções não é apenas uma prática recomendada, mas sim uma necessidade para garantir que sua aplicação seja robusta e confiável. A clareza sobre quando e onde lançar exceções em sua aplicação pode fazer uma diferença significativa na qualidade e na manutenibilidade do código. Este guia oferece uma visão detalhada e aprofundada sobre as melhores práticas e estratégias para o manejo de exceções eficaz em Java e Spring Boot.

--

--

Andrey Nithack

Entusiasta de tecnologia compartilhando insights de programação, melhores práticas e tendências. Acompanhe meu Medium para tudo sobre Java e tecnologia!