Skip to content

Commit

Permalink
feat: Spring boot native image support (#609)
Browse files Browse the repository at this point in the history
  • Loading branch information
Christopher-Chianelli committed Feb 18, 2024
1 parent 46f3caf commit 8462888
Show file tree
Hide file tree
Showing 25 changed files with 2,815 additions and 234 deletions.
9 changes: 9 additions & 0 deletions .github/workflows/pull_request.yml
Expand Up @@ -43,6 +43,15 @@ jobs:
steps:
- uses: actions/checkout@v4

- uses: graalvm/setup-graalvm@v1
with:
java-version: '17'
distribution: 'graalvm-community'
set-java-home: 'false'
components: 'native-image'
github-token: ${{ secrets.GITHUB_TOKEN }}
cache: 'maven'

- uses: actions/setup-java@v4
with:
java-version: '17'
Expand Down
1 change: 1 addition & 0 deletions spring-integration/pom.xml
Expand Up @@ -26,6 +26,7 @@
<modules>
<module>spring-boot-autoconfigure</module>
<module>spring-boot-starter</module>
<module>spring-boot-integration-test</module>
</modules>

<dependencyManagement>
Expand Down
12 changes: 7 additions & 5 deletions spring-integration/spring-boot-autoconfigure/pom.xml
Expand Up @@ -63,6 +63,13 @@
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>

<!-- Required for serializing the SolverConfig in native mode -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
Expand All @@ -78,11 +85,6 @@
<artifactId>spring-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>

<!-- Testing -->
<dependency>
Expand Down
Expand Up @@ -27,7 +27,7 @@
import org.springframework.context.annotation.Lazy;

@Configuration
@AutoConfigureAfter(TimefoldAutoConfiguration.class)
@AutoConfigureAfter(TimefoldSolverAutoConfiguration.class)
@ConditionalOnClass({ PlannerBenchmarkFactory.class })
@ConditionalOnMissingBean({ PlannerBenchmarkFactory.class })
@EnableConfigurationProperties({ TimefoldProperties.class })
Expand Down Expand Up @@ -88,7 +88,7 @@ public PlannerBenchmarkConfig plannerBenchmarkConfig() {
}

if (timefoldProperties.getBenchmark() != null && timefoldProperties.getBenchmark().getSolver() != null) {
TimefoldAutoConfiguration
TimefoldSolverAutoConfiguration
.applyTerminationProperties(benchmarkConfig.getInheritedSolverBenchmarkConfig().getSolverConfig(),
timefoldProperties.getBenchmark().getSolver().getTermination());
}
Expand Down
@@ -0,0 +1,49 @@
package ai.timefold.solver.spring.boot.autoconfigure;

import java.util.Map;

import ai.timefold.solver.core.config.solver.SolverConfig;

import org.springframework.aot.generate.GenerationContext;
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.BeanFactoryInitializationCode;

public class TimefoldSolverAotContribution implements BeanFactoryInitializationAotContribution {
private final Map<String, SolverConfig> solverConfigMap;

public TimefoldSolverAotContribution(Map<String, SolverConfig> solverConfigMap) {
this.solverConfigMap = solverConfigMap;
}

/**
* Register a type for reflection, allowing introspection
* of its members at runtime in a native build.
*/
private static void registerType(ReflectionHints reflectionHints, Class<?> type) {
reflectionHints.registerType(type,
MemberCategory.INTROSPECT_PUBLIC_METHODS,
MemberCategory.INTROSPECT_DECLARED_METHODS,
MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS,
MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS,
MemberCategory.PUBLIC_FIELDS,
MemberCategory.DECLARED_FIELDS,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS,
MemberCategory.INVOKE_PUBLIC_METHODS);
}

@Override
public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
ReflectionHints reflectionHints = generationContext.getRuntimeHints().reflection();
for (SolverConfig solverConfig : solverConfigMap.values()) {
solverConfig.visitReferencedClasses(type -> {
if (type != null) {
registerType(reflectionHints, type);
}
});
}
}
}
@@ -0,0 +1,42 @@
package ai.timefold.solver.spring.boot.autoconfigure;

import java.io.StringReader;

import ai.timefold.solver.core.api.solver.SolverFactory;
import ai.timefold.solver.core.api.solver.SolverManager;
import ai.timefold.solver.core.config.solver.SolverConfig;
import ai.timefold.solver.core.config.solver.SolverManagerConfig;
import ai.timefold.solver.core.impl.io.jaxb.SolverConfigIO;
import ai.timefold.solver.spring.boot.autoconfigure.config.SolverManagerProperties;
import ai.timefold.solver.spring.boot.autoconfigure.config.TimefoldProperties;

import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;

public class TimefoldSolverAotFactory implements EnvironmentAware {
private TimefoldProperties timefoldProperties;

@Override
public void setEnvironment(Environment environment) {
// We need the environment to set run time properties of SolverFactory and SolverManager
BindResult<TimefoldProperties> result = Binder.get(environment).bind("timefold", TimefoldProperties.class);
this.timefoldProperties = result.orElseGet(TimefoldProperties::new);
}

public <Solution_, ProblemId_> SolverManager<Solution_, ProblemId_> solverManagerSupplier(String solverConfigXml) {
SolverFactory<Solution_> solverFactory = SolverFactory.create(solverConfigSupplier(solverConfigXml));
SolverManagerConfig solverManagerConfig = new SolverManagerConfig();
SolverManagerProperties solverManagerProperties = timefoldProperties.getSolverManager();
if (solverManagerProperties != null && solverManagerProperties.getParallelSolverCount() != null) {
solverManagerConfig.setParallelSolverCount(solverManagerProperties.getParallelSolverCount());
}
return SolverManager.create(solverFactory, solverManagerConfig);
}

public SolverConfig solverConfigSupplier(String solverConfigXml) {
SolverConfigIO solverConfigIO = new SolverConfigIO();
return solverConfigIO.read(new StringReader(solverConfigXml));
}
}

0 comments on commit 8462888

Please sign in to comment.