2

I have a Spring Boot 2.0.2 web service that I'm building, I have a number of fields within an entity that I don't want to be empty. When trying to persist an entity with invalid fields, how do I grab the message from that particular field?

For example, I have an entity;

@Entity
@Table(name="users")
public class User {

    @Column(name="password", columnDefinition = "VARCHAR(250)", nullable = false, length = 250)
    @NotNull(message = "Missing password")
    private String password;
}

I have a service class, which attempts to create a new user. When it tries to create a user where the password is missing, an exception is thrown;

2018-07-20 17:03:33.195 ERROR 78017 --- [nio-8080-exec-1] o.h.i.ExceptionMapperStandardImpl        : HHH000346: Error during managed flush [Validation failed for classes [com.nomosso.restapi.models.User] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
    ConstraintViolationImpl{interpolatedMessage='Missing password', propertyPath=password, rootBeanClass=class com.nomosso.restapi.models.User, messageTemplate='Missing password'}
]]
2018-07-20 17:03:33.215 ERROR 78017 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Error while committing the transaction] with root cause

javax.validation.ConstraintViolationException: Validation failed for classes [com.nomosso.restapi.models.User] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
    ConstraintViolationImpl{interpolatedMessage='Missing password', propertyPath=password, rootBeanClass=class com.nomosso.restapi.models.User, messageTemplate='Missing password'}
]

I would like to grab the value of messageTemplate, so that I can handle it and return it in an API response, but I don't seem to be able to catch the Exception and grab the text.

Currently the API response looks like this;

{
    "timestamp": 1532102613231,
    "status": 500,
    "error": "Internal Server Error",
    "message": "Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Error while committing the transaction",
    "path": "/user"
}

This isn't very helpful at all to the user of the service. I would like the response to be this;

{
        "timestamp": 1532102613231,
        "status": 400,
        "error": "Bad request",
        "message": "Missing password",
        "path": "/user"
    }

I am able to generate my own error responses, but in order to do so I need to get the message from the invalid entity.

UPDATE: Here is the service that tries to persist the entity;

@Service
public class UserService implements UserDetailsService {

    private UserRepository userRepository;

    private PasswordEncoder passwordEncoder;

    @Autowired
    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    /**
     * Creates a new user account
     * @param email email address
     * @param password unencoded password
     * @param firstname firstname
     * @param lastname lastname
     * @param displayName display name
     * @return new User
     */
    public User createUser(String email, String password, String firstname, String lastname, String displayName) {
        User user = new User();
        user.setEmail(email);
        user.setFirstname(firstname);
        user.setLastname(lastname);
        user.setDisplayName(displayName);
        if (password != null && !password.isEmpty()) {
            user.setPassword(passwordEncoder.encode(password));
        }
        user.setEmailVerified(false);
        user.setCreatedAt(new Date());
        user.setUpdatedAt(user.getCreatedAt());

        userRepository.save(user);

        return user;
    }

}

Finally, my user repository looks like this (spring data);

public interface UserRepository extends CrudRepository<User, String> {
}

2 Answers 2

3

The validation occurs as the commit is performed. So it sounds that your controller started a transaction and that the commit will be done at the time where the controller returns the response to the client.

So surrounding the repository.save(...) statement by catch(ConstraintViolationException e) will be useless.
You should create a custom exception handler to allow to catch ConstraintViolationException anywhere it occurs.
But you cannot catch it directly as Spring wraps it into a org.springframework.transaction.TransactionSystemException.

In fact to be exact : 1) javax.validation.ConstraintViolationException is wrapped by 2)javax.persistence.RollbackException: Error while committing the transaction that is itself wrapped by 3) org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction

Here is the higher level exception :

org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Error while committing the transaction
        at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:545)
        at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:746)
   ....

So you could write your ControllerAdvice in this way :

@ControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {

    Logger LOGGER = LoggerFactory.getLogger(MyExceptionHandler.class);

    @ExceptionHandler(value = { TransactionSystemException.class })
    protected ResponseEntity<Object> handleConflict(TransactionSystemException ex, WebRequest request) {
        LOGGER.error("Caught", ex);
        Throwable cause = ex.getRootCause();

        if (cause instanceof ConstraintViolationException) {
           Set<ConstraintViolation<?>> constraintViolations = ((ConstraintViolationException) cause).getConstraintViolations();
           // iterate the violations to create your JSON user friendly message
           String msg = ...;
           return handleExceptionInternal(ex, msg , new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
      }
    }        

}

The use of TransactionSystemException.getRootCause() is required to retrieve the original thrown exception : that is ConstraintViolationException in your case.

Sign up to request clarification or add additional context in comments.

5 Comments

Sadly that didn't work, it didn't catch handle the exception. I've updated my original question to include the service that's calling the save() method.
Indeed. You have to unwrap the chained exceptions to retrieve the original exception. I updated to explain. I tested and it works for me.
great explanation! @davidxxx , im new to java developer and curios how did you find about Error C is wrapped with Error B etc, is it documented?
@protrafree Unfortunately no it is not documented. I discovered that thanks to a breakpoint on that code. I think that it is a Spring boot bug because Spring has as principle to not allow low level exceptions as TransactionSystemException to be pulled up. Chances are that the bug has disappears in the last versions of Spring Boot.
oh i havent used breakpoint since i learn java, is it any good resource about how error works in spring like how is it triggered untll it shows to client trough web or REST API response ? @davidxxx i wanna know more about this
0

Your createUser method is not correct. If you know that password field is required (nullable=false) you cannot proceed with entity creation if there is a lack of password value (for me it is a code smell):

 if (password != null && !password.isEmpty()) {
        user.setPassword(passwordEncoder.encode(password));
    }

You should throw custom exception, e.g.:

else{
    throw new NoRequiredFieldException("password"); //customize exception to add required informations
}

then you can use @ControllerAdvice as @davidxxx propose in more clean way.

2 Comments

So you recommend validating the entity within the service layer? That's the way I've always done it, but thought I'd try something different this time.
I usually have request representation object (separate for e.g. create, update) with "first line" validation (e.g. not null, email pattern, etc.). Then I map them into DTOs to transfer the into domain layer. Domain (service) layer perform domain validation (e.g. existence of referenced object id). Finally the data are mapped on entities and save in repository.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.