spring-projects / spring-framework

Spring Framework

Home Page:https://spring.io/projects/spring-framework

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Improve GraalVM native support

aclement opened this issue · comments

native-image is the command that compiles an application (in our case a Spring application) into a native executable. Configuration of the command can be done via command line options (passed directly or via static side files) or by building a Feature. In some cases command line options is fine but a Feature gives maximum flexibility as it is code that participates in the native-image execution process and can make dynamic decisions based on the proposed content of the executable.

There is a prototype of Spring Boot Feature in this project: https://github.com/spring-projects-experimental/spring-graalvm-native

The prototype is crude and currently uses a lot of fixed information which needs computing but the project proves that once that information is collected (and if we workaround that remaining Graal issue), native-image is happy to build Spring projects.

There are four kinds of data that are passed by the Feature as the native-image command executes:

  • reflection. We need to determine which types are going to be reflected on as the application runs, and if we tell the compiler it will generate metadata such that reflection will succeed at runtime.
  • resources. We need to determine which resources are going to be accessed as the application runs. If we tell the compiler about them they will be added to the executable image and available at runtime. This includes obvious files like spring.factories spring.components but also if the system is going to load the bytecode to run asm over it to find something, those class files also need to be available as resources.
  • proxies. We need to determine what proxies will be generated as the system runs. By telling the compiler about them it will generate them at image build time and they will be available at runtime when requested.
  • delayed initialization. The compiler will try to do as much initialization up front for faster execution later. This means executing class initializers as the image is built. This works great if building a data table for later lookup, but it does not work in all cases. For example if the code was creating a DirectByteBuffer. We need to compute any types that need to be delayed for runtime initialization and tell the compiler about them.

Some of this data can be computed easily - some types are already annotated (or meta-annotated) with something that indicates we'll need to add them to the reflection/resource lists. (@Configuration). The current prototype Feature doesn't do much computation (yet) but you can see the kind of data it is passing (and so needs computing) for all these four kinds of data here: https://github.com/aclement/spring-boot-graal-feature/tree/master/src/main/resources
(That is not the minimal set of data, it is just a set that works!)

JDK Proxy usage is allowed in applications being compiled but not dynamic class definition via CGLIB. To avoid the need to do that you can use the new proxyBeanMethods=false option. You can see @SpringBootApplication(proxyBeanMethods = false) in the sample demo app mentioned above. There are other ways we can handle this (annotation processor that generates the proxy at build time).

The sample project above is using the spring-content-indexer maven plugin to create a spring.components file for some processing. Not entirely clear this is necessary yet.

There is a challenge in drawing the line between framework and boot. A basic version of a feature for framework might scan for spring.factories and simply add any listed classes to the reflect/resource lists. The problem is whether that goes far enough to make something work. For example if spring.factories is being used to specify boots EnableAutoConfiguration then just adding the referred auto-configuration classes isn't enough. If those configuration classes use a ConditionalOnClass check, the types referred to in those conditions also need adding in order for the configuration to behave and framework has no knowledge of that need (only boot does).
Going further, and that is what the sample feature above does (as an experiment and to keep the size of the manually maintained json files): What if the ConditionalOnClass check was going to fail because at image build time we know the classpath and it isn't there? There is no need to add the auto configuration to the system because it will fail immediately. You can tweak resource files as the image is built (remove unnecessary entries in spring.factories for example). This would reduce image size and make the compiled result even faster. (As an example, boot 2.2 lists ~120 autoconfigurations in spring.factories, 100 of them are immediately going to fail ConditionalOnClass checks). Can both boot and framework have a feature? Sure, but how would you handle this case above when there are two?
Maybe keep it dumb initially and measure the impact (framework feature simply adds types referred to in spring.factories where the boot feature takes another pass and adds the deeper set of types, no smart exclusion).

Dependencies are likely to add their own features over time. For example the sample project above includes its own configuration for netty but netty now ships their own that we should simply rely on.

Also need to decide how to recommend users run native-image. It can be run as part of a maven build but you probably want it attached to a separate target because it takes a while (you probably wouldn't want to run it all the time). The sample project above unpacks the boot far jar before running it, that may not be necessary with a more sophisticated feature.

Having said all the above... taking a step back there is an alternative to a feature if we wish to go down the compiler plugin route. Annotation processors can produce the more static kind of data (the json flat files) that native-image can then pickup. Possibly slightly more advanced than your standard annotation processor as may need to dig deeply through some types to track down some things that need to be included in those files. In this situation we'd build spring itself with these processors and include the json files in each built artifact. The files would look a bit like those I linked above but be split up by jar that introduces those entries. The users application would also need to be built with the processor to produce the side files. What all this wouldn't allow is the more dynamic behaviour (excluding certain things as the image is built because you know the complete classpath for the system that is going to run).

Spring Framework can work in GraalVM mode will be excited. And Which spring framework release version do we plan to support GraalVM with native image? 5.X Or ? Thanks

@guanchao-yang Thanks for your interest but Spring Framework bug tracker is not expected to be used for asking question about the roadmap, this issue is in the 5.x backlog for now, we will share more information when that will be possible. Feel free to show your interest by adding your 👍 to this issue.

@sdeleuze Thank you very much.

More notes. The original comments were written based on the graal commit #818cccb852ec - and the spring boot sample referenced works against that level. Things have moved around in the GA release of graal and it needs updating, that is a work in progress right now (for example delay initialization is now flipped to specify what should be put through build time initialization instead).

GraalVM has an issue resolving "directory" resources on the classpath (oracle/graal#1108). This may be a blocker for a large class of Spring apps (anything with a component scan, including entity scan). There are also issues with third party tools that do directory traversal in classpath resources (e.g. the issue above was raised by flyway). We are working around it in the @aclement feature jar for now by synthesising spring.components.

Thank you everyone for the effort, would be amazing to be able to scale down already working applications into micro-service level applications!

Perhaps you can tell me if there is a chance to make it working. I'm trying to create a native image for a spring boot application (spring boot 2.1.7 and spring 5.1.9), but got the following exception:

Warning: Aborting stand-alone image build. No instances of org.springframework.util.unit.DataSize are allowed in the image heap as this class should be initialized at image runtime.Object has been initialized without the native-image initialization instrumentation and the stack trace can't be tracked.
Detailed message:
Trace: 	object org.springframework.util.unit.DataUnit
	method org.springframework.util.unit.DataSize.determineDataUnit(String, DataUnit)
Call path from entry point to org.springframework.util.unit.DataSize.determineDataUnit(String, DataUnit): 
	at org.springframework.util.unit.DataSize.determineDataUnit(DataSize.java:175)
	at org.springframework.util.unit.DataSize.parse(DataSize.java:165)
	at org.springframework.boot.convert.StringToDataSizeConverter.convert(StringToDataSizeConverter.java:57)
	at org.springframework.boot.convert.StringToDataSizeConverter.convert(StringToDataSizeConverter.java:48)
	at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41)
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:191)
	at org.springframework.boot.convert.DelimitedStringToCollectionConverter.lambda$convert$0(DelimitedStringToCollectionConverter.java:72)
	at org.springframework.boot.convert.DelimitedStringToCollectionConverter$$Lambda$403/680911666.apply(Unknown Source)
	at java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:269)
	at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.util.stream.SpinedBuffer.forEach(SpinedBuffer.java:246)
	at java.util.stream.SpinedBuffer.toString(SpinedBuffer.java:269)
	at java.lang.String.valueOf(String.java:2994)
	at java.lang.StringBuilder.append(StringBuilder.java:131)
	at com.oracle.svm.core.amd64.AMD64CPUFeatureAccess.verifyHostSupportsArchitecture(AMD64CPUFeatureAccess.java:179)
	at com.oracle.svm.core.JavaMainWrapper.runCore(JavaMainWrapper.java:129)
	at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:186)
	at com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0)

Any idea or workaround ? Do you think it may be a graal issue more than a spring issue ?

Thanks,
Yaakov

@yaakov-berkovitch I have same problem (with --static flag). Tried with spring boot 2.1 and 2.2.M5

@yaakov-berkovitch I will shortly be making available the version that works with Boot 2.2.0.M5 and the relevant framework 5.2. (this is with the recent Graal 19.2). I will then keep that up to date through the boot 2.2.0 release. That are changes in Spring I am relying on so I'm not sure I'd be expecting 2.1.7 to be working at the moment.

The issue you are showing is a Graal configuration issue. (So not graal, more that the config data being passed to graal for your spring app is not right - exactly what the spring-boot-graal-feature is designed to help with).

With spring-graalvm-native 0.7.0 about to be released, I am turning this issue about a general GraalVM native support improvement one, with for now 4 more specific areas where we are going to improve Spring Framework support for GraalVM native:

  • #25151 Provide a flag to disable XML support
  • #25153 Provide a flag to disable SpEL support
  • #25179 Disable and remove unsupported features from native images
  • #25209 Add missing DispatcherServlet default beans to WebMvcConfigurationSupport

The bonus side effect of these changes is to remove as much substitution we can from spring-graalvm-native since they are by nature unmaintainable and not officially supported API.

Similar improvements could potentially be done at Spring Boot and Spring Data level (see for example spring-projects/spring-data-r2dbc#229).

The GraalVM feature and documentation will continue to mature on spring-graalvm-native side, see related roadmap.