【问题标题】:Spring form submission with minum boilerplate使用最小样板提交 Spring 表单
【发布时间】:2018-03-14 08:32:58
【问题描述】:

我一直在试图弄清楚使用 spring 提交表单的最佳实践是什么,以及实现该目标的最小样板是什么。

我认为以下是最佳实践特征

  • 启用验证并在验证失败时保留表单值
  • 禁用表单重新提交F5(即使用重定向)
  • 防止模型值出现在重定向之间的 URL 中 (model.clear())

到目前为止,我已经想出了这个。

@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";
    }
}

有没有办法用 less 样板代码做到这一点,或者这是实现这一目标所需的最少代码量?

【问题讨论】:

标签: java spring spring-mvc boilerplate spring-web


【解决方案1】:

首先,我不会违反Post/Redirect/Get (PRG) 模式,这意味着我只会在表单发布成功的情况下进行重定向。

其次,我将完全摆脱 BindingResult 样式。这对于简单的情况很好,但是一旦您需要更复杂的通知来从服务/域/业务逻辑到达用户,事情就会变得棘手。此外,您的服务的可重用性也不高。

我要做的是将绑定的 DTO 直接传递给服务,该服务将验证 DTO 并在出现错误/警告时发出通知。通过这种方式,您可以将业务逻辑验证与 JSR 303: Bean Validation 结合起来。 为此,您可以在服务中使用Notification Pattern

按照通知模式,您需要一个通用的通知包装器:

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;
    }
}

您的服务将类似于:

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;
}

然后你的控制器会是这样的:

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";

虽然hasErrors() 检查仍然存在,但此解决方案更具可扩展性,因为您的服务可以随着新的业务规则通知继​​续发展。

另一种方法,我将保持非常简短,是从您的服务中抛出一个自定义 RuntimeException,这个自定义 RuntimeException 可以包含必要的消息/模型,并使用 @ControllerAdvice 来捕获这个通用异常,从异常中提取模型和消息并将它们放入模型中。这样,您的控制器只会将绑定的 DTO 转发给服务。

【讨论】:

  • 我喜欢不破坏 Post/Redirect/Get (PRG) 的建议。但是,我个人不喜欢在服务层进行验证并且让服务抛出异常似乎打破了Don't Use Exceptions For Flow Control 前提。不过很好的周到的答案。
  • 这是第二种方法...我也喜欢第一种方法(使用不使用异常的通知模式)。我知道控制流反模式的异常用法,但是,在这种情况下,我会考虑,因为利大于弊。在控制器中拥有零样板和针对此“ValidationException”的集中处理程序对我来说似乎是第二好的选择。关于服务层的验证,我提到了可重用性/业务规则演进,如果你没有它,你的服务很难重用。
【解决方案2】:

根据@isah的回答,如果重定向仅在成功验证后发生,代码可以简化为:

@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";
    }
}

【讨论】:

  • 我喜欢您的回答,但并不完全了解您的域或您的应用程序正在做什么,但要记住的是,如果发生数据库写入错误,无论出于何种原因,该怎么办。我们实际上在我们的代码中忽略了这个用例(就像您在这里所做的那样),但如果这对您的最终用户很重要,您很可能还需要添加样板代码来处理该问题。写的代码不是很有趣,但你正在做的事情可能需要它。
【解决方案3】:

一种可能的方法是使用 Archetype for Web 表单,您可以选择从现有的 web 表单原型创建项目,而不是创建简单的项目。它将为您提供足够的肉鸡板代码。您也可以制作自己的原型。 查看此链接以更深入地了解原型。 Link To Archetypes in Java Spring

【讨论】:

  • 如果原型解决了您的问题,请告诉我,我可以帮助您制作自定义原型。
  • 感谢您的建议。我不是要生成样板,而是要找到重复用例所需的最少样板
猜你喜欢
  • 2016-06-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-04
  • 2023-03-04
  • 2013-05-24
  • 2016-02-17
相关资源
最近更新 更多