ControllerExceptionAdvice.java:

@ControllerAdvice
public class ControllerExceptionAdvice {

  @ExceptionHandler(ResourceNotFoundException.class)
  @ResponseBody
  public ResponseEntity<Error> resourceNotFoundHandler(ResourceNotFoundException ex) {
    Error error = Error.builder()
      .timestamp(new Date())
      .message(ex.getMessage())
      .build();
    return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
  }

  @ExceptionHandler({
    ConstraintViolationException.class,
    DataIntegrityViolationException,
    IllegalArgumentException
  })
  @ResponseBody
  public ResponseEntity<Error> invalidRequestHandler(RuntimeException ex) {
    // ...
    return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
  }
}

Error.java:

public class Error {

  @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSS")
  private LocalDateTime timestamp;

  private String description;
}

ResourceNotFoundException.java:

public class ResourceNotFoundException extends RuntimeException {

  public ResourceNotFoundException(String id) {
    super(String.format("Resource with id=%s not found", id));
  }
}


Usage

@RestController
// ...
public class FilmController {

  @GetMapping(value = "/{id}")
  public ResponseEntity<FilmResource> retrieveFilm(@PathVariable Integer id) {
    Optional<Film> optionalFilm = this.dvdRentalService.retrieveFilm(id);
    return optionalFilm.map(film -> {
      FilmResource resource = FilmResourceMapper.INSTANCE.map(film);
      return new ResponseEntity<>(resource, HttpStatus.OK);
    }).orElseThrow(() -> new ResourceNotFoundException(String.format("Film with id=%s not found", id)));
  }

  @PostMapping(path = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<FilmResource> addFilm(@Valid @RequestBody ...) {
    // ...
  }
// ...
}