esta é a primeira vez que faço uma pergunta aqui, então, por favor, tenham paciência se eu esquecer de alguma coisa.
Criei um projeto bancário simples no Spring Boot com as seguintes APIs REST:
/criar
/obterConta
/depósito
/retirar
/deleteConta
Tudo funciona perfeitamente:
- As APIs funcionam corretamente e atualizam o banco de dados conforme o esperado
- A interface do usuário do Swagger mostra todos os endpoints conforme pretendido
- Os testes do Postman também funcionam sem problemas
Entretanto, quando adicionei tratamento de exceção personalizado usando @ControllerAdvice, o Swagger começou a gerar este erro:
Falha ao carregar a definição da API. O status da resposta é 500 /v3/api-docs
Aqui está a parte estranha: quando eu removo as classes de exceção, o Swagger começa a funcionar novamente! Então, claramente, há algo na configuração de tratamento de exceção que está causando esse problema.
Alguém pode me ajudar a descobrir o que está errado?
Classe ErrorResponse:
package com.bank.bankingApplication.exceptions;
import java.time.LocalDateTime;
public class ErrorResponse {
private String message;
private String timestamp;
private String path;
public ErrorResponse(String message, String path) {
this.message = message;
this.timestamp = LocalDateTime.now().toString();
this.path = path;
}
// Getters and Setters
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
}
Classe GlobalExceptionHandler:
package com.bank.bankingApplication.exceptions;
import com.bank.bankingApplication.controller.BankController;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
@ControllerAdvice
@RestControllerAdvice(assignableTypes = {BankController.class})
public class GlobalExceptionHandler {
// @ExceptionHandler(Exception.class)
// public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex, HttpServletRequest request) {
// ErrorResponse error = new ErrorResponse(ex.getMessage(), request.getRequestURI());
// return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
// }
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ErrorResponse> handleTypeMismatch(MethodArgumentTypeMismatchException ex, HttpServletRequest request) {
String msg = "Invalid type for parameter: " + ex.getName();
ErrorResponse error = new ErrorResponse(msg, request.getRequestURI());
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgs(IllegalArgumentException ex, HttpServletRequest request) {
ErrorResponse error = new ErrorResponse(ex.getMessage(), request.getRequestURI());
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
// You can add more specific ones like @ExceptionHandler(AccountNotFoundException.class) here.
}
Controlador:
package com.bank.bankingApplication.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import com.bank.bankingApplication.model.Account;
import com.bank.bankingApplication.repository.AccountRepository;
import com.bank.bankingApplication.service.AccountService;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
@RestController
@RequestMapping("/bank")
@Tag(name = "Bank Controller", description = "Handles operations related to bank accounts")
public class BankController {
@Autowired
private AccountService accountService;
@Autowired
private AccountRepository accountRepository;
@Operation(
summary = "Create a new bank account",
description = "This endpoint is used to create a new bank account for a user."
)
@PostMapping("/create")
public Account createAccount(@RequestBody Account account){
return accountService.createAccount(account);
}
@Operation(
summary = "Fetches the existing bank account",
description = "This endpoint is used to fetch an existing bank account for a user."
)
@PostMapping("/getAccount")
public ResponseEntity<?> getAccountDetails(@RequestBody Account account) {
Long id = account.getId();
Optional<Account> accountOpt = accountRepository.findById(id);
if (accountOpt.isPresent()) {
return ResponseEntity.ok(accountOpt.get());
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Account not found");
}
}
@Operation(
summary = "Deposits the amount",
description = "This endpoint is used to deposit amount for a user."
)
@PostMapping("/deposit")
public ResponseEntity<DepositAndWithdrawResponse> deposit(@RequestBody DepositAndWithdrawRequest request) {
Optional<Account> optionalAccount = accountRepository.findById(request.getId());
if (optionalAccount.isPresent()) {
Account account = optionalAccount.get();
account.setBalance(account.getBalance() + request.getAmount());
accountRepository.save(account);
DepositAndWithdrawResponse response = new DepositAndWithdrawResponse();
response.setId(account.getId());
response.setAccountHolder(account.getAccountHolder());
response.setBalance(account.getBalance());
response.setRemarks("Deposit successful. New balance: " + account.getBalance());
return ResponseEntity.ok(response);
} else {
return ResponseEntity.notFound().build();
}
}
@Operation(
summary = "withdraws the amount",
description = "This endpoint is used to withdraw amount for a user."
)
@PostMapping("/withdraw")
public ResponseEntity<DepositAndWithdrawResponse> withdraw(@RequestBody DepositAndWithdrawRequest request) {
Optional<Account> optionalAccount = accountRepository.findById(request.getId());
if (optionalAccount.isPresent()) {
Account account = optionalAccount.get();
if(account.getBalance() >= request.getAmount()){
account.setBalance(account.getBalance() - request.getAmount());
accountRepository.save(account);
DepositAndWithdrawResponse response = new DepositAndWithdrawResponse();
response.setId(account.getId());
response.setAccountHolder(account.getAccountHolder());
response.setBalance(account.getBalance());
response.setRemarks("Withdraw successful. New balance: " + account.getBalance());
return ResponseEntity.ok(response);
}
else{
account.setBalance(account.getBalance());
accountRepository.save(account);
DepositAndWithdrawResponse response = new DepositAndWithdrawResponse();
response.setId(account.getId());
response.setAccountHolder(account.getAccountHolder());
response.setBalance(account.getBalance());
response.setRemarks("Withdraw unsuccessful because entered amount was greater than the current balance: " + account.getBalance());
return ResponseEntity.ok(response);
}
} else {
return ResponseEntity.notFound().build();
}
}
@DeleteMapping("/deleteAccount")
@Operation(
summary = "Deletes an existing bank account",
description = "This endpoint is used to delete an existing bank account for a user."
)
public ResponseEntity<String> deleteAccountById(@RequestBody Map<String, Long> request) {
Long id = request.get("id");
Optional<Account> account = accountRepository.findById(id);
if (account.isPresent()) {
accountRepository.deleteById(id);
return ResponseEntity.ok("Account with ID " + id + " has been deleted successfully.");
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Account with ID " + id + " not found.");
}
}
}
propriedades do aplicativo:
spring.datasource.url=jdbc:mysql://localhost:3306/bank
spring.datasource.username=root
spring.datasource.password=MyPassword
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.bank</groupId>
<artifactId>bankingApplication</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>bankingApplication</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Adicionei @ControllerAdvice com classes de exceção personalizadas para retornar respostas de erro estruturadas. Eu esperava que o Swagger continuasse funcionando como antes, mas, em vez disso, recebi um erro 500 em /v3/api-docs. Remover o manipulador de exceção corrige o Swagger, então acredito que algo está errado dentro da camada de tratamento de exceção. Para solucionar o problema, comentei o @ExceptionHandler(Exception.class) generalizado e mantive apenas os específicos, como MethodArgumentTypeMismatchException e IllegalArgumentException, supondo que o Swagger possa depender de algum tratamento de exceção integrado. Mas o resultado ainda foi o mesmo — o Swagger falha ao carregar com um erro 500, enquanto o Postman funciona bem.
Você escreve que remover o manipulador de exceções ajuda e o Swagger começa a funcionar novamente. Talvez, seu
@ControllerAdvice
capture exceções internas, possivelmente durante/v3/api-docs
a geração.Tente remover
assignableTypes
e limitar seu@RestControllerAdvice
combasePackages
. Algo como: