diff --git a/dependencies.properties b/dependencies.properties index 8a0b0d5819..023bc51529 100644 --- a/dependencies.properties +++ b/dependencies.properties @@ -70,6 +70,8 @@ maven.com_google_http_client_google_http_client=com.google.http-client:google-ht maven.com_google_http_client_google_http_client_gson=com.google.http-client:google-http-client-gson:1.41.2 maven.org_codehaus_mojo_animal_sniffer_annotations=org.codehaus.mojo:animal-sniffer-annotations:1.18 maven.javax_annotation_javax_annotation_api=javax.annotation:javax.annotation-api:1.3.2 +maven.org_graalvm_nativeimage_svm=org.graalvm.nativeimage:svm:22.0.0.2 +maven.org_graalvm_sdk=org.graalvm.sdk:graal-sdk:22.0.0.2 # Testing maven artifacts maven.junit_junit=junit:junit:4.13.2 diff --git a/gax-grpc/build.gradle b/gax-grpc/build.gradle index 4886d4efe6..984e5eb5cb 100644 --- a/gax-grpc/build.gradle +++ b/gax-grpc/build.gradle @@ -22,7 +22,9 @@ dependencies { runtimeOnly libraries['maven.io_grpc_grpc_xds'] - compileOnly libraries['maven.com_google_auto_value_auto_value'] + compileOnly(libraries['maven.com_google_auto_value_auto_value'], + libraries['maven.org_graalvm_sdk'], + libraries['maven.org_graalvm_nativeimage_svm']) testImplementation( project(':gax').sourceSets.test.output, libraries['maven.junit_junit'], diff --git a/gax-grpc/src/main/java/com/google/nativeimage/GrpcNettyFeature.java b/gax-grpc/src/main/java/com/google/nativeimage/GrpcNettyFeature.java new file mode 100644 index 0000000000..682ca1ec56 --- /dev/null +++ b/gax-grpc/src/main/java/com/google/nativeimage/GrpcNettyFeature.java @@ -0,0 +1,136 @@ +/* + * Copyright 2022 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.nativeimage; + +import static com.google.api.gax.nativeimage.NativeImageUtils.registerClassForReflection; +import static com.google.api.gax.nativeimage.NativeImageUtils.registerClassHierarchyForReflection; +import static com.google.api.gax.nativeimage.NativeImageUtils.registerForReflectiveInstantiation; +import static com.google.api.gax.nativeimage.NativeImageUtils.registerForUnsafeFieldAccess; + +import com.oracle.svm.core.annotate.AutomaticFeature; +import org.graalvm.nativeimage.hosted.Feature; + +/** Configures Native Image settings for the grpc-netty-shaded dependency. */ +@AutomaticFeature +final class GrpcNettyFeature implements Feature { + + private static final String GRPC_NETTY_SHADED_CLASS = + "io.grpc.netty.shaded.io.grpc.netty.NettyServer"; + + private static final String GOOGLE_AUTH_CLASS = + "com.google.auth.oauth2.ServiceAccountCredentials"; + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + loadGoogleAuthClasses(access); + loadGrpcNettyClasses(access); + loadMiscClasses(access); + } + + private static void loadGoogleAuthClasses(BeforeAnalysisAccess access) { + // For com.google.auth:google-auth-library-oauth2-http + Class authClass = access.findClassByName(GOOGLE_AUTH_CLASS); + if (authClass != null) { + registerClassHierarchyForReflection( + access, "com.google.auth.oauth2.ServiceAccountCredentials"); + registerClassHierarchyForReflection( + access, "com.google.auth.oauth2.ServiceAccountJwtAccessCredentials"); + } + } + + private static void loadGrpcNettyClasses(BeforeAnalysisAccess access) { + // For io.grpc:grpc-netty-shaded + Class nettyShadedClass = access.findClassByName(GRPC_NETTY_SHADED_CLASS); + if (nettyShadedClass != null) { + // Misc. classes used by grpc-netty-shaded + registerForReflectiveInstantiation( + access, "io.grpc.netty.shaded.io.netty.channel.socket.nio.NioSocketChannel"); + registerClassForReflection( + access, "io.grpc.netty.shaded.io.netty.util.internal.NativeLibraryUtil"); + registerClassForReflection(access, "io.grpc.netty.shaded.io.netty.util.ReferenceCountUtil"); + registerClassForReflection( + access, "io.grpc.netty.shaded.io.netty.buffer.AbstractByteBufAllocator"); + + // Epoll Libraries + registerClassForReflection(access, "io.grpc.netty.shaded.io.netty.channel.epoll.Epoll"); + registerClassForReflection( + access, "io.grpc.netty.shaded.io.netty.channel.epoll.EpollChannelOption"); + registerClassForReflection( + access, "io.grpc.netty.shaded.io.netty.channel.epoll.EpollEventLoopGroup"); + registerForReflectiveInstantiation( + access, "io.grpc.netty.shaded.io.netty.channel.epoll.EpollServerSocketChannel"); + registerForReflectiveInstantiation( + access, "io.grpc.netty.shaded.io.netty.channel.epoll.EpollSocketChannel"); + + // Unsafe field accesses + registerForUnsafeFieldAccess( + access, + "io.grpc.netty.shaded.io.netty.util.internal.shaded." + + "org.jctools.queues.MpscArrayQueueProducerIndexField", + "producerIndex"); + registerForUnsafeFieldAccess( + access, + "io.grpc.netty.shaded.io.netty.util.internal.shaded." + + "org.jctools.queues.MpscArrayQueueProducerLimitField", + "producerLimit"); + registerForUnsafeFieldAccess( + access, + "io.grpc.netty.shaded.io.netty.util.internal.shaded." + + "org.jctools.queues.MpscArrayQueueConsumerIndexField", + "consumerIndex"); + registerForUnsafeFieldAccess( + access, + "io.grpc.netty.shaded.io.netty.util.internal.shaded." + + "org.jctools.queues.BaseMpscLinkedArrayQueueProducerFields", + "producerIndex"); + registerForUnsafeFieldAccess( + access, + "io.grpc.netty.shaded.io.netty.util.internal.shaded." + + "org.jctools.queues.BaseMpscLinkedArrayQueueColdProducerFields", + "producerLimit"); + registerForUnsafeFieldAccess( + access, + "io.grpc.netty.shaded.io.netty.util.internal.shaded." + + "org.jctools.queues.BaseMpscLinkedArrayQueueConsumerFields", + "consumerIndex"); + } + } + + /** Miscellaneous classes that need to be registered coming from various JARs. */ + private static void loadMiscClasses(BeforeAnalysisAccess access) { + registerClassHierarchyForReflection(access, "com.google.protobuf.DescriptorProtos"); + registerClassForReflection(access, "com.google.api.FieldBehavior"); + + registerForUnsafeFieldAccess(access, "javax.net.ssl.SSLContext", "contextSpi"); + registerClassForReflection(access, "java.lang.management.ManagementFactory"); + registerClassForReflection(access, "java.lang.management.RuntimeMXBean"); + } +} diff --git a/gax/build.gradle b/gax/build.gradle index f85810a6f9..cbcaffdb3b 100644 --- a/gax/build.gradle +++ b/gax/build.gradle @@ -13,7 +13,9 @@ dependencies { libraries['maven.com_google_guava_guava'], libraries['maven.io_opencensus_opencensus_api']) - compileOnly libraries['maven.com_google_auto_value_auto_value'] + compileOnly(libraries['maven.com_google_auto_value_auto_value'], + libraries['maven.org_graalvm_sdk'], + libraries['maven.org_graalvm_nativeimage_svm']) testImplementation(libraries['maven.junit_junit'], libraries['maven.org_mockito_mockito_core'], diff --git a/gax/src/main/java/com/google/api/gax/nativeimage/NativeImageUtils.java b/gax/src/main/java/com/google/api/gax/nativeimage/NativeImageUtils.java new file mode 100644 index 0000000000..c3c96cf4b5 --- /dev/null +++ b/gax/src/main/java/com/google/api/gax/nativeimage/NativeImageUtils.java @@ -0,0 +1,188 @@ +/* + * Copyright 2022 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.gax.nativeimage; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.logging.Logger; +import org.graalvm.nativeimage.hosted.Feature.FeatureAccess; +import org.graalvm.nativeimage.hosted.RuntimeReflection; + +/** Internal class offering helper methods for registering methods/classes for reflection. */ +public class NativeImageUtils { + + private static final Logger LOGGER = Logger.getLogger(NativeImageUtils.class.getName()); + + /** Returns the method of a class or fails if it is not present. */ + public static Method getMethodOrFail(Class clazz, String methodName, Class... params) { + try { + return clazz.getDeclaredMethod(methodName, params); + } catch (NoSuchMethodException e) { + throw new RuntimeException( + "Failed to find method " + methodName + " for class " + clazz.getName(), e); + } + } + + /** Registers a class for reflective construction via its default constructor. */ + public static void registerForReflectiveInstantiation(FeatureAccess access, String className) { + Class clazz = access.findClassByName(className); + if (clazz != null) { + RuntimeReflection.register(clazz); + RuntimeReflection.registerForReflectiveInstantiation(clazz); + } else { + LOGGER.warning( + "Failed to find " + className + " on the classpath for reflective instantiation."); + } + } + + /** Registers all constructors of a class for reflection. */ + public static void registerConstructorsForReflection(FeatureAccess access, String name) { + Class clazz = access.findClassByName(name); + if (clazz != null) { + RuntimeReflection.register(clazz); + RuntimeReflection.register(clazz.getDeclaredConstructors()); + } else { + LOGGER.warning("Failed to find " + name + " on the classpath for reflection."); + } + } + + /** Registers an entire class for reflection use. */ + public static void registerClassForReflection(FeatureAccess access, String name) { + Class clazz = access.findClassByName(name); + if (clazz != null) { + RuntimeReflection.register(clazz); + RuntimeReflection.register(clazz.getDeclaredConstructors()); + RuntimeReflection.register(clazz.getDeclaredFields()); + RuntimeReflection.register(clazz.getDeclaredMethods()); + } else { + LOGGER.warning("Failed to find " + name + " on the classpath for reflection."); + } + } + + /** + * Registers the transitive class hierarchy of the provided {@code className} for reflection. + * + *

The transitive class hierarchy contains the class itself and its transitive set of + * *non-private* nested subclasses. + */ + public static void registerClassHierarchyForReflection(FeatureAccess access, String className) { + Class clazz = access.findClassByName(className); + if (clazz != null) { + registerClassForReflection(access, className); + for (Class nestedClass : clazz.getDeclaredClasses()) { + if (!Modifier.isPrivate(nestedClass.getModifiers())) { + registerClassHierarchyForReflection(access, nestedClass.getName()); + } + } + } else { + LOGGER.warning("Failed to find " + className + " on the classpath for reflection."); + } + } + + /** Registers a class for unsafe reflective field access. */ + public static void registerForUnsafeFieldAccess( + FeatureAccess access, String className, String... fields) { + Class clazz = access.findClassByName(className); + if (clazz != null) { + RuntimeReflection.register(clazz); + for (String fieldName : fields) { + try { + RuntimeReflection.register(clazz.getDeclaredField(fieldName)); + } catch (NoSuchFieldException ex) { + LOGGER.warning("Failed to register field " + fieldName + " for class " + className); + LOGGER.warning(ex.getMessage()); + } + } + } else { + LOGGER.warning( + "Failed to find " + + className + + " on the classpath for unsafe fields access registration."); + } + } + + /** Registers all the classes under the specified package for reflection. */ + public static void registerPackageForReflection(FeatureAccess access, String packageName) { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + try { + String path = packageName.replace('.', '/'); + + Enumeration resources = classLoader.getResources(path); + while (resources.hasMoreElements()) { + URL url = resources.nextElement(); + + URLConnection connection = url.openConnection(); + if (connection instanceof JarURLConnection) { + List classes = findClassesInJar((JarURLConnection) connection, packageName); + for (String className : classes) { + registerClassHierarchyForReflection(access, className); + } + } + } + } catch (IOException e) { + throw new RuntimeException("Failed to load classes under package name.", e); + } + } + + private static List findClassesInJar(JarURLConnection urlConnection, String packageName) + throws IOException { + + List result = new ArrayList<>(); + + final JarFile jarFile = urlConnection.getJarFile(); + final Enumeration entries = jarFile.entries(); + + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + + if (entryName.endsWith(".class")) { + String javaClassName = entryName.replace(".class", "").replace('/', '.'); + + if (javaClassName.startsWith(packageName)) { + result.add(javaClassName); + } + } + } + + return result; + } +}