diff --git a/common/common/src/main/java/io/helidon/common/Reflected.java b/common/common/src/main/java/io/helidon/common/Reflected.java new file mode 100644 index 00000000000..50483be6042 --- /dev/null +++ b/common/common/src/main/java/io/helidon/common/Reflected.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 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.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A type annotated with this annotation will be added to native image with reflection support for all methods + * and fields (including private). + * A method or field annotated with this method will be added for reflection. + *

+ * This is an alternative to GraalVM native-image's {@code reflect-config.json} file, specific to Helidon. + * Processing of this annotation requires {@code io.helidon.integrations.graal:helidon-graal-native-image-extension} + * on the classpath when building native image. + *

+ * Constructors annotated with this annotation would only be added for reflection if either a field + * or a method is annotated as well (this is current limitation of API of native image). + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR}) +public @interface Reflected { +} diff --git a/dependencies/pom.xml b/dependencies/pom.xml index 6fc3004524c..02d5589f2a3 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -51,7 +51,7 @@ 2.3.1 1.30.5 2.3.3 - 19.2.0 + 20.0.0 1.27.1 28.1-jre 1.4.199 @@ -914,7 +914,7 @@ ${version.lib.graalvm} - com.oracle.substratevm + org.graalvm.nativeimage svm ${version.lib.graalvm} diff --git a/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/Pokemon.java b/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/Pokemon.java index b5384bf7fc7..456b28fbcb0 100644 --- a/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/Pokemon.java +++ b/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/Pokemon.java @@ -15,11 +15,13 @@ */ package io.helidon.examples.dbclient.pokemons; +import io.helidon.common.Reflected; + /** * POJO representing Pokemon. */ +@Reflected public class Pokemon { - private int id; private String name; private int idType; diff --git a/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/PokemonMain.java b/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/PokemonMain.java index a205f148f8c..4c8e147eb8c 100644 --- a/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/PokemonMain.java +++ b/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/PokemonMain.java @@ -36,7 +36,7 @@ */ public final class PokemonMain { - /** MongoDB configuration. Default configuration file {@code appliaction.yaml} contains MySQL/JDBC configuration. */ + /** MongoDB configuration. Default configuration file {@code appliaction.yaml} contains JDBC configuration. */ private static final String MONGO_CFG = "mongo.yaml"; /** Whether MongoDB support is selected. */ @@ -63,7 +63,7 @@ public static void main(final String[] args) throws IOException { System.out.println("MongoDB database selected"); mongo = true; } else { - System.out.println("MySQL/JDBC database selected"); + System.out.println("JDBC database selected"); mongo = false; } startServer(); diff --git a/examples/dbclient/pokemons/src/main/resources/META-INF/native-image/io.helidon.examples.dbclient/helidon-examples-dbclient-pokemon/reflect-config.json b/examples/dbclient/pokemons/src/main/resources/META-INF/native-image/io.helidon.examples.dbclient/helidon-examples-dbclient-pokemon/reflect-config.json deleted file mode 100644 index 938cca637df..00000000000 --- a/examples/dbclient/pokemons/src/main/resources/META-INF/native-image/io.helidon.examples.dbclient/helidon-examples-dbclient-pokemon/reflect-config.json +++ /dev/null @@ -1,9 +0,0 @@ -[{ - "name" : "io.helidon.examples.dbclient.pokemons.Pokemon", - "allDeclaredConstructors" : true, - "allPublicConstructors" : true, - "allDeclaredMethods" : true, - "allPublicMethods" : true, - "allDeclaredFields" : true, - "allPublicFields" : true -}] diff --git a/integrations/db/h2/pom.xml b/integrations/db/h2/pom.xml index eaf0d12b9e1..805a49de3b6 100644 --- a/integrations/db/h2/pom.xml +++ b/integrations/db/h2/pom.xml @@ -31,7 +31,7 @@ true @@ -44,7 +44,7 @@ h2 - com.oracle.substratevm + org.graalvm.nativeimage svm provided diff --git a/integrations/db/ojdbc/src/main/resources/META-INF/helidon/native-image/reflection-config.json b/integrations/db/ojdbc/src/main/resources/META-INF/helidon/native-image/reflection-config.json index 1140ef105c3..2428e6c1697 100644 --- a/integrations/db/ojdbc/src/main/resources/META-INF/helidon/native-image/reflection-config.json +++ b/integrations/db/ojdbc/src/main/resources/META-INF/helidon/native-image/reflection-config.json @@ -1,5 +1,7 @@ { "classes": [ - "org.hibernate.dialect.Oracle10gDialect" + "org.hibernate.dialect.Oracle10gDialect", + "oracle.jdbc.logging.annotations.Supports", + "oracle.jdbc.logging.annotations.Feature" ] } diff --git a/integrations/graal/mp-native-image-extension/pom.xml b/integrations/graal/mp-native-image-extension/pom.xml index 8c0fa56becd..0a6f27a1c0b 100644 --- a/integrations/graal/mp-native-image-extension/pom.xml +++ b/integrations/graal/mp-native-image-extension/pom.xml @@ -57,7 +57,7 @@ provided - com.oracle.substratevm + org.graalvm.nativeimage svm provided diff --git a/integrations/graal/native-image-extension/pom.xml b/integrations/graal/native-image-extension/pom.xml index 05603cb630d..8a3d3552954 100644 --- a/integrations/graal/native-image-extension/pom.xml +++ b/integrations/graal/native-image-extension/pom.xml @@ -30,7 +30,7 @@ true @@ -55,7 +55,7 @@ provided - com.oracle.substratevm + org.graalvm.nativeimage svm provided diff --git a/integrations/graal/native-image-extension/src/main/java/io/helidon/integrations/graal/nativeimage/extension/HelidonReflectionFeature.java b/integrations/graal/native-image-extension/src/main/java/io/helidon/integrations/graal/nativeimage/extension/HelidonReflectionFeature.java index 9e5a270a9bd..87a1d90954a 100644 --- a/integrations/graal/native-image-extension/src/main/java/io/helidon/integrations/graal/nativeimage/extension/HelidonReflectionFeature.java +++ b/integrations/graal/native-image-extension/src/main/java/io/helidon/integrations/graal/nativeimage/extension/HelidonReflectionFeature.java @@ -26,13 +26,14 @@ import java.util.Arrays; import java.util.Collection; import java.util.Enumeration; +import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Supplier; +import java.util.stream.Collectors; import javax.json.Json; import javax.json.JsonArray; @@ -42,6 +43,7 @@ import io.helidon.common.HelidonFeatures; import io.helidon.common.LogConfig; +import io.helidon.common.Reflected; import io.helidon.config.mp.MpConfigProviderResolver; import com.oracle.svm.core.annotate.AutomaticFeature; @@ -82,7 +84,6 @@ public void beforeAnalysis(BeforeAnalysisAccess access) { } catch (Exception e) { e.printStackTrace(); } - //LogConfig.initClass(); // make sure we print all the warnings for native image HelidonFeatures.nativeBuildTime(classLoader); @@ -107,6 +108,10 @@ public void beforeAnalysis(BeforeAnalysisAccess access) { // JPA Entity registration processEntity(context); + // all classes, fields and methods annotated with @Reflected + addAnnotatedWithReflected(context); + + // and finally register with native image registerForReflection(context); } @@ -115,6 +120,75 @@ public void beforeCompilation(BeforeCompilationAccess access) { MpConfigProviderResolver.buildTimeEnd(); } + @SuppressWarnings("unchecked") + private void addAnnotatedWithReflected(BeforeAnalysisContext context) { + FeatureImpl.BeforeAnalysisAccessImpl access = context.access(); + + // want to make sure we use the correct classloader + Class annotation = (Class) access.findClassByName(Reflected.class.getName()); + + traceParsing(() -> "Looking up annotated by " + annotation.getName()); + + // classes + access.findAnnotatedClasses(annotation) + .forEach(it -> { + traceParsing(() -> " class " + it.getName()); + context.register(it).addAll(); + }); + + // methods + access.findAnnotatedMethods(annotation) + .forEach(it -> { + if (context.register(it.getDeclaringClass()).add(it)) { + traceParsing(() -> " method " + it); + } + }); + + // fields + access.findAnnotatedFields(annotation) + .forEach(it -> { + if (context.register(it.getDeclaringClass()).add(it)) { + traceParsing(() -> " field " + it); + } + }); + + // attempt to add annotated constructors if any other field is registered + context.toRegister() + .forEach(register -> addAnnotatedConstructors(register, annotation)); + } + + private void addAnnotatedConstructors(Register register, Class annotation) { + // only do this if constructors is empty, as otherwise they are already all there + if (register.constructors.isEmpty()) { + try { + Constructor[] constructors = register.clazz.getConstructors(); + for (Constructor constructor : constructors) { + if (constructor.getAnnotation(annotation) != null) { + if (register.add(constructor)) { + traceParsing(() -> " constructor " + constructor); + } + } + } + constructors = register.clazz.getDeclaredConstructors(); + for (Constructor constructor : constructors) { + if (constructor.getAnnotation(annotation) != null) { + if (register.add(constructor)) { + traceParsing(() -> " constructor " + constructor); + } + } + } + } catch (NoClassDefFoundError e) { + if (TRACE) { + System.out.println("Constructors of " + + register.clazz.getName() + + " not added to reflection, as a type is not on classpath: " + + e.getMessage()); + } + } + } + } + + @SuppressWarnings("unchecked") private void processEntity(BeforeAnalysisContext context) { final Class annotation = (Class) context.access() .findClassByName(ENTITY_ANNOTATION_CLASS_NAME); @@ -131,7 +205,7 @@ private void processEntity(BeforeAnalysisContext context) { if (!Modifier.isPublic(declaredField.getModifiers()) && declaredField.getAnnotations().length == 0) { RuntimeReflection.register(declaredField); if (TRACE) { - System.out.println(" non annotaded field " + declaredField); + System.out.println(" non annotated field " + declaredField); } } } @@ -168,9 +242,8 @@ private void processRegisterRestClient(BeforeAnalysisContext context) { }); } - @SuppressWarnings("unchecked") private void registerForReflection(BeforeAnalysisContext context) { - Set> toRegister = context.toRegister(); + Collection toRegister = context.toRegister(); if (TRACE) { System.out.println("***********************************"); @@ -179,134 +252,73 @@ private void registerForReflection(BeforeAnalysisContext context) { } // register for reflection - for (Class aClass : toRegister) { - if (TRACE) { - System.out.println("Registering " + aClass.getName() + " for reflection"); - } - RuntimeReflection.register(aClass); - - registerMethods(aClass); + for (Register register : toRegister) { + register(register.clazz); - if (aClass.isInterface()) { - continue; + if (!register.clazz.isInterface()) { + register.fields.forEach(this::register); + register.constructors.forEach(this::register); } - registerFields(aClass); + register.methods.forEach(this::register); + } + } - registerConstructors(aClass); + private void register(Constructor constructor) { + if (TRACE) { + System.out.println(" " + constructor.getDeclaringClass().getSimpleName() + + "(" + + params(constructor.getParameterTypes()) + + ")"); } + RuntimeReflection.register(constructor); } - private void registerConstructors(Class aClass) { - try { - // find all public constructors - Set> constructors = new LinkedHashSet<>(Arrays.asList(aClass.getConstructors())); - // add all declared - constructors.addAll(Arrays.asList(aClass.getDeclaredConstructors())); + private String params(Class[] parameterTypes) { + if (parameterTypes.length == 0) { + return ""; + } + return Arrays.stream(parameterTypes) + .map(Class::getName) + .collect(Collectors.joining(", ")); + } - for (Constructor constructor : constructors) { - RuntimeReflection.register(constructor); - if (TRACE) { - System.out.println(" " + constructor); - } - } - } catch (NoClassDefFoundError e) { - if (TRACE) { - System.out.println("Constructors of " - + aClass.getName() - + " not added to reflection, as a type is not on classpath: " - + e.getMessage()); - } + private void register(Field field) { + if (TRACE) { + System.out.println(" " + field.getType() + " " + field.getName()); } + + RuntimeReflection.register(field); } - private void registerMethods(Class aClass) { - try { - Method[] methods = aClass.getMethods(); - for (Method method : methods) { - boolean register = true; - - // we do not want wait, notify etc - register = (method.getDeclaringClass() != Object.class); - - if (register) { - // we do not want toString(), hashCode(), equals(java.lang.Object) - switch (method.getName()) { - case "hashCode": - case "toString": - register = !hasParams(method); - break; - case "equals": - register = !hasParams(method, Object.class); - break; - default: - // do nothing - } - } + private void register(Method method) { + if (TRACE) { + System.out.println(" " + method.getReturnType().getName() + " " + method + .getName() + "(" + params(method.getParameterTypes()) + ")"); + } + RuntimeReflection.register(method); + } - if (register) { - if (TRACE) { - System.out.println(" " + method.getName() + "(" + Arrays.toString(method.getParameterTypes()) + ")"); - } - RuntimeReflection.register(method); - } - } - } catch (Throwable e) { - if (TRACE) { - System.out.println(" Cannot register methods of " + aClass.getName() + ": " + e.getClass().getName() + ": " + e - .getMessage()); - } + private void register(Class clazz) { + if (TRACE) { + System.out.println("Registering " + clazz.getName() + " for reflection"); } + + RuntimeReflection.register(clazz); } - private boolean hasParams(Method method, Class... params) { + private static boolean hasParams(Method method, Class... params) { Class[] parameterTypes = method.getParameterTypes(); return Arrays.equals(params, parameterTypes); } - private void registerFields(Class aClass) { - try { - // public fields - RuntimeReflection.register(aClass.getFields()); - } catch (NoClassDefFoundError e) { - if (TRACE) { - System.out.println("Public fields of " - + aClass.getName() - + " not added to reflection, as a type is not on classpath: " - + e.getMessage()); - } - } - try { - for (Field declaredField : aClass.getDeclaredFields()) { - // there may be fields referencing classes not on the classpath - if (!Modifier.isPublic(declaredField.getModifiers())) { - // public already registered - Annotation[] annotations = declaredField.getAnnotations(); - if (annotations.length > 0) { - RuntimeReflection.register(declaredField); - if (TRACE) { - System.out.println(" Annotated " + declaredField); - } - } - } - } - } catch (NoClassDefFoundError e) { - if (TRACE) { - System.out.println("Fields of " - + aClass.getName() - + " not added to reflection, as a type is not on classpath: " - + e.getMessage()); - } - } - } - private void addSingleClass(BeforeAnalysisContext context, Class theClass) { if (context.process(theClass)) { traceParsing(theClass::getName); traceParsing(() -> " Added for registration"); superclasses(context, theClass); - context.register(theClass); + context.register(theClass).addDefaults(); } } @@ -314,7 +326,7 @@ private void processClassHierarchy(BeforeAnalysisContext context, Class superclass) { // this class is always registered (interface or class) - context.register(superclass); + context.register(superclass).addDefaults(); traceParsing(() -> "Looking up implementors of " + superclass.getName()); @@ -376,7 +388,7 @@ private void processClasses(BeforeAnalysisContext context, List> classe traceParsing(() -> " Added for registration"); superclasses(context, aClass); - context.register(aClass); + context.register(aClass).addDefaults(); if (!Modifier.isFinal(modifiers)) { findSubclasses(context, aClass); @@ -397,7 +409,7 @@ private void superclasses(BeforeAnalysisContext context, Class aClass) { traceParsing(nextSuper::getName); traceParsing(() -> " Added for registration"); - context.register(nextSuper); + context.register(nextSuper).addDefaults(); nextSuper = nextSuper.getSuperclass(); } } else { @@ -491,8 +503,8 @@ private List> classes() { private static final class BeforeAnalysisContext { private final FeatureImpl.BeforeAnalysisAccessImpl access; private final Set> processed = new HashSet<>(); - private final Set> toRegister = new LinkedHashSet<>(); private final Set> excluded = new HashSet<>(); + private final Map, Register> registers = new HashMap<>(); private BeforeAnalysisContext(BeforeAnalysisAccess access, Set> excluded) { this.access = (FeatureImpl.BeforeAnalysisAccessImpl) access; @@ -507,16 +519,164 @@ public boolean process(Class theClass) { return processed.add(theClass); } - public void register(Class theClass) { - this.toRegister.add(theClass); + public Register register(Class theClass) { + return registers.computeIfAbsent(theClass, Register::new); } - public Set> toRegister() { - return toRegister; + public Collection toRegister() { + return registers.values(); } boolean isExcluded(Class theClass) { return excluded.contains(theClass); } } + + private static class Register { + private final Set methods = new HashSet<>(); + private final Set fields = new HashSet<>(); + private final Set> constructors = new HashSet<>(); + + private final Class clazz; + + private Register(Class clazz) { + this.clazz = clazz; + } + + boolean add(Method m) { + return methods.add(m); + } + + boolean add(Field f) { + return fields.add(f); + } + + boolean add(Constructor c) { + return constructors.add(c); + } + + void addAll() { + addMethods(); + if (clazz.isInterface()) { + return; + } + addConstructors(); + addFields(true); + } + + void addDefaults() { + addMethods(); + if (clazz.isInterface()) { + return; + } + addFields(false); + addConstructors(); + } + + private void addConstructors() { + try { + Constructor[] constructors = clazz.getConstructors(); + for (Constructor constructor : constructors) { + add(constructor); + } + } catch (NoClassDefFoundError e) { + if (TRACE) { + System.out.println("Public constructors of " + + clazz.getName() + + " not added to reflection, as a type is not on classpath: " + + e.getMessage()); + } + } + try { + // add all declared + Constructor[] constructors = clazz.getDeclaredConstructors(); + for (Constructor constructor : constructors) { + add(constructor); + } + } catch (NoClassDefFoundError e) { + if (TRACE) { + System.out.println("Constructors of " + + clazz.getName() + + " not added to reflection, as a type is not on classpath: " + + e.getMessage()); + } + } + } + + void addFields(boolean all) { + Field[] fields = clazz.getFields(); + + try { + // add all public fields + for (Field field : fields) { + add(field); + } + } catch (NoClassDefFoundError e) { + if (TRACE) { + System.out.println("Public fields of " + + clazz.getName() + + " not added to reflection, as a type is not on classpath: " + + e.getMessage()); + } + } + try { + for (Field declaredField : clazz.getDeclaredFields()) { + // there may be fields referencing classes not on the classpath + if (!Modifier.isPublic(declaredField.getModifiers())) { + // public already registered + if (all || declaredField.getAnnotations().length > 0) { + add(declaredField); + } + } + } + } catch (NoClassDefFoundError e) { + if (TRACE) { + System.out.println("Fields of " + + clazz.getName() + + " not added to reflection, as a type is not on classpath: " + + e.getMessage()); + } + } + } + + void addMethods() { + try { + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + boolean register; + + // we do not want wait, notify etc + register = (method.getDeclaringClass() != Object.class); + + if (register) { + // we do not want toString(), hashCode(), equals(java.lang.Object) + switch (method.getName()) { + case "hashCode": + case "toString": + register = !hasParams(method); + break; + case "equals": + register = !hasParams(method, Object.class); + break; + default: + // do nothing + } + } + + if (register) { + if (TRACE) { + System.out.println(" " + method.getName() + "(" + Arrays.toString(method.getParameterTypes()) + ")"); + } + add(method); + } + } + } catch (Throwable e) { + if (TRACE) { + System.out + .println(" Cannot register methods of " + clazz.getName() + ": " + e.getClass().getName() + ": " + e + .getMessage()); + } + } + } + } }