io.micronaut.aot.MicronautAotOptimizer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of micronaut-aot-api Show documentation
Show all versions of micronaut-aot-api Show documentation
Build time optimizations for Micronaut
/*
* Copyright 2017-2021 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.aot;
import com.squareup.javapoet.JavaFile;
import io.micronaut.aot.core.AOTCodeGenerator;
import io.micronaut.aot.core.AOTModule;
import io.micronaut.aot.core.Configuration;
import io.micronaut.aot.core.Environments;
import io.micronaut.aot.core.Option;
import io.micronaut.aot.core.Runtime;
import io.micronaut.aot.core.codegen.ApplicationContextConfigurerGenerator;
import io.micronaut.aot.core.config.DefaultConfiguration;
import io.micronaut.aot.core.config.MetadataUtils;
import io.micronaut.aot.core.config.SourceGeneratorLoader;
import io.micronaut.aot.core.context.ApplicationContextAnalyzer;
import io.micronaut.aot.core.context.DefaultSourceGenerationContext;
import io.micronaut.aot.internal.StreamHelper;
import io.micronaut.core.annotation.Experimental;
import io.micronaut.core.version.SemanticVersion;
import io.micronaut.core.version.VersionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
import static io.micronaut.aot.core.config.MetadataUtils.toPropertiesSample;
/**
* The Micronaut AOT optimizer is the main entry point for code
* generation at build time. Its role is to generate a bunch of
* source code for various optimizations which can be computed
* at build time.
*
* Typically, generated code will involve the generation of an
* "optimized" entry point for the application, which delegates
* to the main entry point, but also performs some static
* initialization by making calls to the
* {@link io.micronaut.core.optim.StaticOptimizations} class.
*
* The Micronaut AOT optimizer is experimental and won't do
* anything by its own: it must be integrated in some form, for
* example via a build plugin, which in turn will make the generated
* classes visible to the user. For example, the build tool may
* call this class to generate the optimization code, and in addition
* create an optimized jar, an optimized native binary or even a
* full distribution.
*
* The optimizer works by passing in the whole application runtime
* classpath and a set of configuration options. It then analyzes
* the classpath, for example to identify the services to be loaded,
* or to provide some alternative implementations to existing
* classes.
*/
@Experimental
public final class MicronautAotOptimizer implements ConfigKeys {
public static final String OUTPUT_RESOURCES_FILE_NAME = "resource-filter.txt";
private static final int MINIMAL_MAJOR = 3;
private static final int MINIMAL_MINOR = 3;
private static final Logger LOGGER = LoggerFactory.getLogger(MicronautAotOptimizer.class);
private final List classpath;
private final File outputSourcesDirectory;
private final File outputClassesDirectory;
private final File logsDirectory;
private MicronautAotOptimizer(List classpath,
File outputSourcesDirectory,
File outputClassesDirectory,
File logsDirectory) {
this.classpath = classpath;
this.outputSourcesDirectory = outputSourcesDirectory;
this.outputClassesDirectory = outputClassesDirectory;
this.logsDirectory = logsDirectory;
}
private void compileGeneratedSources(List extraClasspath, List javaFiles) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector ds = new DiagnosticCollector<>();
try (StandardJavaFileManager mgr = compiler.getStandardFileManager(ds, null, null)) {
List fullClasspath = new ArrayList<>(classpath);
fullClasspath.addAll(extraClasspath);
List options = compilerOptions(outputClassesDirectory, fullClasspath);
List filesToCompile = outputSourceFilesToSourceDir(outputSourcesDirectory, javaFiles);
if (outputClassesDirectory.exists() || outputClassesDirectory.mkdirs()) {
Iterable extends JavaFileObject> sources = mgr.getJavaFileObjectsFromFiles(filesToCompile);
JavaCompiler.CompilationTask task = compiler.getTask(null, mgr, ds, options, null, sources);
task.call();
}
} catch (IOException e) {
throw new RuntimeException("Unable to compile generated classes", e);
}
List> diagnostics = ds.getDiagnostics().stream()
.filter(d -> d.getKind() == Diagnostic.Kind.ERROR)
.collect(Collectors.toList());
if (!diagnostics.isEmpty()) {
throwCompilationError(diagnostics);
}
}
/**
* Scans the list of available optimization services and generates
* a configuration file which includes all entries.
*
* @param runtime the runtime for which to generate a properties file
* @param propertiesFile the generated properties file
*/
public static void exportConfiguration(String runtime, File propertiesFile) {
List list = SourceGeneratorLoader.list(Runtime.valueOf(runtime.toUpperCase(Locale.ENGLISH)));
try (PrintWriter wrt = new PrintWriter(new FileOutputStream(propertiesFile))) {
Deque queue = new ArrayDeque<>(list);
while (!queue.isEmpty()) {
AOTModule generator = queue.pop();
if (!generator.description().isEmpty()) {
Arrays.stream(generator.description().split("\r?\n")).forEach(line ->
wrt.println("# " + line));
}
wrt.println(generator.id() + ".enabled = true");
Arrays.stream(generator.subgenerators())
.map(MetadataUtils::findMetadata)
.filter(Optional::isPresent)
.map(Optional::get)
.sorted(Collections.reverseOrder())
.forEachOrdered(queue::addFirst);
for (Option option : generator.options()) {
wrt.println(toPropertiesSample(option));
}
wrt.println();
}
// Add options which are generic to all AOT modules
wrt.println("# " + Environments.TARGET_ENVIRONMENTS_DESCRIPTION);
wrt.println(Environments.TARGET_ENVIRONMENTS_NAMES + " = " + Environments.TARGET_ENVIRONMENTS_SAMPLE);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
/**
* This convenience method uses properties to load the configuration.
* This is useful because the optimizer must be found on the same
* classloader as the application under optimization, otherwise it
* would mean that we could have a clash between Micronaut runtime
* versions.
*
* @param props the configuration properties
*/
public static void execute(Properties props) {
Configuration config = new DefaultConfiguration(props);
String pkg = config.mandatoryValue(GENERATED_PACKAGE);
File outputDir = new File(config.mandatoryValue(OUTPUT_DIRECTORY));
File sourcesDir = new File(outputDir, "sources");
File classesDir = new File(outputDir, "classes");
File logsDir = new File(outputDir, "logs");
runner(pkg, sourcesDir, classesDir, logsDir, config)
.addClasspath(config.stringList(CLASSPATH).stream().map(File::new).collect(Collectors.toList()))
.execute();
}
public static Runner runner(String generatedPackage,
File outputSourcesDirectory,
File outputClassesDirectory,
File logsDirectory,
Configuration config) {
return new Runner(generatedPackage, outputSourcesDirectory, outputClassesDirectory, logsDirectory, config);
}
private static List outputSourceFilesToSourceDir(File srcDir, List javaFiles) {
List srcFiles = new ArrayList<>(javaFiles.size());
if (srcDir.isDirectory() || srcDir.mkdirs()) {
StreamHelper.trying(() -> {
for (JavaFile javaFile : javaFiles) {
javaFile.writeTo(srcDir);
}
Files.walkFileTree(srcDir.toPath(), new SimpleFileVisitor() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
srcFiles.add(file.toFile());
return super.visitFile(file, attrs);
}
});
});
}
return srcFiles;
}
private static void throwCompilationError(List> diagnostics) {
StringBuilder sb = new StringBuilder("Compilation errors:\n");
for (Diagnostic extends JavaFileObject> d : diagnostics) {
JavaFileObject source = d.getSource();
String srcFile = source == null ? "unknown" : new File(source.toUri()).getName();
String diagLine = String.format("File %s, line: %d, %s", srcFile, d.getLineNumber(), d.getMessage(null));
sb.append(diagLine).append("\n");
}
throw new RuntimeException(sb.toString());
}
private static List compilerOptions(File dstDir,
List classPath) {
List options = new ArrayList<>();
options.add("-source");
options.add("1.8");
options.add("-target");
options.add("1.8");
options.add("-classpath");
String cp = classPath.stream().map(File::getAbsolutePath).collect(Collectors.joining(File.pathSeparator));
options.add(cp);
options.add("-d");
options.add(dstDir.getAbsolutePath());
return options;
}
private void writeLogs(DefaultSourceGenerationContext context) {
if (logsDirectory.isDirectory() || logsDirectory.mkdirs()) {
writeLines(new File(logsDirectory, OUTPUT_RESOURCES_FILE_NAME), context.getExcludedResources());
context.getDiagnostics().forEach((key, messages) -> {
File logFile = new File(logsDirectory, key.toLowerCase(Locale.US) + ".log");
writeLines(logFile, messages);
});
}
}
private static void writeLines(File outputFile, Collection lines) {
try (PrintWriter writer = new PrintWriter(
new OutputStreamWriter(new FileOutputStream(outputFile), StandardCharsets.UTF_8)
)) {
lines.forEach(writer::println);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
private static void assertMinimalMicronautVersion() {
String micronautVersion = VersionUtils.getMicronautVersion();
if (micronautVersion != null && !SemanticVersion.isAtLeastMajorMinor(micronautVersion, MINIMAL_MAJOR, MINIMAL_MINOR)) {
throw new RuntimeException("This version of the AOT optimizer requires at least Micronaut " + MINIMAL_MAJOR + "." + MINIMAL_MINOR + " but found " + micronautVersion);
}
}
/**
* The main AOT optimizer runner.
*/
public static final class Runner {
private final List classpath = new ArrayList<>();
private final String generatedPackage;
private final File outputSourcesDirectory;
private final File outputClassesDirectory;
private final File logsDirectory;
private final Configuration config;
public Runner(String generatedPackage,
File outputSourcesDirectory,
File outputClassesDirectory,
File logsDirectory,
Configuration config
) {
this.generatedPackage = generatedPackage;
this.outputSourcesDirectory = outputSourcesDirectory;
this.outputClassesDirectory = outputClassesDirectory;
this.logsDirectory = logsDirectory;
this.config = config;
}
/**
* Adds elements to the application classpath.
*
* @param elements the files to add to classpath
* @return this builder
*/
public Runner addClasspath(Collection elements) {
classpath.addAll(elements);
return this;
}
@SuppressWarnings("unchecked")
public Runner execute() {
MicronautAotOptimizer optimizer = new MicronautAotOptimizer(
classpath,
outputSourcesDirectory,
outputClassesDirectory,
logsDirectory);
ApplicationContextAnalyzer analyzer = ApplicationContextAnalyzer.create(spec -> {
if (config.containsKey(Environments.TARGET_ENVIRONMENTS_NAMES)) {
List targetEnvs = config.stringList(Environments.TARGET_ENVIRONMENTS_NAMES);
LOGGER.info("Configuration has explicitly set environments: {} ", targetEnvs);
spec.environments(targetEnvs.toArray(new String[0]));
}
assertMinimalMicronautVersion();
});
Set environmentNames = analyzer.getEnvironmentNames();
LOGGER.info("Analysis will be performed with active environments: {}", environmentNames);
DefaultSourceGenerationContext context = new DefaultSourceGenerationContext(generatedPackage, analyzer, config, outputClassesDirectory.toPath());
List sourceGenerators = SourceGeneratorLoader.load(config.getRuntime(), context);
ApplicationContextConfigurerGenerator generator = new ApplicationContextConfigurerGenerator(
sourceGenerators
);
generator.generate(context);
optimizer.compileGeneratedSources(context.getExtraClasspath(), context.getGeneratedJavaFiles());
optimizer.writeLogs(context);
return this;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy