Rossen Stoyanchev opened SPR-5500 and commented
A controller with a @ModelAttribute
method accepting a @PathVariable
cannot have methods without the path variable. This is problematic because it would make sense to be able to support the following using a single controller:
/carts/new /carts/{cartId}
This issue was raised in a forum thread: http://forum.springframework.org/showthread.php?t=67385
Affects: 3.0 M1
Reference URL: http://forum.springframework.org/showthread.php?t=67385
Issue Links:
- #19212 Allow @PathVariable
to be optional
1 votes, 7 watchers
Comment From: spring-projects-issues
Arjen Poutsma commented
I am afraid there is very little we can do here, any possibly solution will be very fragile. There are two workarounds, however:
- write two separate controllers, one with
@ModelAttribute
, one without - do the data binding manually in the handler methods, using
ServletRequestDataBinder
.
Comment From: spring-projects-issues
Roald Bankras commented
Can you explain why the ModelAttribute annotated method is called for every request?
It seems to me like overhead.
The beauty of the new Controller architected is that you can make one Controller class responsible for a particular resource.
This constraint through the @ModelAttribute
is destroying the architecture and thus sounds like a design flaw.
Comment From: spring-projects-issues
Arjen Poutsma commented
The underlying problem becomes clearer when you consider the following @Controller
:
@Controller
public class BasicController {
@ModelAttribute("cart")
public Cart prepareCart(@PathVariable long cartId) {}
@RequestMapping("/carts/{cartId}")
public Cart handleCart1(@ModelAttribute Cart cart) {}
@RequestMapping("/other/{cartId}")
public Cart handleCart2(@ModelAttribute Cart cart) {}
@RequestMapping("/carts")
public Cart handleCarts() {}
}
While it certainly possible to deduce the value of cartId in prepareCart() for the /carts/1 and /other/1 cases, you must also remember that @ModelAttribute
also indicates reference data. So when /carts is requested, handleCarts() is also invoked, and the cart returned from prepareCart() is to be placed in the model as reference data.
What we could do is invoke @ModelAttribute
methods conditionally, only when the @PathVariable
or @RequestParam
(required=true) can be bound. But - as the sample above indicates - it is not that simple. Alternatively, we can add the possibility of using @RequestParam
on @ModelAttribute
methods. That way, it is quite clear where - for instance - the path variables come from.
I have to think about this a bit more.
Comment From: spring-projects-issues
Rossen Stoyanchev commented
It would make sense to specify this information on the @ModelAttribute
method itself but IMO the meaning of @ModelAttribute
is too overloaded given you can place it both on a type and also on a method. It's not very intuitive and perhaps stretches the meaning of the annotation.
Perhaps we need a new annotation to more formally designate a method for invocation before or after request handling methods (e.g. @Interceptor
). This annotation would provide more transference and control over when it is invoked. I imagine it could have many of the same attributes as @RequestMapping
.
@Interceptor
(value="/*/{cartId}")
public Cart prepareCart(@PathVariable
long cartId) {}
Comment From: spring-projects-issues
Roald Bankras commented
I think the sample of Arjen shows exactly the problem.
Is it an option to give the @PathVariable
a required flag? Just like the @RequestParam
.
Comment From: spring-projects-issues
Arjen Poutsma commented
Adding a required flag would not help us. URI templates are always required, otherwise you shouldn't put them in the @RequestMapping
value. For instance, which requests should be handled by the following method?
@RequestMapping("/hotels/{hotelId}/bookings")
public void bookings(@PathVariable(value="hotelId", required=false) int hotelId) {
...
}
Obviously, this would match /hotels/1/bookings
, but also /hotels/bookings
? Overall, I think this only complicates matters more, and doesn't get us further.
Comment From: spring-projects-issues
Rossen Stoyanchev commented
Another example of where @ModelAttribute
poses challenges is if you need to bind different command objects. For example an AccountsController would have methods for editing an account but may also have methods for searching accounts, which may require binding to a different object (e.g. AccountSearchCriteria). As soon as you add a @ModelAttribute
method for preloading an account it becomes necessary to create a separate controller for searching accounts.
Comment From: spring-projects-issues
Ken Sipe commented
I agree with Rossen that the @ModelAttribute
is too overloaded. Consider this configuration for @ModelAttribute
annotation for a method; it would be nice if the annotation supported the ability to support a request method such as:
@ModelAttribute
(method=RequestMethod.GET)
public List\
@RequestMapping
(method=RequestMethod.GET)
public void setupForm(@ModelAttribute
Book book) {...}
@RequestMapping
(method=RequestMethod.POST)
public void processForm(@ModelAttribute
Book book) {...}
The ability to restrict the @ModelAttribute
for the method to just the GET removes the unnecessary call for the POST. This also provides more support for two annotations as this request method configuration would make no sense on a parameter of a request mapping.
Comment From: spring-projects-issues
Leomar Uztariz commented
I partially agree with Ken, but I think that the @ModelAttribute
should specify the information of the RequestMappings it applies for also, for example a configuration could be:
\
@ModelAttribute(method=RequestMethod.GET, requestmappings="/customers/list.htm,/customers/manage.htm")
public List<AnyObject> getAnyObjects(){...}
@ModelAttribute(requestmappings="/customers/edit.htm")
public Customer bindCustomer(@RequestParam(required = false, value = "customerId")Long customerId){...}
@RequestMapping("/customers/list.htm")
public ModelAndView listCustomers(){...}
@RequestMapping(method = RequestMethod.GET, value="/customers/manage.htm")
public ModelAndView manageCustomer(){...}
@RequestMapping(method = RequestMethod.POST, value="/customers/manage.htm")
public ModelAndView saveCustomer(@ModelAttribute("customer")Customer customer....){...}
\
That way we could specify for which requestmapping and methods the @ModelAttribute
should apply thus giving more flexibility and avoiding the current overhead, just my opinion
Comment From: spring-projects-issues
Rob Monie commented
I'd also love to see and elegant solution to this one - see my forum post for my scenario - http://forum.springsource.org/showthread.php?t=73432
On Arjen's advice, i'm using the ServletRequestDataBinder directly in my controller for PUT requests but this seems like something I shouldn't need to do.
Comment From: spring-projects-issues
Arjen Poutsma commented
After talking this over with Jürgen, we decided not to fix this, mainly because any solution would be worse (more complex) than the problem we're trying to solve in the first place.
Methods annotated with @ModelAttribute
are designed to apply to all methods in the controller, to remove repetitive code. If your binding logic does not apply to all methods, you can simply use a ServletRequestDataBinder to bind the request to your form object. You can even refactor this in another method, if you care about repetitiveness.
As an alternative to the data binder, you can use the new binding API which is currently worked on. We will try to provide a binding API that is as convenient as possible in such scenarios, but it will be an API to be used as part of a handler method impl in any case.
Comment From: spring-projects-issues
Adrian Ber commented
What about using @RequestMapping
on @ModelAttribute
methods?
Comment From: spring-projects-issues
Rossen Stoyanchev commented
Also remember that @PathVariable
is supported with Map<String, String>
as the method argument which gives you all available path variables. You can then check if the one you want is present.
Comment From: adrianboimvaser
Wouldn't it be possible to only invoke the @ModelAttribute
methods if the handler method has the appropriate parameters? I.e:
@RestController
@RequestMapping(path = "units")
public class UnitController {
@ModelAttribute
public Unit getModelAttribute(@PathVariable Long id) {
return unitService.findById(id);
}
@GetMapping
public List<Unit> search() {
...
}
@GetMapping("{id}")
public Unit findById(@ModelAttribute Unit unit) {
return unit;
}
getModelAttribute
would be invoked for method findById
because it returns a Unit
, which is the type of one of findById
's parameter, but it would not be invoked for method search
, which doesn't have an argument of type Unit
.