有没有办法知道在 Jersey @__Param fromString 处理程序中解析哪个参数? [英] Is there any way to know which parameter is being parsed in a Jersey @__Param fromString handler?

查看:20
本文介绍了有没有办法知道在 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);如果(空 == uuid){throw new InvalidParametersException(ImmutableList.of("Invalid uuid:" + uuidString));}返回新的 Base32UUID(uuid);}

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

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

但这会破坏 的约定意味着 Jersey 找到解析方法:

<块引用>

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

我还查看了 ParamConverterProvider 也可以注册以提供转换,但它似乎也没有添加足够的上下文.它提供的最接近的是 Annotations 数组,但从我所知道的注释来看,您无法从那里回溯以确定注释所在的变量或方法.我发现 thisthis 示例,但它们没有有效利用 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;@提供者公共类 Base32UUIDParamConverter 实现 ParamConverterProvider {@语境私有 javax.inject.Provider资源信息;private static final Set();annots.add(QueryParam.class);annots.add(FormParam.class);注释 = 集合.<类>unmodifiableSet(annots);}@覆盖公共 <T>ParamConverter <T>getConverter(Class 类型,类型类型1,注释[] 注释) {//检查是@FormParam 还是@QueryParamfor (Annotation annotation : annots) {如果 (!ANNOTATIONS.contains(annotation.annotationType())) {返回空;}}如果(Base32UUID.class == type){返回新的 ParamConverter() {@覆盖公共 T fromString(字符串值){尝试 {Method method = resourceInfo.get().getResourceMethod();Parameter[] 参数 = method.getParameters();参数 actualParam = null;//从方法中找到实际匹配的参数.for (Parameter param : 参数) {Annotation[] annotations = param.getAnnotations();if (matchingAnnotationValues(annotations, annots)) {实际参数 = 参数;}}//空警告,但假设我的逻辑是正确的,//null 应该是不可能的.也许无论如何都要检查:-)String paramName = actualParam.getName();System.out.println("参数名:" + paramName);Base32UUID uuid = new Base32UUID(value, paramName);返回 type.cast(uuid);} catch (Base32UUIDException ex) {throw new BadRequestException(ex.getMessage());} 捕捉(异常前){throw new InternalServerErrorException(ex);}}@覆盖公共字符串 toString(T t) {返回 ((Base32UUID) t).value;}};}返回空;}私有布尔匹配AnnotationValues(Annotation[] annots1,Annotation[] annots2) 抛出异常 {for (Class annotType : ANNOTATIONS) {如果(isMatch(annots1,annots2,annotType)){返回真;}}返回假;}private <T extends Annotation>boolean isMatch(Annotation[] a1,注释[] a2,类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);如果 (value1.equals(value2)) {返回真;}}返回假;}private <T extends Annotation>T getParamAnnotation(Annotation[] annotations,类参数类型) {T paramAnnotation = null;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><version>2.5.1</version><继承的>真</继承的><配置><compilerArgument>-参数</compilerArgument><testCompilerArgument>-参数</testCompilerArgument><来源>1.8</来源><目标>1.8</目标></配置></插件>

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

  • 实现只允许 @FormParam@QueryParam.

  • 需要注意的一件重要事情(我是通过艰难的方式学到的),是所有不属于't 在 ParamConverter 中处理(在这种情况下仅适用于 @QueryParam),将导致 404 没有对问题的解释.因此,如果您想要不同的行为,您需要确保处理您的异常.

<小时>

更新

上面的实现有一个bug:

//检查是@FormParam 还是@QueryParamfor (Annotation annotation : annots) {如果 (!ANNOTATIONS.contains(annotation.annotationType())) {返回空;}}

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

对上述代码的修复将类似于

boolean hasParamAnnotation = false;for (Annotation annotation : annots) {如果(注释.包含(注释.注释类型())){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;

这篇关于有没有办法知道在 Jersey @__Param fromString 处理程序中解析哪个参数?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆