Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A good practice to present service level validation errors to the user using Spring MVC

Are there some good practices to present service layer validation errors using Spring MVC after "shallow" user input validation has been performed using Spring MVC Validators? For example, having this code:

@Autowired
private UserService userService;

@RequestMapping(value = "user/new", method = RequestMethod.POST)
public String createNewUser(@ModelAttribute("userForm") UserForm userForm, BindingResult result, Model model){
    UserFormValidator validator = new UserFormValidator(); //extending org.springframework.validation.Validator
    validator.validate(userForm, result);

    if(result.hasErrors()){
        model.addAttribute("userForm", userForm);
        return "user/new";
    }

    // here, for example, the user already might exist
    userService.createUser(userForm.getName(), userForm.getPassword());

    return "redirect:user/home";
} 

While it may seem trivial having this code as an example, it seems to be a delicate story to me when validation at service layer is a complex task. Despite being an absurd scenario, the UserService might take a list of users to create, and if one of them already exists, then the view tier must somehow be notified about which of them is not valid (e.g. does already exist).

I am looking for a good practice how to design a piece of code, which makes it possible to

1) handle validation errors at the service layer having complex data as input, and

2) to present these validation errors to the user

as easy as possible. Any suggestions?

like image 702
AlexLiesenfeld Avatar asked Jan 28 '14 17:01

AlexLiesenfeld


2 Answers

The choice is typically exceptions vs. error codes (or response codes), but the best practice, at least Bloch's, is to only use exceptions in exceptional circumstances, which disqualifies them in this situation, since a user picking an existing username isn't unheard of.

The issue in your service call is that you assume createUser is an imperative command with no return value. You should think of it as "try to create a user, and give me a result" instead. That result could then be

  • an integer code (horrible idea)
  • a constant from a shared Result enum (still a bad idea due to maintainability)
  • a constant from something more specific like a UserOperationResult enum (better idea since you might want to return USER_ALREADY_EXISTS both when you create a new user and when you try to modify a user)
  • a UserCreationResult object that's completely custom to this call (not a good idea because you'll get an explosion of these)
  • a Result<T> or UserOperationResult<T> wrapper object that combines a response code constant (ResultCode or UserOperationResultCode respectively) and a return value T, or a wildcard ? when there is no return value ... just watch out for pointcuts and the like that don't expect the wrapper)

The beauty of unchecked exceptions is that they avoid all this crap, but they come with their own set of issues. I'd personally stick to the last option, and have had decent luck with it in the past.

like image 134
Emerson Farrugia Avatar answered Sep 21 '22 20:09

Emerson Farrugia


An alternative to throwing an exception/returning an error code would be to pass the Errors to userService.createUser(). The duplicate username check could then be performed at the service level - and any error appended to the Errors. This would ensure that all errors (shallow and more complex) could all be collected up and presented to the view at the same time.

So you could rework your controller method slightly:

@RequestMapping(value = "user/new", method = RequestMethod.POST)
public String createNewUser(@ModelAttribute("userForm") UserForm userForm, BindingResult result, Model model){
    UserFormValidator validator = new UserFormValidator();
    validator.validate(userForm, result);

    // Pass the BindingResult (which extends Errors) to the service layer call
    userService.createUser(userForm.getName(), userForm.getPassword(), result);

    if(result.hasErrors()){
        model.addAttribute("userForm", userForm);
        return "user/new";
    }

    return "redirect:user/home";
} 

And your UserServiceImpl would then check for duplicate users itself - for example:

public void createUser(String name, String password, Errors errors) {
    // Check for a duplicate user
    if (userDao.findByName(name) != null) {
        errors.rejectValue("name", "error.duplicate", new String[] {name}, null);
    }

    // Create the user if no duplicate found
    if (!errors.hasErrors()) {
        userDao.createUser(name, password);
    }
}

The Errors class is part of Spring's validation framework - so although there would be a dependency on Spring, the service layer wouldn't have any dependency on any web related code.

like image 29
Will Keeling Avatar answered Sep 17 '22 20:09

Will Keeling