Created
January 29, 2025 10:05
-
-
Save bfg/afc9a512885cd511929096dc0c7067c2 to your computer and use it in GitHub Desktop.
Java graalvm native-image generic reflect-config.json generator as part of a test suite
This file contains hidden or 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
package foo.bar; | |
import groovy.util.logging.Slf4j | |
import io.github.classgraph.ClassGraph | |
import io.github.classgraph.ClassInfo | |
import java.util.function.Predicate | |
/** | |
* Reflection-config.json generator to allow native-image compiled applications to use reflection on 3rd party libraries | |
* that don't provide it. | |
* | |
* @see <a href="https://www.graalvm.org/jdk21/reference-manual/native-image/dynamic-features/Reflection/#manual-configuration">GraalVM Reflection Manual Configuration</a> | |
*/ | |
@Slf4j | |
class ReflectionConfigGenerator { | |
def queryAllPublicConstructors = false | |
def queryAllDeclaredConstructors = false | |
def queryAllPublicMethods = false | |
def queryAllDeclaredMethods = false | |
def allDeclaredConstructors = true | |
def allPublicConstructors = true | |
def allDeclaredMethods = false | |
def allPublicMethods = false | |
def allDeclaredFields = true | |
def allPublicFields = true | |
def allDeclaredClasses = false | |
def allPublicClasses = false | |
def unsafeAllocated = false | |
/** | |
* Generate reflection-config structure for the given packages and classes. | |
* | |
* @param args map arguments: packages, classes, predicate, options | |
* @param args.packages list of packages to scan | |
* @param args.classes list of classes to include in the result | |
* @param args.predicate predicate to filter scan result, see {@link io.github.classgraph.ClassInfo} | |
* @param args.options graalvm reflection-config options | |
* @return list of maps that can be serialized to reflection-config.json | |
*/ | |
List<Map<String, String>> generate(Map args = [:]) { | |
List<String> packages = args.packages ?: [] | |
List<String> classes = args.classes ?: [] | |
Predicate<ClassInfo> predicate = args.predicate ?: { true } | |
Map<String, Boolean> options = args.options ?: [:] | |
def cg = new ClassGraph() | |
.acceptPackages(packages.toArray(new String[0]) as String[]) | |
.acceptClasses(classes.toArray(new String[0]) as String[]) | |
def opts = getOptions(options) | |
log.info("scanning classpath: packages=$packages, classes=$classes, options=$opts") | |
try (def scanResult = cg.scan()) { | |
def classNames = scanResult.allClasses.names.sort() | |
log.info("found ${classNames.size()} classes") | |
return scanResult.allClasses.stream() | |
.filter(predicate) | |
.map { it.name } | |
.sorted() | |
.map { [name: it] + opts } | |
.toList() | |
} | |
} | |
/** | |
* Generate reflection-config structure for the given packages and classes. | |
* | |
* @param args map arguments: packages, classes, predicate, options | |
* @param args.packages list of packages to scan | |
* @param args.classes list of classes to include in the result | |
* @param args.predicate predicate to filter scan result, see {@link io.github.classgraph.ClassInfo} | |
* @param args.options graalvm reflection-config options | |
* @return list of maps that can be serialized to reflection-config.json | |
*/ | |
def generateFile(Map args = [:]) { | |
def filePackage = (args.remove('filePackage') ?: "").trim() | |
def fileModule = (args.remove('fileModule') ?: "").trim() | |
if (filePackage.isEmpty() || fileModule.isEmpty()) { | |
throw new IllegalArgumentException("filePackage or fileModule must be provided and non-empty") | |
} | |
def result = generate(args) | |
// output to file | |
def baseDir = "src/main/resources/META-INF/native-image" | |
def file = new File("$baseDir/$filePackage/$fileModule/reflect-config.json") | |
def parent = file.parentFile | |
parent.mkdirs() | |
log.debug("writing to file: $file") | |
def json = TestUtils.toJson(result) | |
file.withWriter { it.write(json) } | |
log.info("wrote ${result.size()} entries to file: $file") | |
return file.toString() | |
} | |
private Map<String, String> getOptions(Map<String, Boolean> opts = [:]) { | |
def tmpRes = [ | |
queryAllDeclaredConstructors: queryAllDeclaredConstructors, | |
queryAllPublicConstructors : queryAllPublicConstructors, | |
queryAllDeclaredMethods : queryAllDeclaredMethods, | |
queryAllPublicMethods : queryAllPublicMethods, | |
allDeclaredConstructors : allDeclaredConstructors, | |
allPublicConstructors : allPublicConstructors, | |
allDeclaredMethods : allDeclaredMethods, | |
allPublicMethods : allPublicMethods, | |
allDeclaredFields : allDeclaredFields, | |
allPublicFields : allPublicFields, | |
allDeclaredClasses : allDeclaredClasses, | |
allPublicClasses : allPublicClasses, | |
unsafeAllocated : unsafeAllocated, | |
] | |
tmpRes.putAll(opts) | |
// now remove all keys that are false | |
tmpRes.entrySet().removeIf { it.value == false } | |
//def res = tmpRes.collectEntries { k, v -> [k, v.toString()] } | |
//log.info("returning options: $res") | |
return tmpRes | |
} | |
} |
This file contains hidden or 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
import com.stripe.net.ApiResource | |
import groovy.util.logging.Slf4j | |
import io.github.classgraph.ClassInfo | |
import foo.bar.ReflectionConfigGenerator | |
import spock.lang.See | |
import spock.lang.Specification | |
/** | |
* This spock test case generates graalvm native-image reflection configuration for | |
* stripe-java-sdk to be used successfully in a java app compiled with native-image | |
*/ | |
@Slf4j | |
class StripeGenerateReflectionConfigSpec extends Specification { | |
static def stripePackages = [ | |
"com.stripe.model", | |
//"com.stripe.param", // maybe needed, maybe not | |
] | |
static def stripeClasses = [ | |
ApiResource.name, | |
] | |
def generator = new ReflectionConfigGenerator() | |
@See("https://github.com/romixch/stripe-java-graalvm/blob/main/src/main/resources/META-INF/native-image/ch.romix/stripe-java/reflect-config.json") | |
def "should generate reflection config"() { | |
given: | |
def options = [ | |
allDeclaredConstructors: true, | |
allPublicConstructors : true, | |
allDeclaredFields : true, | |
allPublicMethods : true, | |
unsafeAllocated : true, | |
] | |
when: | |
def res = generator.generateFile( | |
filePackage: 'app-stripe-sdk', | |
fileModule: 'stripe-models', | |
packages: stripePackages, | |
classes: stripeClasses, | |
options: options, | |
predicate: { ClassInfo it -> | |
def name = it.name | |
def isBuilder = name.contains('Builder') | |
// skip builders | |
def res = !isBuilder | |
log.debug("$name => $res") | |
res | |
} | |
) | |
log.info("generated stripe java-sdk reflect-config: $res") | |
then: | |
true | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment