问题
I have a form-handling Spring MVC controller with JSR-303 bean validation by @Valid
.
The only purpose of whole GET
-handler is to act (almost) the same as POST
, but omitting @Valid
annotation to prevent displaying of errors in JSP <form:errors ...>
on the first user GET
request before he submits the form.
My question is:
- How can I remove the redundant
GET
method in a clean way? - How can I make a
POST
-only@Valid
?
Here is my example code:
@RequestMapping( value = "/account/register" , method = RequestMethod.GET )
public String registerGet( @ModelAttribute( "registerForm" ) RegisterForm registerForm ) {
return "account/register";
}
@RequestMapping( value = "/account/register" , method = RequestMethod.POST )
public String registerPost( @ModelAttribute( "registerForm" ) @Valid RegisterForm registerForm ,
BindingResult result ,
RedirectAttributes redirectAttributes ) {
... ADD USER HERE IF !result.hasErrors() ...
return "account/register";
}
回答1:
If you look how spring resolves the arguments passed to the handler it is not very difficult to implement your own that does exactly what you want. By default spring will use the ModelAttributeMethodProcessor for arguments annotated with @ModelAttribute
and simple types.
Just look at the ModelAttributeMethodProcessor.supportsParameter()
method implementation.
/**
* @return true if the parameter is annotated with {@link ModelAttribute}
* or in default resolution mode also if it is not a simple type.
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
return true;
}
else if (this.annotationNotRequired) {
return !BeanUtils.isSimpleProperty(parameter.getParameterType());
}
else {
return false;
}
}
ModelAttributeMethodProcessor
is also responsible for kicking in the validation if the @Valid
annotation is found. It does this in a interesting way to make the code compile without @Valid
being on the classpath. Luckily this makes it easy to exploit in your advantage.
The extracted ModelAttributeMethodProcessor.validateIfApplicable() method.
/**
* Validate the model attribute if applicable.
* <p>The default implementation checks for {@code @javax.validation.Valid}.
* @param binder the DataBinder to be used
* @param parameter the method parameter
*/
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation annot : annotations) {
if (annot.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = AnnotationUtils.getValue(annot);
binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
break;
}
}
}
As you may already noticed it simply checks if there is a annotation present on the parameter that starts with "Valid"
and tells the binder to validate it's value.
Write a custom annotation
The first thing to do is to write a new annotation which your custom HandlerMethodArgumentResolver
will support.
//** Validates only when the request method is a modifying verb e.g. POST/PUT/PATCH/DELETE */
@Target({ PARAMETER })
@Retention(RUNTIME)
public @interface ValidModifyingVerb {}
Notice the name of the annotation intentionally starts with "Valid"
.
Rolling out your own HandlerMethodArgumentResolver
The easiest thing to do is to extend ModelAttributeMethodProcessor
and modify it's behavior. Since the resolveArgument()
method is final you cannot override it. What we can do is override the following three methods:
supportsParameter(final MethodParameter parameter)
:Tells spring this resolver support arguments annotated with
@ValidModifyingVerb
.bindRequestParameters(final WebDataBinder binder, final NativeWebRequest request)
:Will be the perfect candidate to obtain a reference to the request and looking up it's request method. It also gives you the chance to omit binding the parameter when we don't need it to be.
validateIfApplicable(final WebDataBinder binder, final MethodParameter parameter)
:Gives you the opportunity to omit validation. If you need validation it will be automatically picked up since your own annotation starts with
"Valid"
is well.
public class ValidModifyingVerbMethodArgumentResolver extends ModelAttributeMethodProcessor {
private String requestMethod;
/**
* @param annotationNotRequired if "true", non-simple method arguments and
* return values are considered model attributes with or without a
* {@code @ModelAttribute} annotation.
*/
public ValidModifyingVerbMethodArgumentResolver(final boolean annotationNotRequired) {
super(annotationNotRequired);
}
@Override
public boolean supportsParameter(final MethodParameter parameter) {
return super.supportsParameter(parameter) && parameter.hasParameterAnnotation(ValidModifyingVerb.class);
}
@Override
protected void bindRequestParameters(final WebDataBinder binder, final NativeWebRequest request) {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
requestMethod = servletRequest.getMethod();
if (isModifyingMethod(requestMethod)) {
((ServletRequestDataBinder) binder).bind(servletRequest);
}
}
@Override
protected void validateIfApplicable(final WebDataBinder binder, final MethodParameter parameter) {
if (isModifyingMethod(requestMethod)) {
super.validateIfApplicable(binder, parameter);
}
}
private boolean isModifyingMethod(String method) {
return !"GET".equals(method);
}
}
The only thing left is to register ValidModifyingVerbMethodArgumentResolver
as a argument resolver in your application context configuration and you're done.
public class ApplicationConfiguration extends WebMvcConfigurerAdapter {
@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new ValidModifyingVerbMethodArgumentResolver(true));
}
}
The handler methods in your controller can now be reduced to:
@RequestMapping("/account/register")
public String registerPost(
@ValidModifyingVerb RegisterForm registerForm,
BindingResult result,
RedirectAttributes redirectAttributes) {
//... ADD USER HERE IF !result.hasErrors() ...
return "account/register";
}
回答2:
you can not remove the GET method at all as you want to use it to rendering jsp page for first time, what I'm doing is just adding only Model model
as method parameter, and use model.add("form", new MyForm());
, so there is no mapping in GET method. so your get method can be:
@RequestMapping( value = "/account/register" , method = RequestMethod.GET )
public String registerGet( Model model ) {
model.add("registerForm",new RegisterForm());
return "account/register";
}
回答3:
I don't know of a good way to remove the get method, since you need the Controller to do a specific action (instantiate the form bean and add it to the model) on a GET.
However, you can reduce the duplication quite a bit.
@Controller
@RequestMapping("/account/register") //move path to class level for all @RequestMapping methods to inherit
public class MyController {
//just add the form object to the model on GET; infer the view name from the url
@RequestMapping(method = RequestMethod.GET)
@ModelAttribute //let Spring pick a good name for it
public RegisterForm registerGet() {
return new RegisterForm();
}
@RequestMapping(method = RequestMethod.POST)
public void registerPost(
@ModelAttribute @Valid RegisterForm registerForm,
BindingResult result ,
RedirectAttributes redirectAttributes ) {
... ADD USER HERE IF !result.hasErrors() ...
// don't return anything and let spring infer the view name from the url
}
}
Relevant bits from the Spring 3.2 documentation:
The @ModelAttribute annotation can be used on @RequestMapping methods as well. In that case the return value of the @RequestMapping method is interpreted as a model attribute rather than as a view name. The view name is derived from view name conventions instead much like for methods returning void — see Section 17.12.3, “The View - RequestToViewNameTranslator”.
...
The DefaultRequestToViewNameTranslator is tasked with generating a logical view name from the URL of the request. In the case of the above RegistrationController, which is used in conjunction with the ControllerClassNameHandlerMapping, a request URL of
http://localhost/registration.html
results in a logical view name of registration being generated
来源:https://stackoverflow.com/questions/22310127/how-to-remove-redundant-spring-mvc-method-by-providing-post-only-valid