有没有办法知道在 Jersey @__Param fromString 处理程序中解析哪个参数? [英] Is there any way to know which parameter is being parsed in a Jersey @__Param fromString handler?
问题描述
我正在使用的 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 找到解析方法:
<块引用>- 有一个名为
valueOf
或fromString
的静态方法,它接受单个 String 参数(例如,请参见Integer.valueOf(String)
和java.util.UUID.fromString(String))
;
我还查看了 ParamConverterProvider
也可以注册以提供转换,但它似乎也没有添加足够的上下文.它提供的最接近的是 Annotations 数组,但从我所知道的注释来看,您无法从那里回溯以确定注释所在的变量或方法.我发现 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;@提供者公共类 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 extends Annotation> 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
方法中调用/代码>.ParamConverterProvider
的getConverter
方法在应用程序加载过程中被多次调用,因此我们不能在这段时间内尝试调用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()
将导致argX
,X
是参数.实现只允许
@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 :
- Have a static method named
valueOf
orfromString
that accepts a single String argument (see, for example,Integer.valueOf(String)
andjava.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 withjavax.inject.Provider
, which allows us to retrieve the object lazily. When we actually doget()
it, it will be within a request scope.The thing to be cautious about is that it
get()
must be called inside thefromString
method of theParamConverter
. ThegetConverter
method of theParamConverterProvider
is called many times during application load, so we cannot try and call theget()
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 inargX
,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屋!