Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Exception Handling and I18N on Spring Boot APIs, Part 2

DZone's Guide to

Exception Handling and I18N on Spring Boot APIs, Part 2

In the second part our this article, we'll create a proper exception handler, fully integrated with bean validation that is easy to use.

· Web Dev Zone
Free Resource

Get deep insight into Node.js applications with real-time metrics, CPU profiling, and heap snapshots with N|Solid from NodeSource. Learn more.

Welcome back! If you missed Part 1, check it out here

Globally Handling Exceptions on Spring Boot

Every message that our Spring Boot API is going to send to the user will be serialized as a JSON object. Therefore, we need to create a class to represent a structured message. Let's call this class RestMessage and add to the com.questionmarks.util package with the following code:

package com.questionmarks.util;

import lombok.Getter;

import java.util.List;

@Getter
public class RestMessage {
    private String message;
    private List<String> messages;

    public RestMessage(List<String> messages) {
        this.messages = messages;
    }

    public RestMessage(String message) {
        this.message = message;
    }
}

In contrast to RestException, we haven't used any Lombok annotation to create the constructors of this class. As of the time of writing, no feature provided by Lombok creates separate constructors for each property. Therefore, we needed to add the code by ourselves, but at least we could take advantage of the @Getter annotation again.

As the idea is to serialize instances of this class as JSON objects back to the user, we are going to tweak the serialization process a little. By default, Jackson (the JSON serializer used by Spring Boot) serializes all properties in an instance, whether it has these values or not. To avoid adding a bunch of null in these JSON objects, let's edit the application.properties file by adding the following line:

spring.jackson.default-property-inclusion=non_null

With this configuration in place, we can move ahead and implement the class that will handle all exceptions thrown throughout the execution of requests in our application. Let's call this class RestExceptionHandler and create it in the main package (com.questionmark):

package com.questionmarks;

import com.questionmarks.util.RestException;
import com.questionmarks.util.RestMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;

@ControllerAdvice
public class RestExceptionHandler {
    private static final String UNEXPECTED_ERROR = "Exception.unexpected";
    private final MessageSource messageSource;

    @Autowired
    public RestExceptionHandler(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    @ExceptionHandler(RestException.class)
    public ResponseEntity<RestMessage> handleIllegalArgument(RestException ex, Locale locale) {
        String errorMessage = messageSource.getMessage(ex.getMessage(), ex.getArgs(), locale);
        return new ResponseEntity<>(new RestMessage(errorMessage), HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<RestMessage> handleArgumentNotValidException(MethodArgumentNotValidException ex, Locale locale) {
        BindingResult result = ex.getBindingResult();
        List<String> errorMessages = result.getAllErrors()
                .stream()
                .map(objectError -> messageSource.getMessage(objectError, locale))
                .collect(Collectors.toList());
        return new ResponseEntity<>(new RestMessage(errorMessages), HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<RestMessage> handleExceptions(Exception ex, Locale locale) {
        String errorMessage = messageSource.getMessage(UNEXPECTED_ERROR, null, locale);
        ex.printStackTrace();
        return new ResponseEntity<>(new RestMessage(errorMessage), HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

As this class' implementation is not that trivial, let's take a closer look at the details.

Making the Exception Handler Global

To make our exception handler implementation global, we have used the @ControllerAdvice annotation. This annotation is a specialization of @Component and enables developers to apply, among other things, @ExceptionHandler methods globally to all controllers in an application.

This means that the methods defined in this class that handle exceptions will apply to all @Controllers that we define in our application. This helps us to avoid having to define a base class that the controllers have to extend or having to define exception handlers on each controller.

Injecting an I18N Message Resource

Since we aim to support multiple languages, we have defined the constructor of this class to get an instance of MessageSource injected. This instance enables us to search for (I18N) messages defined in the messages.properties files, or on its variations for other languages, based on codes.

As an example, in this class, we've defined a private constant called UNEXPECTED_ERROR. The value of this constant is Exception.unexpected and will point to a message that tells the user that the error was not expected. We will define the messages and their localizations later on in the article.

Handling REST Exceptions

To handle exceptions derived from (or instance of) RestException, we define a method called handleIllegalArgument and annotate it with @ExceptionHandler(RestException.class). Whenever an exception of this class is caught by the method, the code message set in the exception is passed to the MessageSource instance to get a localized message explaining the error. Besides that, the args property and the current locale are passed alongside with the message code so Spring can interpolate the localized final message replacing any placeholders.

String errorMessage = messageSource
          .getMessage(ex.getMessage(), ex.getArgs(), locale);

Handling Bean Validation Exceptions

In the previous article, we developed a solution that transforms DTOs into entities and that triggers the bean validation for these DTOs automatically. This means that, for example, if we define a property as @NotNull in a DTO and a user sends an instance that contains null as the value property, a MethodArgumentNotValidException is thrown saying that this situation is not valid.

To catch this exception and provide a better message, we have defined a method called handleArgumentNotValidException and set it to handle MethodArgumentNotValidExceptions. Since multiple validation errors might occur, we map the error codes to messages defined in messages.propertiesfiles.

List<String> errorMessages = result.getAllErrors()
    .stream()
    .map(objectError -> messageSource.getMessage(objectError, locale))
    .collect(Collectors.toList());

Handling Unexpected Exceptions

The last method defined in the RestExceptionHandler class is responsible for handling exceptions that we have not foreseen. For example, let's say that for some reason Spring is unable to inject a Repository instance in a controller, and we try to use this null reference to hit the database. In this situation, a NullPointerException will be thrown by the application and this method will catch it. Since our application was not expecting this error to occur, and we don't have much to say to the user, we just use the UNEXPECTED_ERROR constant to search for a localized message that tells the user that something went wrong.

String errorMessage = messageSource.getMessage(UNEXPECTED_ERROR, null, locale);
ex.printStackTrace();

We also call printStackTrace method in the exception to log its details to be able to analyze it later.

Using the Global Exception Handler

Now that we have a global exception handler in place, let's change some classes to see it working. In the previous article, we created two DTOs to handle the insertion and update of exams, ExamCreationDTO and ExamUpdateDTO. Both of them used only the @NotNull annotation to avoid null values on their properties. Let's start incrementing the ExamCreationDTO class to add a new validation:

package com.questionmarks.model.dto;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;

@Getter
@Setter
public class ExamCreationDTO {
    @NotNull
    @Size(min = 1, max = 50)
    private String title;

    @NotNull
    @Size(min = 1, max = 512)
    private String description;

    @JsonIgnore
    private final LocalDateTime createdAt = LocalDateTime.now();

    @JsonIgnore
    private final LocalDateTime editedAt = LocalDateTime.now();
}

The difference between this version and the one created in the previous article is that now we use @Size annotations to guarantee that title and description won't exceed the limits defined in the database. To keep everything consistent, let's add the same annotation to the same fields but in the ExamUpdateDTO class:

package com.questionmarks.model.dto;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.Id;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;

@Getter
@Setter
public class ExamUpdateDTO {
    @Id
    @NotNull
    private Long id;

    @NotNull
    @Size(min = 1, max = 50)
    private String title;

    @NotNull
    @Size(min = 1, max = 512)
    private String description;

    @JsonIgnore
    private final LocalDateTime editedAt = LocalDateTime.now();
}

From now on, when the bean validation process gets triggered on instances of these classes, title and description on both classes are checked to guarantee that no null values are set on it and that the values don't exceed the limits defined. In case one or more of these validations fail, an instance of MethodArgumentNotValidException is thrown indicating what properties failed. For example, if the user sends a title with more than 50 characters, the bean validation process will produce an exception with the following code: Size.exam.title. The exception handler will then get this code and search in the messages.properties file for an associated message.

We will define these messages in the following sections, but first, let's make just one more change in our application. We will refactor the DTOModelMapper class to validate if the application managed to find the object persisted with the idprovided on a DTO. For those who didn't read the previous article, this class is responsible for the automatic mapping of DTOs into entities and, for DTOs that include @Id properties, it tries to fetch records from the database. Let's refactor the resolveArgument method in this class to include a call to the Check.notNull() method, as follows:

package com.questionmarks.util;

// ... imports

public class DTOModelMapper extends RequestResponseBodyMethodProcessor {
    // ...

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        Object dto = super.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
        Object id = getEntityId(dto);
        if (id == null) {
            return modelMapper.map(dto, parameter.getParameterType());
        } else {
            Object persistedObject = entityManager.find(parameter.getParameterType(), id);
            Check.notNull(persistedObject, "Exception.notFound",
                    parameter.getParameterType().getSimpleName(), id);
            modelMapper.map(dto, persistedObject);
            return persistedObject;
        }
    }

    // ...
}

By adding the Check.notNull call to the else block, we guarantee that the program is able to find an entity with the id passed before proceeding with the execution. In case no entity is found, a RestException is thrown with the Exception.notFound code and with the simple name (getSimpleName) of the class alongside with the id provided.

Creating the I18N Messages

The last thing we need in order to have a Spring Boot API that supports multiple languages is to map the messages that we will send to our users and translate them to other languages. The messages in English will be stored in a new file called messages.properties that we are going to create in the src/main/resources/folder. Let's add the following messages:

NotNull.exam.id=Please, inform the exam''s id to be updated.
NotNull.exam.title=Please, provide a title to the exam.
NotNull.exam.description=Please, provide a description to the exam.
Size.exam.title=Exam title must contain between {2} and {1} characters.
Size.exam.description=Exam description must contain between {2} and {1} characters.
Exception.notFound=No record of {0} could be found with id {1}.
Exception.unexpected=An unexpected error occurred while processing your request.

Whenever a validation fails, Spring Boot generates a code that starts with the annotation name (e.g. Size), then it adds the entity where the validation failed (e.g. exam), and lastly, it adds the property (e.g. description). Like that, it's easy to know what messages we need to map when dealing with bean validation.

For some messages, we have defined placeholders like {0} and {1}. These placeholders are replaced by Spring to provide a better explanation to users. For example, if the size of an exam's title is too long, Spring gets the message referenced by Size.exam.title and replaces {2} and {1} with the minimum and maximum length set in the @Size annotation.

In the messages added above, only two didn't follow the pattern explained: Exception.notFound and Exception.unexpected. The former was created when refactoring the DTOModelMapper class, and the latter was defined to tell users about errors that we didn't expect.

Supporting Other Languages

To give alternative languages for users, we need to create other files with translated versions of the messages above. The names of these files must start with messages followed by an underscore and the ISO code of the language chosen (e.g. _pt). Optionally, we can add another underscore followed by the ISO code of a specific region (e.g. _BR). This pattern enables us to provide messages to multiple languages and its variations.

In this article, we are going to create a file called messages_pt_BR.properties, in the src/main/resources/ folder, to support users from Brazil (BR):

NotNull.exam.id=Por favor, informe o id do exame a ser editado.
NotNull.exam.title=Por favor, informe um título para o exame.
NotNull.exam.description=Por favor, informe uma descrição para o exame.
Size.exam.title=O título do exame deve conter entre {2} e {1} caracteres.
Size.exam.description=A descrição do exame deve conter entre {2} e {1} caracteres.
Exception.unexpected=Um erro inesperado ocorreu durante a execução da sua requisição.

The official language in Brazil is Portuguese (pt), but as the language spoken there is quite different from Portugal, we opted to have a translation crafted specifically for Brazilians. This is everything we need to do to support the Portuguese variation spoken in Brazil. Now, whenever a user expresses that they want messages in Brazilian Portuguese, Spring Boot will search the messages_pt_BR.properties file to get the appropriate message.

In the next section, we will see how to interact with the API to get user-friendly messages in both languages: English and Brazilian Portuguese.

Interacting With a Localized Spring Boot API

Before start testing our API, let's run the application. This can be accomplished through our IDE or through the gradle bootRun command. When the API finishes bootstrapping, we can send the following request to add a new exam:

# adds a new exam
curl -X POST -H "Content-Type: application/json" -d '{
    "title": "Another show exam",
    "description": "Another show exam desc"
}' http://localhost:8080/exams

The command above must work without problems and no output message is expected from the API. Now, if we send the following request:

# tries to add a new exam without a title
curl -X POST -H "Content-Type: application/json" -d '{
    "description": "Another show exam desc"
}' http://localhost:8080/exams

We can expect a message to be sent back from our API, since we didn't define a title, saying "Please, provide a title for the exam." As the message is structured as JSON, the output from Spring Boot is:

{"messages":["Please, provide a title to the exam."]}}

This proves that RestExceptionHandler worked and crafted a better message for the user. But let's say that we prefer to get messages in Brazilian Portuguese, how do we do that? Easy! We just need to inform the API which language we want through the Accept-Language header in the request:

curl -X POST -H "Content-Type: application/json" -H "Accept-Language: pt-BR" -d '{
  "description": "Another show exam desc"
}' http://localhost:8080/exams

And the output provided by Spring Boot will be in Portuguese:

{"messages":["Por favor, informe um título para o exame."]}

For the sake of completeness, let's see placeholders getting replaced when we send a title that is too long:

curl -X POST -H "Content-Type: application/json" -d '{
  "title": "This title is too long to be accepted and Spring Boot will complain about it",
  "description": "Another show exam desc"
}' http://localhost:8080/exams

As we haven't defined the Accept-Language header in the request above, and as the title exceeded the limits, Spring Boot will send us the following message:

{"messages":["Exam title must contain between 1 and 50 characters."]}

Both the {1} and {2} placeholders in the original, English message, got replaced by the min and max values set in the @Size annotation configured in the ExamCreationDTO.

Next Steps: Integration Testing on Spring Boot APIs

There we go, we now have a proper exception handler in place, fully integrated with bean validation and that is easy to use. We are now ready to add the missing endpoints that our to-be-developed frontend applications will need. As we want these new endpoints to function properly, in the next article we are going to create these endpoints alongside integration tests.

Throughout the article, we are going to use libraries such as JUnit and Hamcrest to simulate interactions with the RESTful API to guarantee that everything works as expected. Stay tuned!

Node.js application metrics sent directly to any statsd-compliant system. Get N|Solid

Topics:
web dev ,spring boot api ,exception handling ,internationalization

Published at DZone with permission of Bruno Krebs, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}