问题描述
我和决定工作API接受的UUID为Base32 EN codeD字符串,而不是标准的十六进制,短线分隔格式,<$c$c>UUID.fromString()预计。这意味着,我不能简单地写 @QueryParam UUID myUuid
作为方法参数,作为转换会失败。
我解决这个工作通过编写自定义对象使用不同的 fromString
转换器与泽西 @QueryString 和
@FormParam
注释。我希望能够访问转换的背景下在 fromString
方法,这样我可以提供更好的错误消息。现在,我所能做的是以下内容:
公共静态Base32UUID fromString(字符串uuidString){
最终UUID UUID = UUIDUtils.fromBase32(uuidString,FALSE);
如果(空== UUID){
抛出新InvalidParametersException(ImmutableList.of(无效的UUID:+ uuidString));
}
返回新Base32UUID(UUID);
}
我希望能够揭露的这的参数有无效的UUID,所以我的记录异常和返回的用户错误都一清二楚。理想情况下,我的转换方法将对细节的额外的参数,像这样:
公共静态Base32UUID fromString(
串uuidString,
字符串参数名称//新参数?
){
最终UUID UUID = UUIDUtils.fromBase32(uuidString,FALSE);
如果(空== UUID){
抛出新InvalidParametersException(ImmutableList.of(无效的UUID:+ uuidString
+参数+参数名称));
}
返回新Base32UUID(UUID);
}
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;
这篇关于有没有什么办法知道哪些参数是在泽西@__Param fromString处理程序被解析?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!