使用Spring表单提交,最小化样板代码。

10

我一直在尝试弄清楚使用Spring进行表单提交的最佳实践是什么,以及实现此目的的最小样板文件是什么。

我认为以下是最佳实践特点:

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

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

(Note: Translated the provided content into Chinese as requested.)
@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";
    }
}

有没有更少的样板代码可以做到这一点,还是说这已经是实现此目标所需的最小代码量了?


3
这个问题是否应该发布在 https://codereview.stackexchange.com/ 上? - Sync
你可以使用AOP(面向切面编程)来减少样板代码。 - msmani
3个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
4
首先,我不会违反Post/Redirect/Get (PRG)模式,这意味着只有在表单提交成功后才会重定向。 其次,我会完全摒弃BindingResult风格。对于简单的情况来说,这样做还可以,但一旦需要更复杂的通知从服务/域/业务逻辑到达用户,事情就变得棘手了。而且,你的服务可重用性也不是很高。 我会直接将绑定的DTO传递给服务,服务将验证DTO并在出现错误/警告时放置通知。这样,您可以将业务逻辑验证与JSR 303:Bean验证结合起来。 为此,您可以在服务中使用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)的建议。然而,我个人不喜欢在服务层进行验证,并且服务抛出异常似乎违反了不要使用异常来控制流程的前提。不过,这是一个很好的深思熟虑的答案。 - hooknc
1
那是第二种方法...我更喜欢第一种(使用通知模式而不是异常)。我知道异常在控制流反模式中的使用,但在这种情况下,我认为优点大于缺点。对于控制器中的零样板代码和此“ValidationException”的集中处理程序,似乎是次佳选择。 关于服务层中的验证,我提到了可重用性/业务规则演变,如果您没有它,您的服务几乎无法重用。 - isah

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

我喜欢你的回答,虽然我不完全了解你的领域或应用程序在做什么,但需要记住的是,如果出现数据库写入错误,无论出于何种原因,该怎么办。我们实际上在我们的代码中忽略了这种情况(就像你在这里所做的一样),但如果这对你的最终用户很重要,你很可能需要添加样板代码来处理这个问题。编写这种代码可能并不是很有趣,但对于你正在做的事情可能是必需的。 - hooknc

1

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


如果原型可以解决您的问题,请告诉我,我可以帮助您制作定制原型。 - Rezwan
2
谢谢您的建议。我不是在寻找生成样板文件,而是要找到重复使用情况下所需的最少必要样板文件。 - Johan Sjöberg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,