有什么方法可以知道 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 找到解析方法:
<块引用>- 有一个名为
valueOf
或fromString
的静态方法,它接受单个 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
方法内调用/代码>.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><版本>2.5.1</版本><inherited>true</inherited><配置><compilerArgument>-参数</compilerArgument><testCompilerArgument>-参数</testCompilerArgument><来源>1.8</来源><目标>1.8</目标></配置></插件>
如果您不这样做,对
Parameter.getName()
的调用将导致argX
,X
是参数.实现只允许
@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 :
- 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;
相关文章