Annotating the exception class with ResponseStatus
which you use from the service layer, is not good practice.
The best solution is to create your own exception classes by extending Exception
or RuntimeException
. Then create a global exception handler using the ControllerAdvice
annotation, where you can easily set the HTTP response status code per exception.
If you want to rollback the JPA transaction, extend the RuntimeException
because the Spring @Repository
does DB rollback in this case by default.
Example custom exception classes:
public class AlreadyRegisteredException extends RuntimeException {
public AlreadyRegisteredException(final String message) {
super(message);
}
}
public class EntityNotFoundException extends RuntimeException {
public EntityNotFoundException(final String message) {
super(message);
}
}
The global exception handler class:
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(AlreadyRegisteredException.class)
public ResponseEntity<String> handleAlreadyRegisteredException(AlreadyRegisteredException ex) {
log.error(ex);
return new ResponseEntity<>(ex.getMessage(), HttpStatus.ALREADY_REPORTED);
}
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<String> handleEntityNotFoundException(EntityNotFoundException ex) {
var reason = ex.getMessage();
log.warn(reason);
return new ResponseEntity<>(reason, HttpStatus.NOT_FOUND);
}
}
USAGE:
Rest endpoint
@Slf4j
@RestController
@RequestMapping("/customer")
@RequiredArgsConstructor
public class CustomerController {
private final CustomerService customerService;
@PostMapping("/register")
public void registerByEmail(@RequestBody Customer customer) {
customerService.register(customer);
}
}
Service layer
@Component
@Slf4j
@RequiredArgsConstructor
public class CustomerService {
private final CustomerRepository customerRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void register(Customer customer) {
Optional<String> email = getEmail(customer);
if (email.isEmpty()) {
throw new EmptyIdException("Unable to register a new customer. Customer's email is null.");
}
Optional<CustomerEntity> existingCustomer = customerRepository.findByEmail(Id.get());
if (existingCustomer.isPresent()){
throw new AlreadyRegisteredException(String.format("Unable to register a new customer. Customer with the "
+ "same email has already been registered: {email: \"%s\"}", email.get()));
} else {
customerRepository.save(...);
}
}
}
I hope that it helps.
@ResponseStatus
smells like poor design to me. In general, your Exception classes should not have platform-specific dependencies. In my organization we use 422 for business logic errors (e.g., schedule time off in a past date). We throw a custom unchecked exception for those. If you're in a Kafka consumer and detect one such error you may throw that exception. But if it's annotated with@ResponseStatus
your Kafka consumer program requires a dependency to spring web. :^( – Hackathorn