-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
native image support for transcoding (#65)
* native image support for transcoding * fix tests
- Loading branch information
1 parent
ae68760
commit 378b8de
Showing
9 changed files
with
275 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,29 @@ | ||
plugins { | ||
id 'org.springframework.boot' | ||
id "org.springframework.boot" | ||
id "org.graalvm.buildtools.native" | ||
} | ||
|
||
dependencies { | ||
implementation("io.grpc:grpc-testing-proto") | ||
implementation(project(":grpc-starters:grpc-server-boot-starter")) | ||
implementation(project(":grpc-starters:grpc-server-boot-starter")){ | ||
exclude(group: 'io.grpc', module: "grpc-netty-shaded") | ||
} | ||
implementation(project(":grpc-starters:grpc-starter-transcoding")) | ||
implementation("org.springframework.boot:spring-boot-starter-web") | ||
runtimeOnly("io.grpc:grpc-netty") | ||
|
||
testImplementation(project(":grpc-starters:grpc-starter-test")) | ||
} | ||
|
||
apply from: "${rootDir}/gradle/protobuf.gradle" | ||
|
||
// https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html#configuration-options | ||
graalvmNative { | ||
testSupport = false | ||
binaries { | ||
main { | ||
verbose = true | ||
sharedLibrary = false | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
153 changes: 153 additions & 0 deletions
153
...cstarter/extensions/transcoding/GrpcTranscodingBeanFactoryInitializationAotProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
package grpcstarter.extensions.transcoding; | ||
|
||
import com.google.protobuf.DescriptorProtos; | ||
import com.google.protobuf.Message; | ||
import io.grpc.BindableService; | ||
import jakarta.annotation.Nonnull; | ||
import jakarta.annotation.Nullable; | ||
import java.lang.reflect.ParameterizedType; | ||
import java.lang.reflect.Type; | ||
import java.util.HashMap; | ||
import java.util.LinkedHashSet; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import lombok.SneakyThrows; | ||
import org.springframework.aot.hint.MemberCategory; | ||
import org.springframework.aot.hint.ReflectionHints; | ||
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; | ||
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; | ||
import org.springframework.beans.factory.config.BeanDefinition; | ||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; | ||
import org.springframework.context.EnvironmentAware; | ||
import org.springframework.core.env.Environment; | ||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.util.ClassUtils; | ||
|
||
/** | ||
* TIP: use 'processAot' task to debug the AOT processing. | ||
* | ||
* @author Freeman | ||
*/ | ||
class GrpcTranscodingBeanFactoryInitializationAotProcessor | ||
implements BeanFactoryInitializationAotProcessor, EnvironmentAware { | ||
|
||
private Environment env; | ||
|
||
@Override | ||
public void setEnvironment(Environment environment) { | ||
this.env = environment; | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public BeanFactoryInitializationAotContribution processAheadOfTime( | ||
@Nonnull ConfigurableListableBeanFactory beanFactory) { | ||
return (generationContext, beanFactoryInitializationCode) -> { | ||
var enabled = env.getProperty(GrpcTranscodingProperties.PREFIX + ".enabled", Boolean.class, true); | ||
if (!enabled) { | ||
return; | ||
} | ||
|
||
var reflection = generationContext.getRuntimeHints().reflection(); | ||
|
||
// See grpcstarter.extensions.transcoding.DefaultHeaderConverter.getHttpHeaders | ||
reflection.registerType(HttpHeaders.class, builder -> builder.withMembers(MemberCategory.PUBLIC_FIELDS)); | ||
|
||
// This will increase the packaging size about 2MB. | ||
// I don't know why this type is needed, don't want to spend much time to figure it out :) | ||
registerReflectionForClassAndInnerClasses(reflection, DescriptorProtos.class); | ||
|
||
// request + response messages | ||
registerReflectionForMessages(reflection, getMessages(listGrpcServiceDefinition(beanFactory))); | ||
}; | ||
} | ||
|
||
private static void registerReflectionForClassAndInnerClasses(ReflectionHints reflection, Class<?> clz) { | ||
|
||
reflection.registerType(clz, MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS); | ||
|
||
for (var declaredClass : clz.getDeclaredClasses()) { | ||
registerReflectionForClassAndInnerClasses(reflection, declaredClass); | ||
} | ||
} | ||
|
||
private static LinkedHashSet<Class<?>> getMessages(Map<String, BeanDefinition> beanNameToBeanDefinition) { | ||
var messages = new LinkedHashSet<Class<?>>(); | ||
for (var entry : beanNameToBeanDefinition.entrySet()) { | ||
var beanDefinition = entry.getValue(); | ||
var clz = beanDefinition.getResolvableType().resolve(); | ||
if (clz == null) { | ||
continue; | ||
} | ||
|
||
var methods = clz.getMethods(); | ||
for (var method : methods) { | ||
Class<?> returnType = method.getReturnType(); | ||
if (returnType != void.class) { // grpc method should return void | ||
continue; | ||
} | ||
if (method.getParameterCount() != 2) { // grpc method should have 2 parameters | ||
continue; | ||
} | ||
Class<?> message1 = method.getParameterTypes()[0]; | ||
if (!Message.class.isAssignableFrom(message1)) { // the first parameter should be a Message | ||
continue; | ||
} | ||
Type arg2 = method.getGenericParameterTypes()[1]; // the second parameter must be a StreamObserver | ||
if (!(arg2 instanceof ParameterizedType pt)) { | ||
continue; | ||
} | ||
var typeArgs = pt.getActualTypeArguments(); | ||
if (typeArgs.length != 1) { | ||
continue; | ||
} | ||
if (!(typeArgs[0] instanceof Class<?> message2)) { | ||
continue; | ||
} | ||
if (!Message.class.isAssignableFrom(message2)) { // the second parameter should be a Message | ||
continue; | ||
} | ||
|
||
messages.add(message1); | ||
messages.add(message2); | ||
} | ||
} | ||
return messages; | ||
} | ||
|
||
@SneakyThrows | ||
private static void registerReflectionForMessages(ReflectionHints reflection, Set<Class<?>> messages) { | ||
for (var message : messages) { | ||
|
||
// register the message and its builder | ||
reflection.registerType( | ||
message, MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS); | ||
|
||
var builderClass = getBuilderClass(message); | ||
if (builderClass != null) { | ||
reflection.registerType( | ||
builderClass, MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS); | ||
} | ||
} | ||
} | ||
|
||
private static Class<?> getBuilderClass(Class<?> message) { | ||
try { | ||
return ClassUtils.forName(message.getName() + "$Builder", null); | ||
} catch (ClassNotFoundException e) { | ||
return null; | ||
} | ||
} | ||
|
||
private static Map<String, BeanDefinition> listGrpcServiceDefinition(ConfigurableListableBeanFactory beanFactory) { | ||
var beanDefinitions = new HashMap<String, BeanDefinition>(); | ||
for (String name : beanFactory.getBeanDefinitionNames()) { | ||
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(name); | ||
Class<?> clz = beanDefinition.getResolvableType().resolve(); | ||
if (clz != null && BindableService.class.isAssignableFrom(clz)) { | ||
beanDefinitions.put(name, beanDefinition); | ||
} | ||
} | ||
return beanDefinitions; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters