【问题标题】:Mapping URL parameters with dashes to object in Spring Web MVC将带有破折号的 URL 参数映射到 Spring Web MVC 中的对象
【发布时间】:2016-02-28 16:43:48
【问题描述】:

如果您在请求中使用 camelCase 参数,则使用 Spring MVC 将 URL 请求参数映射到对象是相当简单的,但是当呈现以连字符分隔的值时,您如何将这些映射到对象?

参考示例:

控制器:

@RestController
public class MyController {

    @RequestMapping(value = "/search", method = RequestMethod.GET)
    public ResponseEntity<String> search(RequestParams requestParams) {
        return new ResponseEntity<>("my-val-1: " + requestParams.getMyVal1() + " my-val-2: " + requestParams.getMyVal2(), HttpStatus.OK);
    }

}

保存参数的对象:

public class RequestParams {

    private String myVal1;
    private String myVal2;

    public RequestParams() {}

    public String getMyVal1() {
        return myVal1;
    }

    public void setMyVal1(String myVal1) {
        this.myVal1 = myVal1;
    }

    public String getMyVal2() {
        return myVal2;
    }

    public void setMyVal2(String myVal2) {
        this.myVal2 = myVal2;
    }
}

这样的请求可以正常工作:

GET http://localhost:8080/search?myVal1=foo&myVal2=bar

但是,我想要的是带有连字符的请求映射到对象,如下所示:

GET http://localhost:8080/search?my-val-1=foo&my-val-2=bar

我需要在 Spring 中配置什么以将带有连字符的 url 请求参数映射到对象中的字段?请记住,我们可能有 许多 参数,因此使用每个字段的 @RequestParam 注释并不理想。

【问题讨论】:

    标签: java spring spring-mvc


    【解决方案1】:

    我扩展了 ServletRequestDataBinder 和 ServletModelAttributeMethodProcessor 来解决这个问题。

    考虑到您的域对象可能已经使用@JsonProperty 或@XmlElement 进行了注释以进行序列化。此示例假设是这种情况。但是您也可以为此目的创建自己的自定义注释,例如@MyParamMapping。

    带注释的域类的一个示例是:

    public class RequestParams {
    
        @XmlElement(name = "my-val-1" )
        @JsonProperty(value = "my-val-1")
        private String myVal1;
    
        @XmlElement(name = "my-val-2")
        @JsonProperty(value = "my-val-2")
        private String myVal2;
    
        public RequestParams() {
        }
    
        public String getMyVal1() {
            return myVal1;
        }
    
        public void setMyVal1(String myVal1) {
            this.myVal1 = myVal1;
        }
    
        public String getMyVal2() {
            return myVal2;
        }
    
        public void setMyVal2(String myVal2) {
            this.myVal2 = myVal2;
        }
    }
    

    您将需要一个 SerletModelAttributeMethodProcessor 来分析目标类、生成映射、调用您的 ServletRequestDataBinder。

        public class KebabCaseProcessor extends ServletModelAttributeMethodProcessor {
    
        public KebabCaseProcessor(boolean annotationNotRequired) {
            super(annotationNotRequired);
        }
    
        @Autowired
        private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
    
        private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<Class<?>, Map<String, String>>();
    
        @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);
            ServletRequestDataBinder kebabCaseDataBinder = new KebabCaseRequestDataBinder(target, binder.getObjectName(), mapping);
            requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(kebabCaseDataBinder, nativeWebRequest);
            super.bindRequestParameters(kebabCaseDataBinder, 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) {
                XmlElement xmlElementAnnotation = field.getAnnotation(XmlElement.class);
                JsonProperty jsonPropertyAnnotation = field.getAnnotation(JsonProperty.class);
                if (xmlElementAnnotation != null && !xmlElementAnnotation.name().isEmpty()) {
                    renameMap.put(xmlElementAnnotation.name(), field.getName());
                } else if (jsonPropertyAnnotation != null && !jsonPropertyAnnotation.value().isEmpty()) {
                    renameMap.put(jsonPropertyAnnotation.value(), field.getName());
                }
            }
            if (renameMap.isEmpty())
                return Collections.emptyMap();
            return renameMap;
        }
    }
    

    此 KebabCaseProcessor 将使用反射来获取您的请求对象的映射列表。然后它将调用 KebabCaseDataBinder - 传入映射。

    @Configuration
    public class KebabCaseRequestDataBinder extends ExtendedServletRequestDataBinder {
    
        private final Map<String, String> renameMapping;
    
        public KebabCaseRequestDataBinder(Object target, String objectName, Map<String, String> mapping) {
            super(target, objectName);
            this.renameMapping = mapping;
        }
    
        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());
                }
            }
        }
    }
    

    现在剩下的就是将此行为添加到您的配置中。以下配置覆盖了@EnableWebMVC 提供的默认配置,并将此行为添加到您的请求处理中。

    @Configuration
    public static class WebContextConfiguration extends WebMvcConfigurationSupport {
        @Override
        protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
            argumentResolvers.add(kebabCaseProcessor());
        }
    
        @Bean
        protected KebabCaseProcessor kebabCaseProcessor() {
            return new KebabCaseProcessor(true);
        }
    } 
    

    应归功于@Jkee。此解决方案源自他在此处发布的示例:How to customize parameter names when binding spring mvc command objects

    【讨论】:

    • 我发现删除 requestMappingHandlerAdapter 也可以
    • 看来这种方式只能将参数转换为原始类型字段,但“s1,s2,s3”不能再转换为List了。
    • 抱歉,需要 requestMappingHandlerAdapter。它用于初始化 binder 的 conversionService,没有它,你不能将 "s1,s2,s3" 转换为列表。但是我有问题将 requestMappingHandlerAdapter 自动连接到 KebabCaseProcessor。所以我通过代码修复它:kebabCaseDataBinder.setConversionService(binder.getConversionService());
    • 您应该将错误从 kebabCaseDataBinder 复制到外部活页夹。如果不这样做,将不会返回数据转换错误。它应该在 bindRequestParameters 的末尾: super.bindRequestParameters(kebabCaseDataBinder, nativeWebRequest); binder.getBindingResult().addAllErrors(kebabCaseDataBinder.getBindingResult());
    【解决方案2】:

    我能想到绕过连字符的一种方法是使用HttpServletRequestWrapper 类来包装原始请求。

    1. 解析该类中的所有请求参数,并将所有带连字符的参数转换为驼峰式。在此之后,spring 将能够自动将这些参数映射到您的 POJO 类。

      public class CustomRequestWrapper extends HttpServletRequestWrapper {
      
          private Map<String, String> camelCasedParams = new Hashmap();
          public CustomRequestWrapper(HttpServletRequest req){
              //Get all params from request.
              //Transform each param name from hyphenated to camel case
              //Put them in camelCasedParams; 
          }
      
          public String getParameter(String name){
              return camelCasedParams.get(name);
          }
      
          //Similarly, override other methods related to request parameters
      }
      
    2. 从 J2EE 过滤器中注入此请求包装器。关于使用过滤器注入请求包装器的教程,您可以参考下面的链接。

      http://www.programcreek.com/java-api-examples/javax.servlet.http.HttpServletRequestWrapper

    3. 更新您的 Web xml 以包含过滤器及其过滤器映射。

    【讨论】:

    • 感谢您的回答,它按要求解决了问题,但不幸的是,由于它实际上是在修改查询参数,因此会导致其他地方出现问题(即 Spring 的一些 HATEOAS 库)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-15
    • 2012-07-15
    • 1970-01-01
    • 1970-01-01
    • 2011-02-14
    • 1970-01-01
    相关资源
    最近更新 更多