How to Customize Parameter Names When Binding Spring MVC Command Objects

How to customize parameter names when binding Spring MVC command objects?

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

}

How to set alias for request parameters in Spring-mvc?

Request parameter are bind by setters. You can add an extra setter with original parameter name. Something like:

public class MyReq {
private String name;
private int age;

public void setDifferentName(String differentName) {
this.name=differentName;
}
}

NOTE: it will work only if your parameter is camel case like differentName=abc. Will not work with different-name=abc.

Mapping all request params into an object in Spring Controller

Since it is an API design requirement, it should be clearly reflected in the corresponding DTO's and endpoints.

Usually, this kind of requirement stems from a parallel change and implies that the old type queries will be disabled during the contract phase.

You could approach the requirement by adding the required mapping "query-parameter-name-to-property-name" by adding it to the ContactDTO. The simplest way would be just to add an additional setter like below

public class ContactDTO {
private Long id;
private String name;
private Long eventId;

public void setEvent_id(Long eventId) {
this.eventId = eventId;
}
}

If you prefer immutable DTO's, then providing a proper constructor should work as well

@Value
public class ContactDTO {
private Long id;
private String name;
private Long eventId;

public ContactDTO(Long id, String name, String eventId, String event_id) {
this.id = id;
this.name = name;
this.eventId = eventId != null ? eventId : event_id;
}
}

Modify Spring MVC Request to List Parameter Binding to Not Separate on Commas

The conversion seems to happen in:

org.springframework.core.convert.support.StringToCollectionConverter

which must be a defult converter registered by the framework.

You can use an @InitBinder method in a controller or a Controller Advice to register converters however I am not sure how you override or disable this default converter.

The simplest thing to do then is just to fall back to accessing the param directly from the HttpServletRequest:

 @Controller
public class TestController {

@RequestMapping("/test")
public String test(@RequestParam("names") List<String> names, HttpServletRequest request) {

//1
System.out.println(request.getParameterValues("names").length);
System.out.println(Arrays.toString(request.getParameterValues("names")));

//2
System.out.println(names.size());
System.out.println(names);

return null;
}
}

After additional experimentation, you can disable the invocation of the framework's StringToCollectionConverter in Spring Boot by explicitly removing the conversion from String.class to Collection.class from the GenericConversionService:

@Autowired
void conversionService(GenericConversionService genericConversionService) {
List<String> names = genericConversionService.convert("Ed, Al", List.class);
System.out.println(names.size()); // 2

genericConversionService.removeConvertible(String.class, Collection.class);

names = genericConversionService.convert("Ed, Al", List.class);
System.out.println(names.size()); // 1

}

Spring MVC Binding Request parameters to POJO fields

Adding @ModelAttribute should bind the individual request parameters into your Item POJO.

public @ResponseBody Page<Item> get(@ModelAttribute Item probe)


Related Topics



Leave a reply



Submit