Spring MVC:验证、Post-Redirect-Get、部分更新、乐观并发、字段安全

Spring MVC: Validation, Post-Redirect-Get, Partial Updates, Optimistic Concurrency, Field Security

[这是我看到的关于 Spring MVC 的常见问题列表,这些问题的解决方法类似。我已经把它们张贴在这里,所以我可以很容易地从其他问题中参考它们]

如何使用表单仅更新模型实体的几个字段?

如何将 Post-Redirect-Get 模式与 Spring MVC 一起使用,尤其是表单验证?

如何保护我实体中的某些字段?

如何实施 Optimistic Concurrency Control

  1. 要部分更新实体,您应该使用 @SessionAttributes 将模型存储在请求之间的会话中。您可以使用隐藏的表单字段,但会话更安全。

  2. 要使用 P/R/G 进行验证,请使用 flashAttributes

  3. 要保护字段,请使用 webDataBinder.setAllowedFields("field1","field2",...) 或创建特定于表单的 class,然后将值复制到您的实体。实体不需要 id 和版本的设置器(如果使用 Hibernate)。

  4. 要使用开放式并发控制,请在您的实体中使用 @Version 注释并在您的控制器上使用 @SessionAttributes

示例代码:

@Controller
@RequestMapping("/foo/edit/{id}")
@SessionAttributes({FooEditController.ATTRIBUTE_NAME})
public class FooEditController {

    static final String ATTRIBUTE_NAME = "foo";
    static final String BINDING_RESULT_NAME = "org.springframework.validation.BindingResult." + ATTRIBUTE_NAME;

    @Autowired
    private FooRepository fooRepository;

    /*
     Without this, user can set any Foo fields they want with a custom HTTP POST
     setAllowedFields disallows all other fields. 
     You don't even need setters for id and version, as Hibernate sets them using reflection
    */
    @InitBinder
    void allowFields(WebDataBinder webDataBinder){
        webDataBinder.setAllowedFields("name"); 
    }

    /*
     Get the edit form, or get the edit form with validation errors
    */
    @RequestMapping(method = RequestMethod.GET)
    String getForm(@PathVariable("id") long id, Model model) {

        /* if "fresh" GET (ie, not redirect w validation errors): */
        if(!model.containsAttribute(BINDING_RESULT_NAME)) {
            Foo foo = fooRepository.findOne(id);
            if(foo == null) throw new ResourceNotFoundException();
            model.addAttribute(ATTRIBUTE_NAME, foo);
        }

        return "foo/edit-form";
    }

    /*
     @Validated is better than @Valid as it can handle http://docs.jboss.org/hibernate/validator/5.1/reference/en-US/html/chapter-groups.html
     @ModelAttribute will load Foo from session but also set values from the form post
     BindingResult contains validation errors
     RedirectAttribute.addFlashAttribute() lets you put stuff in session for ONE request
     SessionStatus lets you clear your SessionAttributes
    */
    @RequestMapping(method = RequestMethod.POST)
    String saveForm(
       @Validated @ModelAttribute(ATTRIBUTE_NAME) Foo foo,
       BindingResult bindingResult, 
       RedirectAttributes redirectAttributes, 
       HttpServletRequest request, 
       SessionStatus sessionStatus
    ) {

        if(!bindingResult.hasErrors()) {
            try {
                fooRepository.save(foo);
            } catch (JpaOptimisticLockingFailureException exp){
                bindingResult.reject("", "This record was modified by another user. Try refreshing the page.");
            }
        }

        if(bindingResult.hasErrors()) {

            //put the validation errors in Flash session and redirect to self
            redirectAttributes.addFlashAttribute(BINDING_RESULT_NAME, bindingResult);
            return "redirect:" + request.getRequestURI();
        }

        sessionStatus.setComplete(); //remove Foo from session

        redirectAttributes.addFlashAttribute("message", "Success. The record was saved");
        return "redirect:" + request.getRequestURI();
    }
}

Foo.java:

@Entity
public class Foo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Version //for optimistic concurrency control
    private int version;

    @NotBlank
    private String name;

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

edit-form.jsp(兼容 Twitter Bootstrap):

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>

<form:form modelAttribute="foo">

    <spring:hasBindErrors name="foo">
        <c:if test="${errors.globalErrorCount > 0}">
            <div class="alert alert-danger" role="alert"><form:errors/></div>
        </c:if>
    </spring:hasBindErrors>

    <c:if test="${not empty message}">
      <div class="alert alert-success"><c:out value="${message}"/></div>
    </c:if>

    <div class="panel panel-default">
        <div class="panel-heading">
            <button class="btn btn-primary" name="btnSave">Save</button>
        </div>

        <div class="panel-body">

            <spring:bind path="name">
                <div class="form-group${status.error?' has-error':''}">
                    <form:label path="name" class="control-label">Name <form:errors path="name"/></form:label>
                    <form:input path="name" class="form-control" />
                </div>
            </spring:bind>

        </div>
    </div>

</form:form>

ResourceNotFoundException.java:

@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
}