Skip to content

Instantly share code, notes, and snippets.

@bfg
Created January 29, 2025 10:05
Show Gist options
  • Save bfg/afc9a512885cd511929096dc0c7067c2 to your computer and use it in GitHub Desktop.
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
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
}
}
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