How to configure parameter names when linking spring mvc objects

I have a command object:

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

What is related to spring -mvc:

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

Which works great for http://example.com/foo?jobType=permanent&location=Stockholm . But now I need to get it working for the following URL:
http://example.com/foo?jt=permanent&loc=Stockholm

Obviously, I do not want to modify my command object, because the field names must remain long (as they are used in the code). How can I customize this? Is there a way to do something like this:

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

This does not work ( @RequestParam cannot be applied to fields).

What I'm thinking of is a custom message converter similar to FormHttpMessageConverter and reading a custom annotation on the target

+57
java spring spring-mvc
Jan 24 '12 at 12:12
source share
10 answers

Here is what I got:

First, the resolver parameter:

 /** * 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 register the parameter recognizer using the post processor. It must be registered as <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; } } 
+13
Jan 25 '12 at 13:12
source share

This solution is more concise, but requires the use of the RequestMappingHandlerAdapter, which Spring is used when <mvc:annotation-driven /> enabled. Hope this helps someone. The idea is to extend ServletRequestDataBinder as follows:

  /** * 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()); } } } } 

Corresponding 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, use (e.g. Bozho's solution):

 public class Job { @ParamName("job-type") private String jobType; @ParamName("loc") private String location; } 
+26
May 13 '13 at 10:55
source share

In Spring 3.1, ServletRequestDataBinder provides a binding for additional binding values:

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

The ExtendedServletRequestDataBinder subclass uses it to add URI template variables as binding values. You can expand it further to add additional field aliases for the command line.

You can override RequestMappingHandlerAdapter.createDataBinderFactory (..) to provide your own instance of WebDataBinder. From the point of view of the controller, it might look like this:

 @InitBinder public void initBinder(MyWebDataBinder binder) { binder.addFieldAlias("jobType", "jt"); // ... } 
+8
Jan 30 2018-12-12T00:
source share

there is no convenient built-in way to do this; you can only choose the appropriate workaround. Difference between processing

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

and

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

lies in the fact that the task is a bean, and the stringjob is not (not surprisingly yet). The real difference is that beans are resolved using the standard Spring bean recognition engine, and string parameters are resolved using Spring MVC, which knows the concept of @RequestParam annotation. In short, there is no way in Spring bean's standard resolution to use classes such as PropertyValues, PropertyValue, GenericTypeAwarePropertyDescriptor) to resolve "jt" to the "jobType" property, or at least I don't know about that.

Workarounds, like others, suggested adding your own PropertyEditor or filter, but I think it just messed up the code. In my opinion, the purest solution would be to declare the class as follows:

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

then use this in your controller

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

UPDATE:

A slightly simpler option is not to expand, just add additional getters, setters to the source class

 public class Job { private String jobType; private String location; public String getJt() { return jobType; } public void setJt(String jt) { jobType = jt; } } 
+3
Jan 24 '12 at 17:18
source share

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

I would try to manipulate the binding itself.

This is done using WebDataBinder and is called from the HandlerMethodInvoker Object[] resolveHandlerArguments(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception

I do not have a deep look at Spring 3.1, but I saw that this part of Spring has changed a lot. Thus, it is possible to exchange WebDataBinder. In Spring 3.0, seams are not possible without redefining HandlerMethodInvoker .

+2
Jan 24 '12 at 12:38
source share

Try to intercept the request using the InterceptorAdaptor , and then using a simple check mechanism, decide whether to defer the request to the controller handler. Also wrap the HttpServletRequestWrapper around the request so that you can override getParameter() requests.

Thus, you can reassign the name of the actual parameter and its value back to the request that the controller will see.

Example:

 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. SelectedAnnotationHandlerMapping allows you to specify which handler will be accepted.

 <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 .

+1
Jan 24 2018-12-12T00:
source share

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

 public class MyRequest { @JsonUnwrapped private NestedObject nested; public NestedObject getNested() { return nested; } } 

And how to 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); ... } 

It's all. A bit of coding. In addition, you can name your usign @JsonProperty details.

+1
03 Feb '17 at 9:17
source share

There is a simple way: you can simply add another setter method, for example, setLoc, setJt.

+1
Feb 26 '17 at 7:04 on
source share

Thanks, answer @jkee.
Here is my solution.
First user annotation:

 @Inherited @Documented @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ParamName { /** * The name of the request parameter to bind to. */ String value(); } 

Client 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()); } } } } 

Parameter Converter:

 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, the component configuration for adding ParamNameProcessor to the first of the argument converters:

 @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); } 

All this.

+1
Jun 30 '18 at 5:02
source share

There is a slight improvement to answer.

To support inheritance, you should also parse 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 the fields of superclasses annotated by @ParamName. It also does not use the initBinder method with 2 parameters, which is not recommended for Spring 5.0. Everything else in jkee answer is ok.

0
Dec 17 '18 at 11:45
source share



All Articles