I am upgrading from Spring 2.5 to Spring 3.2. I have a MVC Controller that previously extended CancellableFormController. It declared,separately, initBinder() and onBind() methods. I have refactored the Controller to use #Controller annotation, and switched the overridden initBinder() method to use Spring MVC 3 annotation #initBinder.
My specific question is, in Spring MVC 3, upgrading from Spring 2.5, how to refactor the overridden onBind() method to use an annotation equivalent? The signature of the existing method is:
#Override
protected void onBind(HttpServletRequest request, Object command, BindException errors) throws Exception {
MyCommand cmd = (MyCommand) command;
....
}
I thought about using #initBinder() and put the code previously in onBind() inside this annotated method. But my confusion here is:
Doing this, would the code be called at the same time in the overall framework process as before?
How to get a handle on the Command object from #initBinder annotated method.
Can I just declare it as another parameter in the signature of the method and Spring MVC framework will ensure I get a copy? It seems that in the old onBind() method the Command object has already been created (by formBackingObject method). Can I safely assume that this is also the case when using #initBinder method?
Thank you anyone for some insights. I am trying to get up to speed with the Spring MVC process flow!
My existing #initBinder method signature is something like:
#InitBinder
protected void initBinder(WebDataBinder binder) {
// register custom editor here.. etc
}
I am hoping I can do something like:
#InitBinder
protected void initBinder(WebDataBinder binder, MyCommand cmd) {
// register custom editor here.. etc
}
Is this the standard best practice approach for upgrading cancellableformcontroller onBind() method using annotations?
Attempt based on answer marked correct, still not working:
#InitBinder("myCommand")
protected void onBind(WebDataBinder binder) throws Exception {
MyCommand cmd = (MyCommand) binder.getTarget();
.... // do whatever was required here...
}
Work around
Please see my comments below to zeroflag. Create a private method with same logic as contained in onBind(), and then after validation of command object in onSubmit() annotated method (POST / GET) make a callout to the now defunct onBind() method passing your Command object in as a parameter. Something like the following:
#RequestMapping(method=RequestMethod.POST)
public ModelAndView onSubmit(#ModelAttribute("myCommand") MyCommand cmd, BindingResult result, <any other params> {
new MyCommandValidator().validate(cmd, result);
if (result.hasErrors()) {
return new ModelAndView("context");
}
onBind(cmd, <any other params>);
... // do business stuff
}
It appears to be an ugly workaround that was ok for my particular case.
You can access the command object with binder.getTarget(). #InitBinder is called for every model parameter of the controller method. Let's assume the following code
#RequestMapping("/somePath")
public String myMethod(#ModelAttribute("user") User user,
#ModelAttribute("info") Info info) {
Then your initBinder method is called at least twice: For the Userobject and for the Infoobject. To have it called only for a specific object add the model name:
#InitBinder("user")
That way it will only be called for the Userobject. Be aware, though, that the method might still be called more than once, the first time even with the target object being null.
If you want to ensure that some fields are not set automatically, then you can use setDisallowedFields() in your initBindermethod.
If you want to do some validation then let JSR 303 (Bean Validation) do that job.
Depending on what you want to do an #ModelAttribute method in your controller could be a solution:
#ModelAttribute("user")
public User createUser(HttpServletRequest request /*or whatever*/) {
User user = new User();
//check some parameters and set some attributes
return user;
}
Creates a user object before binding happens.
One last solution can be a message or type converter for your command object, that creates an object from the request. That's actually similar to the example above, but independent of the controller and there is no binding for an object created with a converter.
Related
I recently added AOP with aspectJ and spring-aop to my existent spring project. The goal was to actually intercept controller calls to modify the response they send back, in order to bind some values to this response I didn't want to add manually to each and everyone of my controllers, for example the expiration date of the actual token used by the end-user (which I wasn't even able to showcase within my controller in any case). I actually managed to get it working until I started my unit tests :
In my unit tests I call directly my controller methods using Reflection feature from java, then replicate usual process (calling the filter chain, pre handler and post handlers, and the controller method itself which is first manually validated using spring validator when annotation #Valid is present on one of my parameters. All this process works fine and gets executed properly). The problem is that now that the controller method is intercepted by spring-aop, it's mentionned as coming from the proxy controller created, and all of my parameters annotations disapear. Here is a controller example :
#Override
public ResponseEntity<Object> editPassword(#Valid #RequestBody PasswordEditForm passwordEditForm, HttpServletRequest request) {
return factorizedUserBaseController.editPassword(passwordEditForm, request, User.class);
}
the parameter PasswordEditForm has the annotation #Valid so in my test cases it was first validated before any other step, but now as I double checked it, the #Valid annotation is not present on the proxy method, and therefore the parameter doesn't get validated, any clue for how to fix this and make my parameters annotation still understandable from my test point of view?
Note : when running the spring through mvn spring-boot:run, parameters with #Valid annotation gets correctly validated and then goes to my error handler method properly.
Problem Solved : from several other stackoverflow posts I understand that CGLIB (aop proxy lib used by Spring) doesn't support annotations. ( see Retain annotations on CGLIB proxies?). But my problem wasn't here, I was literally sure I was finding the method using the controller class itself (the one I coded) but what I was wrong about is that I was giving the controller instance as a parameter to some other parts of my code which in turn would use this controller class to find the method which of course wasn't working because thanks to Spring proxies, it wasn't anymore my controller itself but a proxy class extending my own controller class. Instead, I just had to replace :
Class<?> controllerClass = controllerInstanciationContainer
.getController()
.getClass();
with
Class<?> controllerClass = controllerInstanciationContainer
.getController()
.getClass()
.getSuperclass();
I have a simple MVC controller that I annotate with my custom annotation:
#PreAuthorize("hasRole('GESTION_BENEFICIAIRE')")
#AuthentificationForte(otp = "#{args[0]}",transactionId="#{args[1]}")
#RequestMapping(value = "/ajouter", method = { RequestMethod.POST, RequestMethod.GET })
public String addBeneficiaire(#ModelAttribute("beneficiaireForm") BeneficiaireForm beneficiaireForm,
BindingResult result, Model model, Principal principal) {
[...]
}
My custom annotation is linked with an aspect that throws a RuntimeException when the validation doesn't succeed.
#Around(value = "#annotation(annotation)")
public Object verifyOtp(final ProceedingJoinPoint jointPoint,
final AuthentificationForte annotation) throws Throwable {
try {
if (authentificationForteEnabled) {
[...]
} else {
throw new AuthentificationForteException();
}
} else {
return jointPoint.proceed();
}
} finally {
}
}
So now the behavior is that when the validation fails, I am redirected to a 500 Error page. My goal is to stay in the same page and add a rejected message to the BindingResult:
result.rejectValue("suiteRib", "BeneficiaireForm.InvalidRib");
I haven't found a way to do that, the only way that I've found is to change all my logic and not use the annotation, while using a validation service with a try/catch in the controller code.
Is there any way to handle this and to access the binding result and add the error message when the aspect throws this exception?
Most definitely.
There is an example here of manipulating args:
Aspectj overwrite an argument of a method
You can also autowire services into the aspect class (remember to mark it #Configurable).
If you know the arguments before hand, then I think they can be included into the point cut definition, in which case they can be referred to directly in the around method. This is much nicer was as the arguments come strongly type.
You can read more here: http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html
May be its too late to answer your question but there are two ways you can handle it :
Have a try catch around proceed() in your aspect and when you get runtime exception you can either return the response from the aspect ( like a generic JSP showing the cause of error or generic JSON with error message.)
Second option could be to use Spring Exception Handler Controller advice. Spring MVC provides nice exception handler controller mechanism to call specific handler method for given type of exception class. ( Here is the blog link : https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc )
We currently have an application where we use mix of this approach to handle exception.
I want to validate my MyAccount form using a custom Spring Validator.
Basic validation rules are no problem.
I now have this requirement, which seems pretty obvious and common:
If the user (currently logged in) changes his username, I'll have to check if it's already in use. If it's not been changed, I'm fine (it would definitely be in use when checked, that's why I don't want to check it in that case). The problem is, that the validator is a Spring managed Singleton and I don't seem to have access to the current session (where I store my login context - i.e. not Spring Security). But I need the currently logged in user, in order to compare its e-mail with the one entered in the form.
This is my scenario, but the question is actually about how to validate using an object from the user's session, in general.
The ways I was thinking about solving this:
Do only basic validation in the Validator and do the rest, that I need the session for, in the Controller. Doesn't seem to be a nice solution.
#RequestMapping(value="/myaccount", method=RequestMethod.GET)
public String myAccount(#Valid MyAccountForm form, BindingResult result, HttpSession session)
{
boolean hasUsernameChanged = // check using session
if (hasUsernameChanged && CustomerService.customerAlreadyExists(form.getUsername()))
result.rejectValue("username", "my.error.code");
if (result.hasErrors()) {
// handle errors
} else {
// proceed
}
}
Adding a second validate method to the Validator like so
public void validateWithCurrentCustomer(Customer current) {
...
}
and call it explicitly from the controller, with the appropriate object. Not much better, but at least the validation logic is in one class, although separated in two methods, one of which is not standard.
Not having the Validator a Spring managed (singleton) bean, but create it everytime in the initBinder method. Instead of:
#Autowired
public MyAccountController(MyAccountFormValidator validator)
{
this.validator = validator;
}
#InitBinder
protected void initBinder (WebDataBinder binder)
{
binder.setValidator(validator);
}
do
#InitBinder
protected void initBinder (WebDataBinder binder, HttpSession session)
{
Customer current = ...// get from session
binder.setValidator(new MyAccountFormValidator(current));
}
But here, the problem is, that since the validator is not Spring managed, it's hard to get a service (like CustomerService) injected into the validator, for checking if an e-mail is available or not.
Am I missing something, any other ways to achieve what I want? This seems to be a common problem, but I couldn't find any pattern on Google or SO.
Thanks for your hints.
You may try to access any Spring bean from your validator. Maybe this answer can help.
Where should I place validation code that requires access to database?
Basically, you can make a SpringBeanUtil singleton, which gives you access to any bean you want.
Sorry about the title. I couldn't think of a better wording.
Is there any way to set the default Model that Spring will supply the page without first retrieving it as an argument in the #RequestMapping method?
I'm using aspects to take the return value of controller methods(returning the view) and insert it into the model, then rendering a different global view which then includes what I added into the model. This works fine on methods that request the Model as a parameter.
However, I also want to be able to catch all methods that don't explicitly request the model and still insert the return value into it (via #AfterReturning advice). Any ideas?
I would not use the #Autowired on HttpServletRequest as it will confuse future developers working on the code of threadsafety.
Instead you should use either a #ModelAttribute or an Interceptor.
#ModelAttribute
See:
http://static.springsource.org/spring/docs/3.1.x/spring-framework-reference/html/mvc.html#mvc-ann-modelattrib-method-args
But you can do something like this and add this method to your controller:
#ModelAttribute
public preloadModel(HttpServletRequest request, ModelMap model) {
//Add stuff to model.
}
Interceptor
See: http://static.springsource.org/spring/docs/3.1.x/spring-framework-reference/html/mvc.html#mvc-handlermapping-interceptor
public class PreloadModelInterceptor extends HandlerInterceptorAdapter {
#Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
// add model attibutes for your view to see but not your controller
}
}
Well, I found a workaround. Or maybe it's all that the underlying Spring framework is doing anyway. I just autowired in the HttpServletRequest and called setAttribute. Seems to work fine.
In ASP.NET MVC in the controller I can just have an object from my model be a parameter in one of my methods, and a form submission that gets handled by that method would automatically populate the object.
eg:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(User u){...}
The user object will automatically be populated for be from the form submission.
Is there a way to have this automatically happen using Spring MVC, and if so how do I do it?
In Spring MVC (with Spring MVC 2.5+ annotation-based configuration) it looks exactly the same way:
#RequestMapping(method = RequestMethod.POST)
public ModelAndView edit(User u) { ... }
The User object will be automatically populated. You may also explicitly specify the name of the corresponding model attribute with #ModelAttribute annotation (by default attribute name is a argument's class name with first letter decapitalized, i.e. "user")
... (#ModelAttrbiute("u") User u) ...
http://static.springsource.org/spring/docs/2.0.x/api/org/springframework/web/portlet/mvc/SimpleFormController.html#onSubmitAction(java.lang.Object)
Create a Form Controller, for example PriceIncreaseFormController and make it extend SimpleFormController
override the method public ModelAndView onSubmit(Object command)
there are many variants of the above. look for the right method that suits your need. For simple flow the above method should be sufficient.
Inside the method, you can typecast command and get your Command class.
commandObj = ((PriceIncrease) command)
commandObj will have the parameters populated by spring.
in your springapp-servlet.xml you should tell spring about the PriceIncrease command class as follows and also you should have a POJO for your command class created.
<bean name="/priceincrease.htm" class="springapp.web.PriceIncreaseFormController">
<property name="commandClass" value="springapp.service.PriceIncrease"/>
....
In Servlets no, but in Spring MVC absolutely. Take a look at the web framework docs.
Specifically Section 13.11.4, 9th bullet point.