如何在绑定spring mvc命令对象时自定义参数名称

我有一个命令对象:

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

哪一个是由spring-mvc绑定的:

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

这适用于http://example.com/foo?jobType=permanent&location=Stockholm 。 但是现在我需要使它适用于下面的url:
http://example.com/foo?jt=permanent&loc=Stockholm

显然,我不想改变我的命令对象,因为字段名称必须保持很长(因为它们在代码中使用)。 我如何定制? 有没有办法做这样的事情:

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

这不起作用( @RequestParam不能应用于字段)。

我在想的是一个类似于FormHttpMessageConverter的自定义消息转换器,并读取目标对象上的自定义注释

这个解决scheme更简洁,但需要使用RequestMappingHandlerAdapter,当<mvc:annotation-driven />启用时,Spring使用它。 希望它能帮助别人。 这个想法就是像这样扩展ServletRequestDataBinder:

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

适当的处理器:

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

注解:

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

弹簧configuration:

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

最后,使用(如Bozho解决scheme):

 public class Job { @ParamName("job-type") private String jobType; @ParamName("loc") private String location; } 

这是我的工作:

首先,一个参数parsing器:

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

然后使用后处理器注册参数parsing器。 它应该被注册为一个<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; } } 

在Spring 3.1中,ServletRequestDataBinder为附加绑定值提供了一个钩子:

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

ExtendedServletRequestDataBinder子类使用它来添加URI模板variables作为绑定值。 您可以进一步扩展,以便添加命令特定的字段别名。

您可以重写RequestMappingHandlerAdapter.createDataBinderFactory(..)以提供自定义的WebDataBinder实例。 从控制者的angular度来看,它可能是这样的:

 @InitBinder public void initBinder(MyWebDataBinder binder) { binder.addFieldAlias("jobType", "jt"); // ... } 

有没有好的方式来做到这一点,你只能select你应用的解决方法。 处理之间的区别

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

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

是那个工作是一个bean和stringjob不是(到目前为止没有意外)。 真正的区别在于bean使用标准的Spring beanparsing器机制来parsing,而string参数由Spring MVCparsing,它知道@RequestParam注解的概念。 长话短说,标准的spring bean解决scheme(即使用类PropertyValues,PropertyValue,GenericTypeAwarePropertyDescriptor)没有办法将“jt”parsing为名为“jobType”的属性,或者至less我不知道它。

这个解决方法就像其他人build议添加一个自定义的PropertyEditor或者一个filter一样,但是我认为这只是弄乱了代码。 在我看来,最简洁的解决scheme是宣布这样一个类:

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

然后在你的控制器中使用

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

更新:

一个稍微简单的选项是不要扩展,只是添加额外的获取者,设置者到原来的类

 public class Job { private String jobType; private String location; public String getJt() { return jobType; } public void setJt(String jt) { jobType = jt; } } 

我想指出另一个方向。 但是我不知道它是否有效

我会尝试操纵绑定本身。

它由WebDataBinder完成, WebDataBinderHandlerMethodInvoker方法调用Object[] resolveHandlerArguments(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception

我对Spring 3.1没有深入的了解,但是我所看到的是Spring的这个部分已经改变了很多。 所以有可能交换WebDataBinder。 在Spring 3.0中,如果不重写HandlerMethodInvoker就不可能实现接缝。

有一个简单的方法,你可以只添加一个setter方法,比如“setLoc,setJt”。

尝试使用InterceptorAdaptor拦截请求,然后使用简单的检查机制决定是否向控制器处理程序发送请求。 还要在请求周围包装HttpServletRequestWrapper ,以便覆盖请求getParameter()

这样,您可以将实际的参数名称及其值重新反馈给控制器可以看到的请求。

示例选项:

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

最后,将HandlerInterceptorAdaptor包装在控制器处理程序的周围,如下所示。 SelectedAnnotationHandlerMapping允许您指定哪个处理程序将被切断。

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

编辑

你可以使用Jackson的com.fasterxml.jackson.databind.ObjectMapper把任何地图转换成你的DTO / POJO类的嵌套道具。 你需要用@JsonUnwrapped在嵌套对象上注释你的POJO。 喜欢这个:

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

而不是像这样使用它:

 @RequestMapping(method = RequestMethod.GET, value = "/myMethod") @ResponseBody public Object myMethod(@RequestParam Map<String, Object> allRequestParams) { MyRequest request = new ObjectMapper().convertValue(allRequestParams, MyRequest.class); ... } 

就这样。 一点编码。 此外,你可以给你的道具 usign @JsonProperty 任何名称