27

I'm following this scheme in a Spring application.

  1. Request is sent to the server with the id of the object and some other params to be populated in this object
  2. The object with this id is loaded from the database
  3. getters and setters are invoked in this object to populate the values
  4. the object is then stored

I asked in this other question what was the best way to prepare the object before populate the params of the request. The answer was that the best way was to use a conversion service instead of doing it in a @ModelAtribute annotated method or with an editor in the initBinder.

So I have tried to use a converter, but I haven't found a similar example and I'm a little stuck. I have written a code like the one below: In the init binder I register the conversion service. So before populating the values on the User object convert() method is invoked to load the object from the database. The problem is that this configuration doen't work because it is converting the id (username field) of the Object User into an Object user, but then it tries to make a setUsername() with the object so I get a "java.lang.IllegalArgumentException: argument type mismatch".

Can anyone give me a clue or an example of the way of using the ConversionService to get the desired behaviour?

Thanks.

@Autowired
private ConversionService conversionService;

@InitBinder("user")
public void initBinder(@RequestParam("username")String username, WebDataBinder binder){
    binder.setConversionService(conversionService);
}

@RequestMapping(value="/user/save", method=RequestMethod.POST)
public String save(@ModelAttribute("user") User user, Model model) {        
    ...
}

with something like:

@Component
public class UserConversionService implements ConversionService{
    ...        
    @Override
    public Object convert(Object name, TypeDescriptor arg1, TypeDescriptor arg2) {
        return userService.find((String)name); 
    }
}
Community
  • 1
  • 1
Javi
  • 19,387
  • 30
  • 102
  • 135

2 Answers2

42

You're trying to implement a ConversionService to do the conversion between Strings and User objects. However, it's Converter implementations that do this part. What you want to do is:

  1. Write a Converter
  2. Register that converter with a ConversionService
  3. Make use of the ConversionService.

Your converter would be something like:

final class UserConverter implements Converter<String, User> {
    ...
    public User convert(String username) {
        return userService.find(username);
    }

}

You then need to register that converter. You can either write your own ConversionServiceFactoryBean or override the default:

<bean id="conversionService"
      class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <list>
            <bean class="example.UserConverter"/>
        </list>
    </property>
</bean>

If you want to use the ConversionService explicitly, as you have, just leave it as something that can be autowired. Spring and that factorybean definition will take care of the rest.

If, however, you're already using the <mvc:annotation-driven> tag in your context, you can use its conversion-service attribute to reference your ConversionServiceFactoryBean. You then don't need to have InitBinder or ConversionService in your class at all: by simply having a parameter of a @RequestMapping have your target type, User, the conversion will take place without you having to intervene.

GaryF
  • 23,950
  • 10
  • 60
  • 73
  • I have succeed with registering the conversion-service as you've said with , and I can call binder.convertIfNecessary(username, User.class) and the conveter is invoked. But it is not being called before populating the User. I don't get it invoked before the mapped method. What do you mean with "by simply having a parameter of a @RequestMapping have your target type, User, the conversion will take place". I have tried to add this param like this in the method: (@RequestParam("username")String username, @ModelAttribute("userTest")User user) but it doesn't seem to work. – Javi Dec 04 '10 at 00:17
  • Trying I have realized that if I do in my method public String save(@RequestParam("username")User user) it invokes the converter, but I don't want to remove @ModelAttribute annotation because I want to get values populated (and I cannot annotate the same parameter with both @ModelAttribute and @RequestParam annotations). any idea? – Javi Dec 04 '10 at 00:34
  • @Javi - Hmm, that's interesting behaviour. I would have expected both to work. As a temporary measure, you can give your method a Model parameter and explicitly add the converted user to the model. It's certainly not as neat, but it will definitely work. – GaryF Dec 04 '10 at 13:19
  • @Javai - A (very brief) look at the documents and what you're doing suggests that it's basically a naming issue. @ModelAttribute either puts the annotated parameter into the model with a default name based on the type, or you specify a name to use. I _think_ if you use the same name for the incoming parameter and the model attribute name (i.e. call it "user" everywhere, rather than the form submitting a "username" param) it should work. Still surprised that you can't use: "@ModelAttribute("user") @RequestParam("username") User user" – GaryF Dec 04 '10 at 13:32
  • @GaryF I have tried what you've said: sending from the form instead of username=Myusername I send user=Myusername, and in the controller method @ModelAttribute("user")User user, but with this configuration the converter is not invoked (it only populates the values). – Javi Dec 04 '10 at 16:27
  • There's an improvement opened in Spring to allow both annotations can be aplied to the same parameter: https://jira.springframework.org/browse/SPR-6801 – Javi Dec 04 '10 at 16:32
  • 1
    There is a Java configuration alternative to using the conversion-service mvc:annotation-driven approach. You will have defined an AnnotationMethodHandlerAdapter to enable annotation-driven configuration. This AnnotationMethodHandlerAdapter can have a ConfigurableWebBindingInitializer, and the ConfigurableWebBindingInitializer has a setConversionService method. – Mark Chorley Apr 03 '13 at 14:36
4

I did exactly what Gary is saying above and it worked:

I want to add some more information to the solution. As per the Spring documentation here a URI template variable gets translated to the target object using the Converter/ConversionService. I tried to use a @RequestParam("id") @ModelAttribute("contact") Contact contact, but I was getting an IllegalStateException: Neither BindingResult nor plain target object for bean name 'contact' available as request attribute for not having the model object contact in my view edit.jsp. This can be easily resolved by declaring a Model model and model.addAttribute(contact);. However, there is an even better way; using the URI template variable. It's strange why @RequestParam did not work.

DID NOT WORK

@RequestMapping("edit") //Passing id as .. edit?id=1
public String editWithConverter(@RequestParam("id") @ModelAttribute("contact") Contact contact){
    logger.info("edit with converter");
     return "contact/edit";
}

WHAT WORKED

@RequestMapping("edit/{contact}") //Passing id as .. edit/1
public String editWithConverter(@PathVariable("contact") @ModelAttribute("contact") Contact contact){ // STS gave a warning for using {contact} without @PathVariable 
    logger.info("edit with converter");
     return "contact/edit";
}

So what does this thing do .. a link like ...edit/1 implicitly invokes the converter for String '1' to Contact of id '1' conversion, and brings this contact object to the view. No need for @InitBinder and since its a Converter registered with the ConversionService I can use this anywhere I want - implicitly or explicitly.

Community
  • 1
  • 1
Ajay
  • 411
  • 4
  • 5