Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring form submission with minum boilerplate

I've been trying to figure out what the best practice is for form submission with spring and what the minimum boilerplate is to achieve that.

I think of the following as best practise traits

  • Validation enabled and form values preserved on validation failure
  • Disable form re-submission F5 (i.e. use redirects)
  • Prevent the model values to appear in the URL between redirects (model.clear())

So far I've come up with this.

@Controller
@RequestMapping("/")
public class MyModelController {

    @ModelAttribute("myModel")
    public MyModel myModel() {
        return new MyModel();
    }

    @GetMapping
    public String showPage() {
        return "thepage";
    }

    @PostMapping
    public String doAction(
            @Valid @ModelAttribute("myModel") MyModel myModel,
            BindingResult bindingResult,
            Map<String, Object> model,
            RedirectAttributes redirectAttrs) throws Exception {
        model.clear();
        if (bindingResult.hasErrors()) {
            redirectAttrs.addFlashAttribute("org.springframework.validation.BindingResult.myModel", bindingResult);
            redirectAttrs.addFlashAttribute("myModel", myModel);
        } else {
            // service logic
        }
        return "redirect:/thepage";
    }
}

Is there a way to do this with less boilerplate code or is this the least amount of code required to achieve this?

like image 765
Johan Sjöberg Avatar asked Mar 14 '18 08:03

Johan Sjöberg


People also ask

Does spring helps in reducing boilerplate code?

It is very easy to develop Spring Based applications with Java or Groovy. It reduces lots of development time and increases productivity. It avoids writing lots of boilerplate Code, Annotations and XML Configuration.

How can you read only on parameter value from a form in Spring MVC?

In Spring MVC, the @RequestParam annotation is used to read the form data and bind it automatically to the parameter present in the provided method. So, it ignores the requirement of HttpServletRequest object to read the provided data.


3 Answers

One possible way is to use Archetype for Web forms, Instead of creating simple project, you can choose to create project from existing archetype of web forms. It will provide you with sufficient broiler plate code. You can also make your own archetype. Have a look at this link to get deeper insight into archetypes. Link To Archetypes in Java Spring

like image 103
Rezwan Avatar answered Sep 25 '22 05:09

Rezwan


First, I wouldn't violate the Post/Redirect/Get (PRG) pattern, meaning I would only redirect if the form is posted successfully.

Second, I would get rid of the BindingResult style altogether. It is fine for simple cases, but once you need more complex notifications to reach the user from service/domain/business logic, things get hairy. Also, your services are not much reusable.

What I would do is pass the bound DTO directly to the service, which would validate the DTO and put a notification in case of errors/warning. This way you can combine business logic validation with JSR 303: Bean Validation. For that, you can use the Notification Pattern in the service.

Following the Notification Pattern, you would need a generic notification wrapper:

public class Notification<T> {
    private List<String> errors = new ArrayList<>();
    private T model; // model for which the notifications apply

    public Notification<T> pushError(String message) {
        this.errors.add(message);
        return this;
    }

    public boolean hasErrors() {
        return !this.errors.isEmpty();
    }

    public void clearErrors() {
        this.errors.clear();
    }

    public String getFirstError() {
        if (!hasErrors()) {
            return "";
        }
        return errors.get(0);
    }

    public List<String> getAllErrors() {
        return this.errors;
    }

    public T getModel() {
        return model;
    }

    public void setModel(T model) {
        this.model = model;
    }
}

Your service would be something like:

public Notification<MyModel> addMyModel(MyModelDTO myModelDTO){
    Notification<MyModel> notification = new Notification();
    //if(JSR 303 bean validation errors) -> notification.pushError(...); return notification;
    //if(business logic violations) -> notification.pushError(...); return notification;
    return notification;
}

And then your controller would be something like:

Notification<MyModel> addAction = service.addMyModel(myModelDTO);
if (addAction.hasErrors()) {
    model.addAttribute("myModel", addAction.getModel());
    model.addAttribute("notifications", addAction.getAllErrors());
    return "myModelView"; // no redirect if errors
} 
redirectAttrs.addFlashAttribute("success", "My Model was added successfully");
return "redirect:/thepage";

Although the hasErrors() check is still there, this solution is more extensible as your service can continue evolving with new business rules notifications.

Another approach which I will keep very short, is to throw a custom RuntimeException from your services, this custom RuntimeException can contain the necessary messages/models, and use @ControllerAdvice to catch this generic exception, extract the models and messages from the exception and put them in the model. This way, your controller does nothing but forward the bound DTO to service.

like image 34
isah Avatar answered Sep 24 '22 05:09

isah


Based on the answer by @isah, if redirect happens only after successful validation the code can be simplified to this:

@Controller
@RequestMapping("/")
public class MyModelController {

    @ModelAttribute("myModel")
    public MyModel myModel() {
        return new MyModel();
    }

    @GetMapping
    public String showPage() {
        return "thepage";
    }

    @PostMapping
    public String doAction(
            @Valid @ModelAttribute("myModel") MyModel myModel,
            BindingResult bindingResult,
            RedirectAttributes redirectAttrs) throws Exception {
        if (bindingResult.hasErrors()) {
            return "thepage";
        }
        // service logic
        redirectAttrs.addFlashAttribute("success", "My Model was added successfully");
        return "redirect:/thepage";
    }
}
like image 43
Johan Sjöberg Avatar answered Sep 22 '22 05:09

Johan Sjöberg