mirror of
https://github.com/jlengrand/helidon.git
synced 2026-03-10 08:21:17 +00:00
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:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user