97

I have a command object:

public class Job {
    private String jobType;
    private String location;
}

Which is bound by spring-mvc:

@RequestMapping("/foo")
public String doSomethingWithJob(Job job) {
   ...
}

Which works fine for http://example.com/foo?jobType=permanent&location=Stockholm. But now I need to make it work for the following url instead:

http://example.com/foo?jt=permanent&loc=Stockholm

Obviously, I don't want to change my command object, because the field names have to remain long (as they are used in the code). How can I customize that? Is there an option to do something like this:

public class Job {
    @RequestParam("jt")
    private String jobType;
    @RequestParam("loc")
    private String location;
}

This doesn't work (@RequestParam can't be applied to fields).

The thing I'm thinking about is a custom message converter similar to FormHttpMessageConverter and read a custom annotation on the target object

kiedysktos
  • 3,910
  • 7
  • 31
  • 40
Bozho
  • 588,226
  • 146
  • 1,060
  • 1,140
  • 3
    Isn't there some "native" solution already in Spring 4? – Martin Ždila May 21 '15 at 15:00
  • Please help with my similar question here http://stackoverflow.com/questions/38171022/how-to-map-multiple-parameter-names-to-pojo-when-binding-spring-mvc-command-obje?noredirect=1#comment63769223_38171022 – SourceVisor Jul 03 '16 at 15:23
  • Upstream bug: [Add @FormAttribute attributes to customize x-www-form-urlencoded SPR-13433](https://github.com/spring-projects/spring-framework/issues/18012) – gavenkoa Feb 06 '22 at 13:36

11 Answers11

39

This solution more concise but requires using RequestMappingHandlerAdapter, which Spring use when <mvc:annotation-driven /> enabled. Hope it will help somebody. The idea is to extend ServletRequestDataBinder like this:

 /**
 * ServletRequestDataBinder which supports fields renaming using {@link ParamName}
 *
 * @author jkee
 */
public class ParamNameDataBinder extends ExtendedServletRequestDataBinder {

    private final Map<String, String> renameMapping;

    public ParamNameDataBinder(Object target, String objectName, Map<String, String> renameMapping) {
        super(target, objectName);
        this.renameMapping = renameMapping;
    }

    @Override
    protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
        super.addBindValues(mpvs, request);
        for (Map.Entry<String, String> entry : renameMapping.entrySet()) {
            String from = entry.getKey();
            String to = entry.getValue();
            if (mpvs.contains(from)) {
                mpvs.add(to, mpvs.getPropertyValue(from).getValue());
            }
        }
    }
}

Appropriate processor:

/**
 * Method processor supports {@link ParamName} parameters renaming
 *
 * @author jkee
 */

public class RenamingProcessor extends ServletModelAttributeMethodProcessor {

    @Autowired
    private RequestMappingHandlerAdapter requestMappingHandlerAdapter;

    //Rename cache
    private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<Class<?>, Map<String, String>>();

    public RenamingProcessor(boolean annotationNotRequired) {
        super(annotationNotRequired);
    }

    @Override
    protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
        Object target = binder.getTarget();
        Class<?> targetClass = target.getClass();
        if (!replaceMap.containsKey(targetClass)) {
            Map<String, String> mapping = analyzeClass(targetClass);
            replaceMap.put(targetClass, mapping);
        }
        Map<String, String> mapping = replaceMap.get(targetClass);
        ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), mapping);
        requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(paramNameDataBinder, nativeWebRequest);
        super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
    }

    private static Map<String, String> analyzeClass(Class<?> targetClass) {
        Field[] fields = targetClass.getDeclaredFields();
        Map<String, String> renameMap = new HashMap<String, String>();
        for (Field field : fields) {
            ParamName paramNameAnnotation = field.getAnnotation(ParamName.class);
            if (paramNameAnnotation != null && !paramNameAnnotation.value().isEmpty()) {
                renameMap.put(paramNameAnnotation.value(), field.getName());
            }
        }
        if (renameMap.isEmpty()) return Collections.emptyMap();
        return renameMap;
    }
}

Annotation:

/**
 * Overrides parameter name
 * @author jkee
 */

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ParamName {

    /**
     * The name of the request parameter to bind to.
     */
    String value();

}

Spring config:

<mvc:annotation-driven>
    <mvc:argument-resolvers>
        <bean class="ru.yandex.metrika.util.params.RenamingProcessor">
            <constructor-arg name="annotationNotRequired" value="true"/>
        </bean>
    </mvc:argument-resolvers>
</mvc:annotation-driven> 

And finally, usage (like Bozho solution):

public class Job {
    @ParamName("job-type")
    private String jobType;
    @ParamName("loc")
    private String location;
}
jkee
  • 693
  • 1
  • 6
  • 5
  • Appreciate your solution very much! One note: This preserves the functionality of the `DateTimeFormat` annotation, i.e. `@ParamName` annotated `Date` fields can addiotionally be annotated with `@DateTimeFormat(pattern = "yyyy-MM-dd")`. – Simon Oct 12 '15 at 13:51
  • 6
    For all Java-config fans, a Spring context configuration in Java would look like this: `@Configuration public class WebContextConfiguration extends WebMvcConfigurationSupport { @Override protected void addArgumentResolvers( List argumentResolvers) { argumentResolvers.add(renamingProcessor()); } @Bean protected RenamingProcessor renamingProcessor() { return new RenamingProcessor(true); } }` Note that `extends WebMvcConfigurationSupport` replaces `@EnableWebMvc` and ``. – Simon Oct 12 '15 at 13:54
  • Please help with my similar question here http://stackoverflow.com/questions/38171022/how-to-map-multiple-parameter-names-to-pojo-when-binding-spring-mvc-command-obje?noredirect=1#comment63769223_38171022 – SourceVisor Jul 03 '16 at 15:23
  • 2
    Is there anything as of spring 4.2 that could help make this any easier? – chrismarx Jul 27 '16 at 14:27
  • @chrismarx same question here somebody resolved this in Spring 4? – Gerard Ribas Oct 06 '16 at 18:44
  • @Simon have you tested your java-config with last Spring MVC (4.3.7)? I was trying to use it for params in my DTO as a `@ModelAttribute` in post rest controller and found that `RenamingProcessor.bindRequestParameters` method is never called. Might it work for params are taken from GET requests only? – rvit34 Mar 27 '17 at 13:45
  • No, I only tried it with Spring 3.2.x. Have you tried to add a breakpoint to the line `argumentResolvers.add(renamingProcessor())` to see if that is actually called? – Simon Mar 28 '17 at 07:32
  • @Simon Yeah. That code is called and renamingProcessor is added to resolvers but next nothing happens. – rvit34 Mar 28 '17 at 12:23
  • I solved my issue. Just need to remove @ModelAttribute – rvit34 Apr 01 '17 at 18:50
  • I'm facing the same problem as @rvit34. Removing `@ModelAttribute` doesn't work for me. Any other suggestions? – dtrunk Apr 25 '17 at 18:08
  • I've found the reason why `bindRequestParameters` method is never called: "When Spring needs to invoke your handler method, it'll iterate through the parameter types and through the above list and use the first one that supportsParameter()." As there's already a ServletModelAttributeMethodProcessor in the list before the custom argument resolver it picks up that one as first and ignores the rest. Source: http://stackoverflow.com/a/18944736/1163457 Now I only need to find a way how to workaround that "feature". – dtrunk May 04 '17 at 18:17
  • So removing `@ModelAttribute` annotation in conjunction with annotationNotRequired set to true works but that's not a suitable solution for me. – dtrunk May 04 '17 at 18:24
  • 2
    I've worked around by replacing `addArgumentResolvers` with a bean post processor: https://pastebin.com/07ws0uUZ – dtrunk May 04 '17 at 19:07
  • Note that this overrides the binder and thus the "state" of the binder *such as errors) is lost on return – Andrew White May 11 '17 at 14:30
  • to the solution of dtrunk with the bean post processor I have to add that the bean RenamingProcessor should be instantiate with "false" in the costructor, so it takes only the parameters in the signature that have an annotation. ( @ModelAttribute in this case ) `@Bean protected RenamingProcessor renamingProcessor() { return new RenamingProcessor(false); } }` – Massimo Sep 18 '17 at 06:38
  • Solution is not valid. You define class with Autowired in not a bean class. Adding Component requires creating some beans for annotationNotRequired and requestMappingHandlerAdapter – andrew17 May 29 '22 at 16:56
  • When I use this solution with spring-web 5.3.25 and using `WebMvcConfigurationSupport` I lose the functionality of the `DateTimeFormat` – John B Jul 25 '23 at 12:21
15

Here's what I got working:

First, a parameter resolver:

/**
 * This resolver handles command objects annotated with @SupportsAnnotationParameterResolution
 * that are passed as parameters to controller methods.
 * 
 * It parses @CommandPerameter annotations on command objects to
 * populate the Binder with the appropriate values (that is, the filed names
 * corresponding to the GET parameters)
 * 
 * In order to achieve this, small pieces of code are copied from spring-mvc
 * classes (indicated in-place). The alternative to the copied lines would be to
 * have a decorator around the Binder, but that would be more tedious, and still
 * some methods would need to be copied.
 * 
 * @author bozho
 * 
 */
public class AnnotationServletModelAttributeResolver extends ServletModelAttributeMethodProcessor {

    /**
     * A map caching annotation definitions of command objects (@CommandParameter-to-fieldname mappings)
     */
    private ConcurrentMap<Class<?>, Map<String, String>> definitionsCache = Maps.newConcurrentMap();

    public AnnotationServletModelAttributeResolver(boolean annotationNotRequired) {
        super(annotationNotRequired);
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        if (parameter.getParameterType().isAnnotationPresent(SupportsAnnotationParameterResolution.class)) {
            return true;
        }
        return false;
    }

    @Override
    protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
        ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
        ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
        bind(servletRequest, servletBinder);
    }

    @SuppressWarnings("unchecked")
    public void bind(ServletRequest request, ServletRequestDataBinder binder) {
        Map<String, ?> propertyValues = parsePropertyValues(request, binder);
        MutablePropertyValues mpvs = new MutablePropertyValues(propertyValues);
        MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
        if (multipartRequest != null) {
            bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
        }

        // two lines copied from ExtendedServletRequestDataBinder
        String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
        mpvs.addPropertyValues((Map<String, String>) request.getAttribute(attr));
        binder.bind(mpvs);
    }

    private Map<String, ?> parsePropertyValues(ServletRequest request, ServletRequestDataBinder binder) {

        // similar to WebUtils.getParametersStartingWith(..) (prefixes not supported)
        Map<String, Object> params = Maps.newTreeMap();
        Assert.notNull(request, "Request must not be null");
        Enumeration<?> paramNames = request.getParameterNames();
        Map<String, String> parameterMappings = getParameterMappings(binder);
        while (paramNames != null && paramNames.hasMoreElements()) {
            String paramName = (String) paramNames.nextElement();
            String[] values = request.getParameterValues(paramName);

            String fieldName = parameterMappings.get(paramName);
            // no annotation exists, use the default - the param name=field name
            if (fieldName == null) {
                fieldName = paramName;
            }

            if (values == null || values.length == 0) {
                // Do nothing, no values found at all.
            } else if (values.length > 1) {
                params.put(fieldName, values);
            } else {
                params.put(fieldName, values[0]);
            }
        }

        return params;
    }

    /**
     * Gets a mapping between request parameter names and field names.
     * If no annotation is specified, no entry is added
     * @return
     */
    private Map<String, String> getParameterMappings(ServletRequestDataBinder binder) {
        Class<?> targetClass = binder.getTarget().getClass();
        Map<String, String> map = definitionsCache.get(targetClass);
        if (map == null) {
            Field[] fields = targetClass.getDeclaredFields();
            map = Maps.newHashMapWithExpectedSize(fields.length);
            for (Field field : fields) {
                CommandParameter annotation = field.getAnnotation(CommandParameter.class);
                if (annotation != null && !annotation.value().isEmpty()) {
                    map.put(annotation.value(), field.getName());
                }
            }
            definitionsCache.putIfAbsent(targetClass, map);
            return map;
        } else {
            return map;
        }
    }

    /**
     * Copied from WebDataBinder.
     * 
     * @param multipartFiles
     * @param mpvs
     */
    protected void bindMultipart(Map<String, List<MultipartFile>> multipartFiles, MutablePropertyValues mpvs) {
        for (Map.Entry<String, List<MultipartFile>> entry : multipartFiles.entrySet()) {
            String key = entry.getKey();
            List<MultipartFile> values = entry.getValue();
            if (values.size() == 1) {
                MultipartFile value = values.get(0);
                if (!value.isEmpty()) {
                    mpvs.add(key, value);
                }
            } else {
                mpvs.add(key, values);
            }
        }
    }
}

And then registering the parameter resolver using a post-processor. It should be registered as a <bean>:

/**
 * Post-processor to be used if any modifications to the handler adapter need to be made
 * 
 * @author bozho
 *
 */
public class AnnotationHandlerMappingPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String arg1)
            throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String arg1)
            throws BeansException {
        if (bean instanceof RequestMappingHandlerAdapter) {
            RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
            List<HandlerMethodArgumentResolver> resolvers = adapter.getCustomArgumentResolvers();
            if (resolvers == null) {
                resolvers = Lists.newArrayList();
            }
            resolvers.add(new AnnotationServletModelAttributeResolver(false));
            adapter.setCustomArgumentResolvers(resolvers);
        }

        return bean;
    }

}
Bozho
  • 588,226
  • 146
  • 1,060
  • 1,140
  • It would be very helpful having an complete example of this, as Im not able to build the example above. – Ismar Slomic Dec 12 '13 at 22:15
  • Please help with my similar question here http://stackoverflow.com/questions/38171022/how-to-map-multiple-parameter-names-to-pojo-when-binding-spring-mvc-command-obje?noredirect=1#comment63769223_38171022 – SourceVisor Jul 03 '16 at 15:23
  • How could this incomplete code be be the accepted answer? It is missing several classes like `SupportsAnnotationParameterResolution`, `@CommandPattern` and `@SupportsCustomizedBinding`, as well as the imports for `Maps.*` and `Lists.*` – membersound Jun 06 '17 at 13:26
  • Ok, the SupportsAnnotationParameterResolution is ment to be `SupportsCustomizedBinding`. So when creating both annotations, the approach works! – membersound Jun 07 '17 at 13:20
11

In Spring 3.1, ServletRequestDataBinder provides a hook for additional bind values:

protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
}

The ExtendedServletRequestDataBinder subclass uses it to add URI template variables as binding values. You could extend it further to make it possible to add command-specific field aliases.

You can override RequestMappingHandlerAdapter.createDataBinderFactory(..) to provide a custom WebDataBinder instance. From a controller's perspective it could look like this:

@InitBinder
public void initBinder(MyWebDataBinder binder) {
   binder.addFieldAlias("jobType", "jt");
   // ...
}
Rossen Stoyanchev
  • 4,910
  • 23
  • 26
  • Thanks, but if I need to override .createDtaBinderFactory, this means I should replace the RequestMappingHandlerAdapter, which means that I can't use ``, right? – Bozho Jan 30 '12 at 21:50
  • You're welcome. Yes, with you can't plug a custom RequestMappingHandlerMapping. You can do it quite easily with the MVC Java config however. – Rossen Stoyanchev Feb 10 '12 at 13:56
  • @RossenStoyanchev: can you explain how can I plug in custom `MyWebDataBinder` with `@EnableWebMvc`? I see I have to subclass `ExtendedServletRequestDataBinder` and return it by subclassing `ServletRequestDataBinderFactory`. Now I can return this new factory by subclassing `RequestMappingHandlerAdapter` and overriding `createDataBinderFactory()`. But how can I force Spring MVC to use my subclassed `RequestMappingHandlerAdapter`? It is created in `WebMvcConfigurationSupport`... – Tomasz Nurkiewicz Mar 08 '12 at 22:42
  • 1
    @TomaszNurkiewicz, maybe you figured this out but if not see the section on advanced Java of XML-based Spring MVC configuration in the reference docs .. http://static.springsource.org/spring/docs/3.1.x/spring-framework-reference/html/mvc.html#mvc-config-advanced-java – Rossen Stoyanchev Apr 16 '12 at 18:21
  • @RossenStoyanchev: actually it wasn't urgent but thanks for your suggestion, it finally works, +1! – Tomasz Nurkiewicz Apr 21 '12 at 21:45
6

Thanks the answer of @jkee .
Here is my solution.
First, a custom annotation:

@Inherited
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ParamName {

  /**
   * The name of the request parameter to bind to.
   */
  String value();

}

A customer DataBinder:

public class ParamNameDataBinder extends ExtendedServletRequestDataBinder {

  private final Map<String, String> paramMappings;

  public ParamNameDataBinder(Object target, String objectName, Map<String, String> paramMappings) {
    super(target, objectName);
    this.paramMappings = paramMappings;
  }

  @Override
  protected void addBindValues(MutablePropertyValues mutablePropertyValues, ServletRequest request) {
    super.addBindValues(mutablePropertyValues, request);
    for (Map.Entry<String, String> entry : paramMappings.entrySet()) {
      String paramName = entry.getKey();
      String fieldName = entry.getValue();
      if (mutablePropertyValues.contains(paramName)) {
        mutablePropertyValues.add(fieldName, mutablePropertyValues.getPropertyValue(paramName).getValue());
      }
    }
  }

}

A parameter resolver:

public class ParamNameProcessor extends ServletModelAttributeMethodProcessor {

  @Autowired
  private RequestMappingHandlerAdapter requestMappingHandlerAdapter;

  private static final Map<Class<?>, Map<String, String>> PARAM_MAPPINGS_CACHE = new ConcurrentHashMap<>(256);

  public ParamNameProcessor() {
    super(false);
  }

  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    return parameter.hasParameterAnnotation(RequestParam.class)
        && !BeanUtils.isSimpleProperty(parameter.getParameterType())
        && Arrays.stream(parameter.getParameterType().getDeclaredFields())
        .anyMatch(field -> field.getAnnotation(ParamName.class) != null);
  }

  @Override
  protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
    Object target = binder.getTarget();
    Map<String, String> paramMappings = this.getParamMappings(target.getClass());
    ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), paramMappings);
    requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(paramNameDataBinder, nativeWebRequest);
    super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
  }

  /**
   * Get param mappings.
   * Cache param mappings in memory.
   *
   * @param targetClass
   * @return {@link Map<String, String>}
   */
  private Map<String, String> getParamMappings(Class<?> targetClass) {
    if (PARAM_MAPPINGS_CACHE.containsKey(targetClass)) {
      return PARAM_MAPPINGS_CACHE.get(targetClass);
    }
    Field[] fields = targetClass.getDeclaredFields();
    Map<String, String> paramMappings = new HashMap<>(32);
    for (Field field : fields) {
      ParamName paramName = field.getAnnotation(ParamName.class);
      if (paramName != null && !paramName.value().isEmpty()) {
        paramMappings.put(paramName.value(), field.getName());
      }
    }
    PARAM_MAPPINGS_CACHE.put(targetClass, paramMappings);
    return paramMappings;
  }

}

Finally, a bean configuration for adding ParamNameProcessor into the first of argument resolvers:

@Configuration
public class WebConfig {

  /**
   * Processor for annotation {@link ParamName}.
   *
   * @return ParamNameProcessor
   */
  @Bean
  protected ParamNameProcessor paramNameProcessor() {
    return new ParamNameProcessor();
  }

  /**
   * Custom {@link BeanPostProcessor} for adding {@link ParamNameProcessor} into the first of
   * {@link RequestMappingHandlerAdapter#argumentResolvers}.
   *
   * @return BeanPostProcessor
   */
  @Bean
  public BeanPostProcessor beanPostProcessor() {
    return new BeanPostProcessor() {

      @Override
      public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
      }

      @Override
      public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof RequestMappingHandlerAdapter) {
          RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
          List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>(adapter.getArgumentResolvers());
          argumentResolvers.add(0, paramNameProcessor());
          adapter.setArgumentResolvers(argumentResolvers);
        }
        return bean;
      }
    };
  }

}

Param pojo:

@Data
public class Foo {

  private Integer id;

  @ParamName("first_name")
  private String firstName;

  @ParamName("last_name")
  private String lastName;

  @ParamName("created_at")
  @DateTimeFormat(pattern = "yyyy-MM-dd")
  private Date createdAt;

}

Controller method:

@GetMapping("/foos")
public ResponseEntity<List<Foo>> listFoos(@RequestParam Foo foo, @PageableDefault(sort = "id") Pageable pageable) {
  List<Foo> foos = fooService.listFoos(foo, pageable);
  return ResponseEntity.ok(foos);
}

That's all.

Allen Kerr
  • 153
  • 1
  • 6
  • How is this different from what @jkee contributed? – Frans May 14 '20 at 15:07
  • How you autowiring in the nonbean class?????? – andrew17 May 29 '22 at 16:59
  • paramNameProcessor (field private org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter com.example.jpastudy.config.ParamNameProcessor.requestMappingHandlerAdapter) ↑ ↓ | requestMappingHandlerAdapter defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class] – andrew17 May 29 '22 at 17:06
5

There is a simple way, you can just add one more setter method, like "setLoc,setJt".

Jack
  • 111
  • 2
  • 4
5

there is no nice built in way to do it, you can only choose which workaround you apply. The difference between handling

@RequestMapping("/foo")
public String doSomethingWithJob(Job job)

and

@RequestMapping("/foo")
public String doSomethingWithJob(String stringjob)

is that job is a bean and stringjob isn't (no surprise so far). The real difference is that beans are resolved with the standard Spring bean resolver mechanism, while string params are resolved by spring MVC that knows the concept of the @RequestParam annotation. To make the long story short there is no way in the standard spring bean resolution (that is using classes like PropertyValues, PropertyValue, GenericTypeAwarePropertyDescriptor) to resolve "jt" to a property called "jobType" or at least I dont know about it.

The workarounds coud be as others suggested to add a custom PropertyEditor or a filter, but I think it just messes up the code. In my opinion the cleanest solution would be to declare a class like this :

public class JobParam extends Job {
    public String getJt() {
         return super.job;
    }

    public void setJt(String jt) {
         super.job = jt;
    }

}

then use that in your controller

@RequestMapping("/foo")
public String doSomethingWithJob(JobParam job) {
   ...
}

UPDATE :

A slightly simpler option is to not to extend, just add the extra getters, setters to the original class

public class Job {

    private String jobType;
    private String location;

    public String getJt() {
         return jobType;
    }

    public void setJt(String jt) {
         jobType = jt;
    }

}
Nick
  • 11,475
  • 1
  • 36
  • 47
Peter Szanto
  • 7,568
  • 2
  • 51
  • 53
4

You can use Jackson com.fasterxml.jackson.databind.ObjectMapper to convert any map to your DTO/POJO class with nested props. You need annotate your POJOs with @JsonUnwrapped on nested object. Like this:

public class MyRequest {

    @JsonUnwrapped
    private NestedObject nested;

    public NestedObject getNested() {
        return nested;
    }
}

And than use it like this:

@RequestMapping(method = RequestMethod.GET, value = "/myMethod")
@ResponseBody
public Object myMethod(@RequestParam Map<String, Object> allRequestParams) {

    MyRequest request = new ObjectMapper().convertValue(allRequestParams, MyRequest.class);
    ...
}

That's all. A little coding. Also, you can give any names to your props usign @JsonProperty.

Demel
  • 157
  • 1
  • 5
3

I would like to point you to another direction. But I do not know if it works.

I would try to manipulate the binding itself.

It is done by WebDataBinder and will be invoked from HandlerMethodInvoker method Object[] resolveHandlerArguments(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception

I have no deep look in Spring 3.1, but what I have seen, is that this part of Spring has been changed a lot. So it is may possible to exchange the WebDataBinder. In Spring 3.0 it seams not possible without overriding the HandlerMethodInvoker.

Ralph
  • 118,862
  • 56
  • 287
  • 383
  • Yup, that's where I'm investigating right now. I think I have a working solution, which I'll test tomorrow – Bozho Jan 24 '12 at 17:27
1

There's a little improvement to jkee's answer.

In order to support inheritance you should also analyze parent classes.

/**
 * ServletRequestDataBinder which supports fields renaming using {@link ParamName}
 *
 * @author jkee
 * @author Yauhen Parmon
 */
public class ParamRenamingProcessor extends ServletModelAttributeMethodProcessor {

    @Autowired
    private RequestMappingHandlerAdapter requestMappingHandlerAdapter;

    //Rename cache
    private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<>();

    public ParamRenamingProcessor(boolean annotationNotRequired) {
       super(annotationNotRequired);
    }

    @Override
    protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
        Object target = binder.getTarget();
        Class<?> targetClass = Objects.requireNonNull(target).getClass();
        if (!replaceMap.containsKey(targetClass)) {
            replaceMap.put(targetClass, analyzeClass(targetClass));
        }
        Map<String, String> mapping = replaceMap.get(targetClass);
        ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), mapping);
        Objects.requireNonNull(requestMappingHandlerAdapter.getWebBindingInitializer())
                .initBinder(paramNameDataBinder);    
        super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
    }

    private Map<String, String> analyzeClass(Class<?> targetClass) {
        Map<String, String> renameMap = new HashMap<>();
        for (Field field : targetClass.getDeclaredFields()) {
            ParamName paramNameAnnotation = field.getAnnotation(ParamName.class);
            if (paramNameAnnotation != null && !paramNameAnnotation.value().isEmpty()) {
               renameMap.put(paramNameAnnotation.value(), field.getName());
            }
        }
        if (targetClass.getSuperclass() != Object.class) {
            renameMap.putAll(analyzeClass(targetClass.getSuperclass()));
        }
        return renameMap;
    }
}

This processor will analyze fields of superclasses annotated with @ParamName. It also doesn't use initBinder method with 2 parameters which is deprecated as of Spring 5.0. All the rest in jkee's answer is OK.

Gene Parmon
  • 95
  • 10
1

Try intercepting request using InterceptorAdaptor, and then using simple checking mechanism decide whether to foward the request to the controller handler. Also wrap HttpServletRequestWrapper around the request, to enable you override the requests getParameter().

This way you can repass the actual parameter name and its value back to the request to be seen by the controller.

Example option:

public class JobInterceptor extends HandlerInterceptorAdapter {
 private static final String requestLocations[]={"rt", "jobType"};

 private boolean isEmpty(String arg)
 {
   return (arg !=null && arg.length() > 0);
 }

 public boolean preHandle(HttpServletRequest request,
   HttpServletResponse response, Object handler) throws Exception {

   //Maybe something like this
   if(!isEmpty(request.getParameter(requestLocations[0]))|| !isEmpty(request.getParameter(requestLocations[1]))
   {
    final String value =
       !isEmpty(request.getParameter(requestLocations[0])) ? request.getParameter(requestLocations[0]) : !isEmpty(request
        .getParameter(requestLocations[1])) ? request.getParameter(requestLocations[1]) : null;

    HttpServletRequest wrapper = new HttpServletRequestWrapper(request)
    {
     public String getParameter(String name)
     {
      super.getParameterMap().put("JobType", value);
      return super.getParameter(name);
     }
    };

    //Accepted request - Handler should carry on.
    return super.preHandle(request, response, handler);
   }

   //Ignore request if above condition was false
   return false;
   }
 }

Finally wrap the HandlerInterceptorAdaptor around your controller handler as shown below. The SelectedAnnotationHandlerMapping allows you to specify which handler will be interecepted.

<bean id="jobInterceptor" class="mypackage.JobInterceptor"/>
<bean id="publicMapper" class="org.springplugins.web.SelectedAnnotationHandlerMapping">
    <property name="urls">
        <list>
            <value>/foo</value>
        </list>
    </property>
    <property name="interceptors">
        <list>
            <ref bean="jobInterceptor"/>
        </list>
    </property>
</bean>

EDITED.

Bitmap
  • 12,402
  • 16
  • 64
  • 91
  • the target method is just 1, and it takes a Job object as argument. these strings are parameters, rather than locations – Bozho Jan 24 '12 at 12:54
  • The handler method takes the Job object which is the same object held by the `preHandle` method. Therefore checking your request parameter as shown above and returning true will cause the controller handler to proceed with the request. – Bitmap Jan 24 '12 at 13:04
  • I have edited the post to show how to intercept your job handler. – Bitmap Jan 24 '12 at 13:14
  • still, how would that set the proper parameters on the Job object? – Bozho Jan 24 '12 at 14:09
  • See edited answer - may be this might help. Wrap 'HttpServletRequestWrapper' around the request to enable you repass the parameter name and the value associated to it. – Bitmap Jan 24 '12 at 17:19
0

I tried the above but found that I was missing some configured functionality of the WebBinder so instead I created a solution that uses Jackson instead.

First I implemented an ArgumentResolver that looks for a specific class-level annotation UsingJacksonMapping to determine if it should be responsible for arguments of this type.


public class ObjectMapperParameterArgumentHandler implements HandlerMethodArgumentResolver {

  /**
   * Marks a Spring method argument parameter class as being marshalled via Jackson INSTEAD of
   * Spring's standard WebBinder. This can be helpful in order to use Jackson annotations for
   * property names and format. To use the argument resolver must be registered.
   *
   * <pre>
   * &#064;ObjectMapperParameterArgumentHandler.UsingJacksonMapping
   * </pre>
   */
  @Target(ElementType.TYPE)
  @Retention(RetentionPolicy.RUNTIME)
  @Documented
  public @interface UsingJacksonMapping {}

  private static final ObjectMapper OBJECT_MAPPER = ObjectMapperUtils.createObjectMapper();

  static {
    OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    OBJECT_MAPPER.configure(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS, true);
  }

  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    Class<?> parameterType = parameter.getParameterType();
    boolean result = markedWithUsingJacksonMapping(parameterType);
    if (result) {
      log.debug("Using Jackson mapping for {}", parameterType.getName());
    }
    return result;
  }

  private boolean markedWithUsingJacksonMapping(Class<?> type) {
    if (type.equals(Object.class)) {
      return false;
    } else if (type.getAnnotation(UsingJacksonMapping.class) != null) {
      return true;
    } else {
      return markedWithUsingJacksonMapping(type.getSuperclass());
    }
  }

  @Override
  public Object resolveArgument(
      MethodParameter parameter,
      ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest,
      WebDataBinderFactory binderFactory) {
    Map<String, String[]> parameterMap = webRequest.getParameterMap();
    log.trace("Converting params {}", parameterMap);

    Class<?> parameterType = parameter.getParameterType();
    Object result = OBJECT_MAPPER.convertValue(parameterMap, parameterType);
    log.debug("Result {}", result);
    return result;
  }
}

Next I add it as an argumentResolver:

@Configuration
public class ObjectMapperParameterArgumentResolverConfiguration extends WebMvcConfigurationSupport {

  @Override
  protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
    argumentResolvers.add(new ObjectMapperParameterArgumentHandler());
  }
}

This now allows for the following using Jackson annotations. It also allow for final fields (which would require initDirectFieldAccess using Spring):


@Value
@NoArgsConstructor(force = true)
@ObjectMapperParameterArgumentHandler.UsingJacksonMapping
public class DateBasedQueryParamDto  {

  @Nullable
  @JsonProperty("startDate")
  @JsonFormat(pattern = "yyyy-MM-dd")
  @Parameter(
      required = false,
      description =
          "Oldest UTC time in ISO date time format (eq 2022-07-08). Mutually exclusive to"
              + " daysPast")
  private final LocalDate startDate;

  @Nullable
  @JsonProperty("endDate")
  @JsonFormat(pattern = "yyyy-MM-dd")
  @Parameter(
      required = false,
      description =
          "Latest UTC time in ISO date time format (eq 2022-07-08). Mutually exclusive to "
              + "daysPast.")
  private final LocalDate endDate;
}

John B
  • 32,493
  • 6
  • 77
  • 98