Spring MVC 2.5: BindingResult probleempje
By: Jan-Kees van Andel, 9 October 2008Zojuist liep ik tegen een vervelend probleempje aan. Ik was bezig met het bouwen van een weblog met Spring 2.5 en alles ging goed, tot het laatste schermpje. Namelijk een klein schermpje om je password te wijzigen.
Dit is de controller:
@Controller @SessionAttributes("changePassword") public class ProfileController extends AbstractAdminController { public static final class ChangePasswordBean { private String password1; private String password2; // Getters and setters }; @Autowired private BlogService blogService; @RequestMapping(value="/admin/users/profile", method=RequestMethod.GET) public ModelAndView setupForm(ModelMap model, HttpSession session) { model.addAttribute("changePassword", new ChangePasswordBean()); return new ModelAndView("admin/users/profile", model); } @RequestMapping(value="/admin/users/profile", method=RequestMethod.POST) public String processSubmit( @ModelAttribute ChangePasswordBean changePassword, BindingResult result, HttpSession session, SessionStatus status) { new PasswordValidator().validate(changePassword, result); if (result.hasErrors()) { return "admin/users/profile"; } else { // Call to service layer return "redirect:/admin/home.html"; } } }
Dit is de JSP met het form:
... <form:form modelAttribute="changePassword"> <form:errors /> <table class="form"> <tr> <td><form:label for="password1" path="password1">Password</form:label></td> <td><form:password id="password1" maxlength="20" path="password1" /></td> <td><form:errors path="password1" /></td> </tr> <tr> <td><form:label for="password2" path="password2">Retype password</form:label></td> <td><form:password id="password2" maxlength="20" path="password2" /></td> <td><form:errors path="password2" /></td> </tr> <tr> <td colspan="3"> <input type="reset" value="Cancel" /> | <input type="submit" value="Save" /> </td> </tr> </table> </form:form> ...
Deze opzet werkte bij alle schermen, maar om de een of andere reden niet voor dit “simpele” schermpje. Het form openen ging goed, submitten ging ook goed, zolang er geen validatiefouten waren, maar bij validatiefouten kreeg ik de volgende melding:
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'changePassword' available as request attribute at org.springframework.web.servlet.support.BindStatus.<init>(BindStatus.java:141) at org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag.getBindStatus(AbstractDataBoundFormElementTag.java:172) at org.springframework.web.servlet.tags.form.AbstractHtmlElementTag.resolveCssClass(AbstractHtmlElementTag.java:412) at org.springframework.web.servlet.tags.form.AbstractHtmlElementTag.writeOptionalAttributes(AbstractHtmlElementTag.java:388) ...
Toch creemd. Ik had de JSP en de controller gewoon gekopieerd van een wel werkend scherm, maar om de een of andere reden werkte het toch niet. Ligt het aan de inner class? Nee, vast niet. Hoewel, het is wel een final class. Misschien wil Spring wel een proxy aanmaken en mag de class niet final zijn ofzo. Hup, final eraf. Scheisse, nog steeds hetzelfde probleem. Wat kan het dan zijn?
Oplossing
Toen viel mijn oog op de processSubmit methode in de controller. Daar week de naam van de parameter af van de klassenaam. Class name: ChangePasswordBean, argument name: changePassword.
Hmm, dat zal wel het probleem zijn, want bij alle andere schermen waren deze gelijk. Toevallig wist ik dat in de bytecode van de klasse geen parameternamen staan en dus ook niet @runtime uit te lezen zijn. De mannen van Spring zijn waarschijnlijk tegen dezelfde beperking aan gelopen en hebben hier omheen gewerkt door standaard de type name als request attribute name te gebruiken, in plaats van de parameter naam (wat logischer is, maar dus niet kan in Java).
Dit was de oplossing:
// Before fix (this didn't work) public String processSubmit( @ModelAttribute ChangePasswordBean changePassword, ...) { // After fix (working solution) public String processSubmit( @ModelAttribute("changePassword") ChangePasswordBean changePassword, ...) {
Dusss, gebruik in je JSP’s de klassenaam of geef expliciet de naam op in de @ModelAttribute annotation.


9 October 2008 om 2:47 pm
Tja, dat is het hele nadeel met ‘Convention over Configuration’, als je niet weet wat precies de conventie is werkt het ineens niet meer
Dan kan je lang uitzoeken wat het precies was…
Ben zelf trouwens absoluut geen fan van Spring MVC meer, zeker Spring Portlet MVC niet. Het is verschrikkelijk verwarrend met die Action en Render phase. Ook die model-attributes, soms zijn ze er wel in de action en render, soms niet, en de verwarrende @SessionAttributes, waarvan het de indruk wekt dat het de attributes in de sessie zet (maar niet doet). Argh, heeft veel tijd gekost om allemaal uit te zoeken.
De beste manier die wij nu gebruiken is @ModelAttribute(“changePassword”) op een getter zetten en daarna het object opnemen in je handling method:
doIets(ChangePasswordBean changePassword, BindingResult result, HttpSession session);
Maar toch gebruik ik liever andere web frameworks..
9 October 2008 om 3:12 pm
Tja, verder vind ik het wel prettig werken. Je hebt het framework binnen no-time door. Deze (simpele) blog, maar wel inclusief kleine admin module heb ik in 4 avondjes ofzo gebouwd.
Maar ik vind het wel wat te beperkt. Voor form based applicaties werkt het prima, maar als het iets complexer wordt, gebruik ik liever JSF. Zal wel het gevolg zijn van MVC frameworks.
9 October 2008 om 3:23 pm
Precies, dat is (en blijft) natuurlijk het nadeel van Request-based frameworks tegenover Component-based.
Wij zouden in eerste instantie gaan voor Wicket Portlets, maar hun Portlet ondersteuning is niet goed genoeg, teveel probleempjes mee gehad.
9 October 2008 om 3:29 pm
Hoor ik daar iemand JSF roepen?
10 October 2008 om 8:22 am
Ja, ikke!!
10 October 2008 om 8:42 pm
Die aparte Action en Render phase heeft niet zo veel te maken met Spring MVC maar meer met de Portlet spec.
Spring MVC is van de request based frameworks wat mij betreft een van de betere. Spring vergelijken met Wicket is appels met peren vergelijken.
21 October 2008 om 9:24 pm
Ben trouwens wel benieuwd hoe je een JUnit test voor je Controller schrijft, Jan-Kees… (Zonder Spring Container dus)
22 October 2008 om 9:46 am
Heb ik niet naar gekeken, maar qua achterkant zie ik geen probleem, gewoon een mock service injecteren.
En Spring kennende zullen ze wel een stel mocks hebben voor de servlet variabelen en zo. Anders gewoon zelf een mock maken voor BindingResult en SessionStatus.
Al vraag ik me nog steeds af wat de meerwaarde van een unit test is voor zo’n Controller.
22 October 2008 om 11:53 am
Dat kan dus niet op basis van jouw code.
Je hebt namelijk een private @Autorwired member. Zonder setter of constructor injectie.
De Controller is daarom niet correct geinitialiseerd te krijgen zonder Spring.
@Autowired op private members is leuk voor testclasses, maar nooit voor productiecode.
En de meerwaarde? Dat lijkt me evident de controller implementeert gedrag en dat wil je testen. Afhankelijk van het resultaat krijgt de gebruiker een andere view te zien. En er behoren bepaalde objecten in het model te zitten, anders gaat je view stuk.
22 October 2008 om 1:43 pm
Ja ok, maar een settertje is natuurlijk snel gemaakt. Goed punt btw. Heb ik niet op gelet.
De meerwaarde snap ik op zich wel, het is alleen een kosten/baten afweging. Als het veel werk is om een test op te zetten, bijv. omdat mocken lastig is, hoeft het van mij op zich niet.
22 October 2008 om 2:25 pm
Settertje? Constructor argument bedoel je.
Als het opzetten van een test te moeilijk is, dan is je ontwerp niet goed.
22 October 2008 om 3:06 pm
You got me there. Maar het kan ook iets zeggen over het ontwerp van de libraries waarvan je afhankelijk bent. Als het moeilijk is om een HttpSession te mocken, is mijn design dan niet goed of is bij de Servlet API een steekje laten vallen?
Gelukkig is HttpSession goed te mocken…