有什么方法可以知道 Jersey @__Param fromString 处理程序中正在解析哪个参数?

我正在使用的 API 已决定接受 UUID 作为 Base32 编码的字符串,而不是 UUID.fromString() 期望.这意味着我不能简单地将 @QueryParam UUID myUuid 写为方法参数,因为转换会失败.

我正在通过编写一个自定义对象来解决这个问题,该对象使用不同的 fromString 转换器与 Jersey @QueryString@FormParam 注释.我希望能够在 fromString 方法中访问转换的上下文,以便提供更好的错误消息.现在,我能做的只有以下几点:

public static Base32UUID fromString(String uuidString) {最终的 UUID uuid = UUIDUtils.fromBase32(uuidString, false);if (null == uuid) {throw new InvalidParametersException(ImmutableList.of("无效的 uuid:" + uuidString));}返回新的 Base32UUID(uuid);}

我希望能够公开 which 参数的 UUID 无效,因此我记录的异常和返回的用户错误一目了然.理想情况下,我的转换方法会有一个额外的细节参数,如下所示:

public static Base32UUID fromString(字符串 uuidString,String parameterName//新参数?) {最终的 UUID uuid = UUIDUtils.fromBase32(uuidString, false);if (null == uuid) {throw new InvalidParametersException(ImmutableList.of("无效的 uuid:" + uuidString+ " 为参数 " + parameterName));}返回新的 Base32UUID(uuid);}

但这会打破 按惯例意味着 Jersey 找到解析方法:

<块引用>

  1. 有一个名为 valueOffromString 的静态方法,它接受单个 String 参数(例如,参见 Integer.valueOf(String)java.util.UUID.fromString(String));

我还查看了也可以注册以提供转换的 ParamConverterProvider,但它似乎也没有添加足够的上下文.它提供的最接近的是注解数组,但据我所知,您无法从那里回溯以确定注解所在的变量或方法.我发现 this 和 this 示例,但它们没有有效地使用 Annotations[] 参数或公开我可以看到的任何转换上下文.

有什么方法可以获取这些信息吗?还是我需要回退到端点方法中的显式转换调用?

如果有什么不同,我使用的是 Dropwizard 0.8.0,它使用的是 Jersey 2.16 和 Jetty 9.2.9.v20150224.

解决方案

所以这个可以用 ParamConverter/ParamConverterProvider.我们只需要注入一个 ResourceInfo.从那里我们可以获得资源Method,然后做一些反射.下面是一个我已经测试过并且大部分都可以使用的示例实现.

import java.lang.reflect.Type;导入java.lang.reflect.Method;导入java.lang.reflect.Parameter;导入 java.lang.annotation.Annotation;导入 java.util.Set;导入 java.util.HashSet;导入 java.util.Collections;导入 javax.ws.rs.FormParam;导入 javax.ws.rs.QueryParam;导入 javax.ws.rs.core.Context;导入 javax.ws.rs.ext.Provider;导入 javax.ws.rs.container.ResourceInfo;导入 javax.ws.rs.ext.ParamConverter;导入 javax.ws.rs.ext.ParamConverterProvider;导入 javax.ws.rs.BadRequestException;导入 javax.ws.rs.InternalServerErrorException;@Provider公共类 Base32UUIDParamConverter 实现 ParamConverterProvider {@语境私有 javax.inject.Provider<ResourceInfo>资源信息;私有静态最终集 annotType : ANNOTATIONS) {if (isMatch(annots1, annots2, annotType)) {返回真;}}返回假;}私有的<T扩展注解>boolean isMatch(Annotation[] a1,注释[] a2,类<T>aType) 抛出异常 {T p1 = getParamAnnotation(a1, aType);T p2 = getParamAnnotation(a2, aType);if (p1 != null && p2 != null) {String value1 = (String) p1.annotationType().getMethod("value").invoke(p1);String value2 = (String) p2.annotationType().getMethod("value").invoke(p2);if (value1.equals(value2)) {返回真;}}返回假;}私有的<T扩展注解>T getParamAnnotation(Annotation[] 注释,类<T>参数类型){T参数注释=空;for(注解注解:注解){if (annotation.annotationType() == paramType) {paramAnnotation = (T) 注释;休息;}}返回参数注解;}}

关于实现的一些说明

  • 最重要的部分是如何注入ResourceInfo.由于这需要在请求范围上下文中访问,我注入了 javax.inject.Provider,它允许我们懒惰地检索对象.当我们实际执行 get() 时,它将在请求范围内.

    需要注意的是 get() 必须 在 ParamConverter<的 fromString 方法内调用/代码>.ParamConverterProvidergetConverter 方法在应用程序加载过程中被多次调用,因此在此期间我们无法尝试调用 get().

  • 我使用的 java.lang.reflect.Parameter 类是 Java 8 类,因此要使用此实现,您需要使用 Java 8.如果您没有使用 Java 8,这篇文章可能有助于尝试以其他方式获取参数名称.

  • 与上面的一点有关,编译时需要应用编译器参数-parameters,才能访问形参名,指出这里.我只是把它放在链接中指出的 maven-cmpiler-plugin 中.

    <插件><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><版本>2.5.1</版本><inherited>true</inherited><配置><compilerArgument>-参数</compilerArgument><testCompilerArgument>-参数</testCompilerArgument><来源>1.8</来源><目标>1.8</目标></配置></插件>

    如果您不这样做,对 Parameter.getName() 的调用将导致 argXX 是参数.

  • 实现只允许 @FormParam@QueryParam.

  • 需要注意的重要一点(我学得很辛苦),所有例外't 在 ParamConverter 中处理(在这种情况下仅适用于@QueryParam),将导致 404 没有对问题的解释.因此,如果您想要不同的行为,您需要确保处理您的异常.

<小时>

更新

上面的实现有个bug:

//检查是@FormParam 还是@QueryParamfor(注解注解:注解){if (!ANNOTATIONS.contains(annotation.annotationType())) {返回空值;}}

在模型验证期间,当为每个参数调用 getConverter 时会调用上述方法.上面的代码只有一个注解才有效.如果除了 @QueryParam@FormParam 之外还有另一个注解,比如 @NotNull,它将失败.其余的代码都很好.它确实在假设将不止一个注释的情况下工作.

上述代码的修复方法类似于

boolean hasParamAnnotation = false;for(注解注解:注解){if (ANNOTATIONS.contains(annotation.annotationType())) {hasParamAnnotation = true;休息;}}if (!hasParamAnnotation) 返回空值;

The API I'm working with has decided to accept UUIDs as Base32 encoded strings, instead of the standard hexadecimal, dash separated format that UUID.fromString() expects. This means that I can't simply write @QueryParam UUID myUuid as a method parameter, as the conversion would fail.

I'm working around this by writing a custom object with a different fromString converter to be used with the Jersey @QueryString and @FormParam annotations. I would like to be able to access the context of the conversion in the fromString method so that I can provide better error messages. Right now, all I can do is the following:

public static Base32UUID fromString(String uuidString) {
    final UUID uuid = UUIDUtils.fromBase32(uuidString, false);
    if (null == uuid) {
        throw new InvalidParametersException(ImmutableList.of("Invalid uuid: " + uuidString));
    }
    return new Base32UUID(uuid);
}

I would like to be able to expose which parameter had the invalid UUID, so my logged exceptions and returned user errors are crystal clear. Ideally, my conversion method would have an extra parameter for details, like so:

public static Base32UUID fromString(
    String uuidString,
    String parameterName // New parameter?
) {
    final UUID uuid = UUIDUtils.fromBase32(uuidString, false);
    if (null == uuid) {
        throw new InvalidParametersException(ImmutableList.of("Invalid uuid: " + uuidString
            + " for parameter " + parameterName));
    }
    return new Base32UUID(uuid);
}

But this would break the by-convention means that Jersey finds a parsing method :

  1. Have a static method named valueOf or fromString that accepts a single String argument (see, for example, Integer.valueOf(String) and java.util.UUID.fromString(String));

I've also looked at the ParamConverterProvider that can also be registered to provide conversion, but it doesn't seem to add enough context either. The closest it provides is the an array of Annotations, but from what I can tell of the annotation, you can't backtrack from there to determine which variable or method the annotation is on. I've found this and this examples, but they don't make effective use of of the Annotations[] parameter or expose any conversion context that I can see.

Is there any way to get this information? Or do I need to fallback to an explicit conversion call in my endpoint method?

If it makes a difference, I'm using Dropwizard 0.8.0, which is using Jersey 2.16 and Jetty 9.2.9.v20150224.

解决方案

So this can be accomplished with a ParamConverter/ParamConverterProvider. We just need to inject a ResourceInfo. From there we can obtain the resource Method, and just do some reflection. Below is an example implementation that I've tested and works for the most part.

import java.lang.reflect.Type;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.annotation.Annotation;

import java.util.Set;
import java.util.HashSet;
import java.util.Collections;

import javax.ws.rs.FormParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.InternalServerErrorException;

@Provider
public class Base32UUIDParamConverter implements ParamConverterProvider {

    @Context
    private javax.inject.Provider<ResourceInfo> resourceInfo;

    private static final Set<Class<? extends Annotation>> ANNOTATIONS;

    static {
        Set<Class<? extends Annotation>> annots = new HashSet<>();
        annots.add(QueryParam.class);
        annots.add(FormParam.class);
        ANNOTATIONS = Collections.<Class<? extends Annotation>>unmodifiableSet(annots);
    }

    @Override
    public <T> ParamConverter<T> getConverter(Class<T> type, 
                                              Type type1,
                                              Annotation[] annots) {

        // Check if it is @FormParam or @QueryParam
        for (Annotation annotation : annots) {
            if (!ANNOTATIONS.contains(annotation.annotationType())) {
                return null;
            }
        }

        if (Base32UUID.class == type) {
            return new ParamConverter<T>() {

                @Override
                public T fromString(String value) {
                    try {
                        Method method = resourceInfo.get().getResourceMethod();

                        Parameter[] parameters = method.getParameters();
                        Parameter actualParam = null;

                        // Find the actual matching parameter from the method.
                        for (Parameter param : parameters) {
                            Annotation[] annotations = param.getAnnotations();
                            if (matchingAnnotationValues(annotations, annots)) {
                                actualParam = param;
                            }
                        }

                        // null warning, but assuming my logic is correct, 
                        // null shouldn't be possible. Maybe check anyway :-)
                        String paramName = actualParam.getName();
                        System.out.println("Param name : " + paramName);

                        Base32UUID uuid = new Base32UUID(value, paramName);
                        return type.cast(uuid);
                    } catch (Base32UUIDException ex) {
                        throw new BadRequestException(ex.getMessage());
                    } catch (Exception ex) {
                        throw new InternalServerErrorException(ex);
                    }
                }

                @Override
                public String toString(T t) {
                    return ((Base32UUID) t).value;
                }
            };
        }

        return null;
    }

    private boolean matchingAnnotationValues(Annotation[] annots1, 
                                             Annotation[] annots2) throws Exception {

        for (Class<? extends Annotation> annotType : ANNOTATIONS) {
            if (isMatch(annots1, annots2, annotType)) {
                return true;
            }
        }
        return false;
    }

    private <T extends Annotation> boolean isMatch(Annotation[] a1, 
                                                   Annotation[] a2, 
                                                   Class<T> aType) throws Exception {
        T p1 = getParamAnnotation(a1, aType);
        T p2 = getParamAnnotation(a2, aType);
        if (p1 != null && p2 != null) {
            String value1 = (String) p1.annotationType().getMethod("value").invoke(p1);
            String value2 = (String) p2.annotationType().getMethod("value").invoke(p2);
            if (value1.equals(value2)) {
                return true;
            }
        }

        return false;
    }

    private <T extends Annotation> T getParamAnnotation(Annotation[] annotations, 
                                                        Class<T> paramType) {
        T paramAnnotation = null;
        for (Annotation annotation : annotations) {
            if (annotation.annotationType() == paramType) {
                paramAnnotation = (T) annotation;
                break;
            }
        }
        return paramAnnotation;
    }
}

Some notes about the implementation

  • The most important part is how the ResourceInfo is injected. Since this needs to be accessed in a request scope context, I injected with javax.inject.Provider, which allows us to retrieve the object lazily. When we actually do get() it, it will be within a request scope.

    The thing to be cautious about is that it get() must be called inside the fromString method of the ParamConverter. The getConverter method of the ParamConverterProvider is called many times during application load, so we cannot try and call the get() during this time.

  • The java.lang.reflect.Parameter class I used is a Java 8 class, so in order to use this implementation, you need to be working on Java 8. If you are not using Java 8, this post may help in trying to get the parameter name some other way.

  • Related to the above point, the compiler argument -parameters needs to be applied when compiling, to be able to access the formal parameter name, as pointed out here. I just put it in the maven-cmpiler-plugin as pointed out in the link.

    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.5.1</version>
        <inherited>true</inherited>
        <configuration>
            <compilerArgument>-parameters</compilerArgument>
            <testCompilerArgument>-parameters</testCompilerArgument>
            <source>1.8</source>
            <target>1.8</target>
        </configuration>
    </plugin>
    

    If you don't do this, a call to Parameter.getName() will result in argX, X being the index of the parameter.

  • The implementation only allows for @FormParam and @QueryParam.

  • One important thing to note (that I learned the hard way), is that all exceptions that aren't handle in the ParamConverter (only applies to @QueryParam in this case), will lead to a 404 with no explanation of the problem. So you you need to make sure you handle your exception if you want a different behavior.


UPDATE

There is a bug in the above implementation:

// Check if it is @FormParam or @QueryParam
for (Annotation annotation : annots) {
    if (!ANNOTATIONS.contains(annotation.annotationType())) {
        return null;
    }
}

The above is called during model validation when getConverter is called for each parameter. The above code only works is there is only one annotation. If there is another annotation aside from @QueryParam or @FormParam, say @NotNull, it will fail. The rest of the code is fine. It does actually work under the assumption that there will be more than one annotation.

The fix to the above code, would be something like

boolean hasParamAnnotation = false;
for (Annotation annotation : annots) {
    if (ANNOTATIONS.contains(annotation.annotationType())) {
        hasParamAnnotation = true;
        break;
    }
}

if (!hasParamAnnotation) return null;

相关文章