diff --git a/google-cloud-core-bom/pom.xml b/google-cloud-core-bom/pom.xml index e9aee4ef81..105abce31a 100644 --- a/google-cloud-core-bom/pom.xml +++ b/google-cloud-core-bom/pom.xml @@ -75,6 +75,11 @@ google-cloud-core-http 2.3.6-SNAPSHOT + + com.google.cloud + native-image-support + 0.11.0-SNAPSHOT + diff --git a/native-image-support/pom.xml b/native-image-support/pom.xml new file mode 100644 index 0000000000..fb74248dcb --- /dev/null +++ b/native-image-support/pom.xml @@ -0,0 +1,47 @@ + + + + 4.0.0 + Google Cloud Native Image Support + com.google.cloud + native-image-support + 0.11.0-SNAPSHOT + jar + + + google-cloud-core-parent + com.google.cloud + 2.3.6-SNAPSHOT + + + + Core gRPC module for the google-cloud. + + + + + com.google.guava + guava + + + + io.grpc + grpc-netty-shaded + provided + + + + org.graalvm.nativeimage + svm + provided + + + + org.graalvm.sdk + graal-sdk + provided + + + \ No newline at end of file diff --git a/native-image-support/src/main/java/com/google/cloud/nativeimage/features/GoogleJsonClientFeature.java b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/GoogleJsonClientFeature.java new file mode 100644 index 0000000000..8cc3107f4e --- /dev/null +++ b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/GoogleJsonClientFeature.java @@ -0,0 +1,99 @@ +/* + * Copyright 2022 Google LLC + * + * 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 + * + * https://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 com.google.cloud.nativeimage.features; + +import static com.google.cloud.nativeimage.features.NativeImageUtils.registerClassForReflection; + +import com.oracle.svm.core.annotate.AutomaticFeature; +import com.oracle.svm.core.configure.ResourcesRegistry; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.hosted.Feature; + +/** Configures Native Image settings for the Google JSON Client. */ +@AutomaticFeature +final class GoogleJsonClientFeature implements Feature { + + private static final String GOOGLE_API_CLIENT_CLASS = + "com.google.api.client.googleapis.services.json.AbstractGoogleJsonClient"; + + private static final String GOOGLE_API_CLIENT_REQUEST_CLASS = + "com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest"; + + private static final String GENERIC_JSON_CLASS = "com.google.api.client.json.GenericJson"; + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + loadApiClient(access); + loadHttpClient(access); + loadMiscClasses(access); + } + + private void loadApiClient(BeforeAnalysisAccess access) { + // For com.google.api-client:google-api-client + Class googleApiClientClass = access.findClassByName(GOOGLE_API_CLIENT_CLASS); + + if (googleApiClientClass != null) { + // All reachable instances of the AbstractGoogleJsonClient must be registered. + access.registerSubtypeReachabilityHandler( + (duringAccess, subtype) -> registerClassForReflection(access, subtype.getName()), + googleApiClientClass); + + // All reachable instances of the AbstractGoogleJsonClientRequest must be registered. + access.registerSubtypeReachabilityHandler( + (duringAccess, subtype) -> registerClassForReflection(access, subtype.getName()), + access.findClassByName(GOOGLE_API_CLIENT_REQUEST_CLASS)); + + // Resources + ResourcesRegistry resourcesRegistry = ImageSingletons.lookup(ResourcesRegistry.class); + resourcesRegistry.addResources( + "\\Qcom/google/api/client/googleapis/google-api-client.properties\\E"); + resourcesRegistry.addResources("\\Qcom/google/api/client/googleapis/google.p12\\E"); + resourcesRegistry.addResources( + "\\Qcom/google/api/client/http/google-http-client.properties\\E"); + } + } + + private void loadHttpClient(BeforeAnalysisAccess access) { + // For com.google.http-client:google-http-client + Class genericJsonClass = access.findClassByName(GENERIC_JSON_CLASS); + + if (genericJsonClass != null) { + // All reachable instances of GenericJson must be registered. + access.registerSubtypeReachabilityHandler( + (duringAccess, subtype) -> registerClassForReflection(access, subtype.getName()), + genericJsonClass); + + registerClassForReflection(access, "com.google.api.client.util.GenericData"); + registerClassForReflection(access, "com.google.api.client.json.webtoken.JsonWebToken"); + registerClassForReflection(access, "com.google.api.client.json.webtoken.JsonWebToken$Header"); + registerClassForReflection( + access, "com.google.api.client.json.webtoken.JsonWebToken$Payload"); + registerClassForReflection( + access, "com.google.api.client.json.webtoken.JsonWebSignature$Header"); + registerClassForReflection(access, "com.google.api.client.json.webtoken.JsonWebSignature"); + registerClassForReflection(access, "com.google.api.client.http.UrlEncodedContent"); + registerClassForReflection(access, "com.google.api.client.http.GenericUrl"); + registerClassForReflection(access, "com.google.api.client.http.HttpRequest"); + registerClassForReflection(access, "com.google.api.client.http.HttpHeaders"); + } + } + + private void loadMiscClasses(BeforeAnalysisAccess access) { + registerClassForReflection(access, "com.google.common.util.concurrent.AbstractFuture"); + registerClassForReflection(access, "com.google.common.util.concurrent.AbstractFuture$Waiter"); + } +} diff --git a/native-image-support/src/main/java/com/google/cloud/nativeimage/features/GrpcNettyFeature.java b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/GrpcNettyFeature.java new file mode 100644 index 0000000000..754f9005e2 --- /dev/null +++ b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/GrpcNettyFeature.java @@ -0,0 +1,122 @@ +/* + * Copyright 2022 Google LLC + * + * 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 + * + * https://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 com.google.cloud.nativeimage.features; + +import static com.google.cloud.nativeimage.features.NativeImageUtils.registerClassForReflection; +import static com.google.cloud.nativeimage.features.NativeImageUtils.registerClassHierarchyForReflection; +import static com.google.cloud.nativeimage.features.NativeImageUtils.registerForReflectiveInstantiation; +import static com.google.cloud.nativeimage.features.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/native-image-support/src/main/java/com/google/cloud/nativeimage/features/NativeImageUtils.java b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/NativeImageUtils.java new file mode 100644 index 0000000000..8b19f4b84f --- /dev/null +++ b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/NativeImageUtils.java @@ -0,0 +1,174 @@ +/* + * Copyright 2022 Google LLC + * + * 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 + * + * https://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 com.google.cloud.nativeimage.features; + +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; + } +} diff --git a/native-image-support/src/main/java/com/google/cloud/nativeimage/features/ProtobufMessageFeature.java b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/ProtobufMessageFeature.java new file mode 100644 index 0000000000..40251e85e4 --- /dev/null +++ b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/ProtobufMessageFeature.java @@ -0,0 +1,105 @@ +/* + * Copyright 2022 Google LLC + * + * 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 + * + * https://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 com.google.cloud.nativeimage.features; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.RuntimeReflection; + +/** + * A optional feature which registers reflective usages of the GRPC Protobuf libraries. + * + *

This feature is only needed if you need to access proto objects reflectively (such as + * printing/logging proto objects). + * + *

To add this feature, add "--feature + * com.google.cloud.nativeimage.features.ProtobufMessageFeature" to your GraalVM configuration. + */ +final class ProtobufMessageFeature implements Feature { + + // Proto classes to check on the classpath. + private static final String PROTO_MESSAGE_CLASS = "com.google.protobuf.GeneratedMessageV3"; + private static final String PROTO_ENUM_CLASS = "com.google.protobuf.ProtocolMessageEnum"; + private static final String ENUM_VAL_DESCRIPTOR_CLASS = + "com.google.protobuf.Descriptors$EnumValueDescriptor"; + + // Prefixes of methods accessed reflectively by + // com.google.protobuf.GeneratedMessageV3$ReflectionInvoker + private static final List METHOD_ACCESSOR_PREFIXES = + Arrays.asList("get", "set", "has", "add", "clear", "newBuilder"); + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + Class protoMessageClass = access.findClassByName(PROTO_MESSAGE_CLASS); + if (protoMessageClass != null) { + Method internalAccessorMethod = + NativeImageUtils.getMethodOrFail(protoMessageClass, "internalGetFieldAccessorTable"); + + // Finds every class whose `internalGetFieldAccessorTable()` is reached and registers it. + // `internalGetFieldAccessorTable()` is used downstream to access the class reflectively. + access.registerMethodOverrideReachabilityHandler( + (duringAccess, method) -> { + registerFieldAccessors(method.getDeclaringClass()); + registerFieldAccessors(getBuilderClass(method.getDeclaringClass())); + }, + internalAccessorMethod); + } + + Class protoEnumClass = access.findClassByName(PROTO_ENUM_CLASS); + if (protoEnumClass != null) { + // Finds every reachable proto enum class and registers specific methods for reflection. + access.registerSubtypeReachabilityHandler( + (duringAccess, subtypeClass) -> { + if (!PROTO_ENUM_CLASS.equals(subtypeClass.getName())) { + Method method = + NativeImageUtils.getMethodOrFail( + subtypeClass, + "valueOf", + duringAccess.findClassByName(ENUM_VAL_DESCRIPTOR_CLASS)); + RuntimeReflection.register(method); + + method = NativeImageUtils.getMethodOrFail(subtypeClass, "getValueDescriptor"); + RuntimeReflection.register(method); + } + }, + protoEnumClass); + } + } + + /** Given a proto class, registers the public accessor methods for the provided proto class. */ + private static void registerFieldAccessors(Class protoClass) { + for (Method method : protoClass.getMethods()) { + boolean hasAccessorPrefix = + METHOD_ACCESSOR_PREFIXES.stream().anyMatch(prefix -> method.getName().startsWith(prefix)); + if (hasAccessorPrefix) { + RuntimeReflection.register(method); + } + } + } + + /** Given a proto class, returns the Builder nested class. */ + private static Class getBuilderClass(Class protoClass) { + for (Class clazz : protoClass.getClasses()) { + if (clazz.getName().endsWith("Builder")) { + return clazz; + } + } + return null; + } +} diff --git a/native-image-support/src/main/java/com/google/cloud/nativeimage/features/clients/CloudFunctionsFeature.java b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/clients/CloudFunctionsFeature.java new file mode 100644 index 0000000000..7b7eaaebaf --- /dev/null +++ b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/clients/CloudFunctionsFeature.java @@ -0,0 +1,146 @@ +/* + * Copyright 2022 Google LLC + * + * 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 + * + * https://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 com.google.cloud.nativeimage.features.clients; + +import static com.google.cloud.nativeimage.features.NativeImageUtils.registerClassForReflection; +import static com.google.cloud.nativeimage.features.NativeImageUtils.registerClassHierarchyForReflection; + +import com.oracle.svm.core.annotate.AutomaticFeature; +import com.oracle.svm.core.configure.ResourcesRegistry; +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import java.util.function.Consumer; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.RuntimeReflection; + +/** A feature which registers reflective usages of the Cloud Functions library. */ +@AutomaticFeature +final class CloudFunctionsFeature implements Feature { + + private static final String FUNCTION_INVOKER_CLASS = + "com.google.cloud.functions.invoker.runner.Invoker"; + + private static final List FUNCTIONS_CLASSES = + Arrays.asList( + "com.google.cloud.functions.HttpFunction", + "com.google.cloud.functions.RawBackgroundFunction", + "com.google.cloud.functions.BackgroundFunction"); + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + Class invokerClass = access.findClassByName(FUNCTION_INVOKER_CLASS); + if (invokerClass != null) { + // JCommander libraries + registerClassForReflection(access, "com.beust.jcommander.converters.StringConverter"); + registerClassForReflection(access, "com.beust.jcommander.validators.NoValidator"); + registerClassForReflection(access, "com.beust.jcommander.validators.NoValueValidator"); + + // Jetty libraries + registerClassForReflection(access, "org.eclipse.jetty.http.HttpTokens"); + registerClassForReflection(access, "org.eclipse.jetty.util.TypeUtil"); + + // Cloud Functions core + registerClassForReflection( + access, "com.google.cloud.functions.invoker.runner.Invoker$Options"); + + // Register Jetty Resources. + ResourcesRegistry resourcesRegistry = ImageSingletons.lookup(ResourcesRegistry.class); + resourcesRegistry.addResources( + "\\QMETA-INF/services/org.eclipse.jetty.http.HttpFieldPreEncoder\\E"); + resourcesRegistry.addResources("\\Qorg/eclipse/jetty/http/encoding.properties\\E"); + resourcesRegistry.addResources("\\Qorg/eclipse/jetty/http/mime.properties\\E"); + resourcesRegistry.addResources("\\Qorg/eclipse/jetty/version/build.properties\\E"); + resourcesRegistry.addResourceBundles("javax.servlet.LocalStrings"); + resourcesRegistry.addResourceBundles("javax.servlet.http.LocalStrings"); + + // Register user-implemented Function classes + List> functionClasses = + FUNCTIONS_CLASSES.stream() + .map(name -> access.findClassByName(name)) + .collect(Collectors.toList()); + + scanJarClasspath( + access, + clazz -> { + boolean isFunctionSubtype = + functionClasses.stream() + .anyMatch( + function -> + function.isAssignableFrom(clazz) + && !Modifier.isAbstract(clazz.getModifiers())); + + if (isFunctionSubtype) { + RuntimeReflection.register(clazz); + RuntimeReflection.register(clazz.getDeclaredConstructors()); + RuntimeReflection.register(clazz.getDeclaredMethods()); + + // This part is to register the parameterized class of BackgroundFunctions + // for reflection; i.e. register type T in BackgroundFunction + for (Type type : clazz.getGenericInterfaces()) { + if (type instanceof ParameterizedType) { + ParameterizedType paramType = (ParameterizedType) type; + for (Type argument : paramType.getActualTypeArguments()) { + registerClassHierarchyForReflection(access, argument.getTypeName()); + } + } + } + } + }); + } + } + + /** + * Scan the JAR classpath for classes. The {@code classProcessorFunction} is run once for each + * class in the classpath. + */ + private static void scanJarClasspath( + FeatureAccess access, Consumer> classProcessorCallback) { + + List classPath = access.getApplicationClassPath(); + try { + for (Path path : classPath) { + JarFile jarFile = new JarFile(path.toFile()); + Enumeration entries = jarFile.entries(); + + while (entries.hasMoreElements()) { + JarEntry jarEntry = entries.nextElement(); + String fileName = jarEntry.getName(); + if (fileName.endsWith(".class")) { + String className = fileName.substring(0, fileName.length() - 6).replaceAll("/", "."); + + Class clazz = access.findClassByName(className); + if (clazz != null) { + classProcessorCallback.accept(clazz); + } + } + } + } + } catch (IOException e) { + throw new RuntimeException("Failed to read classpath: ", e); + } + } +} diff --git a/native-image-support/src/main/java/com/google/cloud/nativeimage/features/clients/CloudSqlFeature.java b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/clients/CloudSqlFeature.java new file mode 100644 index 0000000000..48b4c1e4ef --- /dev/null +++ b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/clients/CloudSqlFeature.java @@ -0,0 +1,90 @@ +/* + * Copyright 2022 Google LLC + * + * 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 + * + * https://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 com.google.cloud.nativeimage.features.clients; + +import com.google.cloud.nativeimage.features.NativeImageUtils; +import com.oracle.svm.core.annotate.AutomaticFeature; +import com.oracle.svm.core.configure.ResourcesRegistry; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; +import org.graalvm.nativeimage.hosted.RuntimeReflection; + +/** Registers GraalVM configuration for the Cloud SQL libraries for MySQL and Postgres. */ +@AutomaticFeature +final class CloudSqlFeature implements Feature { + + private static final String CLOUD_SQL_SOCKET_CLASS = + "com.google.cloud.sql.core.CoreSocketFactory"; + + private static final String POSTGRES_SOCKET_CLASS = "com.google.cloud.sql.postgres.SocketFactory"; + + private static final String MYSQL_SOCKET_CLASS = "com.google.cloud.sql.mysql.SocketFactory"; + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + if (access.findClassByName(CLOUD_SQL_SOCKET_CLASS) == null) { + return; + } + + // The Core Cloud SQL Socket + NativeImageUtils.registerClassForReflection(access, CLOUD_SQL_SOCKET_CLASS); + + // Resources for Cloud SQL + ResourcesRegistry resourcesRegistry = ImageSingletons.lookup(ResourcesRegistry.class); + resourcesRegistry.addResources("\\Qcom.google.cloud.sql/project.properties\\E"); + resourcesRegistry.addResources("\\QMETA-INF/services/java.sql.Driver\\E"); + + // Register Hikari configs if used with Cloud SQL. + if (access.findClassByName("com.zaxxer.hikari.HikariConfig") != null) { + NativeImageUtils.registerClassForReflection(access, "com.zaxxer.hikari.HikariConfig"); + + RuntimeReflection.register( + access.findClassByName("[Lcom.zaxxer.hikari.util.ConcurrentBag$IConcurrentBagEntry;")); + + RuntimeReflection.register(access.findClassByName("[Ljava.sql.Statement;")); + } + + // Register PostgreSQL driver config. + if (access.findClassByName(POSTGRES_SOCKET_CLASS) != null) { + NativeImageUtils.registerClassForReflection( + access, "com.google.cloud.sql.postgres.SocketFactory"); + NativeImageUtils.registerClassForReflection(access, "org.postgresql.PGProperty"); + } + + // Register MySQL driver config. + if (access.findClassByName(MYSQL_SOCKET_CLASS) != null) { + NativeImageUtils.registerClassForReflection(access, MYSQL_SOCKET_CLASS); + + NativeImageUtils.registerConstructorsForReflection( + access, "com.mysql.cj.conf.url.SingleConnectionUrl"); + + NativeImageUtils.registerConstructorsForReflection(access, "com.mysql.cj.log.StandardLogger"); + + access.registerSubtypeReachabilityHandler( + (duringAccess, exceptionClass) -> + NativeImageUtils.registerClassForReflection(duringAccess, exceptionClass.getName()), + access.findClassByName("com.mysql.cj.exceptions.CJException")); + + // JDBC classes create socket connections which must be initialized at run time. + RuntimeClassInitialization.initializeAtRunTime("com.mysql.cj.jdbc"); + + // Additional MySQL resources. + resourcesRegistry.addResourceBundles("com.mysql.cj.LocalizedErrorMessages"); + } + } +} diff --git a/native-image-support/src/main/java/com/google/cloud/nativeimage/features/clients/OpenCensusFeature.java b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/clients/OpenCensusFeature.java new file mode 100644 index 0000000000..f2b6cb2098 --- /dev/null +++ b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/clients/OpenCensusFeature.java @@ -0,0 +1,38 @@ +/* + * Copyright 2022 Google LLC + * + * 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 + * + * https://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 com.google.cloud.nativeimage.features.clients; + +import static com.google.cloud.nativeimage.features.NativeImageUtils.registerForReflectiveInstantiation; + +import com.oracle.svm.core.annotate.AutomaticFeature; +import org.graalvm.nativeimage.hosted.Feature; + +/** Registers reflection usage in OpenCensus libraries. */ +@AutomaticFeature +final class OpenCensusFeature implements Feature { + + private static final String OPEN_CENSUS_CLASS = "io.opencensus.impl.tags.TagsComponentImpl"; + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + if (access.findClassByName(OPEN_CENSUS_CLASS) != null) { + registerForReflectiveInstantiation(access, "io.opencensus.impl.metrics.MetricsComponentImpl"); + registerForReflectiveInstantiation(access, "io.opencensus.impl.tags.TagsComponentImpl"); + registerForReflectiveInstantiation(access, "io.opencensus.impl.trace.TraceComponentImpl"); + } + } +} diff --git a/native-image-support/src/main/java/com/google/cloud/nativeimage/features/clients/SpannerFeature.java b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/clients/SpannerFeature.java new file mode 100644 index 0000000000..e902c77928 --- /dev/null +++ b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/clients/SpannerFeature.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 Google LLC + * + * 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 + * + * https://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 com.google.cloud.nativeimage.features.clients; + +import com.google.cloud.nativeimage.features.NativeImageUtils; +import com.oracle.svm.core.annotate.AutomaticFeature; +import com.oracle.svm.core.configure.ResourcesRegistry; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.hosted.Feature; + +/** Registers Spanner library classes for reflection. */ +@AutomaticFeature +final class SpannerFeature implements Feature { + + private static final String SPANNER_CLASS = "com.google.spanner.v1.SpannerGrpc"; + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + Class spannerClass = access.findClassByName(SPANNER_CLASS); + if (spannerClass != null) { + NativeImageUtils.registerClassHierarchyForReflection( + access, "com.google.spanner.admin.database.v1.Database"); + NativeImageUtils.registerClassHierarchyForReflection( + access, "com.google.spanner.admin.instance.v1.Instance"); + NativeImageUtils.registerClassForReflection( + access, "com.google.spanner.admin.database.v1.RestoreInfo"); + + // Resources + ResourcesRegistry resourcesRegistry = ImageSingletons.lookup(ResourcesRegistry.class); + resourcesRegistry.addResources( + "\\Qcom/google/cloud/spanner/connection/ClientSideStatements.json\\E"); + resourcesRegistry.addResources( + "\\Qcom/google/cloud/spanner/spi/v1/grpc-gcp-apiconfig.json\\E"); + } + } +} diff --git a/native-image-support/src/main/java/com/google/cloud/nativeimage/features/substitutions/ApiClientVersionSubstitutions.java b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/substitutions/ApiClientVersionSubstitutions.java new file mode 100644 index 0000000000..c170c2ea7a --- /dev/null +++ b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/substitutions/ApiClientVersionSubstitutions.java @@ -0,0 +1,64 @@ +/* + * Copyright 2022 Google LLC + * + * 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 + * + * https://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 com.google.cloud.nativeimage.features.substitutions; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; +import java.util.function.BooleanSupplier; + +/** Substitution for setting Java version correctly in the Google Java Http Client. */ +@TargetClass( + className = + "com.google.api.client.googleapis.services.AbstractGoogleClientRequest$ApiClientVersion", + onlyWith = ApiClientVersionSubstitutions.OnlyIfInClassPath.class) +final class ApiClientVersionSubstitutions { + + @Alias private String versionString; + + @Substitute + public String toString() { + String[] tokens = versionString.split(" "); + + if (tokens.length > 0 && tokens[0].startsWith("gl-java")) { + tokens[0] += "-graalvm"; + return String.join(" ", tokens); + } else { + return versionString; + } + } + + private ApiClientVersionSubstitutions() {} + + static class OnlyIfInClassPath implements BooleanSupplier { + + @Override + public boolean getAsBoolean() { + try { + // Note: Set initialize = false to avoid initializing the class when looking it up. + Class.forName( + "com.google.api.client.googleapis.services." + + "AbstractGoogleClientRequest$ApiClientVersion", + false, + Thread.currentThread().getContextClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + } +} diff --git a/native-image-support/src/main/java/com/google/cloud/nativeimage/features/substitutions/GaxPropertiesSubstitutions.java b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/substitutions/GaxPropertiesSubstitutions.java new file mode 100644 index 0000000000..54245a8ee8 --- /dev/null +++ b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/substitutions/GaxPropertiesSubstitutions.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022 Google LLC + * + * 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 + * + * https://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 com.google.cloud.nativeimage.features.substitutions; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.RecomputeFieldValue; +import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind; +import com.oracle.svm.core.annotate.TargetClass; +import java.util.function.BooleanSupplier; + +/** + * This file contains the GaxProperties substitution to correctly set the Java language string in + * API call headers for Native Image users. + */ +@TargetClass( + className = "com.google.api.gax.core.GaxProperties", + onlyWith = GaxPropertiesSubstitutions.OnlyIfInClassPath.class) +final class GaxPropertiesSubstitutions { + + @Alias + @RecomputeFieldValue(kind = Kind.FromAlias) + private static String JAVA_VERSION = System.getProperty("java.version") + "-graalvm"; + + private GaxPropertiesSubstitutions() {} + + static class OnlyIfInClassPath implements BooleanSupplier { + + @Override + public boolean getAsBoolean() { + try { + // Note: Set initialize = false to avoid initializing the class when looking it up. + Class.forName( + "com.google.api.gax.core.GaxProperties", + false, + Thread.currentThread().getContextClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + } +} diff --git a/native-image-support/src/main/java/com/google/cloud/nativeimage/features/substitutions/NettyInternalLoggerFactorySubstitutions.java b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/substitutions/NettyInternalLoggerFactorySubstitutions.java new file mode 100644 index 0000000000..7fcea16318 --- /dev/null +++ b/native-image-support/src/main/java/com/google/cloud/nativeimage/features/substitutions/NettyInternalLoggerFactorySubstitutions.java @@ -0,0 +1,55 @@ +/* + * Copyright 2022 Google LLC + * + * 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 + * + * https://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 com.google.cloud.nativeimage.features.substitutions; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; +import io.grpc.netty.shaded.io.netty.util.internal.logging.InternalLoggerFactory; +import io.grpc.netty.shaded.io.netty.util.internal.logging.JdkLoggerFactory; +import java.util.function.BooleanSupplier; + +/** + * Substitutions for {@link InternalLoggerFactory} which are needed to avoid dynamic loading of + * logging library. + */ +@TargetClass( + className = "io.grpc.netty.shaded.io.netty.util.internal.logging.InternalLoggerFactory", + onlyWith = NettyInternalLoggerFactorySubstitutions.OnlyIfInClassPath.class) +final class NettyInternalLoggerFactorySubstitutions { + + @Substitute + private static InternalLoggerFactory newDefaultFactory(String name) { + return JdkLoggerFactory.INSTANCE; + } + + static class OnlyIfInClassPath implements BooleanSupplier { + + @Override + public boolean getAsBoolean() { + try { + // Note: Set initialize = false to avoid initializing the class when looking it up. + Class.forName( + "io.grpc.netty.shaded.io.netty.util.internal.logging.InternalLoggerFactory", + false, + Thread.currentThread().getContextClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + } +} diff --git a/native-image-support/src/main/resources/META-INF/native-image/com.google.cloud/google-cloud-core/native-image.properties b/native-image-support/src/main/resources/META-INF/native-image/com.google.cloud/google-cloud-core/native-image.properties new file mode 100644 index 0000000000..aa61561066 --- /dev/null +++ b/native-image-support/src/main/resources/META-INF/native-image/com.google.cloud/google-cloud-core/native-image.properties @@ -0,0 +1,14 @@ +Args = --allow-incomplete-classpath \ +--enable-url-protocols=https,http \ +--initialize-at-build-time=org.conscrypt \ +--initialize-at-run-time=io.grpc.netty.shaded.io.netty.handler.ssl.OpenSsl,\ + io.grpc.netty.shaded.io.netty.internal.tcnative.SSL,\ + io.grpc.netty.shaded.io.netty.internal.tcnative.CertificateVerifier,\ + io.grpc.netty.shaded.io.netty.internal.tcnative.SSLPrivateKeyMethod,\ + io.grpc.netty.shaded.io.grpc.netty,\ + io.grpc.netty.shaded.io.netty.channel.epoll,\ + io.grpc.netty.shaded.io.netty.channel.unix,\ + io.grpc.netty.shaded.io.netty.handler.ssl,\ + io.grpc.internal.RetriableStream,\ + com.google.api.client.googleapis.services.AbstractGoogleClientRequest$ApiClientVersion,\ + com.google.cloud.firestore.FirestoreImpl diff --git a/native-image-support/src/main/resources/META-INF/native-image/com.google.cloud/google-cloud-core/resource-config.json b/native-image-support/src/main/resources/META-INF/native-image/com.google.cloud/google-cloud-core/resource-config.json new file mode 100644 index 0000000000..3afe5ffebc --- /dev/null +++ b/native-image-support/src/main/resources/META-INF/native-image/com.google.cloud/google-cloud-core/resource-config.json @@ -0,0 +1,13 @@ +{ + "resources":[ + {"pattern":"\\QMETA-INF/native/libio_grpc_netty_shaded_netty_tcnative_linux_x86_64.so\\E"}, + {"pattern":"\\QMETA-INF/native/libio_grpc_netty_shaded_netty_transport_native_epoll_x86_64.so\\E"}, + {"pattern":"\\QMETA-INF/services/io.grpc.LoadBalancerProvider\\E"}, + {"pattern":"\\QMETA-INF/services/io.grpc.ManagedChannelProvider\\E"}, + {"pattern":"\\QMETA-INF/services/io.grpc.NameResolverProvider\\E"}, + {"pattern":"\\QMETA-INF/services/jdk.vm.ci.services.JVMCIServiceLocator\\E"}, + {"pattern":"\\Qdependencies.properties\\E"}, + {"pattern":"\\Qgoogle-http-client.properties\\E"} + ], + "bundles":[] +} diff --git a/pom.xml b/pom.xml index 50c970c096..66b0425521 100644 --- a/pom.xml +++ b/pom.xml @@ -158,6 +158,7 @@ 1.3.0 1.33.0 1.41.0 + 21.3.0 1.43.1 3.19.2 0.28.0 @@ -283,6 +284,21 @@ ${gson.version} + + + org.graalvm.nativeimage + svm + ${graalvm.version} + provided + + + + org.graalvm.sdk + graal-sdk + ${graalvm.version} + provided + + com.google.truth @@ -322,6 +338,7 @@ google-cloud-core-http google-cloud-core-grpc google-cloud-core-bom + native-image-support diff --git a/versions.txt b/versions.txt index cdb2601a39..4cd1c2e1fd 100644 --- a/versions.txt +++ b/versions.txt @@ -1,4 +1,5 @@ # Format: # module:released-version:current-version -google-cloud-core:2.3.5:2.3.6-SNAPSHOT \ No newline at end of file +google-cloud-core:2.3.5:2.3.6-SNAPSHOT +native-image-support:0.10.0:0.11.0-SNAPSHOT \ No newline at end of file