Support for non-String parameters in JAX-RS for native image (#2303)

* Support for non-String parameters (query, header, path) in JAX-RS for native image.

Signed-off-by: Tomas Langer <tomas.langer@oracle.com>
This commit is contained in:
Tomas Langer
2020-09-01 18:08:12 +02:00
committed by GitHub
parent 99ab6eb60f
commit 59a67b2658
5 changed files with 180 additions and 2 deletions

View File

@@ -36,6 +36,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.json.Json;
import javax.json.JsonArray;
@@ -50,9 +51,16 @@ import io.helidon.config.mp.MpConfigProviderResolver;
import com.oracle.svm.core.jdk.Resources;
import com.oracle.svm.core.jdk.proxy.DynamicProxyRegistry;
import io.github.classgraph.BaseTypeSignature;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.ClassRefTypeSignature;
import io.github.classgraph.FieldInfo;
import io.github.classgraph.MethodParameterInfo;
import io.github.classgraph.ReferenceTypeSignature;
import io.github.classgraph.ScanResult;
import io.github.classgraph.TypeArgument;
import io.github.classgraph.TypeSignature;
import org.graalvm.nativeimage.ImageSingletons;
import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.RuntimeReflection;
@@ -69,6 +77,20 @@ public class HelidonReflectionFeature implements Feature {
private static final String AT_ENTITY = "javax.persistence.Entity";
private static final String AT_REGISTER_REST_CLIENT = "org.eclipse.microprofile.rest.client.inject.RegisterRestClient";
private static final Map<Class<?>, Class<?>> PRIMITIVES_TO_OBJECT = new HashMap<>();
static {
PRIMITIVES_TO_OBJECT.put(byte.class, Byte.class);
PRIMITIVES_TO_OBJECT.put(char.class, Character.class);
PRIMITIVES_TO_OBJECT.put(double.class, Double.class);
PRIMITIVES_TO_OBJECT.put(float.class, Float.class);
PRIMITIVES_TO_OBJECT.put(int.class, Integer.class);
PRIMITIVES_TO_OBJECT.put(long.class, Long.class);
PRIMITIVES_TO_OBJECT.put(short.class, Short.class);
PRIMITIVES_TO_OBJECT.put(boolean.class, Boolean.class);
PRIMITIVES_TO_OBJECT.put(void.class, Void.class);
}
@Override
public boolean isInConfiguration(IsInConfigurationAccess access) {
return ENABLED;
@@ -119,6 +141,9 @@ public class HelidonReflectionFeature implements Feature {
// all classes, fields and methods annotated with @Reflected
addAnnotatedWithReflected(context);
// JAX-RS types required for headers, query params etc.
addJaxRsConversions(context);
// and finally register with native image
registerForReflection(context);
}
@@ -180,6 +205,112 @@ public class HelidonReflectionFeature implements Feature {
.collect(Collectors.toList());
}
private void addJaxRsConversions(BeforeAnalysisContext context) {
addJaxRsConversions(context, "javax.ws.rs.QueryParam");
addJaxRsConversions(context, "javax.ws.rs.PathParam");
addJaxRsConversions(context, "javax.ws.rs.HeaderParam");
}
private void addJaxRsConversions(BeforeAnalysisContext context, String annotation) {
traceParsing(() -> "Looking up annotated by " + annotation);
Set<Class<?>> allTypes = new HashSet<>();
// we need fields and method parameters
context.scan()
.getClassesWithFieldAnnotation(annotation)
.stream()
.flatMap(theClass -> theClass.getFieldInfo().stream())
.filter(field -> field.hasAnnotation(annotation))
.map(fieldInfo -> getSimpleType(context, fieldInfo))
.filter(Objects::nonNull)
.forEach(allTypes::add);
// method annotations
context.scan()
.getClassesWithMethodParameterAnnotation(annotation)
.stream()
.flatMap(theClass -> theClass.getMethodInfo().stream())
.flatMap(theMethod -> Stream.of(theMethod.getParameterInfo()))
.filter(param -> param.hasAnnotation(annotation))
.map(param -> getSimpleType(context, param))
.filter(Objects::nonNull)
.forEach(allTypes::add);
// now let's find all static methods `valueOf` and `fromString`
for (Class<?> type : allTypes) {
try {
Method valueOf = type.getDeclaredMethod("valueOf", String.class);
RuntimeReflection.register(valueOf);
traceParsing(() -> "Registering " + valueOf);
} catch (NoSuchMethodException ignored) {
try {
Method fromString = type.getDeclaredMethod("fromString", String.class);
RuntimeReflection.register(fromString);
traceParsing(() -> "Registering " + fromString);
} catch (NoSuchMethodException ignored2) {
}
}
}
}
private Class<?> getSimpleType(BeforeAnalysisContext context, MethodParameterInfo paramInfo) {
return getSimpleType(context, paramInfo::getTypeSignature, paramInfo::getTypeDescriptor);
}
private Class<?> getSimpleType(BeforeAnalysisContext context, FieldInfo fieldInfo) {
return getSimpleType(context, fieldInfo::getTypeSignature, fieldInfo::getTypeDescriptor);
}
private Class<?> getSimpleType(BeforeAnalysisContext context,
Supplier<TypeSignature> typeSignatureSupplier,
Supplier<TypeSignature> typeDescriptorSupplier) {
TypeSignature typeSignature = typeSignatureSupplier.get();
if (typeSignature == null) {
// not a generic type
TypeSignature typeDescriptor = typeDescriptorSupplier.get();
return getSimpleType(context, typeDescriptor);
}
ClassRefTypeSignature refType = (ClassRefTypeSignature) typeSignature;
List<TypeArgument> typeArguments = refType.getTypeArguments();
if (typeArguments.size() != 1) {
return getSimpleType(context, typeSignature);
}
TypeArgument typeArgument = typeArguments.get(0);
ReferenceTypeSignature ref = typeArgument.getTypeSignature();
if (ref == null) {
return Object.class;
}
return getSimpleType(context, ref);
}
private Class<?> getSimpleType(BeforeAnalysisContext context, TypeSignature typeSignature) {
// this is the type used
// may be: array, primitive type
if (typeSignature instanceof BaseTypeSignature) {
// primitive types
BaseTypeSignature bts = (BaseTypeSignature) typeSignature;
return toObjectType(bts.getType());
}
if (typeSignature instanceof ClassRefTypeSignature) {
ClassRefTypeSignature crts = (ClassRefTypeSignature) typeSignature;
return context.access().findClassByName(crts.getFullyQualifiedClassName());
}
return Object.class;
}
private static Class<?> toObjectType(Class<?> primitiveClass) {
Class<?> type = PRIMITIVES_TO_OBJECT.get(primitiveClass);
if (type == null) {
traceParsing(() -> "Failed to understand primitive type: " + primitiveClass);
type = Object.class;
}
return type;
}
private void addAnnotatedWithReflected(BeforeAnalysisContext context) {
// want to make sure we use the correct classloader
String annotation = Reflected.class.getName();

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@ import javax.json.JsonObject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.container.ResourceContext;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Configuration;
@@ -161,4 +162,16 @@ public class JaxRsResource {
public TestDto jsonBinding() {
return new TestDto("json-b");
}
@GET
@Path("/queryparam")
public String queryParam(@QueryParam("long") Long longParam) {
return String.valueOf(longParam);
}
@GET
@Path("/queryparam2")
public String queryParam(@QueryParam("boolean") Boolean booleanParam) {
return String.valueOf(booleanParam);
}
}

View File

@@ -190,6 +190,9 @@ public final class Mp1Main {
invoke(collector, "Rest client JSON-P", "json-p", aBean::restClientJsonP);
// + JSON-B
invoke(collector, "Rest client JSON-B", "json-b", aBean::restClientJsonB);
// + query params
invoke(collector, "Rest client Query param long", "1020", () -> aBean.restClientQuery(1020L));
invoke(collector, "Rest client Query param boolean", "true", () -> aBean.restClientQuery(true));
// Message from rest client, originating in BeanClass.BeanType
invoke(collector, "Rest client bean type", "Properties message", aBean::restClientBeanType);

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ import javax.json.JsonObject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@@ -45,4 +46,12 @@ public interface RestClientIface {
@Path("/jsonb")
@Produces(MediaType.APPLICATION_JSON)
TestDto jsonBinding();
@GET
@Path("/queryparam")
String queryParam(@QueryParam("long") Long longParam);
@GET
@Path("/queryparam2")
String queryParam(@QueryParam("boolean") Boolean booleanParam);
}

View File

@@ -1,5 +1,17 @@
/*
* Copyright (c) 2019, 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.tests.integration.nativeimage.mp1;
@@ -80,6 +92,16 @@ public class TestBean {
.getMessage();
}
public String restClientQuery(Long param) {
return restClient()
.queryParam(param);
}
public String restClientQuery(Boolean param) {
return restClient()
.queryParam(param);
}
@Fallback(fallbackMethod = "fallbackTo")
public String fallback() {
throw new RuntimeException("intentional failure");