简体   繁体   中英

Spring WebFlux: how to display a message from custom exception class on a Web page in case this exception is thrown?

Here is the fragment of my model's class, Student :

@Table
public class Student {

    @Id
    private Long id;

    private String name;

    private String surname;

    private Integer age;

    public Student(String name, String surname, Integer age) {

        validateInput(name, surname, age);

        this.name = name;
        this.surname = surname;
        this.age = age;
    }

    private void validateInput(String name, String surname, Integer age) {

        if (name == null || name.isBlank()) throw new InvalidNewStudentNameException(name);

        if (surname == null || surname.isBlank()) throw new InvalidNewStudentSurnameException(surname);

        if (age == null || age < 16) throw new InvalidNewStudentAgeException(age);
    }
}

So, in case of an attempt to create an object with invalid data, corresponding custom exception is thrown, and the object is not created.

One such custom class, InvalidNewStudentAgeException , goes as follows:

public class InvalidNewStudentAgeException extends RuntimeException {

    public InvalidNewStudentAgeException(Integer age) {

        super("Could not create a student: student's age, " + age + ", should not be empty or less than 16.");
    }
}

My endpoint for adding new student in StudentController :

@GetMapping("/add")
@ResponseBody
public Mono<Long> add(@RequestParam String name, @RequestParam String surname, @RequestParam Integer age) {

    return studentService.add(name, surname, age).map(student -> student.getId());
} 

And corresponding method in StudentService :

@Override
public Mono<Student> add(String name, String surname, Integer age) {

    return client.execute("INSERT INTO student (age, name, surname) VALUES(:age, :name, :surname)")
                 .bind("age", age)
                 .bind("name", name)
                 .bind("surname", surname)
                 .as(Student.class).fetch().one();
}

I run the app and try to add invalid student (the one with age less than 16) via:

http://localhost:8080/add/?name=SomeName&surname=SomeSurname&age=14

I do indeed get my custom exception thrown, however, the message is only visible within my IDE in stack trace, but not in browser. The app terminates, and if I then access http://localhost:8080/, I get either Whitelabel Error Page with Internal Server Error (500) , or just the error without the page if I add server.error.whitelabel.enabled=false to my application.properties

I tried various ways to handle this exception so that I'll get my custom error message for the user (browser), but no success so far.

Please help me with a proper way to catch my custom exceptions in the controller and handle it in a way that it'll display a message about invalid data in client's browser.

Even the dirty and obviously WRONG workaround of disabling whitelabel pages and adding error.html file in src\\main\\resources\\templates project directory doesn't work for some reason...

Here is one way I tried to do this. I changed my StudentController 's add() method like this:

@GetMapping("/add")
@ResponseBody
public Mono<Long> add(@RequestParam String name, @RequestParam String surname, @RequestParam Integer age) {
    try {
        return studentService.add(name, surname, age).map(student -> student.getId());
    }
    catch (InvalidNewStudentAgeException invNewStudAgeEx) {
        return Mono.error(invNewStudAgeEx);
    }
    catch (InvalidNewStudentNameException invNewStudNameEx) {
        return Mono.error(invNewStudNameEx);
    }
    catch (InvalidNewStudentSurnameException invNewStudSurnameEx) {
        return Mono.error(invNewStudSurnameEx);
    }
}

I've added handlers like this:

 @ResponseStatus(
      value = HttpStatus.BAD_REQUEST,
      reason = "Invalid student's age: age should not be empty or less than 16.")
    @ExceptionHandler(InvalidNewStudentAgeException.class)
    public void invalidNewStudentAgeHandler() {

    }

    @ResponseStatus(
      value = HttpStatus.BAD_REQUEST,
      reason = "Invalid student's name: the name should not be empty.")
    @ExceptionHandler(InvalidNewStudentNameException.class)
    public void invalidNewStudentNameHandler() {

    }

    @ResponseStatus(
      value = HttpStatus.BAD_REQUEST,
      reason = "Invalid student's surname: the surname should not be empty.")
    @ExceptionHandler(InvalidNewStudentSurnameException.class)
    public void invalidNewStudentSurnameHandler() {

    }

I figured out that if I change HttpStatus 's ENUM for invalid age handler and then try to add under-aged student, it does return different response code depending on the ENUM. So it does catch the exception in the handler, but how can I obtain MORE than just a number like 500 or 404? Can I somehow make the browser to display "Invadin student's age..." instead of just HTTP ERROR 509 ???

I've found the solution thanks to this question:

How can I add custom message after HTTP status code

My endpoint's final add() method from StudentController looks like this:

@GetMapping("/add")
@ResponseBody
public Mono<Long> add(@RequestParam String name, @RequestParam String surname, @RequestParam Integer age) {
    try {
        return studentService.add(name, surname, age).map(student -> student.getId());
    }
    catch (InvalidInputException invalidInputException) {
        return Mono.error(invalidInputException);
    }
}

Now I have only one handler method in StudentController :

@ResponseStatus(
  value = HttpStatus.UNPROCESSABLE_ENTITY,
  reason = "Invalid input data for new student.")
@ExceptionHandler(InvalidInputException.class)
public ResponseEntity invalidInputHandler(InvalidInputException e) {
    return ResponseEntity.status(e.getHttpStatus()).body(e.getMessage());
}

I've added a new class for generic InvalidInputException like this:

public class InvalidInputException extends RuntimeException {

    protected HttpStatus httpStatus;

    public InvalidInputException(HttpStatus httpStatus, String message) {

        super(message);

        this.httpStatus = httpStatus;
    }

    public HttpStatus getHttpStatus() {
        return httpStatus;
    }
}

And one specific class for each invalid input. Here is an example of one such class, InvalidNewStudentAgeException :

public class InvalidNewStudentAgeException extends InvalidInputException {

    public InvalidNewStudentAgeException(HttpStatus httpStatus, Integer age) {
        super(httpStatus, "Could not create a student: student's age, " + age + ", should not be empty or less than 16.");
    }
}

Everything else is left unchanged, apart from specific exception constructors' signature. Exceptions are still being thrown as they were earlier, in the constructor of Student class through private method called there:

public Student(String name, String surname, Integer age) {

    validateInput(name, surname, age);

    this.name = name;
    this.surname = surname;
    this.age = age;
}

private void validateInput(String name, String surname, Integer age) {

    if (name == null || name.isBlank()) throw new InvalidNewStudentNameException(HttpStatus.UNPROCESSABLE_ENTITY, name);

    if (surname == null || surname.isBlank()) throw new InvalidNewStudentSurnameException(HttpStatus.UNPROCESSABLE_ENTITY, surname);

    if (age == null || age < 16) throw new InvalidNewStudentAgeException(HttpStatus.UNPROCESSABLE_ENTITY, age);
}

Now everything works as expected. The flow is as follows:

  1. Run the app;
  2. Add "invalid" student via the link: http://localhost:8080/add/?name=TestName&surname=TestSurname&age=14
  3. After that, go to localhost:8080 and see plain text message from my custom exception's constructor!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM