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
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?
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.
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.
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
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.
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";
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With