使用 Spring MVC 向用户呈现服务级别验证错误的好习惯

Posted

技术标签:

【中文标题】使用 Spring MVC 向用户呈现服务级别验证错误的好习惯【英文标题】:A good practice to present service level validation errors to the user using Spring MVC 【发布时间】:2014-02-20 04:22:52 【问题描述】:

在使用 Spring MVC Validators 执行“浅”用户输入验证后,是否有一些好的做法可以使用 Spring MVC 呈现服务层验证错误?例如,有这样的代码:

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

虽然以这段代码为例似乎微不足道,但当服务层的验证是一项复杂的任务时,这对我来说似乎是一个微妙的故事。尽管这是一个荒谬的场景,但 UserService 可能会创建一个用户列表,如果其中一个已经存在,那么必须以某种方式通知视图层哪些是无效的(例如,已经存在)。

我正在寻找如何设计一段代码的良好实践,这使得

1) 在服务层处理具有复杂数据作为输入的验证错误,以及

2) 将这些验证错误呈现给用户

尽可能简单。有什么建议吗?

【问题讨论】:

jeejava.com/spring-service-layer-bean-validation/ 【参考方案1】:

引发异常/返回错误代码的替代方法是将Errors 传递给userService.createUser()。然后可以在服务级别执行重复用户名检查 - 并将任何错误附加到 Errors。这将确保所有错误(浅的和更复杂的)都可以收集起来并同时呈现给视图。

所以你可以稍微修改你的控制器方法:

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

然后您的UserServiceImpl 将检查重复用户本身 - 例如:

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

Errors 类是 Spring 验证框架的一部分 - 因此虽然会依赖 Spring,但服务层不会依赖任何与 Web 相关的代码。

【讨论】:

被推荐为有效的替代方案;我个人不喜欢将可变簿记对象向下推到调用层次结构中。我还想知道Errors 是否会被ConstraintViolation 淘汰,因为后者已被标准化为Bean Validation API 的一部分。 如果将来我想将服务层与 Spring MVC 以外的其他表示层技术一起使用,例如: JSF?我的意思是,当服务层在错误对象中生成路径时,如下所示,它将使转换变得可怕:errors.rejectValue("someList[" + index + "].someValue", "someValue.isEmpty ", "值不能为空"); .您必须先手动解析诸如“someList[5].someValue”之类的字符串,然后才能将正确的错误消息传递给 JSF 视图。 恐怕我对 JSF 以及它如何使用 Spring 的验证框架能够进行评论还不够熟悉。答案有利有弊。主要优点是简单。主要缺点是与服务层中的验证框架的耦合。如果您打算切换表示层,那么也许返回某种 Result 对您来说可能是一个更好的选择 - 正如艾默生在他的回答中所建议的那样。 @Will Keeling:谢谢。我将发布我的解决方案以分享我的讨论结果。【参考方案2】:

选择通常是异常与错误代码(或响应代码),但最佳实践(至少 Bloch 的做法)是仅在异常情况下使用异常,这在这种情况下会取消它们的资格,因为用户选择了现有的用户名并非闻所未闻。

您的服务调用中的问题是您假设createUser 是一个没有返回值的命令式命令。您应该将其视为“尝试创建用户,然后给我一个结果”。那么结果可能是

一个整数代码(可怕的想法) 来自共享 Result 枚举的常量(由于可维护性仍然是个坏主意) 来自更具体的常量,例如 UserOperationResult 枚举(更好的主意,因为您可能希望在创建新用户和尝试修改用户时都返回 USER_ALREADY_EXISTS) 这个调用完全自定义的 UserCreationResult 对象(这不是一个好主意,因为你会得到这些东西的爆炸式增长) 一个Result<T>UserOperationResult<T> 包装对象,它结合了响应代码常量(分别为ResultCodeUserOperationResultCode)和返回值T,或在没有返回值时使用通配符?。 .. 只需注意切入点等不期望包装器的东西)

未经检查的异常的美妙之处在于它们避免了所有这些废话,但它们也有自己的一系列问题。我个人会坚持最后一个选项,并且过去运气不错。

【讨论】:

您是让控制器处理Result 的处理方式还是将其传递给视图并让其处理? 控制器将决定如何处理服务调用结果,更新模型,视图将呈现它需要的任何内容。有几个原因;了解服务调用枚举的视图可能会混合关注点,控制器可能需要进行多次服务调用并组合结果,控制器可能需要对结果负载的子集进行操作,等等。 非常感谢。那非常有帮助。我已经有了类似的想法,我只希望有一些神奇的 Spring 方式来做到这一点。不过,这个解决方案很简单,阅读我的代码的其他人很可能会立即理解。 你可以在你的控制器上使用 Valid 或 Validated 注解,但是一旦你在服务层,Spring 就不会妨碍耦合。话虽如此,您仍然可以为 @Valid 使用方面(请参阅 Hibernate Validator 5 中的方法验证支持)您还可以为自定义的 AvailableUsername 注释创建自己的 ConstraintValidator,从控制器自动检查它,并从服务中抛出异常如果违反了层,但也会变得混乱。

以上是关于使用 Spring MVC 向用户呈现服务级别验证错误的好习惯的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Spring Ldap Authentication 和 spring mvc 对用户进行身份验证

控制器或服务层中的 Spring MVC 验证?

Spring MVC:JSP模板执行后获取文本

ASP .NET MVC在每个字段级别禁用客户端验证

如何向我的 Spring MVC REST 服务添加错误?

Spring MVC + Spring Security 登录与一个 REST Web 服务