Jersey中单个值的多个@QueryParam键

时间:2015-12-30 08:53:31

标签: java jersey jax-rs query-parameters

是否可以在Jersey中为单个对象/变量提供多个 @QueryParam个键?

enter image description here

实际

@POST
public Something getThings(@QueryParam("customer-number") Integer n) {
    ...
}

所以,如果我在其工作的URL之后添加?customer-number=3

预期

如果我添加以下任何值,我想获得上述行为:

  • ?customer-number=3
  • ?customerNumber=3
  • ?customerNo=3

观测值:

  1. QueryParam注释如下所示:

    ...
    public @interface QueryParam {
       String value();
    }
    

    因此,它不能接受多个String值(如@Produces)。

  2. 以下方法允许用户同时使用具有相同含义的多个键(并且我希望它们之间具有" OR"条件):

    @POST
    public Something getThings(@QueryParam("customer-number") Integer n1,
                               @QueryParam("customerNumber") Integer n2,
                               @QueryParam("customerNo") Integer n3) {
        ...
    }
    
  3. 这样的事情不起作用:

    @POST
    public Something getThings(@QueryParam("customer-number|customerNumber|customerNo") Integer n) {
        ...
    }
    
  4. 我该怎么做?

    详细信息:

    • Jersey 2.22.1
    • Java 8

3 个答案:

答案 0 :(得分:1)

使用自定义@BeanParam

可能是最简单最简单的方法

首先定义自定义bean,将所有查询参数合并为:

class MergedIntegerValue {

  private final Integer value;

  public MergedIntegerValue(
      @QueryParam("n1") Integer n1,
      @QueryParam("n2") Integer n2,
      @QueryParam("n3") Integer n3) {
    this.value = n1 != null ? n1 
        : n2 != null ? n2 
        : n3 != null ? n3 
        : null;
    // Throw an exception if value == null ?
  }

  public Integer getValue() {
    return value;
  }
}

然后在资源方法中将其与@BeanParam一起使用:

public Something getThings(
  @BeanParam MergedIntegerValue n) {

  // Use n.getValue() ...
}

参考:https://jersey.java.net/documentation/latest/user-guide.html#d0e2403

答案 1 :(得分:1)

老实说:这不是Web服务的设计方式。你制定了严格的合同,客户和服务器都遵循这个合同;你定义了一个参数,那就是它。

但当然,这将是一个完美的世界,你可以自由地决定将会发生什么。因此,如果您必须允许三个参数,那么您必须签订合同。这是遵循方法#2的一种方法,我必须提供这种方法,而不能为goofs进行测试:

public Something getThings(@QueryParam("customer-number") Integer n1,
                           @QueryParam("customerNumber") Integer n2,
                           @QueryParam("customerNo") Integer n3) throws YourFailureException {

  Integer customerNumber = getNonNullValue("Customer number", n1, n2, n3);
  // things with stuff
}

private static Integer getNonNullValue(String label, Integer... params) throws YourFailureException {

  Integer value = null;

  for(Integer choice : params){
    if(choice != null){
      if(value != null){
        // this means there are at least two query parameters passed with a value
        throw new YourFailureException("Ambiguous " + label + " parameters");
      }

      value = choice;
    }
  }

  if(value == null){
    throw new YourFailureException("Missing " + label + " parameter");
  }

  return value;
}

所以基本上拒绝任何没有通过其中一个参数的调用,并让异常映射器将您抛出的异常转换为4xx范围内的HTTP响应代码。

(我将getNonNullValue()方法设为静态,它让我觉得它是一个可重用的实用函数。)

答案 2 :(得分:1)

您可以创建自定义注释。我不会过多地介绍如何操作,您可以看到this documentationthis post。基本上它依赖于与通常的泽西依赖注入不同的基础设施。您可以在Jersey项目中看到this post。这是所有注射提供者生活的地方,处理@XxxParam注射。如果检查源代码,您将看到实现完全相同。我上面提供的两个链接遵循相同的模式,以及下面的代码。

我所做的是创建了一个自定义注释

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface VaryingParam {

    String value();

    @SuppressWarnings("AnnotationAsSuperInterface")
    public static class Factory
            extends AnnotationLiteral<VaryingParam> implements VaryingParam {

        private final String value;

        public static VaryingParam create(final String newValue) {
            return new Factory(newValue);
        }

        public Factory(String newValue) {
            this.value = newValue;
        }

        @Override
        public String value() {
            return this.value;
        }
    }
}

我有一个工厂来创建它可能看起来很奇怪,但这是实现下面代码所必需的,我将分割String的值,最后为每个分割值创建一个新的注释实例。

以下是ValueFactoryProvider(如果您已阅读上述任一文章,则会看到自定义方法参数注入需要这样做)。这是一个很大的类,只是因为我将所有必需的类放在一个类中,遵循您在Jersey项目中看到的模式。

public class VaryingParamValueFactoryProvider extends AbstractValueFactoryProvider {

    @Inject
    public VaryingParamValueFactoryProvider(
            final MultivaluedParameterExtractorProvider mpep,
            final ServiceLocator locator) {
        super(mpep, locator, Parameter.Source.UNKNOWN);
    }

    @Override
    protected Factory<?> createValueFactory(final Parameter parameter) {
        VaryingParam annotation = parameter.getAnnotation(VaryingParam.class);
        if (annotation == null) {
            return null;
        }
        String value = annotation.value();
        if (value == null || value.length() == 0) {
            return null;
        }
        String[] variations = value.split("\\s*\\|\\s*");
        return new VaryingParamFactory(variations, parameter);
    }

    private static Parameter cloneParameter(final Parameter original, final String value) {
        Annotation[] annotations = changeVaryingParam(original.getAnnotations(), value);
        Parameter clone = Parameter.create(
                original.getRawType(),
                original.getRawType(),
                true,
                original.getRawType(),
                original.getRawType(),
                annotations);
        return clone;
    }

    private static Annotation[] changeVaryingParam(final Annotation[] annos, final String value) {
        for (int i = 0; i < annos.length; i++) {
            if (annos[i] instanceof VaryingParam) {
                annos[i] = VaryingParam.Factory.create(value);
                break;
            }
        }
        return annos;
    }

    private class VaryingParamFactory extends AbstractContainerRequestValueFactory<Object> {

        private final String[] variations;
        private final Parameter parameter;
        private final boolean decode;
        private final Class<?> paramType;
        private final boolean isList;
        private final boolean isSet;

        VaryingParamFactory(final String[] variations, final Parameter parameter) {
            this.variations = variations;
            this.parameter = parameter;
            this.decode = !parameter.isEncoded();
            this.paramType = parameter.getRawType();
            this.isList = paramType == List.class;
            this.isSet = paramType == Set.class;
        }

        @Override
        public Object provide() {
            MultivaluedParameterExtractor<?> e = null;
            try {
                Object value = null;
                MultivaluedMap<String, String> params
                        = getContainerRequest().getUriInfo().getQueryParameters(decode);
                for (String variant : variations) {
                    e = get(cloneParameter(parameter, variant));
                    if (e == null) {
                        return null;
                    }
                    if (isList) {
                        List list = (List<?>) e.extract(params);
                        if (value == null) {
                            value = new ArrayList();
                        }
                        ((List<?>) value).addAll(list);
                    } else if (isSet) {
                        Set set = (Set<?>) e.extract(params);
                        if (value == null) {
                            value = new HashSet();
                        }
                        ((Set<?>) value).addAll(set);
                    } else {
                        value = e.extract(params);
                        if (value != null) {
                            return value;
                        }         
                    }
                }
                return value;
            } catch (ExtractorException ex) {
                if (e == null) {
                    throw new ParamException.QueryParamException(ex.getCause(),
                            parameter.getSourceName(), parameter.getDefaultValue());
                } else {
                    throw new ParamException.QueryParamException(ex.getCause(),
                            e.getName(), e.getDefaultValueString());
                }
            }
        }
    }

    private static class Resolver extends ParamInjectionResolver<VaryingParam> {

        public Resolver() {
            super(VaryingParamValueFactoryProvider.class);
        }
    }

    public static class Binder extends AbstractBinder {

        @Override
        protected void configure() {
            bind(VaryingParamValueFactoryProvider.class)
                    .to(ValueFactoryProvider.class)
                    .in(Singleton.class);
            bind(VaryingParamValueFactoryProvider.Resolver.class)
                    .to(new TypeLiteral<InjectionResolver<VaryingParam>>() {
                    })
                    .in(Singleton.class);
        }
    }
}

您需要在泽西岛注册此课程“Binder(课程底层)”才能使用它。

此类与Jersey this package的区别在于,它不是仅处理注释的单个String值,而是拆分该值,并尝试从查询参数映射中提取值。找到的第一个值将被返回。如果参数是ListSet,它会继续查找所有选项,并将它们添加到列表中。

在大多数情况下,这保留了@XxxParam注释所期望的所有功能。唯一难以实现的(因此我省略了支持此用例)是多个参数,例如

@GET
@Path("multiple")
public String getMultipleVariants(@VaryingParam("param-1|param-2|param-3") String value1,
                                  @VaryingParam("param-1|param-2|param-3") String value2) {
    return value1 + ":" + value2;
}

我实际上并不认为它应该难以实现,如果你真的需要它,只需要创建一个新的MultivaluedMap,如果找到它就删除一个值。这将在上面provide()的{​​{1}}方法中实施。如果您需要此用例,则可以使用VaryingParamFactoryList代替。

使用Jersey Test Framework查看完整测试用例QueryParamValueFactoryProvider(相当长)。您可以在Set中查看我测试的所有用例,并在测试QueryTestResource方法中使用Binder注册ResourceConfig