Compare commits
1 Commits
main
...
v4.0.0-RC2
| Author | SHA1 | Date |
|---|---|---|
|
|
c7ec1fa734 |
|
|
@ -126,7 +126,7 @@ jobs:
|
|||
- name: Publish to SDKMAN!
|
||||
uses: ./.github/actions/publish-to-sdkman
|
||||
with:
|
||||
make-default: true
|
||||
make-default: false
|
||||
sdkman-consumer-key: ${{ secrets.SDKMAN_CONSUMER_KEY }}
|
||||
sdkman-consumer-token: ${{ secrets.SDKMAN_CONSUMER_TOKEN }}
|
||||
spring-boot-version: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,18 +1,20 @@
|
|||
{
|
||||
"scripts": {
|
||||
"antora": "node npm/antora.js"
|
||||
"antora": "node npm/antora.js",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@antora/cli": "3.2.0-alpha.10",
|
||||
"@antora/site-generator": "3.2.0-alpha.10",
|
||||
"@antora/site-generator": "3.2.0-alpha.4",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.5",
|
||||
"@springio/antora-extensions": "1.14.7",
|
||||
"@springio/antora-xref-extension": "1.0.0-alpha.5",
|
||||
"@springio/antora-zip-contents-collector-extension": "1.0.0-alpha.10",
|
||||
"@springio/antora-xref-extension": "1.0.0-alpha.4",
|
||||
"@springio/antora-zip-contents-collector-extension": "1.0.0-alpha.8",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.17",
|
||||
"patch-package": "^8.0.1"
|
||||
},
|
||||
"config": {
|
||||
"ui-bundle-url": "https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.25/ui-bundle.zip"
|
||||
"ui-bundle-url": "https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,285 @@
|
|||
diff --git a/node_modules/@vscode/gulp-vinyl-zip/lib/src/index.js b/node_modules/@vscode/gulp-vinyl-zip/lib/src/index.js
|
||||
index 17d902d..0448dec 100644
|
||||
--- a/node_modules/@vscode/gulp-vinyl-zip/lib/src/index.js
|
||||
+++ b/node_modules/@vscode/gulp-vinyl-zip/lib/src/index.js
|
||||
@@ -1,135 +1,157 @@
|
||||
-'use strict';
|
||||
-
|
||||
-var fs = require('fs');
|
||||
-var constants = fs.constants;
|
||||
-var yauzl = require('yauzl');
|
||||
-var File = require('../vinyl-zip');
|
||||
-var queue = require('queue');
|
||||
-var through = require('through');
|
||||
-var map = require('through2').obj;
|
||||
-
|
||||
-function modeFromEntry(entry) {
|
||||
- var attr = entry.externalFileAttributes >> 16 || 33188;
|
||||
-
|
||||
- // The following constants are not available on all platforms:
|
||||
- // 448 = constants.S_IRWXU, 56 = constants.S_IRWXG, 7 = constants.S_IRWXO
|
||||
- return [448, 56, 7]
|
||||
- .map(function (mask) { return attr & mask; })
|
||||
- .reduce(function (a, b) { return a + b; }, attr & constants.S_IFMT);
|
||||
+'use strict'
|
||||
+
|
||||
+// This is fork of vinyl-zip with the following updates:
|
||||
+// - unzipFile has an additional `.on('error'` handler
|
||||
+// - toStream has an additional `zip.on('error'` handler
|
||||
+
|
||||
+const fs = require('fs')
|
||||
+const constants = fs.constants
|
||||
+const yauzl = require('yauzl')
|
||||
+const File = require('vinyl')
|
||||
+const queue = require('queue')
|
||||
+const through = require('through')
|
||||
+const map = require('through2').obj
|
||||
+
|
||||
+function modeFromEntry (entry) {
|
||||
+ const attr = entry.externalFileAttributes >> 16 || 33188
|
||||
+ return [448, 56, 7]
|
||||
+ .map(function (mask) {
|
||||
+ return attr & mask
|
||||
+ })
|
||||
+ .reduce(function (a, b) {
|
||||
+ return a + b
|
||||
+ }, attr & constants.S_IFMT)
|
||||
}
|
||||
|
||||
-function mtimeFromEntry(entry) {
|
||||
- return yauzl.dosDateTimeToDate(entry.lastModFileDate, entry.lastModFileTime);
|
||||
+function mtimeFromEntry (entry) {
|
||||
+ return yauzl.dosDateTimeToDate(entry.lastModFileDate, entry.lastModFileTime)
|
||||
}
|
||||
|
||||
-function toStream(zip) {
|
||||
- var result = through();
|
||||
- var q = queue();
|
||||
- var didErr = false;
|
||||
-
|
||||
- q.on('error', function (err) {
|
||||
- didErr = true;
|
||||
- result.emit('error', err);
|
||||
- });
|
||||
-
|
||||
- zip.on('entry', function (entry) {
|
||||
- if (didErr) { return; }
|
||||
-
|
||||
- var stat = new fs.Stats();
|
||||
- stat.mode = modeFromEntry(entry);
|
||||
- stat.mtime = mtimeFromEntry(entry);
|
||||
-
|
||||
- // directories
|
||||
- if (/\/$/.test(entry.fileName)) {
|
||||
- stat.mode = (stat.mode & ~constants.S_IFMT) | constants.S_IFDIR;
|
||||
- }
|
||||
-
|
||||
- var file = {
|
||||
- path: entry.fileName,
|
||||
- stat: stat
|
||||
- };
|
||||
-
|
||||
- if (stat.isFile()) {
|
||||
- stat.size = entry.uncompressedSize;
|
||||
- if (entry.uncompressedSize === 0) {
|
||||
- file.contents = Buffer.alloc(0);
|
||||
- result.emit('data', new File(file));
|
||||
- } else {
|
||||
- q.push(function (cb) {
|
||||
- zip.openReadStream(entry, function (err, readStream) {
|
||||
- if (err) { return cb(err); }
|
||||
- file.contents = readStream;
|
||||
- result.emit('data', new File(file));
|
||||
- cb();
|
||||
- });
|
||||
- });
|
||||
-
|
||||
- q.start();
|
||||
- }
|
||||
- } else if (stat.isSymbolicLink()) {
|
||||
- stat.size = entry.uncompressedSize;
|
||||
- q.push(function (cb) {
|
||||
- zip.openReadStream(entry, function (err, readStream) {
|
||||
- if (err) { return cb(err); }
|
||||
- file.symlink = '';
|
||||
- readStream.on('data', function (c) { file.symlink += c; });
|
||||
- readStream.on('error', cb);
|
||||
- readStream.on('end', function () {
|
||||
- result.emit('data', new File(file));
|
||||
- cb();
|
||||
- });
|
||||
- });
|
||||
- });
|
||||
-
|
||||
- q.start();
|
||||
- } else if (stat.isDirectory()) {
|
||||
- result.emit('data', new File(file));
|
||||
- } else {
|
||||
- result.emit('data', new File(file));
|
||||
- }
|
||||
- });
|
||||
-
|
||||
- zip.on('end', function () {
|
||||
- if (didErr) {
|
||||
- return;
|
||||
- }
|
||||
-
|
||||
- if (q.length === 0) {
|
||||
- result.end();
|
||||
- } else {
|
||||
- q.on('end', function () {
|
||||
- result.end();
|
||||
- });
|
||||
- }
|
||||
- });
|
||||
-
|
||||
- return result;
|
||||
+function toStream (zip) {
|
||||
+ const result = through()
|
||||
+ const q = queue()
|
||||
+ let didErr = false
|
||||
+
|
||||
+ q.on('error', function (err) {
|
||||
+ didErr = true
|
||||
+ result.emit('error', err)
|
||||
+ })
|
||||
+
|
||||
+ zip.on('error', function (err) {
|
||||
+ didErr = true
|
||||
+ result.emit('error', err)
|
||||
+ })
|
||||
+
|
||||
+ zip.on('entry', function (entry) {
|
||||
+ if (didErr) {
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
+ const stat = new fs.Stats()
|
||||
+ stat.mode = modeFromEntry(entry)
|
||||
+ stat.mtime = mtimeFromEntry(entry)
|
||||
+
|
||||
+ // directories
|
||||
+ if (/\/$/.test(entry.fileName)) {
|
||||
+ stat.mode = (stat.mode & ~constants.S_IFMT) | constants.S_IFDIR
|
||||
+ }
|
||||
+
|
||||
+ const file = {
|
||||
+ path: entry.fileName,
|
||||
+ stat,
|
||||
+ }
|
||||
+
|
||||
+ if (stat.isFile()) {
|
||||
+ stat.size = entry.uncompressedSize
|
||||
+ if (entry.uncompressedSize === 0) {
|
||||
+ file.contents = Buffer.alloc(0)
|
||||
+ result.emit('data', new File(file))
|
||||
+ } else {
|
||||
+ q.push(function (cb) {
|
||||
+ zip.openReadStream(entry, function (err, readStream) {
|
||||
+ if (err) {
|
||||
+ return cb(err)
|
||||
+ }
|
||||
+ file.contents = readStream
|
||||
+ result.emit('data', new File(file))
|
||||
+ cb()
|
||||
+ })
|
||||
+ })
|
||||
+
|
||||
+ q.start()
|
||||
+ }
|
||||
+ } else if (stat.isSymbolicLink()) {
|
||||
+ stat.size = entry.uncompressedSize
|
||||
+ q.push(function (cb) {
|
||||
+ zip.openReadStream(entry, function (err, readStream) {
|
||||
+ if (err) {
|
||||
+ return cb(err)
|
||||
+ }
|
||||
+ file.symlink = ''
|
||||
+ readStream.on('data', function (c) {
|
||||
+ file.symlink += c
|
||||
+ })
|
||||
+ readStream.on('error', cb)
|
||||
+ readStream.on('end', function () {
|
||||
+ result.emit('data', new File(file))
|
||||
+ cb()
|
||||
+ })
|
||||
+ })
|
||||
+ })
|
||||
+
|
||||
+ q.start()
|
||||
+ } else if (stat.isDirectory()) {
|
||||
+ result.emit('data', new File(file))
|
||||
+ } else {
|
||||
+ result.emit('data', new File(file))
|
||||
+ }
|
||||
+ })
|
||||
+
|
||||
+ zip.on('end', function () {
|
||||
+ if (didErr) {
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
+ if (q.length === 0) {
|
||||
+ result.end()
|
||||
+ } else {
|
||||
+ q.on('end', function () {
|
||||
+ result.end()
|
||||
+ })
|
||||
+ }
|
||||
+ })
|
||||
+
|
||||
+ return result
|
||||
}
|
||||
|
||||
-function unzipFile(zipPath) {
|
||||
- var result = through();
|
||||
- yauzl.open(zipPath, function (err, zip) {
|
||||
- if (err) { return result.emit('error', err); }
|
||||
- toStream(zip).pipe(result);
|
||||
- });
|
||||
- return result;
|
||||
+function unzipFile (zipPath) {
|
||||
+ const result = through()
|
||||
+ yauzl.open(zipPath, function (err, zip) {
|
||||
+ if (err) {
|
||||
+ return result.emit('error', err)
|
||||
+ }
|
||||
+ toStream(zip)
|
||||
+ .on('error', (err) => result.emit('error', err))
|
||||
+ .pipe(result)
|
||||
+ })
|
||||
+ return result
|
||||
}
|
||||
|
||||
-function unzip() {
|
||||
- return map(function (file, enc, next) {
|
||||
- if (!file.isBuffer()) return next(new Error('Only supports buffers'));
|
||||
- yauzl.fromBuffer(file.contents, (err, zip) => {
|
||||
- if (err) return this.emit('error', err);
|
||||
- toStream(zip)
|
||||
- .on('error', next)
|
||||
- .on('data', (data) => this.push(data))
|
||||
- .on('end', next);
|
||||
- });
|
||||
- });
|
||||
+function unzip () {
|
||||
+ return map(function (file, enc, next) {
|
||||
+ if (!file.isBuffer()) return next(new Error('Only supports buffers'))
|
||||
+ yauzl.fromBuffer(file.contents, (err, zip) => {
|
||||
+ if (err) return this.emit('error', err)
|
||||
+ toStream(zip)
|
||||
+ .on('error', next)
|
||||
+ .on('data', (data) => this.push(data))
|
||||
+ .on('end', next)
|
||||
+ })
|
||||
+ })
|
||||
}
|
||||
|
||||
-function src(zipPath) {
|
||||
- return zipPath ? unzipFile(zipPath) : unzip();
|
||||
+function src (zipPath) {
|
||||
+ return zipPath ? unzipFile(zipPath) : unzip()
|
||||
}
|
||||
|
||||
-module.exports = src;
|
||||
+module.exports = src
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
<settings defaultResolver="chain" />
|
||||
<resolvers>
|
||||
<chain name="chain">
|
||||
<!-- NOTE: You should declare only repositories that you need here -->
|
||||
<filesystem name="local" local="true" m2compatible="true">
|
||||
<artifact pattern="${user.home}/.m2/[organisation]/[module]/[revision]/[module]-[revision].[ext]" />
|
||||
<ivy pattern="${user.home}/.m2/[organisation]/[module]/[revision]/[module]-[revision].pom" />
|
||||
|
|
@ -10,5 +11,6 @@
|
|||
<!-- {spring.mavenRepositories} -->
|
||||
</chain>
|
||||
</resolvers>
|
||||
<!-- {spring.mavenCredentials} -->
|
||||
<credentials host="${env.COMMERCIAL_RELEASE_REPO_URL}" username="${env.COMMERCIAL_REPO_USERNAME}" passwd="${env.COMMERCIAL_REPO_PASSWORD}"/>
|
||||
<credentials host="${env.COMMERCIAL_SNAPSHOT_REPO_URL}" username="${env.COMMERCIAL_REPO_USERNAME}" passwd="${env.COMMERCIAL_REPO_PASSWORD}"/>
|
||||
</ivysettings>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ apply plugin: 'io.spring.dependency-management'
|
|||
|
||||
// tag::dependencies[]
|
||||
dependencies {
|
||||
implementation('org.springframework.boot:spring-boot-starter-webmvc')
|
||||
providedRuntime('org.springframework.boot:spring-boot-starter-tomcat-runtime')
|
||||
implementation('org.springframework.boot:spring-boot-starter-web')
|
||||
providedRuntime('org.springframework.boot:spring-boot-tomcat-runtime')
|
||||
}
|
||||
// end::dependencies[]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@ apply(plugin = "io.spring.dependency-management")
|
|||
// tag::dependencies[]
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
providedRuntime("org.springframework.boot:spring-boot-starter-tomcat-runtime")
|
||||
providedRuntime("org.springframework.boot:spring-boot-tomcat-runtime")
|
||||
}
|
||||
// end::dependencies[]
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ The `assemble` task is automatically configured to depend upon the `bootWar` tas
|
|||
=== Packaging Executable and Deployable Wars
|
||||
|
||||
A war file can be packaged such that it can be executed using `java -jar` and deployed to an external container.
|
||||
To do so, the embedded servlet container runtime should be added to the `providedRuntime` configuration, for example:
|
||||
To do so, the embedded servlet runtime should be added to the `providedRuntime` configuration, for example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
|
|
@ -45,7 +45,7 @@ include::example$packaging/war-container-dependency.gradle.kts[tags=dependencies
|
|||
----
|
||||
======
|
||||
|
||||
This ensures that the runtime jars are packaged in the war file's `WEB-INF/lib-provided` directory from where they will not conflict with the external container's own classes.
|
||||
This ensures that the runtime is packaged in the war file's `WEB-INF/lib-provided` directory from where it will not conflict with the external container's own classes.
|
||||
|
||||
NOTE: `providedRuntime` is preferred to Gradle's `compileOnly` configuration as, among other limitations, `compileOnly` dependencies are not on the test classpath so any web-based integration tests will fail.
|
||||
|
||||
|
|
|
|||
|
|
@ -86,8 +86,6 @@ class WarPluginAction implements PluginApplicationAction {
|
|||
.minus((developmentOnly.minus(productionRuntimeClasspath)))
|
||||
.minus((testAndDevelopmentOnly.minus(productionRuntimeClasspath)))
|
||||
.filter(new JarTypeFileSpec());
|
||||
Callable<FileCollection> providedClasspath = () -> providedRuntimeConfiguration(project)
|
||||
.filter(new JarTypeFileSpec());
|
||||
TaskProvider<ResolveMainClassName> resolveMainClassName = project.getTasks()
|
||||
.named(SpringBootPlugin.RESOLVE_MAIN_CLASS_NAME_TASK_NAME, ResolveMainClassName.class);
|
||||
TaskProvider<BootWar> bootWarProvider = project.getTasks()
|
||||
|
|
@ -95,7 +93,7 @@ class WarPluginAction implements PluginApplicationAction {
|
|||
bootWar.setGroup(BasePlugin.BUILD_GROUP);
|
||||
bootWar.setDescription("Assembles an executable war archive containing webapp"
|
||||
+ " content, and the main classes and their dependencies.");
|
||||
bootWar.providedClasspath(providedClasspath);
|
||||
bootWar.providedClasspath(providedRuntimeConfiguration(project));
|
||||
bootWar.setClasspath(classpath);
|
||||
Provider<String> manifestStartClass = project
|
||||
.provider(() -> (String) bootWar.getManifest().getAttributes().get("Start-Class"));
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ import org.springframework.boot.loader.tools.ReachabilityMetadataProperties;
|
|||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.function.ThrowingSupplier;
|
||||
|
||||
/**
|
||||
* A {@link CopyAction} for creating a Spring Boot zip archive (typically a jar or war).
|
||||
|
|
@ -326,7 +325,7 @@ class BootZipCopyAction implements CopyAction {
|
|||
private void writeJarModeLibrary(String location, JarModeLibrary library) throws IOException {
|
||||
String name = location + library.getName();
|
||||
writeEntry(name, ZipEntryContentWriter.fromInputStream(library.openStream()), false,
|
||||
(entry) -> prepareStoredEntry(library::openStream, false, entry));
|
||||
(entry) -> prepareStoredEntry(library.openStream(), false, entry));
|
||||
if (BootZipCopyAction.this.layerResolver != null) {
|
||||
Layer layer = BootZipCopyAction.this.layerResolver.getLayer(library);
|
||||
Assert.state(this.layerIndex != null, "'layerIndex' must not be null");
|
||||
|
|
@ -430,12 +429,12 @@ class BootZipCopyAction implements CopyAction {
|
|||
}
|
||||
|
||||
private void prepareStoredEntry(FileCopyDetails details, ZipArchiveEntry archiveEntry) throws IOException {
|
||||
prepareStoredEntry(details::open, BootZipCopyAction.this.requiresUnpack.isSatisfiedBy(details),
|
||||
prepareStoredEntry(details.open(), BootZipCopyAction.this.requiresUnpack.isSatisfiedBy(details),
|
||||
archiveEntry);
|
||||
}
|
||||
|
||||
private void prepareStoredEntry(ThrowingSupplier<InputStream> input, boolean unpack,
|
||||
ZipArchiveEntry archiveEntry) throws IOException {
|
||||
private void prepareStoredEntry(InputStream input, boolean unpack, ZipArchiveEntry archiveEntry)
|
||||
throws IOException {
|
||||
new StoredEntryPreparator(input, unpack).prepareStoredEntry(archiveEntry);
|
||||
}
|
||||
|
||||
|
|
@ -565,10 +564,10 @@ class BootZipCopyAction implements CopyAction {
|
|||
|
||||
private long size;
|
||||
|
||||
StoredEntryPreparator(ThrowingSupplier<InputStream> input, boolean unpack) throws IOException {
|
||||
StoredEntryPreparator(InputStream inputStream, boolean unpack) throws IOException {
|
||||
this.unpack = unpack;
|
||||
try (InputStream stream = input.get()) {
|
||||
load(stream);
|
||||
try (inputStream) {
|
||||
load(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
|
|||
*/
|
||||
public class PluginClasspathGradleBuild extends GradleBuild {
|
||||
|
||||
private boolean kotlin;
|
||||
private boolean kotlin = false;
|
||||
|
||||
public PluginClasspathGradleBuild(BuildOutput buildOutput) {
|
||||
super(buildOutput);
|
||||
|
|
|
|||
|
|
@ -87,8 +87,8 @@ class SpringRepositoriesExtension {
|
|||
addRepositories { }
|
||||
}
|
||||
|
||||
def mavenRepositoriesFor(version) {
|
||||
addRepositories(version) { }
|
||||
def mavenRepositories(condition) {
|
||||
if (condition) addRepositories { }
|
||||
}
|
||||
|
||||
def mavenRepositoriesExcludingBootGroup() {
|
||||
|
|
@ -100,13 +100,9 @@ class SpringRepositoriesExtension {
|
|||
}
|
||||
|
||||
private void addRepositories(action) {
|
||||
addRepositories(this.version, action)
|
||||
}
|
||||
|
||||
private void addRepositories(version, action) {
|
||||
addCommercialRepositoryIfNecessary("release", false, "/spring-enterprise-maven-prod-local", action)
|
||||
if (version.endsWith("-SNAPSHOT")) {
|
||||
addCommercialRepositoryIfNecessary("snapshot", true, "/spring-enterprise-maven-dev-local", action)
|
||||
addCommercialRepository("release", false, "/spring-enterprise-maven-prod-local", action)
|
||||
if (this.version.endsWith("-SNAPSHOT")) {
|
||||
addCommercialRepository("snapshot", true, "/spring-enterprise-maven-dev-local", action)
|
||||
addOssRepository("snapshot", true, "/snapshot", action)
|
||||
}
|
||||
}
|
||||
|
|
@ -117,7 +113,7 @@ class SpringRepositoriesExtension {
|
|||
addRepository(name, snapshot, url, action)
|
||||
}
|
||||
|
||||
private void addCommercialRepositoryIfNecessary(id, snapshot, path, action) {
|
||||
private void addCommercialRepository(id, snapshot, path, action) {
|
||||
if (!"commercial".equalsIgnoreCase(this.buildType)) return
|
||||
def name = "spring-commercial-" + id
|
||||
def url = fromEnv("COMMERCIAL_%SREPO_URL", id, "https://usw1.packages.broadcom.com" + path)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ plugins {
|
|||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
spring.mavenRepositoriesFor("${springFrameworkVersion}")
|
||||
gradlePluginPortal()
|
||||
}
|
||||
|
||||
|
|
@ -33,6 +32,10 @@ java {
|
|||
targetCompatibility = 17
|
||||
}
|
||||
|
||||
repositories {
|
||||
spring.mavenRepositories("${springFrameworkVersion}".contains("-"))
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
toolVersion = "${checkstyleToolVersion}"
|
||||
}
|
||||
|
|
@ -111,10 +114,6 @@ gradlePlugin {
|
|||
id = "org.springframework.boot.bom"
|
||||
implementationClass = "org.springframework.boot.build.bom.BomPlugin"
|
||||
}
|
||||
configurationMetadataPlugin {
|
||||
id = "org.springframework.boot.configuration-metadata"
|
||||
implementationClass = "org.springframework.boot.build.context.properties.ConfigurationMetadataPlugin"
|
||||
}
|
||||
configurationPropertiesPlugin {
|
||||
id = "org.springframework.boot.configuration-properties"
|
||||
implementationClass = "org.springframework.boot.build.context.properties.ConfigurationPropertiesPlugin"
|
||||
|
|
|
|||
|
|
@ -230,18 +230,10 @@ class JavaConventions {
|
|||
CoreJavadocOptions options = (CoreJavadocOptions) javadoc.getOptions();
|
||||
options.source("17");
|
||||
options.encoding("UTF-8");
|
||||
addValuelessOption(options, "Xdoclint:none");
|
||||
addValuelessOption(options, "quiet");
|
||||
if (!javadoc.getName().contains("aggregated")) {
|
||||
addValuelessOption(options, "-no-fonts");
|
||||
}
|
||||
options.addStringOption("Xdoclint:none", "-quiet");
|
||||
});
|
||||
}
|
||||
|
||||
private void addValuelessOption(CoreJavadocOptions options, String option) {
|
||||
options.addMultilineMultiValueOption(option).setValue(List.of(Collections.emptyList()));
|
||||
}
|
||||
|
||||
private void configureJavaConventions(Project project) {
|
||||
if (!project.hasProperty("toolchainVersion")) {
|
||||
JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class);
|
||||
|
|
|
|||
|
|
@ -20,9 +20,7 @@ import java.net.URI;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
|
|
@ -39,8 +37,6 @@ import org.gradle.api.artifacts.repositories.MavenArtifactRepository;
|
|||
*/
|
||||
public class RepositoryTransformersExtension {
|
||||
|
||||
private static final String CREDENTIALS_MARKER = "{spring.mavenCredentials}";
|
||||
|
||||
private static final String REPOSITORIES_MARKER = "{spring.mavenRepositories}";
|
||||
|
||||
private static final String PLUGIN_REPOSITORIES_MARKER = "{spring.mavenPluginRepositories}";
|
||||
|
|
@ -64,19 +60,6 @@ public class RepositoryTransformersExtension {
|
|||
return "%s<ibiblio name=\"%s\" m2compatible=\"true\" root=\"%s\" />".formatted(indent, name, url);
|
||||
});
|
||||
}
|
||||
if (line.contains(CREDENTIALS_MARKER)) {
|
||||
Map<String, MavenCredential> hostCredentials = new LinkedHashMap<>();
|
||||
getSpringRepositories().forEach((repository) -> {
|
||||
if (repository.getName().startsWith("spring-commercial-")) {
|
||||
String host = repository.getUrl().getHost();
|
||||
hostCredentials.put(host,
|
||||
new MavenCredential("${env.COMMERCIAL_REPO_USERNAME}", "${env.COMMERCIAL_REPO_PASSWORD"));
|
||||
}
|
||||
});
|
||||
return transform(line, hostCredentials.entrySet(), (entry,
|
||||
indent) -> "%s<credentials host=\"%s\" realm=\"Artifactory Realm\" username=\"%s\" passwd=\"%s\" />%n"
|
||||
.formatted(indent, entry.getKey(), entry.getValue().username(), entry.getValue().password()));
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
|
|
@ -116,14 +99,10 @@ public class RepositoryTransformersExtension {
|
|||
}
|
||||
|
||||
private String transform(String line, BiFunction<MavenArtifactRepository, String, String> generator) {
|
||||
return transform(line, getSpringRepositories(), generator);
|
||||
}
|
||||
|
||||
private <T> String transform(String line, Iterable<T> iterable, BiFunction<T, String, String> generator) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
String indent = getIndent(line);
|
||||
iterable.forEach((item) -> {
|
||||
String fragment = generator.apply(item, indent);
|
||||
getSpringRepositories().forEach((repository) -> {
|
||||
String fragment = generator.apply(repository, indent);
|
||||
if (fragment != null) {
|
||||
result.append(!result.isEmpty() ? "\n" : "");
|
||||
result.append(fragment);
|
||||
|
|
@ -157,8 +136,4 @@ public class RepositoryTransformersExtension {
|
|||
project.getExtensions().create("springRepositoryTransformers", RepositoryTransformersExtension.class, project);
|
||||
}
|
||||
|
||||
record MavenCredential(String username, String password) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import java.nio.file.StandardOpenOption;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
|
@ -43,7 +42,6 @@ import org.gradle.api.file.DirectoryProperty;
|
|||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.file.FileTree;
|
||||
import org.gradle.api.provider.ListProperty;
|
||||
import org.gradle.api.provider.MapProperty;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.provider.Provider;
|
||||
import org.gradle.api.provider.SetProperty;
|
||||
|
|
@ -74,27 +72,19 @@ import org.gradle.api.tasks.VerificationException;
|
|||
*/
|
||||
public abstract class ArchitectureCheck extends DefaultTask {
|
||||
|
||||
static final String CONDITIONAL_ON_CLASS = "ConditionalOnClass";
|
||||
|
||||
static final String DEPRECATED_CONFIGURATION_PROPERTY = "DeprecatedConfigurationProperty";
|
||||
|
||||
private static final String CONDITIONAL_ON_CLASS_ANNOTATION = "org.springframework.boot.autoconfigure.condition.ConditionalOnClass";
|
||||
|
||||
private static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.context.properties.DeprecatedConfigurationProperty";
|
||||
|
||||
private FileCollection classes;
|
||||
|
||||
public ArchitectureCheck() {
|
||||
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
|
||||
getAnnotationClasses().convention(Map.of(CONDITIONAL_ON_CLASS, CONDITIONAL_ON_CLASS_ANNOTATION,
|
||||
DEPRECATED_CONFIGURATION_PROPERTY, DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION));
|
||||
getConditionalOnClassAnnotation().convention(CONDITIONAL_ON_CLASS_ANNOTATION);
|
||||
getRules().addAll(getProhibitObjectsRequireNonNull().convention(true)
|
||||
.map(whenTrue(ArchitectureRules::noClassesShouldCallObjectsRequireNonNull)));
|
||||
getRules().addAll(ArchitectureRules.standard());
|
||||
getRules().addAll(whenMainSources(() -> ArchitectureRules
|
||||
.beanMethods(annotationClassFor(CONDITIONAL_ON_CLASS, CONDITIONAL_ON_CLASS_ANNOTATION))));
|
||||
getRules().addAll(whenMainSources(() -> ArchitectureRules.configurationProperties(
|
||||
annotationClassFor(DEPRECATED_CONFIGURATION_PROPERTY, DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION))));
|
||||
getRules().addAll(whenMainSources(() -> List
|
||||
.of(ArchitectureRules.allBeanMethodsShouldReturnNonPrivateType(), ArchitectureRules
|
||||
.allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(getConditionalOnClassAnnotation().get()))));
|
||||
getRules().addAll(and(getNullMarkedEnabled(), isMainSourceSet()).map(whenTrue(() -> Collections.singletonList(
|
||||
ArchitectureRules.packagesShouldBeAnnotatedWithNullMarked(getNullMarkedIgnoredPackages().get())))));
|
||||
getRuleDescriptions().set(getRules().map(this::asDescriptions));
|
||||
|
|
@ -120,10 +110,6 @@ public abstract class ArchitectureCheck extends DefaultTask {
|
|||
return rules.stream().map(ArchRule::getDescription).toList();
|
||||
}
|
||||
|
||||
private String annotationClassFor(String name, String defaultValue) {
|
||||
return getAnnotationClasses().get().getOrDefault(name, defaultValue);
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
void checkArchitecture() throws Exception {
|
||||
withCompileClasspath(() -> {
|
||||
|
|
@ -223,7 +209,7 @@ public abstract class ArchitectureCheck extends DefaultTask {
|
|||
@Internal
|
||||
abstract SetProperty<String> getNullMarkedIgnoredPackages();
|
||||
|
||||
@Input
|
||||
abstract MapProperty<String, String> getAnnotationClasses();
|
||||
@Internal
|
||||
abstract Property<String> getConditionalOnClassAnnotation();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,17 +16,17 @@
|
|||
|
||||
package org.springframework.boot.build.architecture;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.Task;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.plugins.JavaPluginExtension;
|
||||
import org.gradle.api.tasks.SourceSet;
|
||||
import org.gradle.api.tasks.TaskProvider;
|
||||
import org.gradle.api.tasks.compile.JavaCompile;
|
||||
import org.gradle.language.base.plugins.LifecycleBasePlugin;
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompileTool;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
|
|
@ -46,45 +46,28 @@ public class ArchitecturePlugin implements Plugin<Project> {
|
|||
|
||||
private void registerTasks(Project project, ArchitectureCheckExtension extension) {
|
||||
JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class);
|
||||
List<TaskProvider<ArchitectureCheck>> packageTangleChecks = new ArrayList<>();
|
||||
for (SourceSet sourceSet : javaPluginExtension.getSourceSets()) {
|
||||
registerArchitectureCheck(sourceSet, "java", project).configure((task) -> {
|
||||
task.setClasses(project.files(project.getTasks()
|
||||
.named(sourceSet.getCompileTaskName("java"), JavaCompile.class)
|
||||
.flatMap((compile) -> compile.getDestinationDirectory())));
|
||||
TaskProvider<ArchitectureCheck> checkPackageTangles = project.getTasks()
|
||||
.register("checkArchitecture" + StringUtils.capitalize(sourceSet.getName()), ArchitectureCheck.class,
|
||||
(task) -> {
|
||||
task.getSourceSet().set(sourceSet.getName());
|
||||
task.getCompileClasspath().from(sourceSet.getCompileClasspath());
|
||||
task.setClasses(sourceSet.getOutput().getClassesDirs());
|
||||
task.getResourcesDirectory().set(sourceSet.getOutput().getResourcesDir());
|
||||
task.dependsOn(sourceSet.getProcessResourcesTaskName());
|
||||
task.setDescription("Checks the architecture of the classes of the " + sourceSet.getName()
|
||||
+ " source set.");
|
||||
task.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
|
||||
task.getNullMarkedEnabled().set(extension.getNullMarked().getEnabled());
|
||||
task.getNullMarkedIgnoredPackages().set(extension.getNullMarked().getIgnoredPackages());
|
||||
});
|
||||
project.getPlugins()
|
||||
.withId("org.jetbrains.kotlin.jvm",
|
||||
(kotlinPlugin) -> registerArchitectureCheck(sourceSet, "kotlin", project).configure((task) -> {
|
||||
task.setClasses(project.files(project.getTasks()
|
||||
.named(sourceSet.getCompileTaskName("kotlin"), KotlinCompileTool.class)
|
||||
.flatMap((compile) -> compile.getDestinationDirectory())));
|
||||
task.getNullMarkedEnabled().set(false);
|
||||
task.getNullMarkedIgnoredPackages().set(Collections.emptySet());
|
||||
}));
|
||||
packageTangleChecks.add(checkPackageTangles);
|
||||
}
|
||||
if (!packageTangleChecks.isEmpty()) {
|
||||
TaskProvider<Task> checkTask = project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME);
|
||||
checkTask.configure((check) -> check.dependsOn(packageTangleChecks));
|
||||
}
|
||||
}
|
||||
|
||||
private TaskProvider<ArchitectureCheck> registerArchitectureCheck(SourceSet sourceSet, String language,
|
||||
Project project) {
|
||||
TaskProvider<ArchitectureCheck> checkArchitecture = project.getTasks()
|
||||
.register(
|
||||
"checkArchitecture"
|
||||
+ StringUtils.capitalize(sourceSet.getName() + StringUtils.capitalize(language)),
|
||||
ArchitectureCheck.class, (task) -> {
|
||||
task.getSourceSet().set(sourceSet.getName());
|
||||
task.getCompileClasspath().from(sourceSet.getCompileClasspath());
|
||||
task.getResourcesDirectory().set(sourceSet.getOutput().getResourcesDir());
|
||||
task.dependsOn(sourceSet.getProcessResourcesTaskName());
|
||||
task.setDescription("Checks the architecture of the " + language + " classes of the "
|
||||
+ sourceSet.getName() + " source set.");
|
||||
task.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
|
||||
});
|
||||
project.getTasks()
|
||||
.named(LifecycleBasePlugin.CHECK_TASK_NAME)
|
||||
.configure((check) -> check.dependsOn(checkArchitecture));
|
||||
return checkArchitecture;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,16 +116,7 @@ final class ArchitectureRules {
|
|||
return List.copyOf(rules);
|
||||
}
|
||||
|
||||
static List<ArchRule> beanMethods(String annotationName) {
|
||||
return List.of(allBeanMethodsShouldReturnNonPrivateType(),
|
||||
allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(annotationName));
|
||||
}
|
||||
|
||||
static List<ArchRule> configurationProperties(String annotationName) {
|
||||
return List.of(allDeprecatedConfigurationPropertiesShouldIncludeSince(annotationName));
|
||||
}
|
||||
|
||||
private static ArchRule allBeanMethodsShouldReturnNonPrivateType() {
|
||||
static ArchRule allBeanMethodsShouldReturnNonPrivateType() {
|
||||
return methodsThatAreAnnotatedWith("org.springframework.context.annotation.Bean").should(check(
|
||||
"not return types declared with the %s modifier, as such types are incompatible with Spring AOT processing"
|
||||
.formatted(JavaModifier.PRIVATE),
|
||||
|
|
@ -139,7 +130,7 @@ final class ArchitectureRules {
|
|||
.allowEmptyShould(true);
|
||||
}
|
||||
|
||||
private static ArchRule allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(String annotationName) {
|
||||
static ArchRule allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(String annotationName) {
|
||||
return methodsThatAreAnnotatedWith("org.springframework.context.annotation.Bean").should()
|
||||
.notBeAnnotatedWith(annotationName)
|
||||
.because("@ConditionalOnClass on @Bean methods is ineffective - it doesn't prevent "
|
||||
|
|
@ -383,20 +374,6 @@ final class ArchitectureRules {
|
|||
.allowEmptyShould(true);
|
||||
}
|
||||
|
||||
private static ArchRule allDeprecatedConfigurationPropertiesShouldIncludeSince(String annotationName) {
|
||||
return methodsThatAreAnnotatedWith(annotationName)
|
||||
.should(check("include a non-empty 'since' attribute", (method, events) -> {
|
||||
JavaAnnotation<JavaMethod> annotation = method.getAnnotationOfType(annotationName);
|
||||
Map<String, Object> properties = annotation.getProperties();
|
||||
Object since = properties.get("since");
|
||||
if (!(since instanceof String) || ((String) since).isEmpty()) {
|
||||
addViolation(events, method, annotation.getDescription()
|
||||
+ " should include a non-empty 'since' attribute of @DeprecatedConfigurationProperty");
|
||||
}
|
||||
}))
|
||||
.allowEmptyShould(true);
|
||||
}
|
||||
|
||||
private static ArchRule autoConfigurationClassesShouldBePublicAndFinal() {
|
||||
return ArchRuleDefinition.classes()
|
||||
.that(areRegularAutoConfiguration())
|
||||
|
|
@ -421,8 +398,6 @@ final class ArchitectureRules {
|
|||
return ArchRuleDefinition.members()
|
||||
.that()
|
||||
.areDeclaredInClassesThat(areRegularAutoConfiguration())
|
||||
.and()
|
||||
.areDeclaredInClassesThat(areNotKotlinClasses())
|
||||
.and(areNotDefaultConstructors())
|
||||
.and(areNotConstants())
|
||||
.and(dontOverridePublicMethods())
|
||||
|
|
@ -442,18 +417,13 @@ final class ArchitectureRules {
|
|||
}
|
||||
|
||||
static DescribedPredicate<JavaClass> areRegularAutoConfiguration() {
|
||||
return DescribedPredicate.describe("are regular @AutoConfiguration",
|
||||
return DescribedPredicate.describe("Regular @AutoConfiguration",
|
||||
(javaClass) -> javaClass.isAnnotatedWith(AUTOCONFIGURATION_ANNOTATION)
|
||||
&& !javaClass.getName().contains("TestAutoConfiguration") && !javaClass.isAnnotation());
|
||||
}
|
||||
|
||||
static DescribedPredicate<JavaClass> areNotKotlinClasses() {
|
||||
return DescribedPredicate.describe("are not Kotlin classes",
|
||||
(javaClass) -> !javaClass.isAnnotatedWith("kotlin.Metadata"));
|
||||
}
|
||||
|
||||
static DescribedPredicate<JavaClass> areTestAutoConfiguration() {
|
||||
return DescribedPredicate.describe("are test @AutoConfiguration",
|
||||
return DescribedPredicate.describe("Test @AutoConfiguration",
|
||||
(javaClass) -> javaClass.isAnnotatedWith(AUTOCONFIGURATION_ANNOTATION)
|
||||
&& javaClass.getName().contains("TestAutoConfiguration") && !javaClass.isAnnotation());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,16 +31,12 @@ import org.springframework.util.ReflectionUtils;
|
|||
/**
|
||||
* Finds all configurations from auto-configurations (either nested configurations or
|
||||
* imported ones) and checks that these classes don't contain public members.
|
||||
* <p>
|
||||
* Kotlin classes are ignored as Kotlin does not have package-private visibility and
|
||||
* {@code internal} isn't a good substitute.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class AutoConfigurationChecker {
|
||||
|
||||
private final DescribedPredicate<JavaClass> isAutoConfiguration = ArchitectureRules.areRegularAutoConfiguration()
|
||||
.and(ArchitectureRules.areNotKotlinClasses());
|
||||
private final DescribedPredicate<JavaClass> isAutoConfiguration = ArchitectureRules.areRegularAutoConfiguration();
|
||||
|
||||
EvaluationResult check(JavaClasses javaClasses) {
|
||||
AutoConfigurations autoConfigurations = new AutoConfigurations();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,15 @@ package org.springframework.boot.build.context.properties;
|
|||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.gradle.api.file.FileTree;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
|
|
@ -28,8 +37,8 @@ import org.gradle.api.tasks.PathSensitivity;
|
|||
import org.gradle.api.tasks.SourceTask;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.gradle.api.tasks.VerificationException;
|
||||
|
||||
import org.springframework.boot.build.context.properties.ConfigurationPropertiesAnalyzer.Report;
|
||||
import tools.jackson.core.StreamReadFeature;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
/**
|
||||
* {@link SourceTask} that checks additional Spring configuration metadata files.
|
||||
|
|
@ -56,16 +65,98 @@ public abstract class CheckAdditionalSpringConfigurationMetadata extends SourceT
|
|||
|
||||
@TaskAction
|
||||
void check() throws IOException {
|
||||
ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(getSource().getFiles());
|
||||
Report report = new Report(this.projectDir);
|
||||
analyzer.analyzeSort(report);
|
||||
analyzer.analyzeDeprecationSince(report);
|
||||
Report report = createReport();
|
||||
File reportFile = getReportLocation().get().getAsFile();
|
||||
report.write(reportFile);
|
||||
Files.write(reportFile.toPath(), report, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
if (report.hasProblems()) {
|
||||
throw new VerificationException(
|
||||
"Problems found in additional Spring configuration metadata. See " + reportFile + " for details.");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Report createReport() {
|
||||
JsonMapper jsonMapper = JsonMapper.builder().enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION).build();
|
||||
Report report = new Report();
|
||||
for (File file : getSource().getFiles()) {
|
||||
Analysis analysis = report.analysis(this.projectDir.toPath().relativize(file.toPath()));
|
||||
Map<String, Object> json = jsonMapper.readValue(file, Map.class);
|
||||
check("groups", json, analysis);
|
||||
check("properties", json, analysis);
|
||||
check("hints", json, analysis);
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void check(String key, Map<String, Object> json, Analysis analysis) {
|
||||
List<Map<String, Object>> groups = (List<Map<String, Object>>) json.getOrDefault(key, Collections.emptyList());
|
||||
List<String> names = groups.stream().map((group) -> (String) group.get("name")).toList();
|
||||
List<String> sortedNames = sortedCopy(names);
|
||||
for (int i = 0; i < names.size(); i++) {
|
||||
String actual = names.get(i);
|
||||
String expected = sortedNames.get(i);
|
||||
if (!actual.equals(expected)) {
|
||||
analysis.problems.add("Wrong order at $." + key + "[" + i + "].name - expected '" + expected
|
||||
+ "' but found '" + actual + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> sortedCopy(Collection<String> original) {
|
||||
List<String> copy = new ArrayList<>(original);
|
||||
Collections.sort(copy);
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static final class Report implements Iterable<String> {
|
||||
|
||||
private final List<Analysis> analyses = new ArrayList<>();
|
||||
|
||||
private Analysis analysis(Path path) {
|
||||
Analysis analysis = new Analysis(path);
|
||||
this.analyses.add(analysis);
|
||||
return analysis;
|
||||
}
|
||||
|
||||
private boolean hasProblems() {
|
||||
for (Analysis analysis : this.analyses) {
|
||||
if (!analysis.problems.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<String> iterator() {
|
||||
List<String> lines = new ArrayList<>();
|
||||
for (Analysis analysis : this.analyses) {
|
||||
lines.add(analysis.source.toString());
|
||||
lines.add("");
|
||||
if (analysis.problems.isEmpty()) {
|
||||
lines.add("No problems found.");
|
||||
}
|
||||
else {
|
||||
lines.addAll(analysis.problems);
|
||||
}
|
||||
lines.add("");
|
||||
}
|
||||
return lines.iterator();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class Analysis {
|
||||
|
||||
private final List<String> problems = new ArrayList<>();
|
||||
|
||||
private final Path source;
|
||||
|
||||
private Analysis(Path source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
/*
|
||||
* Copyright 2025 the original author or 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 org.springframework.boot.build.context.properties;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.Task;
|
||||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.tasks.InputFiles;
|
||||
import org.gradle.api.tasks.OutputFile;
|
||||
import org.gradle.api.tasks.PathSensitive;
|
||||
import org.gradle.api.tasks.PathSensitivity;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.gradle.api.tasks.VerificationException;
|
||||
|
||||
/**
|
||||
* {@link Task} that checks aggregated Spring configuration metadata.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public abstract class CheckAggregatedSpringConfigurationMetadata extends DefaultTask {
|
||||
|
||||
private FileCollection configurationPropertyMetadata;
|
||||
|
||||
@OutputFile
|
||||
public abstract RegularFileProperty getReportLocation();
|
||||
|
||||
@InputFiles
|
||||
@PathSensitive(PathSensitivity.RELATIVE)
|
||||
public FileCollection getConfigurationPropertyMetadata() {
|
||||
return this.configurationPropertyMetadata;
|
||||
}
|
||||
|
||||
public void setConfigurationPropertyMetadata(FileCollection configurationPropertyMetadata) {
|
||||
this.configurationPropertyMetadata = configurationPropertyMetadata;
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
void check() throws IOException {
|
||||
Report report = createReport();
|
||||
File reportFile = getReportLocation().get().getAsFile();
|
||||
Files.write(reportFile.toPath(), report, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
if (report.hasProblems()) {
|
||||
throw new VerificationException(
|
||||
"Problems found in aggregated Spring configuration metadata. See " + reportFile + " for details.");
|
||||
}
|
||||
}
|
||||
|
||||
private Report createReport() {
|
||||
ConfigurationProperties configurationProperties = ConfigurationProperties
|
||||
.fromFiles(this.configurationPropertyMetadata);
|
||||
Set<String> propertyNames = configurationProperties.stream()
|
||||
.map(ConfigurationProperty::getName)
|
||||
.collect(Collectors.toSet());
|
||||
List<ConfigurationProperty> missingReplacement = configurationProperties.stream()
|
||||
.filter(ConfigurationProperty::isDeprecated)
|
||||
.filter((deprecated) -> {
|
||||
String replacement = deprecated.getDeprecation().replacement();
|
||||
return replacement != null && !propertyNames.contains(replacement);
|
||||
})
|
||||
.toList();
|
||||
return new Report(missingReplacement);
|
||||
}
|
||||
|
||||
private static final class Report implements Iterable<String> {
|
||||
|
||||
private final List<ConfigurationProperty> propertiesWithMissingReplacement;
|
||||
|
||||
private Report(List<ConfigurationProperty> propertiesWithMissingReplacement) {
|
||||
this.propertiesWithMissingReplacement = propertiesWithMissingReplacement;
|
||||
}
|
||||
|
||||
private boolean hasProblems() {
|
||||
return !this.propertiesWithMissingReplacement.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<String> iterator() {
|
||||
List<String> lines = new ArrayList<>();
|
||||
if (this.propertiesWithMissingReplacement.isEmpty()) {
|
||||
lines.add("No problems found.");
|
||||
}
|
||||
else {
|
||||
lines.add("The following properties have a replacement that does not exist:");
|
||||
lines.add("");
|
||||
lines.addAll(this.propertiesWithMissingReplacement.stream()
|
||||
.map((property) -> "\t" + property.getName() + " (replacement "
|
||||
+ property.getDeprecation().replacement() + ")")
|
||||
.toList());
|
||||
}
|
||||
lines.add("");
|
||||
return lines.iterator();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012-present the original author or 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 org.springframework.boot.build.context.properties;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.provider.ListProperty;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.InputFile;
|
||||
import org.gradle.api.tasks.OutputFile;
|
||||
import org.gradle.api.tasks.PathSensitive;
|
||||
import org.gradle.api.tasks.PathSensitivity;
|
||||
import org.gradle.api.tasks.SourceTask;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.gradle.api.tasks.VerificationException;
|
||||
|
||||
import org.springframework.boot.build.context.properties.ConfigurationPropertiesAnalyzer.Report;
|
||||
|
||||
/**
|
||||
* {@link SourceTask} that checks manual Spring configuration metadata files.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public abstract class CheckManualSpringConfigurationMetadata extends DefaultTask {
|
||||
|
||||
private final File projectDir;
|
||||
|
||||
public CheckManualSpringConfigurationMetadata() {
|
||||
this.projectDir = getProject().getProjectDir();
|
||||
}
|
||||
|
||||
@OutputFile
|
||||
public abstract RegularFileProperty getReportLocation();
|
||||
|
||||
@InputFile
|
||||
@PathSensitive(PathSensitivity.RELATIVE)
|
||||
public abstract Property<File> getMetadataLocation();
|
||||
|
||||
@Input
|
||||
public abstract ListProperty<String> getExclusions();
|
||||
|
||||
@TaskAction
|
||||
void check() throws IOException {
|
||||
ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(
|
||||
List.of(getMetadataLocation().get()));
|
||||
Report report = new Report(this.projectDir);
|
||||
analyzer.analyzeSort(report);
|
||||
analyzer.analyzePropertyDescription(report, getExclusions().get());
|
||||
analyzer.analyzeDeprecationSince(report);
|
||||
File reportFile = getReportLocation().get().getAsFile();
|
||||
report.write(reportFile);
|
||||
if (report.hasProblems()) {
|
||||
throw new VerificationException(
|
||||
"Problems found in manual Spring configuration metadata. See " + reportFile + " for details.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -18,7 +18,13 @@ package org.springframework.boot.build.context.properties;
|
|||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
|
|
@ -31,8 +37,7 @@ import org.gradle.api.tasks.PathSensitivity;
|
|||
import org.gradle.api.tasks.SourceTask;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.gradle.api.tasks.VerificationException;
|
||||
|
||||
import org.springframework.boot.build.context.properties.ConfigurationPropertiesAnalyzer.Report;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
/**
|
||||
* {@link SourceTask} that checks {@code spring-configuration-metadata.json} files.
|
||||
|
|
@ -41,10 +46,10 @@ import org.springframework.boot.build.context.properties.ConfigurationProperties
|
|||
*/
|
||||
public abstract class CheckSpringConfigurationMetadata extends DefaultTask {
|
||||
|
||||
private final File projectRoot;
|
||||
private final Path projectRoot;
|
||||
|
||||
public CheckSpringConfigurationMetadata() {
|
||||
this.projectRoot = getProject().getProjectDir();
|
||||
this.projectRoot = getProject().getProjectDir().toPath();
|
||||
}
|
||||
|
||||
@OutputFile
|
||||
|
|
@ -59,16 +64,86 @@ public abstract class CheckSpringConfigurationMetadata extends DefaultTask {
|
|||
|
||||
@TaskAction
|
||||
void check() throws IOException {
|
||||
Report report = new Report(this.projectRoot);
|
||||
ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(
|
||||
List.of(getMetadataLocation().get().getAsFile()));
|
||||
analyzer.analyzePropertyDescription(report, getExclusions().get());
|
||||
Report report = createReport();
|
||||
File reportFile = getReportLocation().get().getAsFile();
|
||||
report.write(reportFile);
|
||||
Files.write(reportFile.toPath(), report, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
if (report.hasProblems()) {
|
||||
throw new VerificationException(
|
||||
"Problems found in Spring configuration metadata. See " + reportFile + " for details.");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Report createReport() {
|
||||
JsonMapper jsonMapper = new JsonMapper();
|
||||
File file = getMetadataLocation().get().getAsFile();
|
||||
Report report = new Report(this.projectRoot.relativize(file.toPath()));
|
||||
Map<String, Object> json = jsonMapper.readValue(file, Map.class);
|
||||
List<Map<String, Object>> properties = (List<Map<String, Object>>) json.get("properties");
|
||||
for (Map<String, Object> property : properties) {
|
||||
String name = (String) property.get("name");
|
||||
if (!isDeprecated(property) && !isDescribed(property) && !isExcluded(name)) {
|
||||
report.propertiesWithNoDescription.add(name);
|
||||
}
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
private boolean isExcluded(String propertyName) {
|
||||
for (String exclusion : getExclusions().get()) {
|
||||
if (propertyName.equals(exclusion)) {
|
||||
return true;
|
||||
}
|
||||
if (exclusion.endsWith(".*")) {
|
||||
if (propertyName.startsWith(exclusion.substring(0, exclusion.length() - 2))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private boolean isDeprecated(Map<String, Object> property) {
|
||||
return (Map<String, Object>) property.get("deprecation") != null;
|
||||
}
|
||||
|
||||
private boolean isDescribed(Map<String, Object> property) {
|
||||
return property.get("description") != null;
|
||||
}
|
||||
|
||||
private static final class Report implements Iterable<String> {
|
||||
|
||||
private final List<String> propertiesWithNoDescription = new ArrayList<>();
|
||||
|
||||
private final Path source;
|
||||
|
||||
private Report(Path source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
private boolean hasProblems() {
|
||||
return !this.propertiesWithNoDescription.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<String> iterator() {
|
||||
List<String> lines = new ArrayList<>();
|
||||
lines.add(this.source.toString());
|
||||
lines.add("");
|
||||
if (this.propertiesWithNoDescription.isEmpty()) {
|
||||
lines.add("No problems found.");
|
||||
}
|
||||
else {
|
||||
lines.add("The following properties have no description:");
|
||||
lines.add("");
|
||||
lines.addAll(this.propertiesWithNoDescription.stream().map((line) -> "\t" + line).toList());
|
||||
}
|
||||
lines.add("");
|
||||
return lines.iterator();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,93 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012-present the original author or 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 org.springframework.boot.build.context.properties;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.attributes.Category;
|
||||
import org.gradle.api.attributes.Usage;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.plugins.JavaPluginExtension;
|
||||
import org.gradle.api.provider.Provider;
|
||||
import org.gradle.api.tasks.SourceSet;
|
||||
import org.gradle.api.tasks.TaskProvider;
|
||||
import org.gradle.language.base.plugins.LifecycleBasePlugin;
|
||||
import org.gradle.language.jvm.tasks.ProcessResources;
|
||||
|
||||
/**
|
||||
* {@link Plugin} for projects that <em>only</em> define manual configuration metadata.
|
||||
* When applied, the plugin registers a {@link CheckManualSpringConfigurationMetadata}
|
||||
* task and configures the {@code check} task to depend upon it.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class ConfigurationMetadataPlugin implements Plugin<Project> {
|
||||
|
||||
private static final String CONFIGURATION_PROPERTIES_METADATA_CONFIGURATION_NAME = "configurationPropertiesMetadata";
|
||||
|
||||
/**
|
||||
* Name of the {@link CheckAdditionalSpringConfigurationMetadata} task.
|
||||
*/
|
||||
public static final String CHECK_MANUAL_SPRING_CONFIGURATION_METADATA_TASK_NAME = "checkManualSpringConfigurationMetadata";
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> registerCheckAdditionalMetadataTask(project));
|
||||
}
|
||||
|
||||
private void registerCheckAdditionalMetadataTask(Project project) {
|
||||
TaskProvider<CheckManualSpringConfigurationMetadata> checkConfigurationMetadata = project.getTasks()
|
||||
.register(CHECK_MANUAL_SPRING_CONFIGURATION_METADATA_TASK_NAME,
|
||||
CheckManualSpringConfigurationMetadata.class);
|
||||
SourceSet mainSourceSet = project.getExtensions()
|
||||
.getByType(JavaPluginExtension.class)
|
||||
.getSourceSets()
|
||||
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
|
||||
Provider<File> manualMetadataLocation = project.getTasks()
|
||||
.named(mainSourceSet.getProcessResourcesTaskName(), ProcessResources.class)
|
||||
.map((processResources) -> new File(processResources.getDestinationDir(),
|
||||
"META-INF/spring-configuration-metadata.json"));
|
||||
checkConfigurationMetadata.configure((check) -> {
|
||||
check.getMetadataLocation().set(manualMetadataLocation);
|
||||
check.getReportLocation()
|
||||
.set(project.getLayout()
|
||||
.getBuildDirectory()
|
||||
.file("reports/manual-spring-configuration-metadata/check.txt"));
|
||||
});
|
||||
addMetadataArtifact(project, manualMetadataLocation);
|
||||
project.getTasks()
|
||||
.named(LifecycleBasePlugin.CHECK_TASK_NAME)
|
||||
.configure((check) -> check.dependsOn(checkConfigurationMetadata));
|
||||
}
|
||||
|
||||
private void addMetadataArtifact(Project project, Provider<File> metadataLocation) {
|
||||
project.getConfigurations()
|
||||
.consumable(CONFIGURATION_PROPERTIES_METADATA_CONFIGURATION_NAME, (configuration) -> {
|
||||
configuration.attributes((attributes) -> {
|
||||
attributes.attribute(Category.CATEGORY_ATTRIBUTE,
|
||||
project.getObjects().named(Category.class, Category.DOCUMENTATION));
|
||||
attributes.attribute(Usage.USAGE_ATTRIBUTE,
|
||||
project.getObjects().named(Usage.class, "configuration-properties-metadata"));
|
||||
});
|
||||
});
|
||||
project.getArtifacts().add(CONFIGURATION_PROPERTIES_METADATA_CONFIGURATION_NAME, metadataLocation);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,272 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012-present the original author or 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 org.springframework.boot.build.context.properties;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import tools.jackson.core.StreamReadFeature;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
|
||||
/**
|
||||
* Check configuration metadata for inconsistencies. The available checks are:
|
||||
* <ul>
|
||||
* <li>Metadata element should be sorted alphabetically: {@link #analyzeSort(Report)}</li>
|
||||
* <li>Property must have a description:
|
||||
* {@link #analyzePropertyDescription(Report, List)}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class ConfigurationPropertiesAnalyzer {
|
||||
|
||||
private final Collection<File> sources;
|
||||
|
||||
private final SingletonSupplier<JsonMapper> jsonMapperSupplier;
|
||||
|
||||
ConfigurationPropertiesAnalyzer(Collection<File> sources) {
|
||||
if (sources.isEmpty()) {
|
||||
throw new IllegalArgumentException("At least one source should be provided");
|
||||
}
|
||||
this.sources = sources;
|
||||
this.jsonMapperSupplier = SingletonSupplier
|
||||
.of(() -> JsonMapper.builder().enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION).build());
|
||||
}
|
||||
|
||||
void analyzeSort(Report report) {
|
||||
for (File source : this.sources) {
|
||||
report.registerAnalysis(source, analyzeSort(source));
|
||||
}
|
||||
}
|
||||
|
||||
private Analysis analyzeSort(File source) {
|
||||
Map<String, Object> json = readJsonContent(source);
|
||||
Analysis analysis = new Analysis("Metadata element order:");
|
||||
analyzeMetadataElementsSort("groups", json, analysis);
|
||||
analyzeMetadataElementsSort("properties", json, analysis);
|
||||
analyzeMetadataElementsSort("hints", json, analysis);
|
||||
return analysis;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void analyzeMetadataElementsSort(String key, Map<String, Object> json, Analysis analysis) {
|
||||
List<Map<String, Object>> groups = (List<Map<String, Object>>) json.getOrDefault(key, Collections.emptyList());
|
||||
List<String> names = groups.stream().map((group) -> (String) group.get("name")).toList();
|
||||
List<String> sortedNames = names.stream().sorted().toList();
|
||||
for (int i = 0; i < names.size(); i++) {
|
||||
String actual = names.get(i);
|
||||
String expected = sortedNames.get(i);
|
||||
if (!actual.equals(expected)) {
|
||||
analysis.addItem("Wrong order at $." + key + "[" + i + "].name - expected '" + expected
|
||||
+ "' but found '" + actual + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void analyzePropertyDescription(Report report, List<String> exclusions) {
|
||||
for (File source : this.sources) {
|
||||
report.registerAnalysis(source, analyzePropertyDescription(source, exclusions));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Analysis analyzePropertyDescription(File source, List<String> exclusions) {
|
||||
Map<String, Object> json = readJsonContent(source);
|
||||
Analysis analysis = new Analysis("The following properties have no description:");
|
||||
List<Map<String, Object>> properties = (List<Map<String, Object>>) json.get("properties");
|
||||
for (Map<String, Object> property : properties) {
|
||||
String name = (String) property.get("name");
|
||||
if (!isDeprecated(property) && !isDescribed(property) && !isExcluded(exclusions, name)) {
|
||||
analysis.addItem(name);
|
||||
}
|
||||
}
|
||||
return analysis;
|
||||
}
|
||||
|
||||
private boolean isExcluded(List<String> exclusions, String propertyName) {
|
||||
for (String exclusion : exclusions) {
|
||||
if (propertyName.equals(exclusion)) {
|
||||
return true;
|
||||
}
|
||||
if (exclusion.endsWith(".*")) {
|
||||
if (propertyName.startsWith(exclusion.substring(0, exclusion.length() - 2))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isDeprecated(Map<String, Object> property) {
|
||||
return property.get("deprecation") != null;
|
||||
}
|
||||
|
||||
private boolean isDescribed(Map<String, Object> property) {
|
||||
return property.get("description") != null;
|
||||
}
|
||||
|
||||
void analyzeDeprecationSince(Report report) throws IOException {
|
||||
for (File source : this.sources) {
|
||||
report.registerAnalysis(source, analyzeDeprecationSince(source));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Analysis analyzeDeprecationSince(File source) throws IOException {
|
||||
Analysis analysis = new Analysis("The following properties are deprecated without a 'since' version:");
|
||||
Map<String, Object> json = readJsonContent(source);
|
||||
List<Map<String, Object>> properties = (List<Map<String, Object>>) json.get("properties");
|
||||
properties.stream().filter((property) -> property.containsKey("deprecation")).forEach((property) -> {
|
||||
Map<String, Object> deprecation = (Map<String, Object>) property.get("deprecation");
|
||||
if (!deprecation.containsKey("since")) {
|
||||
analysis.addItem(property.get("name").toString());
|
||||
}
|
||||
});
|
||||
return analysis;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> readJsonContent(File source) {
|
||||
return this.jsonMapperSupplier.obtain().readValue(source, Map.class);
|
||||
}
|
||||
|
||||
private static <T> void writeAll(PrintWriter writer, Iterable<T> elements, Consumer<T> itemWriter) {
|
||||
Iterator<T> it = elements.iterator();
|
||||
while (it.hasNext()) {
|
||||
itemWriter.accept(it.next());
|
||||
if (it.hasNext()) {
|
||||
writer.println();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class Report {
|
||||
|
||||
private final File baseDirectory;
|
||||
|
||||
private final MultiValueMap<File, Analysis> analyses = new LinkedMultiValueMap<>();
|
||||
|
||||
Report(File baseDirectory) {
|
||||
this.baseDirectory = baseDirectory;
|
||||
}
|
||||
|
||||
void registerAnalysis(File path, Analysis analysis) {
|
||||
this.analyses.add(path, analysis);
|
||||
}
|
||||
|
||||
boolean hasProblems() {
|
||||
return this.analyses.values()
|
||||
.stream()
|
||||
.anyMatch((candidates) -> candidates.stream().anyMatch(Analysis::hasProblems));
|
||||
}
|
||||
|
||||
List<Analysis> getAnalyses(File source) {
|
||||
return this.analyses.getOrDefault(source, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Write this report to the given {@code file}.
|
||||
* @param file the file to write the report to
|
||||
*/
|
||||
void write(File file) throws IOException {
|
||||
Files.writeString(file.toPath(), createContent(), StandardOpenOption.CREATE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING);
|
||||
}
|
||||
|
||||
private String createContent() {
|
||||
if (this.analyses.isEmpty()) {
|
||||
return "No problems found.";
|
||||
}
|
||||
StringWriter out = new StringWriter();
|
||||
try (PrintWriter writer = new PrintWriter(out)) {
|
||||
writeAll(writer, this.analyses.entrySet(), (entry) -> {
|
||||
writer.println(this.baseDirectory.toPath().relativize(entry.getKey().toPath()));
|
||||
boolean hasProblems = entry.getValue().stream().anyMatch(Analysis::hasProblems);
|
||||
if (hasProblems) {
|
||||
writeAll(writer, entry.getValue(), (analysis) -> analysis.createDetails(writer));
|
||||
}
|
||||
else {
|
||||
writer.println("No problems found.");
|
||||
}
|
||||
});
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class Analysis {
|
||||
|
||||
private final String header;
|
||||
|
||||
private final List<String> items;
|
||||
|
||||
Analysis(String header) {
|
||||
this.header = header;
|
||||
this.items = new ArrayList<>();
|
||||
}
|
||||
|
||||
void addItem(String item) {
|
||||
this.items.add(item);
|
||||
}
|
||||
|
||||
boolean hasProblems() {
|
||||
return !this.items.isEmpty();
|
||||
}
|
||||
|
||||
List<String> getItems() {
|
||||
return this.items;
|
||||
}
|
||||
|
||||
void createDetails(PrintWriter writer) {
|
||||
writer.println(this.header);
|
||||
if (this.items.isEmpty()) {
|
||||
writer.println("No problems found.");
|
||||
}
|
||||
else {
|
||||
for (String item : this.items) {
|
||||
writer.println("\t- " + item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringWriter out = new StringWriter();
|
||||
PrintWriter writer = new PrintWriter(out);
|
||||
createDetails(writer);
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -123,7 +123,7 @@ public abstract class DocumentConfigurationProperties extends DefaultTask {
|
|||
private void jsonPrefixes(Config config) {
|
||||
config.accept("spring.jackson");
|
||||
config.accept("spring.gson");
|
||||
config.accept("spring.kotlinx.serialization.json");
|
||||
config.accept("spring.kotlin.serialization");
|
||||
}
|
||||
|
||||
private void dataPrefixes(Config config) {
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ class PluginXmlParser {
|
|||
|
||||
return new Iterator<>() {
|
||||
|
||||
private int index;
|
||||
private int index = 0;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
|
|
|
|||
|
|
@ -19,11 +19,9 @@ package org.springframework.boot.build.architecture;
|
|||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
|
|
@ -44,7 +42,6 @@ import org.junit.jupiter.params.ParameterizedTest;
|
|||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
|
||||
import org.springframework.boot.build.architecture.annotations.TestConditionalOnClass;
|
||||
import org.springframework.boot.build.architecture.annotations.TestDeprecatedConfigurationProperty;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
|
|
@ -343,16 +340,6 @@ class ArchitectureCheckTests {
|
|||
build(gradleBuild, Task.CHECK_ARCHITECTURE_TEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenDeprecatedConfigurationPropertyIsMissingSinceShouldFailAndWriteReport() throws IOException {
|
||||
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties", "annotations");
|
||||
GradleBuild gradleBuild = this.gradleBuild.withDependencies(SPRING_CONTEXT)
|
||||
.withDeprecatedConfigurationPropertyAnnotation(TestDeprecatedConfigurationProperty.class.getName());
|
||||
buildAndFail(gradleBuild, Task.CHECK_ARCHITECTURE_MAIN,
|
||||
"should include a non-empty 'since' attribute of @DeprecatedConfigurationProperty",
|
||||
"DeprecatedConfigurationPropertySince.getProperty");
|
||||
}
|
||||
|
||||
private void prepareTask(Task task, String... sourceDirectories) throws IOException {
|
||||
for (String sourceDirectory : sourceDirectories) {
|
||||
FileSystemUtils.copyRecursively(
|
||||
|
|
@ -385,13 +372,8 @@ class ArchitectureCheckTests {
|
|||
try {
|
||||
BuildResult buildResult = gradleBuild.buildAndFail(task.toString());
|
||||
assertThat(buildResult.taskPaths(TaskOutcome.FAILED)).as(buildResult.getOutput()).contains(":" + task);
|
||||
try {
|
||||
assertThat(task.getFailureReport(gradleBuild.getProjectDir())).contains(messages);
|
||||
}
|
||||
catch (NoSuchFileException ex) {
|
||||
throw new AssertionError("Expected failure report not found\n" + buildResult.getOutput());
|
||||
}
|
||||
}
|
||||
catch (UnexpectedBuildSuccess ex) {
|
||||
throw new AssertionError("Expected build to fail but it succeeded\n" + ex.getBuildResult().getOutput(), ex);
|
||||
}
|
||||
|
|
@ -423,7 +405,7 @@ class ArchitectureCheckTests {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "checkArchitecture" + StringUtils.capitalize(this.sourceSetName) + "Java";
|
||||
return "checkArchitecture" + StringUtils.capitalize(this.sourceSetName);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -454,18 +436,9 @@ class ArchitectureCheckTests {
|
|||
return this;
|
||||
}
|
||||
|
||||
GradleBuild withConditionalOnClassAnnotation(String annotationClass) {
|
||||
GradleBuild withConditionalOnClassAnnotation(String annotationName) {
|
||||
for (Task task : Task.values()) {
|
||||
configureTask(task, (configuration) -> configuration
|
||||
.withAnnotation(ArchitectureCheck.CONDITIONAL_ON_CLASS, annotationClass));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
GradleBuild withDeprecatedConfigurationPropertyAnnotation(String annotationClass) {
|
||||
for (Task task : Task.values()) {
|
||||
configureTask(task, (configuration) -> configuration
|
||||
.withAnnotation(ArchitectureCheck.DEPRECATED_CONFIGURATION_PROPERTY, annotationClass));
|
||||
configureTask(task, (configuration) -> configuration.withConditionalOnClassAnnotation(annotationName));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
|
@ -525,18 +498,18 @@ class ArchitectureCheckTests {
|
|||
for (String dependency : this.dependencies) {
|
||||
buildFile.append("\n implementation ").append(StringUtils.quote(dependency));
|
||||
}
|
||||
buildFile.append("\n}\n\n");
|
||||
buildFile.append("}\n");
|
||||
}
|
||||
this.taskConfigurations.forEach((task, configuration) -> {
|
||||
buildFile.append(task).append(" {");
|
||||
if (configuration.conditionalOnClassAnnotation() != null) {
|
||||
buildFile.append("\n conditionalOnClassAnnotation = ")
|
||||
.append(StringUtils.quote(configuration.conditionalOnClassAnnotation()));
|
||||
}
|
||||
if (configuration.prohibitObjectsRequireNonNull() != null) {
|
||||
buildFile.append("\n prohibitObjectsRequireNonNull = ")
|
||||
.append(configuration.prohibitObjectsRequireNonNull());
|
||||
}
|
||||
if (configuration.annotations() != null && !configuration.annotations().isEmpty()) {
|
||||
buildFile.append("\n annotationClasses = ")
|
||||
.append(toGroovyMapString(configuration.annotations()));
|
||||
}
|
||||
buildFile.append("\n}\n");
|
||||
});
|
||||
NullMarkedExtension nullMarkedExtension = this.nullMarkedExtension;
|
||||
|
|
@ -563,13 +536,6 @@ class ArchitectureCheckTests {
|
|||
.withPluginClasspath();
|
||||
}
|
||||
|
||||
static String toGroovyMapString(Map<String, String> map) {
|
||||
return map.entrySet()
|
||||
.stream()
|
||||
.map((entry) -> "'" + entry.getKey() + "' : '" + entry.getValue() + "'")
|
||||
.collect(Collectors.joining(", ", "[", "]"));
|
||||
}
|
||||
|
||||
private record NullMarkedExtension(Boolean enabled, Set<String> ignoredPackages) {
|
||||
|
||||
private NullMarkedExtension withEnabled(Boolean enabled) {
|
||||
|
|
@ -582,24 +548,15 @@ class ArchitectureCheckTests {
|
|||
|
||||
}
|
||||
|
||||
private record TaskConfiguration(Boolean prohibitObjectsRequireNonNull, Map<String, String> annotations) {
|
||||
private record TaskConfiguration(Boolean prohibitObjectsRequireNonNull, String conditionalOnClassAnnotation) {
|
||||
|
||||
public TaskConfiguration {
|
||||
if (annotations == null) {
|
||||
annotations = new HashMap<>();
|
||||
}
|
||||
private TaskConfiguration withConditionalOnClassAnnotation(String annotationName) {
|
||||
return new TaskConfiguration(this.prohibitObjectsRequireNonNull, annotationName);
|
||||
}
|
||||
|
||||
private TaskConfiguration withProhibitObjectsRequireNonNull(Boolean prohibitObjectsRequireNonNull) {
|
||||
return new TaskConfiguration(prohibitObjectsRequireNonNull, this.annotations);
|
||||
return new TaskConfiguration(prohibitObjectsRequireNonNull, this.conditionalOnClassAnnotation);
|
||||
}
|
||||
|
||||
private TaskConfiguration withAnnotation(String name, String annotationClass) {
|
||||
Map<String, String> map = new HashMap<>(this.annotations);
|
||||
map.put(name, annotationClass);
|
||||
return new TaskConfiguration(this.prohibitObjectsRequireNonNull, map);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012-present the original author or 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 org.springframework.boot.build.architecture.annotations;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* {@code @DeprecatedConfigurationProperty} analogue for architecture checks.
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface TestDeprecatedConfigurationProperty {
|
||||
|
||||
/**
|
||||
* The reason for the deprecation.
|
||||
* @return the deprecation reason
|
||||
*/
|
||||
String reason() default "";
|
||||
|
||||
/**
|
||||
* The field that should be used instead (if any).
|
||||
* @return the replacement field
|
||||
*/
|
||||
String replacement() default "";
|
||||
|
||||
/**
|
||||
* The version in which the property became deprecated.
|
||||
* @return the version
|
||||
*/
|
||||
String since() default "";
|
||||
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012-present the original author or 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 org.springframework.boot.build.architecture.configurationproperties;
|
||||
|
||||
import org.springframework.boot.build.architecture.annotations.TestDeprecatedConfigurationProperty;
|
||||
|
||||
public class DeprecatedConfigurationPropertySince {
|
||||
|
||||
private String property;
|
||||
|
||||
@TestDeprecatedConfigurationProperty(reason = "no longer used")
|
||||
@Deprecated
|
||||
public String getProperty() {
|
||||
return this.property;
|
||||
}
|
||||
|
||||
public void setProperty(String property) {
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012-present the original author or 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 org.springframework.boot.build.context.properties;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.build.context.properties.ConfigurationPropertiesAnalyzer.Analysis;
|
||||
import org.springframework.boot.build.context.properties.ConfigurationPropertiesAnalyzer.Report;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link ConfigurationPropertiesAnalyzer}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class ConfigurationPropertiesAnalyzerTests {
|
||||
|
||||
@Test
|
||||
void createAnalyzerWithNoSource() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new ConfigurationPropertiesAnalyzer(Collections.emptyList()))
|
||||
.withMessage("At least one source should be provided");
|
||||
}
|
||||
|
||||
@Test
|
||||
void analyzeSortWithAlphabeticalOrder(@TempDir File tempDir) throws IOException {
|
||||
File metadata = new File(tempDir, "metadata.json");
|
||||
Files.writeString(metadata.toPath(), """
|
||||
{ "properties": [
|
||||
{ "name": "abc"}, {"name": "def"}, {"name": "xyz"}
|
||||
]
|
||||
}""");
|
||||
Report report = new Report(tempDir);
|
||||
ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(List.of(metadata));
|
||||
analyzer.analyzeSort(report);
|
||||
assertThat(report.hasProblems()).isFalse();
|
||||
assertThat(report.getAnalyses(metadata)).singleElement()
|
||||
.satisfies(((analysis) -> assertThat(analysis.getItems()).isEmpty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void analyzeSortWithViolations(@TempDir File tempDir) throws IOException {
|
||||
File metadata = new File(tempDir, "metadata.json");
|
||||
Files.writeString(metadata.toPath(), """
|
||||
{ "properties": [
|
||||
{ "name": "def"}, {"name": "abc"}, {"name": "xyz"}
|
||||
]
|
||||
}""");
|
||||
Report report = new Report(tempDir);
|
||||
ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(List.of(metadata));
|
||||
analyzer.analyzeSort(report);
|
||||
assertThat(report.hasProblems()).isTrue();
|
||||
assertThat(report.getAnalyses(metadata)).singleElement()
|
||||
.satisfies((analysis) -> assertThat(analysis.getItems()).containsExactly(
|
||||
"Wrong order at $.properties[0].name - expected 'abc' but found 'def'",
|
||||
"Wrong order at $.properties[1].name - expected 'def' but found 'abc'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void analyzePropertyDescription(@TempDir File tempDir) throws IOException {
|
||||
File metadata = new File(tempDir, "metadata.json");
|
||||
Files.writeString(metadata.toPath(), """
|
||||
{ "properties": [
|
||||
{ "name": "abc", "description": "This is abc." },
|
||||
{ "name": "def", "description": "This is def." },
|
||||
{ "name": "xyz", "description": "This is xyz." }
|
||||
]
|
||||
}""");
|
||||
Report report = new Report(tempDir);
|
||||
ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(List.of(metadata));
|
||||
analyzer.analyzePropertyDescription(report, List.of());
|
||||
assertThat(report.hasProblems()).isFalse();
|
||||
assertThat(report.getAnalyses(metadata)).singleElement()
|
||||
.satisfies(((analysis) -> assertThat(analysis.getItems()).isEmpty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void analyzePropertyDescriptionWithMissingDescription(@TempDir File tempDir) throws IOException {
|
||||
File metadata = new File(tempDir, "metadata.json");
|
||||
Files.writeString(metadata.toPath(), """
|
||||
{ "properties": [
|
||||
{ "name": "abc", "description": "This is abc." },
|
||||
{ "name": "def" },
|
||||
{ "name": "xyz", "description": "This is xyz." }
|
||||
]
|
||||
}""");
|
||||
Report report = new Report(tempDir);
|
||||
ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(List.of(metadata));
|
||||
analyzer.analyzePropertyDescription(report, List.of());
|
||||
assertThat(report.hasProblems()).isTrue();
|
||||
assertThat(report.getAnalyses(metadata)).singleElement()
|
||||
.satisfies(((analysis) -> assertThat(analysis.getItems()).containsExactly("def")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void analyzeDeprecatedPropertyWithMissingSince(@TempDir File tempDir) throws IOException {
|
||||
File metadata = new File(tempDir, "metadata.json");
|
||||
Files.writeString(metadata.toPath(), """
|
||||
{ "properties": [
|
||||
{
|
||||
"name": "abc",
|
||||
"description": "This is abc.",
|
||||
"deprecation": { "reason": "abc reason", "since": "3.0.0" }
|
||||
},
|
||||
{ "name": "def", "description": "This is def." },
|
||||
{
|
||||
"name": "xyz",
|
||||
"description": "This is xyz.",
|
||||
"deprecation": { "reason": "xyz reason" }
|
||||
}
|
||||
]
|
||||
}""");
|
||||
Report report = new Report(tempDir);
|
||||
ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(List.of(metadata));
|
||||
analyzer.analyzeDeprecationSince(report);
|
||||
assertThat(report.hasProblems()).isTrue();
|
||||
assertThat(report.getAnalyses(metadata)).singleElement()
|
||||
.satisfies(((analysis) -> assertThat(analysis.getItems()).containsExactly("xyz")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeEmptyReport(@TempDir File tempDir) throws IOException {
|
||||
assertThat(writeToFile(tempDir, new Report(tempDir))).hasContent("No problems found.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeReportWithNoProblemsFound(@TempDir File tempDir) throws IOException {
|
||||
Report report = new Report(tempDir);
|
||||
File first = new File(tempDir, "metadata-1.json");
|
||||
report.registerAnalysis(first, new Analysis("Check for things:"));
|
||||
File second = new File(tempDir, "metadata-2.json");
|
||||
report.registerAnalysis(second, new Analysis("Check for other things:"));
|
||||
assertThat(writeToFile(tempDir, report)).content().isEqualToIgnoringNewLines("""
|
||||
metadata-1.json
|
||||
No problems found.
|
||||
|
||||
metadata-2.json
|
||||
No problems found.
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeReportWithOneProblem(@TempDir File tempDir) throws IOException {
|
||||
Report report = new Report(tempDir);
|
||||
File metadata = new File(tempDir, "metadata-1.json");
|
||||
Analysis analysis = new Analysis("Check for things:");
|
||||
analysis.addItem("Should not be deprecated");
|
||||
report.registerAnalysis(metadata, analysis);
|
||||
report.registerAnalysis(metadata, new Analysis("Check for other things:"));
|
||||
assertThat(writeToFile(tempDir, report)).content().isEqualToIgnoringNewLines("""
|
||||
metadata-1.json
|
||||
Check for things:
|
||||
- Should not be deprecated
|
||||
|
||||
Check for other things:
|
||||
No problems found.
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeReportWithSeveralProblems(@TempDir File tempDir) throws IOException {
|
||||
Report report = new Report(tempDir);
|
||||
File metadata = new File(tempDir, "metadata-1.json");
|
||||
Analysis firstAnalysis = new Analysis("Check for things:");
|
||||
firstAnalysis.addItem("Should not be deprecated");
|
||||
firstAnalysis.addItem("Should not be public");
|
||||
report.registerAnalysis(metadata, firstAnalysis);
|
||||
Analysis secondAnalysis = new Analysis("Check for other things:");
|
||||
secondAnalysis.addItem("Field 'this' not expected");
|
||||
report.registerAnalysis(metadata, secondAnalysis);
|
||||
assertThat(writeToFile(tempDir, report)).content().isEqualToIgnoringNewLines("""
|
||||
metadata-1.json
|
||||
Check for things:
|
||||
- Should not be deprecated
|
||||
- Should not be public
|
||||
|
||||
Check for other things:
|
||||
- Field 'this' not expected
|
||||
""");
|
||||
}
|
||||
|
||||
private File writeToFile(File directory, Report report) throws IOException {
|
||||
File file = new File(directory, "report.txt");
|
||||
report.write(file);
|
||||
return file;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -142,14 +142,14 @@ class SpringRepositoriesExtensionTests {
|
|||
@Test
|
||||
void mavenRepositoriesWhenConditionMatches() {
|
||||
SpringRepositoriesExtension extension = createExtension("0.0.0-SNAPSHOT", "oss");
|
||||
extension.mavenRepositoriesFor("1.2.3-SNAPSHOT");
|
||||
extension.mavenRepositories(true);
|
||||
assertThat(this.repositories).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mavenRepositoriesWhenConditionDoesNotMatch() {
|
||||
SpringRepositoriesExtension extension = createExtension("0.0.0-SNAPSHOT", "oss");
|
||||
extension.mavenRepositoriesFor("1.2.3");
|
||||
extension.mavenRepositories(false);
|
||||
assertThat(this.repositories).isEmpty();
|
||||
}
|
||||
|
||||
|
|
@ -252,7 +252,7 @@ class SpringRepositoriesExtensionTests {
|
|||
|
||||
void mavenRepositories();
|
||||
|
||||
void mavenRepositoriesFor(Object version);
|
||||
void mavenRepositories(boolean condition);
|
||||
|
||||
void mavenRepositoriesExcludingBootGroup();
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ final class ApiVersions {
|
|||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ApiVersions other = (ApiVersions) obj;
|
||||
return Arrays.equals(this.apiVersions, other.apiVersions);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,25 +106,17 @@ public class Builder {
|
|||
this.log.start(request);
|
||||
validateBindings(request.getBindings());
|
||||
PullPolicy pullPolicy = request.getPullPolicy();
|
||||
ImagePlatform platform = request.getImagePlatform();
|
||||
boolean specifiedPlatform = request.getImagePlatform() != null;
|
||||
DockerRegistryAuthentication registryAuthentication = this.dockerConfiguration.builderRegistryAuthentication();
|
||||
ImageFetcher imageFetcher = new ImageFetcher(registryAuthentication, pullPolicy);
|
||||
Image builderImage = imageFetcher.fetchImage(ImageType.BUILDER, request.getBuilder(), platform);
|
||||
ImageFetcher imageFetcher = new ImageFetcher(this.dockerConfiguration.builderRegistryAuthentication(),
|
||||
pullPolicy, request.getImagePlatform());
|
||||
Image builderImage = imageFetcher.fetchImage(ImageType.BUILDER, request.getBuilder());
|
||||
BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage);
|
||||
request = withRunImageIfNeeded(request, builderMetadata);
|
||||
Assert.state(request.getRunImage() != null, "'request.getRunImage()' must not be null");
|
||||
platform = (platform != null) ? platform : ImagePlatform.from(builderImage);
|
||||
Image runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage(), platform);
|
||||
if (specifiedPlatform && runImage.getPrimaryDigest() != null) {
|
||||
request = request.withRunImage(request.getRunImage().withDigest(runImage.getPrimaryDigest()));
|
||||
runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage(), platform);
|
||||
}
|
||||
Image runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage());
|
||||
assertStackIdsMatch(runImage, builderImage);
|
||||
BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv());
|
||||
BuildpackLayersMetadata buildpackLayersMetadata = BuildpackLayersMetadata.fromImage(builderImage);
|
||||
Buildpacks buildpacks = getBuildpacks(request, imageFetcher, platform, builderMetadata,
|
||||
buildpackLayersMetadata);
|
||||
Buildpacks buildpacks = getBuildpacks(request, imageFetcher, builderMetadata, buildpackLayersMetadata);
|
||||
EphemeralBuilder ephemeralBuilder = new EphemeralBuilder(buildOwner, builderImage, request.getName(),
|
||||
builderMetadata, request.getCreator(), request.getEnv(), buildpacks);
|
||||
executeLifecycle(request, ephemeralBuilder);
|
||||
|
|
@ -168,9 +160,9 @@ public class Builder {
|
|||
}
|
||||
}
|
||||
|
||||
private Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher, ImagePlatform platform,
|
||||
BuilderMetadata builderMetadata, BuildpackLayersMetadata buildpackLayersMetadata) {
|
||||
BuildpackResolverContext resolverContext = new BuilderResolverContext(imageFetcher, platform, builderMetadata,
|
||||
private Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher, BuilderMetadata builderMetadata,
|
||||
BuildpackLayersMetadata buildpackLayersMetadata) {
|
||||
BuildpackResolverContext resolverContext = new BuilderResolverContext(imageFetcher, builderMetadata,
|
||||
buildpackLayersMetadata);
|
||||
return BuildpackResolvers.resolveAll(resolverContext, request.getBuildpacks());
|
||||
}
|
||||
|
|
@ -233,74 +225,49 @@ public class Builder {
|
|||
|
||||
private final PullPolicy pullPolicy;
|
||||
|
||||
ImageFetcher(@Nullable DockerRegistryAuthentication registryAuthentication, PullPolicy pullPolicy) {
|
||||
private @Nullable ImagePlatform defaultPlatform;
|
||||
|
||||
ImageFetcher(@Nullable DockerRegistryAuthentication registryAuthentication, PullPolicy pullPolicy,
|
||||
@Nullable ImagePlatform platform) {
|
||||
this.registryAuthentication = registryAuthentication;
|
||||
this.pullPolicy = pullPolicy;
|
||||
this.defaultPlatform = platform;
|
||||
}
|
||||
|
||||
Image fetchImage(ImageType type, ImageReference reference, @Nullable ImagePlatform platform)
|
||||
throws IOException {
|
||||
Image fetchImage(ImageType type, ImageReference reference) throws IOException {
|
||||
Assert.notNull(type, "'type' must not be null");
|
||||
Assert.notNull(reference, "'reference' must not be null");
|
||||
if (this.pullPolicy == PullPolicy.ALWAYS) {
|
||||
return pullImageAndCheckForPlatformMismatch(type, reference, platform);
|
||||
return checkPlatformMismatch(pullImage(reference, type), reference);
|
||||
}
|
||||
try {
|
||||
Image image = Builder.this.docker.image().inspect(reference, platform);
|
||||
return checkPlatformMismatch(image, reference, platform);
|
||||
return checkPlatformMismatch(Builder.this.docker.image().inspect(reference), reference);
|
||||
}
|
||||
catch (DockerEngineException ex) {
|
||||
if (this.pullPolicy == PullPolicy.IF_NOT_PRESENT && ex.getStatusCode() == 404) {
|
||||
return pullImageAndCheckForPlatformMismatch(type, reference, platform);
|
||||
return checkPlatformMismatch(pullImage(reference, type), reference);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private Image pullImageAndCheckForPlatformMismatch(ImageType type, ImageReference reference,
|
||||
@Nullable ImagePlatform platform) throws IOException {
|
||||
try {
|
||||
Image image = pullImage(reference, type, platform);
|
||||
return checkPlatformMismatch(image, reference, platform);
|
||||
}
|
||||
catch (DockerEngineException ex) {
|
||||
// Try to throw our own exception for consistent log output. Matching
|
||||
// on the message is a little brittle, but it doesn't matter too much
|
||||
// if it fails as the original exception is still enough to stop the build
|
||||
if (platform != null && ex.getMessage() != null
|
||||
&& ex.getMessage().contains("does not provide the specified platform")) {
|
||||
throwAsPlatformMismatchException(type, reference, platform, ex);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private void throwAsPlatformMismatchException(ImageType type, ImageReference reference, ImagePlatform platform,
|
||||
@Nullable Throwable cause) throws IOException {
|
||||
try {
|
||||
Image image = pullImage(reference, type, null);
|
||||
throw new PlatformMismatchException(reference, platform, ImagePlatform.from(image), cause);
|
||||
}
|
||||
catch (DockerEngineException ex) {
|
||||
}
|
||||
}
|
||||
|
||||
private Image pullImage(ImageReference reference, ImageType imageType, @Nullable ImagePlatform platform)
|
||||
throws IOException {
|
||||
private Image pullImage(ImageReference reference, ImageType imageType) throws IOException {
|
||||
TotalProgressPullListener listener = new TotalProgressPullListener(
|
||||
Builder.this.log.pullingImage(reference, platform, imageType));
|
||||
Builder.this.log.pullingImage(reference, this.defaultPlatform, imageType));
|
||||
String authHeader = authHeader(this.registryAuthentication, reference);
|
||||
Image image = Builder.this.docker.image().pull(reference, platform, listener, authHeader);
|
||||
Image image = Builder.this.docker.image().pull(reference, this.defaultPlatform, listener, authHeader);
|
||||
Builder.this.log.pulledImage(image, imageType);
|
||||
if (this.defaultPlatform == null) {
|
||||
this.defaultPlatform = ImagePlatform.from(image);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
private Image checkPlatformMismatch(Image image, ImageReference reference,
|
||||
@Nullable ImagePlatform requestedPlatform) {
|
||||
if (requestedPlatform != null) {
|
||||
ImagePlatform actualPlatform = ImagePlatform.from(image);
|
||||
if (!actualPlatform.equals(requestedPlatform)) {
|
||||
throw new PlatformMismatchException(reference, requestedPlatform, actualPlatform, null);
|
||||
private Image checkPlatformMismatch(Image image, ImageReference imageReference) {
|
||||
if (this.defaultPlatform != null) {
|
||||
ImagePlatform imagePlatform = ImagePlatform.from(image);
|
||||
if (!imagePlatform.equals(this.defaultPlatform)) {
|
||||
throw new PlatformMismatchException(imageReference, this.defaultPlatform, imagePlatform);
|
||||
}
|
||||
}
|
||||
return image;
|
||||
|
|
@ -311,9 +278,9 @@ public class Builder {
|
|||
private static final class PlatformMismatchException extends RuntimeException {
|
||||
|
||||
private PlatformMismatchException(ImageReference imageReference, ImagePlatform requestedPlatform,
|
||||
ImagePlatform actualPlatform, @Nullable Throwable cause) {
|
||||
ImagePlatform actualPlatform) {
|
||||
super("Image platform mismatch detected. The configured platform '%s' is not supported by the image '%s'. Requested platform '%s' but got '%s'"
|
||||
.formatted(requestedPlatform, imageReference, requestedPlatform, actualPlatform), cause);
|
||||
.formatted(requestedPlatform, imageReference, requestedPlatform, actualPlatform));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -359,16 +326,13 @@ public class Builder {
|
|||
|
||||
private final ImageFetcher imageFetcher;
|
||||
|
||||
private final ImagePlatform platform;
|
||||
|
||||
private final BuilderMetadata builderMetadata;
|
||||
|
||||
private final BuildpackLayersMetadata buildpackLayersMetadata;
|
||||
|
||||
BuilderResolverContext(ImageFetcher imageFetcher, ImagePlatform platform, BuilderMetadata builderMetadata,
|
||||
BuilderResolverContext(ImageFetcher imageFetcher, BuilderMetadata builderMetadata,
|
||||
BuildpackLayersMetadata buildpackLayersMetadata) {
|
||||
this.imageFetcher = imageFetcher;
|
||||
this.platform = platform;
|
||||
this.builderMetadata = builderMetadata;
|
||||
this.buildpackLayersMetadata = buildpackLayersMetadata;
|
||||
}
|
||||
|
|
@ -385,13 +349,13 @@ public class Builder {
|
|||
|
||||
@Override
|
||||
public Image fetchImage(ImageReference reference, ImageType imageType) throws IOException {
|
||||
return this.imageFetcher.fetchImage(imageType, reference, this.platform);
|
||||
return this.imageFetcher.fetchImage(imageType, reference);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportImageLayers(ImageReference reference, IOBiConsumer<String, TarArchive> exports)
|
||||
throws IOException {
|
||||
Builder.this.docker.image().exportLayers(reference, this.platform, exports);
|
||||
Builder.this.docker.image().exportLayers(reference, exports);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package org.springframework.boot.buildpack.platform.docker;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
|
@ -31,13 +30,10 @@ import org.springframework.util.Assert;
|
|||
* @author Scott Frederick
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public final class ApiVersion implements Comparable<ApiVersion> {
|
||||
public final class ApiVersion {
|
||||
|
||||
private static final Pattern PATTERN = Pattern.compile("^v?(\\d+)\\.(\\d*)$");
|
||||
|
||||
private static final Comparator<ApiVersion> COMPARATOR = Comparator.comparing(ApiVersion::getMajor)
|
||||
.thenComparing(ApiVersion::getMinor);
|
||||
|
||||
private final int major;
|
||||
|
||||
private final int minor;
|
||||
|
|
@ -142,9 +138,4 @@ public final class ApiVersion implements Comparable<ApiVersion> {
|
|||
return new ApiVersion(major, minor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ApiVersion other) {
|
||||
return COMPARATOR.compare(this, other);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,9 +62,11 @@ public class DockerApi {
|
|||
|
||||
private static final List<String> FORCE_PARAMS = Collections.unmodifiableList(Arrays.asList("force", "1"));
|
||||
|
||||
static final ApiVersion UNKNOWN_API_VERSION = ApiVersion.of(0, 0);
|
||||
static final ApiVersion API_VERSION = ApiVersion.of(1, 24);
|
||||
|
||||
static final ApiVersion PREFERRED_API_VERSION = ApiVersion.of(1, 50);
|
||||
static final ApiVersion PLATFORM_API_VERSION = ApiVersion.of(1, 41);
|
||||
|
||||
static final ApiVersion UNKNOWN_API_VERSION = ApiVersion.of(0, 0);
|
||||
|
||||
static final String API_VERSION_HEADER_NAME = "API-Version";
|
||||
|
||||
|
|
@ -124,38 +126,18 @@ public class DockerApi {
|
|||
return this.jsonStream;
|
||||
}
|
||||
|
||||
URI buildPlatformJsonUrl(Feature feature, @Nullable ImagePlatform platform, String path) {
|
||||
if (platform != null && getApiVersion().supports(feature.minimumVersion())) {
|
||||
return buildUrl(feature, path, "platform", platform.toJson());
|
||||
}
|
||||
return buildUrl(path);
|
||||
}
|
||||
|
||||
private URI buildUrl(String path, @Nullable Collection<?> params) {
|
||||
return buildUrl(Feature.BASELINE, path, (params != null) ? params.toArray() : null);
|
||||
return buildUrl(API_VERSION, path, (params != null) ? params.toArray() : null);
|
||||
}
|
||||
|
||||
private URI buildUrl(String path, Object... params) {
|
||||
return buildUrl(Feature.BASELINE, path, params);
|
||||
return buildUrl(API_VERSION, path, params);
|
||||
}
|
||||
|
||||
URI buildUrl(Feature feature, String path, Object @Nullable ... params) {
|
||||
ApiVersion version = getApiVersion();
|
||||
if (version.equals(UNKNOWN_API_VERSION) || (version.compareTo(PREFERRED_API_VERSION) >= 0
|
||||
&& version.compareTo(feature.minimumVersion()) >= 0)) {
|
||||
return buildVersionedUrl(PREFERRED_API_VERSION, path, params);
|
||||
}
|
||||
if (version.compareTo(feature.minimumVersion()) >= 0) {
|
||||
return buildVersionedUrl(version, path, params);
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
"Docker API version must be at least %s to support this feature, but current API version is %s"
|
||||
.formatted(feature.minimumVersion(), version));
|
||||
}
|
||||
|
||||
private URI buildVersionedUrl(ApiVersion version, String path, Object @Nullable ... params) {
|
||||
private URI buildUrl(ApiVersion apiVersion, String path, Object @Nullable ... params) {
|
||||
verifyApiVersion(apiVersion);
|
||||
try {
|
||||
URIBuilder builder = new URIBuilder("/v" + version + path);
|
||||
URIBuilder builder = new URIBuilder("/v" + apiVersion + path);
|
||||
if (params != null) {
|
||||
int param = 0;
|
||||
while (param < params.length) {
|
||||
|
|
@ -169,6 +151,13 @@ public class DockerApi {
|
|||
}
|
||||
}
|
||||
|
||||
private void verifyApiVersion(ApiVersion minimumVersion) {
|
||||
ApiVersion actualVersion = getApiVersion();
|
||||
Assert.state(actualVersion.equals(UNKNOWN_API_VERSION) || actualVersion.supports(minimumVersion),
|
||||
() -> "Docker API version must be at least " + minimumVersion
|
||||
+ " to support this feature, but current API version is " + actualVersion);
|
||||
}
|
||||
|
||||
private ApiVersion getApiVersion() {
|
||||
ApiVersion apiVersion = this.apiVersion;
|
||||
if (apiVersion == null) {
|
||||
|
|
@ -236,8 +225,9 @@ public class DockerApi {
|
|||
UpdateListener<PullImageUpdateEvent> listener, @Nullable String registryAuth) throws IOException {
|
||||
Assert.notNull(reference, "'reference' must not be null");
|
||||
Assert.notNull(listener, "'listener' must not be null");
|
||||
URI createUri = (platform != null) ? buildUrl(Feature.PLATFORM_IMAGE_PULL, "/images/create", "fromImage",
|
||||
reference, "platform", platform) : buildUrl("/images/create", "fromImage", reference);
|
||||
URI createUri = (platform != null)
|
||||
? buildUrl(PLATFORM_API_VERSION, "/images/create", "fromImage", reference, "platform", platform)
|
||||
: buildUrl("/images/create", "fromImage", reference);
|
||||
DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener();
|
||||
listener.onStart();
|
||||
try {
|
||||
|
|
@ -247,7 +237,7 @@ public class DockerApi {
|
|||
listener.onUpdate(event);
|
||||
});
|
||||
}
|
||||
return inspect(reference, platform);
|
||||
return inspect((platform != null) ? PLATFORM_API_VERSION : API_VERSION, reference);
|
||||
}
|
||||
finally {
|
||||
listener.onFinish();
|
||||
|
|
@ -319,24 +309,9 @@ public class DockerApi {
|
|||
*/
|
||||
public void exportLayers(ImageReference reference, IOBiConsumer<String, TarArchive> exports)
|
||||
throws IOException {
|
||||
exportLayers(reference, null, exports);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the layers of an image as {@link TarArchive TarArchives}.
|
||||
* @param reference the reference to export
|
||||
* @param platform the platform (os/architecture/variant) of the image to export.
|
||||
* Ignored on older versions of Docker.
|
||||
* @param exports a consumer to receive the layers (contents can only be accessed
|
||||
* during the callback)
|
||||
* @throws IOException on IO error
|
||||
* @since 3.4.12
|
||||
*/
|
||||
public void exportLayers(ImageReference reference, @Nullable ImagePlatform platform,
|
||||
IOBiConsumer<String, TarArchive> exports) throws IOException {
|
||||
Assert.notNull(reference, "'reference' must not be null");
|
||||
Assert.notNull(exports, "'exports' must not be null");
|
||||
URI uri = buildPlatformJsonUrl(Feature.PLATFORM_IMAGE_EXPORT, platform, "/images/" + reference + "/get");
|
||||
URI uri = buildUrl("/images/" + reference + "/get");
|
||||
try (Response response = http().get(uri)) {
|
||||
try (ExportedImageTar exportedImageTar = new ExportedImageTar(reference, response.getContent())) {
|
||||
exportedImageTar.exportLayers(exports);
|
||||
|
|
@ -364,25 +339,13 @@ public class DockerApi {
|
|||
* @throws IOException on IO error
|
||||
*/
|
||||
public Image inspect(ImageReference reference) throws IOException {
|
||||
return inspect(reference, null);
|
||||
return inspect(API_VERSION, reference);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect an image.
|
||||
* @param reference the image reference
|
||||
* @param platform the platform (os/architecture/variant) of the image to inspect.
|
||||
* Ignored on older versions of Docker.
|
||||
* @return the image from the local repository
|
||||
* @throws IOException on IO error
|
||||
* @since 3.4.12
|
||||
*/
|
||||
public Image inspect(ImageReference reference, @Nullable ImagePlatform platform) throws IOException {
|
||||
// The Docker documentation is incomplete but platform parameters
|
||||
// are supported since 1.49 (see https://github.com/moby/moby/pull/49586)
|
||||
private Image inspect(ApiVersion apiVersion, ImageReference reference) throws IOException {
|
||||
Assert.notNull(reference, "'reference' must not be null");
|
||||
URI inspectUrl = buildPlatformJsonUrl(Feature.PLATFORM_IMAGE_INSPECT, platform,
|
||||
"/images/" + reference + "/json");
|
||||
try (Response response = http().get(inspectUrl)) {
|
||||
URI imageUri = buildUrl(apiVersion, "/images/" + reference + "/json");
|
||||
try (Response response = http().get(imageUri)) {
|
||||
return Image.of(response.getContent());
|
||||
}
|
||||
}
|
||||
|
|
@ -430,7 +393,7 @@ public class DockerApi {
|
|||
private ContainerReference createContainer(ContainerConfig config, @Nullable ImagePlatform platform)
|
||||
throws IOException {
|
||||
URI createUri = (platform != null)
|
||||
? buildUrl(Feature.PLATFORM_CONTAINER_CREATE, "/containers/create", "platform", platform)
|
||||
? buildUrl(PLATFORM_API_VERSION, "/containers/create", "platform", platform)
|
||||
: buildUrl("/containers/create");
|
||||
try (Response response = http().post(createUri, "application/json", config::writeTo)) {
|
||||
return ContainerReference
|
||||
|
|
@ -635,28 +598,4 @@ public class DockerApi {
|
|||
|
||||
}
|
||||
|
||||
enum Feature {
|
||||
|
||||
BASELINE(ApiVersion.of(1, 24)),
|
||||
|
||||
PLATFORM_IMAGE_PULL(ApiVersion.of(1, 41)),
|
||||
|
||||
PLATFORM_CONTAINER_CREATE(ApiVersion.of(1, 41)),
|
||||
|
||||
PLATFORM_IMAGE_INSPECT(ApiVersion.of(1, 49)),
|
||||
|
||||
PLATFORM_IMAGE_EXPORT(ApiVersion.of(1, 48));
|
||||
|
||||
private final ApiVersion minimumVersion;
|
||||
|
||||
Feature(ApiVersion minimumVersion) {
|
||||
this.minimumVersion = minimumVersion;
|
||||
}
|
||||
|
||||
ApiVersion minimumVersion() {
|
||||
return this.minimumVersion;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,12 +19,9 @@ package org.springframework.boot.buildpack.platform.docker;
|
|||
import java.util.Objects;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.docker.type.Image;
|
||||
import org.springframework.boot.buildpack.platform.json.SharedJsonMapper;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A platform specification for a Docker image.
|
||||
|
|
@ -105,20 +102,4 @@ public class ImagePlatform {
|
|||
return new ImagePlatform(image.getOs(), image.getArchitecture(), image.getVariant());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a JSON-encoded representation of this platform.
|
||||
* @return the JSON string
|
||||
*/
|
||||
public String toJson() {
|
||||
ObjectNode json = SharedJsonMapper.get().createObjectNode();
|
||||
json.put("os", this.os);
|
||||
if (StringUtils.hasText(this.architecture)) {
|
||||
json.put("architecture", this.architecture);
|
||||
}
|
||||
if (StringUtils.hasText(this.variant)) {
|
||||
json.put("variant", this.variant);
|
||||
}
|
||||
return json.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,13 +22,11 @@ import java.lang.invoke.MethodHandles;
|
|||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.json.MappedObject;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
|
|
@ -54,8 +52,6 @@ public class Image extends MappedObject {
|
|||
|
||||
private final @Nullable String created;
|
||||
|
||||
private final @Nullable Descriptor descriptor;
|
||||
|
||||
Image(JsonNode node) {
|
||||
super(node, MethodHandles.lookup());
|
||||
this.digests = childrenAt("/RepoDigests", JsonNode::asString);
|
||||
|
|
@ -65,9 +61,6 @@ public class Image extends MappedObject {
|
|||
this.architecture = valueAt("/Architecture", String.class);
|
||||
this.variant = valueAt("/Variant", String.class);
|
||||
this.created = valueAt("/Created", String.class);
|
||||
JsonNode descriptorNode = getNode().path("Descriptor");
|
||||
this.descriptor = (descriptorNode.isMissingNode() || descriptorNode.isNull()) ? null
|
||||
: new Descriptor(descriptorNode);
|
||||
}
|
||||
|
||||
private List<LayerId> extractLayers(String @Nullable [] layers) {
|
||||
|
|
@ -133,35 +126,6 @@ public class Image extends MappedObject {
|
|||
return this.created;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the descriptor for this image as reported by Docker Engine inspect.
|
||||
* @return the image descriptor or {@code null}
|
||||
*/
|
||||
public @Nullable Descriptor getDescriptor() {
|
||||
return this.descriptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the primary digest of the image or {@code null}. Checks the
|
||||
* {@code Descriptor.digest} first, falling back to {@code RepoDigest}.
|
||||
* @return the primary digest or {@code null}
|
||||
* @since 3.4.12
|
||||
*/
|
||||
public @Nullable String getPrimaryDigest() {
|
||||
if (this.descriptor != null && StringUtils.hasText(this.descriptor.getDigest())) {
|
||||
return this.descriptor.getDigest();
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(this.digests)) {
|
||||
try {
|
||||
String digest = this.digests.get(0);
|
||||
return (digest != null) ? ImageReference.of(digest).getDigest() : null;
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link Image} instance from the specified JSON content.
|
||||
* @param content the JSON content
|
||||
|
|
@ -172,24 +136,4 @@ public class Image extends MappedObject {
|
|||
return of(content, Image::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Descriptor details as reported in the {@code Docker inspect} response.
|
||||
*
|
||||
* @since 3.4.12
|
||||
*/
|
||||
public final class Descriptor extends MappedObject {
|
||||
|
||||
private final String digest;
|
||||
|
||||
Descriptor(JsonNode node) {
|
||||
super(node, MethodHandles.lookup());
|
||||
this.digest = Objects.requireNonNull(valueAt("/digest", String.class));
|
||||
}
|
||||
|
||||
public String getDigest() {
|
||||
return this.digest;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,9 +78,6 @@ class BuilderTests {
|
|||
|
||||
private static final ImageReference BASE_CNB = ImageReference.of("docker.io/cloudfoundry/run:base-cnb");
|
||||
|
||||
private static final ImageReference PLATFORM_CNB = ImageReference
|
||||
.of("docker.io/cloudfoundry/run@sha256:fb5ecb90a42b2067a859aab23fc1f5e9d9c2589d07ba285608879e7baa415aad");
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("NullAway") // Test null check
|
||||
void createWhenLogIsNullThrowsException() {
|
||||
|
|
@ -281,8 +278,8 @@ class BuilderTests {
|
|||
.willAnswer(withPulledImage(builderImage));
|
||||
given(docker.image().pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), isNull()))
|
||||
.willAnswer(withPulledImage(runImage));
|
||||
given(docker.image().inspect(eq(DEFAULT_BUILDER), any())).willReturn(builderImage);
|
||||
given(docker.image().inspect(eq(BASE_CNB), any())).willReturn(runImage);
|
||||
given(docker.image().inspect(eq(DEFAULT_BUILDER))).willReturn(builderImage);
|
||||
given(docker.image().inspect(eq(BASE_CNB))).willReturn(runImage);
|
||||
Builder builder = new Builder(BuildLog.to(out), docker, null);
|
||||
BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.NEVER);
|
||||
builder.build(request);
|
||||
|
|
@ -294,7 +291,7 @@ class BuilderTests {
|
|||
assertThat(tag).isNotNull();
|
||||
then(docker.image()).should().remove(tag, true);
|
||||
then(docker.image()).should(never()).pull(any(), any(), any());
|
||||
then(docker.image()).should(times(2)).inspect(any(), any());
|
||||
then(docker.image()).should(times(2)).inspect(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -333,11 +330,11 @@ class BuilderTests {
|
|||
.willAnswer(withPulledImage(builderImage));
|
||||
given(docker.image().pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), isNull()))
|
||||
.willAnswer(withPulledImage(runImage));
|
||||
given(docker.image().inspect(eq(DEFAULT_BUILDER), any()))
|
||||
given(docker.image().inspect(eq(DEFAULT_BUILDER)))
|
||||
.willThrow(new TestDockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null,
|
||||
null, null))
|
||||
.willReturn(builderImage);
|
||||
given(docker.image().inspect(eq(BASE_CNB), any()))
|
||||
given(docker.image().inspect(eq(BASE_CNB)))
|
||||
.willThrow(new TestDockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null,
|
||||
null, null))
|
||||
.willReturn(runImage);
|
||||
|
|
@ -351,7 +348,7 @@ class BuilderTests {
|
|||
ImageReference tag = archive.getValue().getTag();
|
||||
assertThat(tag).isNotNull();
|
||||
then(docker.image()).should().remove(tag, true);
|
||||
then(docker.image()).should(times(2)).inspect(any(), any());
|
||||
then(docker.image()).should(times(2)).inspect(any());
|
||||
then(docker.image()).should(times(2)).pull(any(), any(), any(), isNull());
|
||||
}
|
||||
|
||||
|
|
@ -428,8 +425,6 @@ class BuilderTests {
|
|||
given(docker.image().pull(eq(DEFAULT_BUILDER), eq(platform), any(), isNull()))
|
||||
.willAnswer(withPulledImage(builderImage));
|
||||
given(docker.image().pull(eq(BASE_CNB), eq(platform), any(), isNull())).willAnswer(withPulledImage(runImage));
|
||||
given(docker.image().pull(eq(PLATFORM_CNB), eq(platform), any(), isNull()))
|
||||
.willAnswer(withPulledImage(runImage));
|
||||
Builder builder = new Builder(BuildLog.to(out), docker, null);
|
||||
BuildRequest request = getTestRequest().withImagePlatform("linux/arm64/v1");
|
||||
builder.build(request);
|
||||
|
|
@ -438,7 +433,6 @@ class BuilderTests {
|
|||
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
|
||||
then(docker.image()).should().pull(eq(DEFAULT_BUILDER), eq(platform), any(), isNull());
|
||||
then(docker.image()).should().pull(eq(BASE_CNB), eq(platform), any(), isNull());
|
||||
then(docker.image()).should().pull(eq(PLATFORM_CNB), eq(platform), any(), isNull());
|
||||
then(docker.image()).should().load(archive.capture(), any());
|
||||
ImageReference tag = archive.getValue().getTag();
|
||||
assertThat(tag).isNotNull();
|
||||
|
|
|
|||
|
|
@ -111,17 +111,6 @@ class ApiVersionTests {
|
|||
assertThat(v12a).isEqualTo(v12a).isEqualTo(v12b).isNotEqualTo(v13);
|
||||
}
|
||||
|
||||
@Test
|
||||
void compareTo() {
|
||||
assertThat(ApiVersion.of(0, 0).compareTo(ApiVersion.of(0, 0))).isZero();
|
||||
assertThat(ApiVersion.of(0, 1).compareTo(ApiVersion.of(0, 1))).isZero();
|
||||
assertThat(ApiVersion.of(1, 0).compareTo(ApiVersion.of(1, 0))).isZero();
|
||||
assertThat(ApiVersion.of(0, 0).compareTo(ApiVersion.of(0, 1))).isLessThan(0);
|
||||
assertThat(ApiVersion.of(0, 1).compareTo(ApiVersion.of(0, 0))).isGreaterThan(0);
|
||||
assertThat(ApiVersion.of(1, 0).compareTo(ApiVersion.of(0, 1))).isGreaterThan(0);
|
||||
assertThat(ApiVersion.of(0, 1).compareTo(ApiVersion.of(1, 0))).isLessThan(0);
|
||||
}
|
||||
|
||||
private boolean supports(String v1, String v2) {
|
||||
return ApiVersion.parse(v1).supports(ApiVersion.parse(v2));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ import java.io.InputStream;
|
|||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
||||
|
|
@ -43,7 +41,6 @@ import org.mockito.Mock;
|
|||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi;
|
||||
import org.springframework.boot.buildpack.platform.docker.DockerApi.Feature;
|
||||
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
|
||||
import org.springframework.boot.buildpack.platform.docker.DockerApi.SystemApi;
|
||||
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
|
||||
|
|
@ -89,21 +86,22 @@ import static org.mockito.Mockito.times;
|
|||
@ExtendWith({ MockitoExtension.class, OutputCaptureExtension.class })
|
||||
class DockerApiTests {
|
||||
|
||||
private static final String API_URL = "/v" + DockerApi.PREFERRED_API_VERSION;
|
||||
private static final String API_URL = "/v" + DockerApi.API_VERSION;
|
||||
|
||||
private static final String PLATFORM_API_URL = "/v" + DockerApi.PLATFORM_API_VERSION;
|
||||
|
||||
public static final String PING_URL = "/_ping";
|
||||
|
||||
private static final String IMAGES_URL = API_URL + "/images";
|
||||
|
||||
private static final String PLATFORM_IMAGES_URL = PLATFORM_API_URL + "/images";
|
||||
|
||||
private static final String CONTAINERS_URL = API_URL + "/containers";
|
||||
|
||||
private static final String PLATFORM_CONTAINERS_URL = PLATFORM_API_URL + "/containers";
|
||||
|
||||
private static final String VOLUMES_URL = API_URL + "/volumes";
|
||||
|
||||
private static final ImagePlatform LINUX_ARM64_PLATFORM = ImagePlatform.of("linux/arm64/v1");
|
||||
|
||||
private static final String ENCODED_LINUX_ARM64_PLATFORM_JSON = URLEncoder.encode(LINUX_ARM64_PLATFORM.toJson(),
|
||||
StandardCharsets.UTF_8);
|
||||
|
||||
@Mock
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
private HttpTransport http;
|
||||
|
|
@ -170,52 +168,6 @@ class DockerApiTests {
|
|||
assertThat(api).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildUrlWhenUnknownVersionUsesPreferredVersion() throws Exception {
|
||||
setVersion("0.0");
|
||||
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
|
||||
.isEqualTo(URI.create("/v" + DockerApi.PREFERRED_API_VERSION + "/test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildUrlWhenVersionIsGreaterThanPreferredUsesPreferred() throws Exception {
|
||||
setVersion("1000.0");
|
||||
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
|
||||
.isEqualTo(URI.create("/v" + DockerApi.PREFERRED_API_VERSION + "/test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildUrlWhenVersionIsEqualToPreferredUsesPreferred() throws Exception {
|
||||
setVersion(DockerApi.PREFERRED_API_VERSION.toString());
|
||||
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
|
||||
.isEqualTo(URI.create("/v" + DockerApi.PREFERRED_API_VERSION + "/test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildUrlWhenVersionIsLessThanPreferredAndGreaterThanMinimumUsesVersionVersion() throws Exception {
|
||||
setVersion("1.48");
|
||||
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test")).isEqualTo(URI.create("/v1.48/test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildUrlWhenVersionIsLessThanPreferredAndEqualToMinimumUsesVersionVersion() throws Exception {
|
||||
setVersion(Feature.BASELINE.minimumVersion().toString());
|
||||
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test")).isEqualTo(URI.create("/v1.24/test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildUrlWhenVersionIsLessThanMinimumThrowsException() throws Exception {
|
||||
setVersion("1.23");
|
||||
assertThatIllegalStateException().isThrownBy(() -> this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
|
||||
.withMessage("Docker API version must be at least 1.24 "
|
||||
+ "to support this feature, but current API version is 1.23");
|
||||
}
|
||||
|
||||
private void setVersion(String version) throws IOException, URISyntaxException {
|
||||
given(http().head(eq(new URI(PING_URL))))
|
||||
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, version)));
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ImageDockerApiTests {
|
||||
|
||||
|
|
@ -290,14 +242,15 @@ class DockerApiTests {
|
|||
@Test
|
||||
void pullWithPlatformPullsImageAndProducesEvents() throws Exception {
|
||||
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
|
||||
URI createUri = new URI(
|
||||
"/v1.49/images/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1");
|
||||
URI imageUri = new URI("/v1.49/images/gcr.io/paketo-buildpacks/builder:base/json?platform="
|
||||
+ ENCODED_LINUX_ARM64_PLATFORM_JSON);
|
||||
setVersion("1.49");
|
||||
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
|
||||
URI createUri = new URI(PLATFORM_IMAGES_URL
|
||||
+ "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1");
|
||||
URI imageUri = new URI(PLATFORM_IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json");
|
||||
given(http().head(eq(new URI(PING_URL))))
|
||||
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.41")));
|
||||
given(http().post(eq(createUri), isNull())).willReturn(responseOf("pull-stream.json"));
|
||||
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
|
||||
Image image = this.api.pull(reference, LINUX_ARM64_PLATFORM, this.pullListener);
|
||||
Image image = this.api.pull(reference, platform, this.pullListener);
|
||||
assertThat(image.getLayers()).hasSize(46);
|
||||
InOrder ordered = inOrder(this.pullListener);
|
||||
ordered.verify(this.pullListener).onStart();
|
||||
|
|
@ -309,7 +262,8 @@ class DockerApiTests {
|
|||
void pullWithPlatformAndInsufficientApiVersionThrowsException() throws Exception {
|
||||
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
|
||||
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
|
||||
setVersion("1.24");
|
||||
given(http().head(eq(new URI(PING_URL)))).willReturn(
|
||||
responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.API_VERSION)));
|
||||
assertThatIllegalStateException().isThrownBy(() -> this.api.pull(reference, platform, this.pullListener))
|
||||
.withMessageContaining("must be at least 1.41")
|
||||
.withMessageContaining("current API version is 1.24");
|
||||
|
|
@ -446,30 +400,6 @@ class DockerApiTests {
|
|||
URI imageUri = new URI(IMAGES_URL + "/docker.io/paketobuildpacks/builder:base/json");
|
||||
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
|
||||
Image image = this.api.inspect(reference);
|
||||
assertThat(image.getArchitecture()).isEqualTo("amd64");
|
||||
assertThat(image.getLayers()).hasSize(46);
|
||||
}
|
||||
|
||||
@Test
|
||||
void inspectWithPlatformWhenSupportedVersionInspectImage() throws Exception {
|
||||
ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base");
|
||||
URI imageUri = new URI("/v1.49/images/docker.io/paketobuildpacks/builder:base/json?platform="
|
||||
+ ENCODED_LINUX_ARM64_PLATFORM_JSON);
|
||||
setVersion("1.49");
|
||||
given(http().get(imageUri)).willReturn(responseOf("type/image-platform.json"));
|
||||
Image image = this.api.inspect(reference, LINUX_ARM64_PLATFORM);
|
||||
assertThat(image.getArchitecture()).isEqualTo("arm64");
|
||||
assertThat(image.getLayers()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void inspectWithPlatformWhenOldVersionInspectImage() throws Exception {
|
||||
ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base");
|
||||
URI imageUri = new URI("/v1.48/images/docker.io/paketobuildpacks/builder:base/json");
|
||||
setVersion("1.48");
|
||||
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
|
||||
Image image = this.api.inspect(reference, LINUX_ARM64_PLATFORM);
|
||||
assertThat(image.getArchitecture()).isEqualTo("amd64");
|
||||
assertThat(image.getLayers()).hasSize(46);
|
||||
}
|
||||
|
||||
|
|
@ -501,65 +431,6 @@ class DockerApiTests {
|
|||
.containsExactly("/cnb/stack.toml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportLayersExportsLayerTarsWithPlatformWhenSupportedVersion() throws Exception {
|
||||
setVersion("1.48");
|
||||
ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base");
|
||||
URI exportUri = new URI("/v1.48/images/docker.io/paketobuildpacks/builder:base/get?platform="
|
||||
+ ENCODED_LINUX_ARM64_PLATFORM_JSON);
|
||||
given(DockerApiTests.this.http.get(exportUri)).willReturn(responseOf("export.tar"));
|
||||
MultiValueMap<String, String> contents = new LinkedMultiValueMap<>();
|
||||
this.api.exportLayers(reference, LINUX_ARM64_PLATFORM, (name, archive) -> {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
archive.writeTo(out);
|
||||
try (TarArchiveInputStream in = new TarArchiveInputStream(
|
||||
new ByteArrayInputStream(out.toByteArray()))) {
|
||||
TarArchiveEntry entry = in.getNextEntry();
|
||||
while (entry != null) {
|
||||
contents.add(name, entry.getName());
|
||||
entry = in.getNextEntry();
|
||||
}
|
||||
}
|
||||
});
|
||||
assertThat(contents).hasSize(3)
|
||||
.containsKeys("70bb7a3115f3d5c01099852112c7e05bf593789e510468edb06b6a9a11fa3b73/layer.tar",
|
||||
"74a9a50ece13c025cf10e9110d9ddc86c995079c34e2a22a28d1a3d523222c6e/layer.tar",
|
||||
"a69532b5b92bb891fbd9fa1a6b3af9087ea7050255f59ba61a796f8555ecd783/layer.tar");
|
||||
assertThat(contents.get("70bb7a3115f3d5c01099852112c7e05bf593789e510468edb06b6a9a11fa3b73/layer.tar"))
|
||||
.containsExactly("/cnb/order.toml");
|
||||
assertThat(contents.get("74a9a50ece13c025cf10e9110d9ddc86c995079c34e2a22a28d1a3d523222c6e/layer.tar"))
|
||||
.containsExactly("/cnb/stack.toml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportLayersExportsLayerTarsWithPlatformWhenOldVersionInspectImage() throws Exception {
|
||||
setVersion("1.47");
|
||||
ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base");
|
||||
URI exportUri = new URI("/v1.47/images/docker.io/paketobuildpacks/builder:base/get");
|
||||
given(DockerApiTests.this.http.get(exportUri)).willReturn(responseOf("export.tar"));
|
||||
MultiValueMap<String, String> contents = new LinkedMultiValueMap<>();
|
||||
this.api.exportLayers(reference, LINUX_ARM64_PLATFORM, (name, archive) -> {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
archive.writeTo(out);
|
||||
try (TarArchiveInputStream in = new TarArchiveInputStream(
|
||||
new ByteArrayInputStream(out.toByteArray()))) {
|
||||
TarArchiveEntry entry = in.getNextEntry();
|
||||
while (entry != null) {
|
||||
contents.add(name, entry.getName());
|
||||
entry = in.getNextEntry();
|
||||
}
|
||||
}
|
||||
});
|
||||
assertThat(contents).hasSize(3)
|
||||
.containsKeys("70bb7a3115f3d5c01099852112c7e05bf593789e510468edb06b6a9a11fa3b73/layer.tar",
|
||||
"74a9a50ece13c025cf10e9110d9ddc86c995079c34e2a22a28d1a3d523222c6e/layer.tar",
|
||||
"a69532b5b92bb891fbd9fa1a6b3af9087ea7050255f59ba61a796f8555ecd783/layer.tar");
|
||||
assertThat(contents.get("70bb7a3115f3d5c01099852112c7e05bf593789e510468edb06b6a9a11fa3b73/layer.tar"))
|
||||
.containsExactly("/cnb/order.toml");
|
||||
assertThat(contents.get("74a9a50ece13c025cf10e9110d9ddc86c995079c34e2a22a28d1a3d523222c6e/layer.tar"))
|
||||
.containsExactly("/cnb/stack.toml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportLayersWithSymlinksExportsLayerTars() throws Exception {
|
||||
ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base");
|
||||
|
|
@ -693,19 +564,7 @@ class DockerApiTests {
|
|||
|
||||
@Test
|
||||
void createWithPlatformCreatesContainer() throws Exception {
|
||||
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
|
||||
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
|
||||
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
|
||||
setVersion("1.41");
|
||||
URI createUri = new URI("/v1.41/containers/create?platform=linux%2Farm64%2Fv1");
|
||||
given(http().post(eq(createUri), eq("application/json"), any()))
|
||||
.willReturn(responseOf("create-container-response.json"));
|
||||
ContainerReference containerReference = this.api.create(config, platform);
|
||||
assertThat(containerReference).hasToString("e90e34656806");
|
||||
then(http()).should().post(any(), any(), this.writer.capture());
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
this.writer.getValue().accept(out);
|
||||
assertThat(out.toByteArray()).hasSize(config.toString().length());
|
||||
createWithPlatform("1.41");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -717,7 +576,11 @@ class DockerApiTests {
|
|||
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
|
||||
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
|
||||
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
|
||||
URI createUri = new URI(CONTAINERS_URL + "/create?platform=linux%2Farm64%2Fv1");
|
||||
if (apiVersion != null) {
|
||||
given(http().head(eq(new URI(PING_URL))))
|
||||
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, apiVersion)));
|
||||
}
|
||||
URI createUri = new URI(PLATFORM_CONTAINERS_URL + "/create?platform=linux%2Farm64%2Fv1");
|
||||
given(http().post(eq(createUri), eq("application/json"), any()))
|
||||
.willReturn(responseOf("create-container-response.json"));
|
||||
ContainerReference containerReference = this.api.create(config, platform);
|
||||
|
|
@ -733,7 +596,8 @@ class DockerApiTests {
|
|||
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
|
||||
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
|
||||
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
|
||||
setVersion("1.24");
|
||||
given(http().head(eq(new URI(PING_URL))))
|
||||
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.24")));
|
||||
assertThatIllegalStateException().isThrownBy(() -> this.api.create(config, platform))
|
||||
.withMessageContaining("must be at least 1.41")
|
||||
.withMessageContaining("current API version is 1.24");
|
||||
|
|
|
|||
|
|
@ -64,20 +64,6 @@ class ImagePlatformTests extends AbstractJsonTests {
|
|||
assertThat(platform.toString()).isEqualTo("linux/amd64/v1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toJsonString() {
|
||||
ImagePlatform platform = ImagePlatform.of("linux/amd64/v1");
|
||||
assertThat(platform.toJson()).isEqualTo("""
|
||||
{"os":"linux","architecture":"amd64","variant":"v1"}""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toJsonStringWhenOnlyOs() {
|
||||
ImagePlatform platform = ImagePlatform.of("linux");
|
||||
assertThat(platform.toJson()).isEqualTo("""
|
||||
{"os":"linux"}""");
|
||||
}
|
||||
|
||||
private Image getImage() throws IOException {
|
||||
return Image.of(getContent("type/image.json"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class DockerRegistryConfigAuthenticationTests {
|
|||
}
|
||||
""")
|
||||
@Test
|
||||
void getAuthHeaderWhenAuthForDockerDomain(@ResourcesRoot Path directory) {
|
||||
void getAuthHeaderWhenAuthForDockerDomain(@ResourcesRoot Path directory) throws Exception {
|
||||
this.environment.put("DOCKER_CONFIG", directory.toString());
|
||||
ImageReference imageReference = ImageReference.of("docker.io/ubuntu:latest");
|
||||
String authHeader = getAuthHeader(imageReference);
|
||||
|
|
@ -99,7 +99,7 @@ class DockerRegistryConfigAuthenticationTests {
|
|||
}
|
||||
""")
|
||||
@Test
|
||||
void getAuthHeaderWhenAuthForLegacyDockerDomain(@ResourcesRoot Path directory) {
|
||||
void getAuthHeaderWhenAuthForLegacyDockerDomain(@ResourcesRoot Path directory) throws Exception {
|
||||
this.environment.put("DOCKER_CONFIG", directory.toString());
|
||||
ImageReference imageReference = ImageReference.of("index.docker.io/ubuntu:latest");
|
||||
String authHeader = getAuthHeader(imageReference);
|
||||
|
|
@ -121,7 +121,7 @@ class DockerRegistryConfigAuthenticationTests {
|
|||
}
|
||||
""")
|
||||
@Test
|
||||
void getAuthHeaderWhenAuthForCustomDomain(@ResourcesRoot Path directory) {
|
||||
void getAuthHeaderWhenAuthForCustomDomain(@ResourcesRoot Path directory) throws Exception {
|
||||
this.environment.put("DOCKER_CONFIG", directory.toString());
|
||||
ImageReference imageReference = ImageReference.of("my-registry.example.com/ubuntu:latest");
|
||||
String authHeader = getAuthHeader(imageReference);
|
||||
|
|
@ -143,7 +143,7 @@ class DockerRegistryConfigAuthenticationTests {
|
|||
}
|
||||
""")
|
||||
@Test
|
||||
void getAuthHeaderWhenAuthForCustomDomainWithLegacyFormat(@ResourcesRoot Path directory) {
|
||||
void getAuthHeaderWhenAuthForCustomDomainWithLegacyFormat(@ResourcesRoot Path directory) throws Exception {
|
||||
this.environment.put("DOCKER_CONFIG", directory.toString());
|
||||
ImageReference imageReference = ImageReference.of("my-registry.example.com/ubuntu:latest");
|
||||
String authHeader = getAuthHeader(imageReference);
|
||||
|
|
@ -160,7 +160,7 @@ class DockerRegistryConfigAuthenticationTests {
|
|||
}
|
||||
""")
|
||||
@Test
|
||||
void getAuthHeaderWhenEmptyConfigDirectoryReturnsFallback(@ResourcesRoot Path directory) {
|
||||
void getAuthHeaderWhenEmptyConfigDirectoryReturnsFallback(@ResourcesRoot Path directory) throws Exception {
|
||||
this.environment.put("DOCKER_CONFIG", directory.toString());
|
||||
ImageReference imageReference = ImageReference.of("docker.io/ubuntu:latest");
|
||||
String authHeader = getAuthHeader(imageReference, DockerRegistryAuthentication.EMPTY_USER);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import java.util.Map;
|
|||
import org.jspecify.annotations.Nullable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.docker.type.Image.Descriptor;
|
||||
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
|
@ -99,35 +98,6 @@ class ImageTests extends AbstractJsonTests {
|
|||
assertThat(image.getCreated()).isEqualTo("2019-10-30T19:34:56.296666503Z");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDescriptorReturnsDescriptor() throws Exception {
|
||||
Image image = getImage();
|
||||
Descriptor descriptor = image.getDescriptor();
|
||||
assertThat(descriptor).isNotNull();
|
||||
assertThat(descriptor.getDigest())
|
||||
.isEqualTo("sha256:c0537ff6a5218ef531ece93d4984efc99bbf3f7497c0a7726c88e2bb7584dc96");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPrimaryDigestWhenHasDescriptor() throws Exception {
|
||||
Image image = getImage();
|
||||
assertThat(image.getPrimaryDigest())
|
||||
.isEqualTo("sha256:c0537ff6a5218ef531ece93d4984efc99bbf3f7497c0a7726c88e2bb7584dc96");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPrimaryDigestWhenNoDescriptor() throws Exception {
|
||||
Image image = Image.of(getContent("image-no-descriptor.json"));
|
||||
assertThat(image.getPrimaryDigest())
|
||||
.isEqualTo("sha256:21635a6b4880772f3fabbf8b660907fa38636558cf787cc26f1779fc4b4e2cba");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPrimaryDigestWhenNoDigest() throws Exception {
|
||||
Image image = Image.of(getContent("image-no-digest.json"));
|
||||
assertThat(image.getPrimaryDigest()).isNull();
|
||||
}
|
||||
|
||||
private Image getImage() throws IOException {
|
||||
return Image.of(getContent("image.json"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class FileDescriptorTests {
|
|||
|
||||
private final int sourceHandle = 123;
|
||||
|
||||
private int closedHandle;
|
||||
private int closedHandle = 0;
|
||||
|
||||
@Test
|
||||
void acquireReturnsHandle() throws Exception {
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"Id": "sha256:21635a6b4880772f3fabbf8b660907fa38636558cf787cc26f1779fc4b4e2cba",
|
||||
"RepoTags": [
|
||||
"ghcr.io/spring-io/spring-boot-cnb-test-builder:0.0.1"
|
||||
],
|
||||
"RepoDigests": [
|
||||
"ghcr.io/spring-io/spring-boot-cnb-test-builder@sha256:21635a6b4880772f3fabbf8b660907fa38636558cf787cc26f1779fc4b4e2cba"
|
||||
],
|
||||
"Parent": "",
|
||||
"Comment": "",
|
||||
"DockerVersion": "",
|
||||
"Author": "",
|
||||
"Config": null,
|
||||
"Architecture": "",
|
||||
"Os": "",
|
||||
"Size": 166797518,
|
||||
"GraphDriver": {
|
||||
"Data": null,
|
||||
"Name": "overlayfs"
|
||||
},
|
||||
"RootFS": {},
|
||||
"Metadata": {
|
||||
"LastTagTime": "2025-04-10T22:41:27.520294922Z"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"Id": "sha256:21635a6b4880772f3fabbf8b660907fa38636558cf787cc26f1779fc4b4e2cba",
|
||||
"RepoTags": [
|
||||
"ghcr.io/spring-io/spring-boot-cnb-test-builder:0.0.1"
|
||||
],
|
||||
"Parent": "",
|
||||
"Comment": "",
|
||||
"DockerVersion": "",
|
||||
"Author": "",
|
||||
"Config": null,
|
||||
"Architecture": "",
|
||||
"Os": "",
|
||||
"Size": 166797518,
|
||||
"GraphDriver": {
|
||||
"Data": null,
|
||||
"Name": "overlayfs"
|
||||
},
|
||||
"RootFS": {},
|
||||
"Metadata": {
|
||||
"LastTagTime": "2025-04-10T22:41:27.520294922Z"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,27 +1,5 @@
|
|||
{
|
||||
"Id": "sha256:9b450bffdb05bcf660d464d0bfdf344ee6ca38e9b8de4f408c8080b0c9319349",
|
||||
"Descriptor": {
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:c0537ff6a5218ef531ece93d4984efc99bbf3f7497c0a7726c88e2bb7584dc96",
|
||||
"size": 424,
|
||||
"urls": [
|
||||
"https://example.com"
|
||||
],
|
||||
"annotations": {
|
||||
"com.docker.official-images.bashbrew.arch": "amd64",
|
||||
"org.opencontainers.image.version": "24.04"
|
||||
},
|
||||
"data": null,
|
||||
"platform": {
|
||||
"architecture": "arm",
|
||||
"os": "windows",
|
||||
"os.version": "10.0.19041.1165",
|
||||
"os.features": [
|
||||
],
|
||||
"variant": "v7"
|
||||
},
|
||||
"artifactType": null
|
||||
},
|
||||
"RepoTags": [
|
||||
"paketo-buildpacks/cnb:latest"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
<module name="com.puppycrawl.tools.checkstyle.TreeWalker">
|
||||
<module name="io.spring.javaformat.checkstyle.check.SpringDeprecatedCheck" />
|
||||
<module name="io.spring.javaformat.checkstyle.check.SpringJUnit5Check" />
|
||||
<module name="ExplicitInitialization" />
|
||||
<module
|
||||
name="com.puppycrawl.tools.checkstyle.checks.imports.IllegalImportCheck">
|
||||
<property name="regexp" value="true" />
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ architectureCheck {
|
|||
|
||||
def dependenciesOf(String version) {
|
||||
if (version.startsWith("4.0")) {
|
||||
return [
|
||||
def modules = [
|
||||
"spring-boot",
|
||||
"spring-boot-activemq",
|
||||
"spring-boot-actuator",
|
||||
|
|
@ -105,8 +105,6 @@ def dependenciesOf(String version) {
|
|||
"spring-boot-micrometer-metrics-test",
|
||||
"spring-boot-micrometer-observation",
|
||||
"spring-boot-micrometer-tracing",
|
||||
"spring-boot-micrometer-tracing-brave",
|
||||
"spring-boot-micrometer-tracing-opentelemetry",
|
||||
"spring-boot-micrometer-tracing-test",
|
||||
"spring-boot-mongodb",
|
||||
"spring-boot-mustache",
|
||||
|
|
@ -150,6 +148,13 @@ def dependenciesOf(String version) {
|
|||
"spring-boot-webservices-test",
|
||||
"spring-boot-zipkin"
|
||||
]
|
||||
if (version.equals("4.0.0-RC2")) {
|
||||
modules += [
|
||||
"spring-boot-micrometer-tracing-brave",
|
||||
"spring-boot-micrometer-tracing-opentelemetry"
|
||||
]
|
||||
}
|
||||
return modules
|
||||
}
|
||||
return [
|
||||
"spring-boot",
|
||||
|
|
|
|||
|
|
@ -211,9 +211,8 @@ public class AutoConfigurationImportSelector implements DeferredImportSelector,
|
|||
|
||||
private void checkExcludedClasses(List<String> configurations, Set<String> exclusions) {
|
||||
List<String> invalidExcludes = new ArrayList<>(exclusions.size());
|
||||
ClassLoader classLoader = (this.beanClassLoader != null) ? this.beanClassLoader : getClass().getClassLoader();
|
||||
for (String exclusion : exclusions) {
|
||||
if (ClassUtils.isPresent(exclusion, classLoader) && !configurations.contains(exclusion)) {
|
||||
if (ClassUtils.isPresent(exclusion, getClass().getClassLoader()) && !configurations.contains(exclusion)) {
|
||||
invalidExcludes.add(exclusion);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -472,19 +472,13 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat
|
|||
}
|
||||
|
||||
private List<String> getPrimaryBeans(Map<String, @Nullable BeanDefinition> beanDefinitions) {
|
||||
return getMatchingBeans(beanDefinitions, this::isPrimary);
|
||||
}
|
||||
|
||||
private boolean isPrimary(@Nullable BeanDefinition beanDefinition) {
|
||||
return (beanDefinition != null) && beanDefinition.isPrimary();
|
||||
return getMatchingBeans(beanDefinitions,
|
||||
(beanDefinition) -> beanDefinition != null && beanDefinition.isPrimary());
|
||||
}
|
||||
|
||||
private List<String> getNonFallbackBeans(Map<String, @Nullable BeanDefinition> beanDefinitions) {
|
||||
return getMatchingBeans(beanDefinitions, this::isNotFallback);
|
||||
}
|
||||
|
||||
private boolean isNotFallback(@Nullable BeanDefinition beanDefinition) {
|
||||
return (beanDefinition == null) || !beanDefinition.isFallback();
|
||||
return getMatchingBeans(beanDefinitions,
|
||||
Predicate.not((beanDefinition) -> beanDefinition != null && beanDefinition.isFallback()));
|
||||
}
|
||||
|
||||
private List<String> getMatchingBeans(Map<String, @Nullable BeanDefinition> beanDefinitions,
|
||||
|
|
|
|||
|
|
@ -76,13 +76,13 @@ public class MessageSourceProperties {
|
|||
* Whether to always apply the MessageFormat rules, parsing even messages without
|
||||
* arguments.
|
||||
*/
|
||||
private boolean alwaysUseMessageFormat;
|
||||
private boolean alwaysUseMessageFormat = false;
|
||||
|
||||
/**
|
||||
* Whether to use the message code as the default message instead of throwing a
|
||||
* "NoSuchMessageException". Recommended during development only.
|
||||
*/
|
||||
private boolean useCodeAsDefaultMessage;
|
||||
private boolean useCodeAsDefaultMessage = false;
|
||||
|
||||
public List<String> getBasename() {
|
||||
return this.basename;
|
||||
|
|
|
|||
|
|
@ -33,12 +33,12 @@ public class JmxProperties {
|
|||
/**
|
||||
* Expose Spring's management beans to the JMX domain.
|
||||
*/
|
||||
private boolean enabled;
|
||||
private boolean enabled = false;
|
||||
|
||||
/**
|
||||
* Whether unique runtime object names should be ensured.
|
||||
*/
|
||||
private boolean uniqueNames;
|
||||
private boolean uniqueNames = false;
|
||||
|
||||
/**
|
||||
* MBeanServer bean name.
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import java.util.concurrent.TimeUnit;
|
|||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.convert.DurationUnit;
|
||||
import org.springframework.http.CacheControl;
|
||||
|
|
@ -51,9 +50,6 @@ public class WebProperties {
|
|||
|
||||
private final Resources resources = new Resources();
|
||||
|
||||
@NestedConfigurationProperty
|
||||
private final ErrorProperties error = new ErrorProperties();
|
||||
|
||||
public @Nullable Locale getLocale() {
|
||||
return this.locale;
|
||||
}
|
||||
|
|
@ -70,10 +66,6 @@ public class WebProperties {
|
|||
this.localeResolver = localeResolver;
|
||||
}
|
||||
|
||||
public ErrorProperties getError() {
|
||||
return this.error;
|
||||
}
|
||||
|
||||
public Resources getResources() {
|
||||
return this.resources;
|
||||
}
|
||||
|
|
@ -109,7 +101,7 @@ public class WebProperties {
|
|||
*/
|
||||
private boolean addMappings = true;
|
||||
|
||||
private boolean customized;
|
||||
private boolean customized = false;
|
||||
|
||||
private final Chain chain = new Chain();
|
||||
|
||||
|
|
@ -159,7 +151,7 @@ public class WebProperties {
|
|||
*/
|
||||
public static class Chain {
|
||||
|
||||
boolean customized;
|
||||
boolean customized = false;
|
||||
|
||||
/**
|
||||
* Whether to enable the Spring Resource Handling chain. By default, disabled
|
||||
|
|
@ -177,7 +169,7 @@ public class WebProperties {
|
|||
* brotli). Checks for a resource name with the '.gz' or '.br' file
|
||||
* extensions.
|
||||
*/
|
||||
private boolean compressed;
|
||||
private boolean compressed = false;
|
||||
|
||||
private final Strategy strategy = new Strategy();
|
||||
|
||||
|
|
@ -254,7 +246,7 @@ public class WebProperties {
|
|||
*/
|
||||
public static class Content {
|
||||
|
||||
private boolean customized;
|
||||
private boolean customized = false;
|
||||
|
||||
/**
|
||||
* Whether to enable the content Version Strategy.
|
||||
|
|
@ -295,7 +287,7 @@ public class WebProperties {
|
|||
*/
|
||||
public static class Fixed {
|
||||
|
||||
private boolean customized;
|
||||
private boolean customized = false;
|
||||
|
||||
/**
|
||||
* Whether to enable the fixed Version Strategy.
|
||||
|
|
@ -354,7 +346,7 @@ public class WebProperties {
|
|||
*/
|
||||
public static class Cache {
|
||||
|
||||
private boolean customized;
|
||||
private boolean customized = false;
|
||||
|
||||
/**
|
||||
* Cache period for the resources served by the resource handler. If a
|
||||
|
|
@ -406,7 +398,7 @@ public class WebProperties {
|
|||
*/
|
||||
public static class Cachecontrol {
|
||||
|
||||
private boolean customized;
|
||||
private boolean customized = false;
|
||||
|
||||
/**
|
||||
* Maximum time the response should be cached, in seconds if no duration
|
||||
|
|
|
|||
|
|
@ -36,8 +36,7 @@
|
|||
"description": "Resource reference to a generated git info properties file.",
|
||||
"deprecation": {
|
||||
"replacement": "spring.info.git.location",
|
||||
"level": "error",
|
||||
"since": "1.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -59,8 +58,7 @@
|
|||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.add-mappings",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -68,8 +66,7 @@
|
|||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.cache.cachecontrol.cache-private",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -77,8 +74,7 @@
|
|||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.cache.cachecontrol.cache-public",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -86,8 +82,7 @@
|
|||
"type": "java.time.Duration",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.cache.cachecontrol.max-age",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -95,8 +90,7 @@
|
|||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.cache.cachecontrol.must-revalidate",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -104,8 +98,7 @@
|
|||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.cache.cachecontrol.no-cache",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -113,8 +106,7 @@
|
|||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.cache.cachecontrol.no-store",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -122,8 +114,7 @@
|
|||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.cache.cachecontrol.no-transform",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -131,8 +122,7 @@
|
|||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.cache.cachecontrol.proxy-revalidate",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -140,8 +130,7 @@
|
|||
"type": "java.time.Duration",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.cache.cachecontrol.s-max-age",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -149,8 +138,7 @@
|
|||
"type": "java.time.Duration",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.cache.cachecontrol.stale-if-error",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -158,8 +146,7 @@
|
|||
"type": "java.time.Duration",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.cache.cachecontrol.stale-while-revalidate",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -167,8 +154,7 @@
|
|||
"type": "java.time.Duration",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.cache.period",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -176,8 +162,7 @@
|
|||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.cache.use-last-modified",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -185,8 +170,7 @@
|
|||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.chain.cache",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -194,8 +178,7 @@
|
|||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.chain.compressed",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -203,8 +186,7 @@
|
|||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.chain.enabled",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -212,16 +194,14 @@
|
|||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.chain.compressed",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.resources.chain.html-application-cache",
|
||||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -229,8 +209,7 @@
|
|||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.chain.strategy.content.enabled",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -238,8 +217,7 @@
|
|||
"type": "java.lang.String[]",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.chain.strategy.content.paths",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -247,8 +225,7 @@
|
|||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.chain.strategy.fixed.enabled",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -256,8 +233,7 @@
|
|||
"type": "java.lang.String[]",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.chain.strategy.fixed.paths",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -265,8 +241,7 @@
|
|||
"type": "java.lang.String",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.chain.strategy.fixed.version",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -274,8 +249,7 @@
|
|||
"type": "java.lang.String[]",
|
||||
"deprecation": {
|
||||
"replacement": "spring.web.resources.static-locations",
|
||||
"level": "error",
|
||||
"since": "2.4.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
defaults.spring.template.provider.cache=false
|
||||
defaults.spring.web.error.include-binding-errors=always
|
||||
defaults.spring.web.error.include-message=always
|
||||
defaults.spring.web.error.include-stacktrace=always
|
||||
defaults.spring.web.resources.cache.period=0
|
||||
defaults.spring.web.resources.chain.cache=false
|
||||
|
|
|
|||
|
|
@ -195,16 +195,6 @@ class ConditionalOnSingleCandidateTests {
|
|||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleCandidateDoesNotMatchWhenMultipleRegisteredAsSingletonCandidates() {
|
||||
this.contextRunner.withInitializer((context) -> {
|
||||
context.getBeanFactory().registerSingleton("alpha", "alpha");
|
||||
context.getBeanFactory().registerSingleton("bravo", "bravo");
|
||||
})
|
||||
.withUserConfiguration(OnBeanSingleCandidateConfiguration.class)
|
||||
.run((context) -> assertThat(context).doesNotHaveBean("consumer"));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnSingleCandidate(String.class)
|
||||
static class OnBeanSingleCandidateConfiguration {
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ class JmxAutoConfigurationTests {
|
|||
@ManagedResource
|
||||
public static class Counter {
|
||||
|
||||
private int counter;
|
||||
private int counter = 0;
|
||||
|
||||
@ManagedAttribute
|
||||
public int get() {
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ class FileWatcherTests {
|
|||
|
||||
private CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
volatile boolean changed;
|
||||
volatile boolean changed = false;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
plugins {
|
||||
id "java-library"
|
||||
id "org.springframework.boot.configuration-metadata"
|
||||
id "org.springframework.boot.deployed"
|
||||
id "org.springframework.boot.optional-dependencies"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,8 @@
|
|||
{
|
||||
"name": "spring.test.observability.auto-configure",
|
||||
"deprecation": {
|
||||
"level": "error",
|
||||
"reason": "Superseded by 'spring.test.metrics.export' and 'spring.test.tracing.export'.",
|
||||
"since": "4.0.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
class SpringBootTestWebEnvironmentMockTests {
|
||||
|
||||
@Value("${value}")
|
||||
private int value;
|
||||
private int value = 0;
|
||||
|
||||
@Autowired
|
||||
private WebApplicationContext context;
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ class ImportTestcontainersTests {
|
|||
@ImportTestcontainers
|
||||
static class NullContainer {
|
||||
|
||||
static @Nullable PostgreSQLContainer container;
|
||||
static @Nullable PostgreSQLContainer container = null;
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ class SpringApplicationShutdownHook implements Runnable {
|
|||
|
||||
private final AtomicBoolean shutdownHookAdded = new AtomicBoolean();
|
||||
|
||||
private volatile boolean shutdownHookAdditionEnabled;
|
||||
private volatile boolean shutdownHookAdditionEnabled = false;
|
||||
|
||||
private boolean inProgress;
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class SpringBootExceptionHandler implements UncaughtExceptionHandler {
|
|||
|
||||
private final List<Throwable> loggedExceptions = new ArrayList<>();
|
||||
|
||||
private int exitCode;
|
||||
private int exitCode = 0;
|
||||
|
||||
SpringBootExceptionHandler(@Nullable UncaughtExceptionHandler parent) {
|
||||
this.parent = parent;
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ public class LoggingApplicationListener implements GenericApplicationListener {
|
|||
|
||||
private boolean parseArgs = true;
|
||||
|
||||
private @Nullable LogLevel springBootLogging;
|
||||
private @Nullable LogLevel springBootLogging = null;
|
||||
|
||||
@Override
|
||||
public boolean supportsEventType(ResolvableType resolvableType) {
|
||||
|
|
|
|||
|
|
@ -373,7 +373,7 @@ class SpringIterableConfigurationPropertySource extends SpringConfigurationPrope
|
|||
|
||||
private final @Nullable ConfigurationPropertyName[] names;
|
||||
|
||||
private int index;
|
||||
private int index = 0;
|
||||
|
||||
ConfigurationPropertyNamesIterator(@Nullable ConfigurationPropertyName[] names) {
|
||||
this.names = names;
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ public final class CorrelationIdFormatter {
|
|||
* @return a formatted correlation id
|
||||
*/
|
||||
public String format(UnaryOperator<@Nullable String> resolver) {
|
||||
StringBuilder result = new StringBuilder(this.blank.length());
|
||||
StringBuilder result = new StringBuilder();
|
||||
formatTo(resolver, result);
|
||||
return result.toString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ public final class RetryPolicySettings {
|
|||
/**
|
||||
* Default number of retry attempts.
|
||||
*/
|
||||
public static final long DEFAULT_MAX_RETRIES = RetryPolicy.Builder.DEFAULT_MAX_RETRIES;
|
||||
public static final long DEFAULT_MAX_ATTEMPTS = RetryPolicy.Builder.DEFAULT_MAX_ATTEMPTS;
|
||||
|
||||
/**
|
||||
* Default initial delay.
|
||||
|
|
@ -62,7 +62,7 @@ public final class RetryPolicySettings {
|
|||
|
||||
private @Nullable Predicate<Throwable> exceptionPredicate;
|
||||
|
||||
private Long maxRetries = DEFAULT_MAX_RETRIES;
|
||||
private Long maxAttempts = DEFAULT_MAX_ATTEMPTS;
|
||||
|
||||
private Duration delay = DEFAULT_DELAY;
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ public final class RetryPolicySettings {
|
|||
map.from(this::getExceptionIncludes).to(builder::includes);
|
||||
map.from(this::getExceptionExcludes).to(builder::excludes);
|
||||
map.from(this::getExceptionPredicate).to(builder::predicate);
|
||||
map.from(this::getMaxRetries).to(builder::maxRetries);
|
||||
map.from(this::getMaxAttempts).to(builder::maxAttempts);
|
||||
map.from(this::getDelay).to(builder::delay);
|
||||
map.from(this::getJitter).to(builder::jitter);
|
||||
map.from(this::getMultiplier).to(builder::multiplier);
|
||||
|
|
@ -153,19 +153,18 @@ public final class RetryPolicySettings {
|
|||
/**
|
||||
* Return the maximum number of retry attempts.
|
||||
* @return the maximum number of retry attempts
|
||||
* @see #DEFAULT_MAX_RETRIES
|
||||
* @see #DEFAULT_MAX_ATTEMPTS
|
||||
*/
|
||||
public Long getMaxRetries() {
|
||||
return this.maxRetries;
|
||||
public Long getMaxAttempts() {
|
||||
return this.maxAttempts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the maximum number of retry attempts.
|
||||
* @param maxRetries the maximum number of retry attempts (must be equal or greater
|
||||
* than zero)
|
||||
* @param maxAttempts the max attempts (must be equal or greater than zero)
|
||||
*/
|
||||
public void setMaxRetries(Long maxRetries) {
|
||||
this.maxRetries = maxRetries;
|
||||
public void setMaxAttempts(Long maxAttempts) {
|
||||
this.maxAttempts = maxAttempts;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -53,15 +53,14 @@ class SpringFactoriesEnvironmentPostProcessorsFactory implements EnvironmentPost
|
|||
argumentResolver = argumentResolver.and(BootstrapRegistry.class, bootstrapContext);
|
||||
List<Object> postProcessors = new ArrayList<>();
|
||||
postProcessors.addAll(this.loader.load(EnvironmentPostProcessor.class, argumentResolver));
|
||||
postProcessors.addAll(loadDeprecatedPostProcessors(argumentResolver));
|
||||
postProcessors.addAll(loadDeprecatedPostProcessors());
|
||||
AnnotationAwareOrderComparator.sort(postProcessors);
|
||||
return postProcessors.stream().map(Adapter::apply).collect(Collectors.toCollection(ArrayList::new));
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
private List<org.springframework.boot.env.EnvironmentPostProcessor> loadDeprecatedPostProcessors(
|
||||
ArgumentResolver argumentResolver) {
|
||||
return this.loader.load(org.springframework.boot.env.EnvironmentPostProcessor.class, argumentResolver);
|
||||
private List<org.springframework.boot.env.EnvironmentPostProcessor> loadDeprecatedPostProcessors() {
|
||||
return this.loader.load(org.springframework.boot.env.EnvironmentPostProcessor.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ import org.springframework.util.StringUtils;
|
|||
* {@link SystemEnvironmentOrigin} for every system environment property.
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class SystemEnvironmentPropertySourceEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
|
||||
|
|
@ -54,14 +53,10 @@ public class SystemEnvironmentPropertySourceEnvironmentPostProcessor implements
|
|||
|
||||
@Override
|
||||
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
|
||||
postProcessEnvironment(environment, application.getEnvironmentPrefix());
|
||||
}
|
||||
|
||||
private void postProcessEnvironment(ConfigurableEnvironment environment, @Nullable String environmentPrefix) {
|
||||
String sourceName = StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME;
|
||||
PropertySource<?> propertySource = environment.getPropertySources().get(sourceName);
|
||||
if (propertySource != null) {
|
||||
replacePropertySource(environment, sourceName, propertySource, environmentPrefix);
|
||||
replacePropertySource(environment, sourceName, propertySource, application.getEnvironmentPrefix());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,23 +78,6 @@ public class SystemEnvironmentPropertySourceEnvironmentPostProcessor implements
|
|||
this.order = order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-process the given {@link ConfigurableEnvironment} by copying appropriate
|
||||
* settings from a parent {@link ConfigurableEnvironment}.
|
||||
* @param environment the environment to post-process
|
||||
* @param parentEnvironment the parent environment
|
||||
* @since 3.4.12
|
||||
*/
|
||||
public static void postProcessEnvironment(ConfigurableEnvironment environment,
|
||||
ConfigurableEnvironment parentEnvironment) {
|
||||
PropertySource<?> parentSystemEnvironmentPropertySource = parentEnvironment.getPropertySources()
|
||||
.get(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME);
|
||||
if (parentSystemEnvironmentPropertySource instanceof OriginAwareSystemEnvironmentPropertySource parentOriginAwareSystemEnvironmentPropertySource) {
|
||||
new SystemEnvironmentPropertySourceEnvironmentPostProcessor().postProcessEnvironment(environment,
|
||||
parentOriginAwareSystemEnvironmentPropertySource.getPrefix());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link SystemEnvironmentPropertySource} that also tracks {@link Origin}.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -48,8 +48,7 @@
|
|||
"description": "Log file name (for instance, `myapp.log`). Names can be an exact location or relative to the current directory.",
|
||||
"deprecation": {
|
||||
"replacement": "logging.file.name",
|
||||
"level": "error",
|
||||
"since": "2.2.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -59,8 +58,7 @@
|
|||
"sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener",
|
||||
"defaultValue": false,
|
||||
"deprecation": {
|
||||
"replacement": "logging.logback.rollingpolicy.clean-history-on-start",
|
||||
"since": "2.4.0"
|
||||
"replacement": "logging.logback.rollingpolicy.clean-history-on-start"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -70,8 +68,7 @@
|
|||
"sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener",
|
||||
"defaultValue": 7,
|
||||
"deprecation": {
|
||||
"replacement": "logging.logback.rollingpolicy.max-history",
|
||||
"since": "2.4.0"
|
||||
"replacement": "logging.logback.rollingpolicy.max-history"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -81,8 +78,7 @@
|
|||
"sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener",
|
||||
"defaultValue": "10MB",
|
||||
"deprecation": {
|
||||
"replacement": "logging.logback.rollingpolicy.max-file-size",
|
||||
"since": "2.4.0"
|
||||
"replacement": "logging.logback.rollingpolicy.max-file-size"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -104,8 +100,7 @@
|
|||
"sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener",
|
||||
"defaultValue": "0B",
|
||||
"deprecation": {
|
||||
"replacement": "logging.logback.rollingpolicy.total-size-cap",
|
||||
"since": "2.4.0"
|
||||
"replacement": "logging.logback.rollingpolicy.total-size-cap"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -180,8 +175,7 @@
|
|||
"description": "Location of the log file. For instance, `/var/log`.",
|
||||
"deprecation": {
|
||||
"replacement": "logging.file.path",
|
||||
"level": "error",
|
||||
"since": "2.2.0"
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -223,8 +217,7 @@
|
|||
"sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener",
|
||||
"defaultValue": "${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz",
|
||||
"deprecation": {
|
||||
"replacement": "logging.logback.rollingpolicy.file-name-pattern",
|
||||
"since": "2.4.0"
|
||||
"replacement": "logging.logback.rollingpolicy.file-name-pattern"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -362,8 +355,7 @@
|
|||
"description": "Application index.",
|
||||
"deprecation": {
|
||||
"level": "error",
|
||||
"reason": "Application context ids are now unique by default.",
|
||||
"since": "2.0.0"
|
||||
"reason": "Application context ids are now unique by default."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -388,8 +380,7 @@
|
|||
"type": "java.lang.Integer",
|
||||
"deprecation": {
|
||||
"level": "error",
|
||||
"reason": "Support for image banners has been removed.",
|
||||
"since": "3.0.0"
|
||||
"reason": "Support for image banners has been removed."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -397,8 +388,7 @@
|
|||
"type": "java.lang.Integer",
|
||||
"deprecation": {
|
||||
"level": "error",
|
||||
"reason": "Support for image banners has been removed.",
|
||||
"since": "3.0.0"
|
||||
"reason": "Support for image banners has been removed."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -406,8 +396,7 @@
|
|||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"level": "error",
|
||||
"reason": "Support for image banners has been removed.",
|
||||
"since": "3.0.0"
|
||||
"reason": "Support for image banners has been removed."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -415,8 +404,7 @@
|
|||
"type": "org.springframework.core.io.Resource",
|
||||
"deprecation": {
|
||||
"level": "error",
|
||||
"reason": "Support for image banners has been removed.",
|
||||
"since": "3.0.0"
|
||||
"reason": "Support for image banners has been removed."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -424,8 +412,7 @@
|
|||
"type": "java.lang.Integer",
|
||||
"deprecation": {
|
||||
"level": "error",
|
||||
"reason": "Support for image banners has been removed.",
|
||||
"since": "3.0.0"
|
||||
"reason": "Support for image banners has been removed."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -433,8 +420,7 @@
|
|||
"type": "org.springframework.boot.ImageBanner$PixelMode",
|
||||
"deprecation": {
|
||||
"level": "error",
|
||||
"reason": "Support for image banners has been removed.",
|
||||
"since": "3.0.0"
|
||||
"reason": "Support for image banners has been removed."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -442,8 +428,7 @@
|
|||
"type": "java.lang.Integer",
|
||||
"deprecation": {
|
||||
"level": "error",
|
||||
"reason": "Support for image banners has been removed.",
|
||||
"since": "3.0.0"
|
||||
"reason": "Support for image banners has been removed."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -562,8 +547,7 @@
|
|||
"description": "Display the banner when the application runs.",
|
||||
"defaultValue": true,
|
||||
"deprecation": {
|
||||
"replacement": "spring.main.banner-mode",
|
||||
"since": "1.3.0"
|
||||
"replacement": "spring.main.banner-mode"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -584,8 +568,7 @@
|
|||
"sourceType": "org.springframework.boot.SpringApplication",
|
||||
"description": "Run the application in a web environment (auto-detected by default).",
|
||||
"deprecation": {
|
||||
"replacement": "spring.main.web-application-type",
|
||||
"since": "2.0.0"
|
||||
"replacement": "spring.main.web-application-type"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import org.jspecify.annotations.Nullable;
|
|||
|
||||
class TestLog4J2LoggingSystem extends Log4J2LoggingSystem {
|
||||
|
||||
private boolean disableSelfInitialization;
|
||||
private boolean disableSelfInitialization = false;
|
||||
|
||||
TestLog4J2LoggingSystem(String contextName) {
|
||||
// Tests add resources to the thread context classloader
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ class RetryPolicySettingsTests {
|
|||
@Test
|
||||
void createRetryPolicyWithCustomAttributes() {
|
||||
RetryPolicySettings settings = new RetryPolicySettings();
|
||||
settings.setMaxRetries(10L);
|
||||
settings.setMaxAttempts(10L);
|
||||
settings.setDelay(Duration.ofSeconds(2));
|
||||
settings.setJitter(Duration.ofMillis(500));
|
||||
settings.setMultiplier(2.0);
|
||||
|
|
|
|||
|
|
@ -111,19 +111,4 @@ class SystemEnvironmentPropertySourceEnvironmentPostProcessorTests {
|
|||
assertThat(replaced.getPrefix()).isEqualTo("my");
|
||||
}
|
||||
|
||||
@Test
|
||||
void postProcessWithParentEnvironmentShouldApplyPrefix() {
|
||||
SpringApplication application = new SpringApplication();
|
||||
application.setEnvironmentPrefix("my");
|
||||
new SystemEnvironmentPropertySourceEnvironmentPostProcessor().postProcessEnvironment(this.environment,
|
||||
application);
|
||||
StandardEnvironment child = new StandardEnvironment();
|
||||
SystemEnvironmentPropertySourceEnvironmentPostProcessor.postProcessEnvironment(child, this.environment);
|
||||
OriginAwareSystemEnvironmentPropertySource replaced = (OriginAwareSystemEnvironmentPropertySource) child
|
||||
.getPropertySources()
|
||||
.get("systemEnvironment");
|
||||
assertThat(replaced).isNotNull();
|
||||
assertThat(replaced.getPrefix()).isEqualTo("my");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,9 +150,6 @@ dependencies {
|
|||
implementation("io.micrometer:micrometer-tracing")
|
||||
implementation("io.micrometer:micrometer-registry-graphite")
|
||||
implementation("io.micrometer:micrometer-registry-jmx")
|
||||
implementation("io.opentelemetry:opentelemetry-exporter-otlp")
|
||||
implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
|
||||
implementation("io.opentelemetry:opentelemetry-sdk-metrics")
|
||||
implementation("io.opentelemetry.instrumentation:opentelemetry-logback-appender-1.0")
|
||||
implementation("io.projectreactor.netty:reactor-netty-http")
|
||||
implementation("jakarta.annotation:jakarta.annotation-api")
|
||||
|
|
@ -370,12 +367,6 @@ def configurationPropertiesMetadataAggregate = aggregates.create("configurationP
|
|||
usage = "configuration-properties-metadata"
|
||||
}
|
||||
|
||||
def checkAggregatedSpringConfigurationMetadata = tasks.register("checkAggregatedSpringConfigurationMetadata", org.springframework.boot.build.context.properties.CheckAggregatedSpringConfigurationMetadata) {
|
||||
configurationPropertyMetadata = configurationPropertiesMetadataAggregate.files
|
||||
reportLocation = layout.buildDirectory.file("checkAggregatedSpringConfigurationMetadata/report.txt")
|
||||
}
|
||||
tasks.named("check") { dependsOn checkAggregatedSpringConfigurationMetadata }
|
||||
|
||||
tasks.register("documentConfigurationProperties", org.springframework.boot.build.context.properties.DocumentConfigurationProperties) {
|
||||
configurationPropertyMetadata = configurationPropertiesMetadataAggregate.files
|
||||
deprecated = false
|
||||
|
|
|
|||
|
|
@ -479,6 +479,8 @@
|
|||
* xref:how-to:deployment/cloud.adoc#howto.deployment.cloud.aws.beanstalk.tomcat-platform[#deployment.cloud.aws.beanstalk.tomcat-platform]
|
||||
* xref:how-to:deployment/cloud.adoc#howto.deployment.cloud.aws.beanstalk[#cloud-deployment-aws-beanstalk]
|
||||
* xref:how-to:deployment/cloud.adoc#howto.deployment.cloud.aws.beanstalk[#deployment.cloud.aws.beanstalk]
|
||||
* xref:how-to:deployment/cloud.adoc#howto.deployment.cloud.aws.summary[#cloud-deployment-aws-summary]
|
||||
* xref:how-to:deployment/cloud.adoc#howto.deployment.cloud.aws.summary[#deployment.cloud.aws.summary]
|
||||
* xref:how-to:deployment/cloud.adoc#howto.deployment.cloud.aws[#cloud-deployment-aws]
|
||||
* xref:how-to:deployment/cloud.adoc#howto.deployment.cloud.aws[#deployment.cloud.aws]
|
||||
* xref:how-to:deployment/cloud.adoc#howto.deployment.cloud.azure[#deployment.cloud.azure]
|
||||
|
|
|
|||
|
|
@ -207,20 +207,41 @@ This file will not be packaged in your uber jar or your container.
|
|||
[[howto.data-initialization.migration-tool.liquibase-tests]]
|
||||
=== Use Liquibase for Test-only Migrations
|
||||
|
||||
If you want to create Liquibase migrations which populate your test database, you can leverage https://docs.liquibase.com/reference-guide/changelog-attributes/what-are-contexts[Liquibase contexts].
|
||||
See also the related https://www.liquibase.com/blog/contexts-vs-labels[blog post].
|
||||
If you want to create Liquibase migrations which populate your test database, you have to create a test changelog which also includes the production changelog.
|
||||
|
||||
In practical terms, this translates into adding a `context:@test` attribute to changesets containing test data, for example:
|
||||
First, you need to configure Liquibase to use a different changelog when running the tests.
|
||||
One way to do this is to create a Spring Boot `test` profile and put the Liquibase properties in there.
|
||||
For that, create a file named `src/test/resources/application-test.properties` and put the following property in there:
|
||||
|
||||
[source,sql]
|
||||
[configprops,yaml]
|
||||
----
|
||||
--liquibase formatted sql
|
||||
|
||||
--changeset alice:1 context:@test
|
||||
insert into project (id, name) values (1, 'Spring Boot');
|
||||
spring:
|
||||
liquibase:
|
||||
change-log: "classpath:/db/changelog/db.changelog-test.yaml"
|
||||
----
|
||||
|
||||
And using `spring.liquibase.contexts=test` in environments where you would like changesets containing test data to be applied.
|
||||
This configures Liquibase to use a different changelog when running in the `test` profile.
|
||||
|
||||
Now create the changelog file at `src/test/resources/db/changelog/db.changelog-test.yaml`:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
databaseChangeLog:
|
||||
- include:
|
||||
file: classpath:/db/changelog/db.changelog-master.yaml
|
||||
- changeSet:
|
||||
runOrder: "last"
|
||||
id: "test"
|
||||
changes:
|
||||
# Insert your changes here
|
||||
----
|
||||
|
||||
This changelog will be used when the tests are run and it will not be packaged in your uber jar or your container.
|
||||
It includes the production changelog and then declares a new changeset, whose `runOrder: last` setting specifies that it runs after all the production changesets have been run.
|
||||
You can now use for example the https://docs.liquibase.com/change-types/insert.html[insert changeset] to insert data or the https://docs.liquibase.com/change-types/sql.html[sql changeset] to execute SQL directly.
|
||||
|
||||
The last thing to do is to configure Spring Boot to activate the `test` profile when running tests.
|
||||
To do this, you can add the `@ActiveProfiles("test")` annotation to your javadoc:org.springframework.boot.test.context.SpringBootTest[format=annotation] annotated test classes.
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -252,25 +252,17 @@ https://www.openshift.com/[OpenShift] has many resources describing how to deplo
|
|||
[[howto.deployment.cloud.aws]]
|
||||
== Amazon Web Services (AWS)
|
||||
|
||||
Amazon Web Services provides several options that are suitable for running Spring Boot-based applications, either as containers, traditional web applications (war), or self-contained executable jar files.
|
||||
Popular options are:
|
||||
Amazon Web Services offers multiple ways to install Spring Boot-based applications, either as traditional web applications (war) or as executable jar files with an embedded web server.
|
||||
The options include:
|
||||
|
||||
* Amazon Elastic Container Service (ECS)
|
||||
* AWS Elastic Beanstalk
|
||||
* AWS Code Deploy
|
||||
* AWS OPS Works
|
||||
* AWS Cloud Formation
|
||||
* AWS Container Registry
|
||||
|
||||
|
||||
|
||||
[[howto.deployment.cloud.aws.ecs]]
|
||||
=== Amazon Elastic Container Service (ECS)
|
||||
|
||||
Official https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html[Amazon ECS developer guide] provides comprehensive overview of platform's features and includes https://docs.aws.amazon.com/AmazonECS/latest/developerguide/getting-started.html[getting started guide] that walks you through the steps needed to get your containers up and running.
|
||||
|
||||
NOTE: Spring Boot applications can be packaged in Docker containers using techniques described in xref:reference:packaging/container-images/index.adoc[].
|
||||
|
||||
In addition to the developer guide, AWS also provides a https://docs.aws.amazon.com/AmazonECS/latest/developerguide/create-container-image.html[topical guide] for deploying containerized Java services on Amazon ECS using AWS Fargate.
|
||||
|
||||
TIP: Spring Boot auto-detects AWS ECS deployment environments by checking the environment for the `AWS_EXECUTION_ENV` variable.
|
||||
You can override this detection with the configprop:spring.main.cloud-platform[] configuration property.
|
||||
Each has different features and pricing models.
|
||||
In this document, we describe to approach using AWS Elastic Beanstalk.
|
||||
|
||||
|
||||
|
||||
|
|
@ -286,9 +278,8 @@ You can either use the "`Tomcat Platform`" or the "`Java SE platform`".
|
|||
==== Using the Tomcat Platform
|
||||
|
||||
This option applies to Spring Boot projects that produce a war file.
|
||||
Follow the official guide and https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/tomcat-quickstart.html[Java on Tomcat tutorial].
|
||||
|
||||
NOTE: Creating deployable war for a Spring Boot application is described in xref:deployment/traditional-deployment.adoc[].
|
||||
No special configuration is required.
|
||||
You need only follow the official guide.
|
||||
|
||||
|
||||
|
||||
|
|
@ -296,8 +287,51 @@ NOTE: Creating deployable war for a Spring Boot application is described in xref
|
|||
==== Using the Java SE Platform
|
||||
|
||||
This option applies to Spring Boot projects that produce a jar file and run an embedded web container.
|
||||
Follow the official guide and https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/java-quickstart.html[Java tutorial].
|
||||
There are also community provided tutorials such as https://www.baeldung.com/spring-boot-deploy-aws-beanstalk[this one].
|
||||
Elastic Beanstalk environments run an nginx instance on port 80 to proxy the actual application, running on port 5000.
|
||||
To configure it, add the following line to your `application.properties` file:
|
||||
|
||||
[configprops,yaml]
|
||||
----
|
||||
server:
|
||||
port: 5000
|
||||
----
|
||||
|
||||
|
||||
[TIP]
|
||||
.Upload binaries instead of sources
|
||||
====
|
||||
By default, Elastic Beanstalk uploads sources and compiles them in AWS.
|
||||
However, it is best to upload the binaries instead.
|
||||
To do so, add lines similar to the following to your `.elasticbeanstalk/config.yml` file:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
deploy:
|
||||
artifact: target/demo-0.0.1-SNAPSHOT.jar
|
||||
----
|
||||
====
|
||||
|
||||
[TIP]
|
||||
.Reduce costs by setting the environment type
|
||||
====
|
||||
By default an Elastic Beanstalk environment is load balanced.
|
||||
The load balancer has a significant cost.
|
||||
To avoid that cost, set the environment type to "`Single instance`", as described in https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/environments-create-wizard.html#environments-create-wizard-capacity[the Amazon documentation].
|
||||
You can also create single instance environments by using the CLI and the following command:
|
||||
|
||||
[source]
|
||||
----
|
||||
eb create -s
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
|
||||
[[howto.deployment.cloud.aws.summary]]
|
||||
=== Summary
|
||||
|
||||
This is one of the easiest ways to get to AWS, but there are more things to cover, such as how to integrate Elastic Beanstalk into any CI / CD tool, use the Elastic Beanstalk Maven plugin instead of the CLI, and others.
|
||||
There is a https://exampledriven.wordpress.com/2017/01/09/spring-boot-aws-elastic-beanstalk-example/[blog post] covering these topics more in detail.
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ apply plugin: 'war'
|
|||
----
|
||||
|
||||
The final step in the process is to ensure that the embedded servlet container does not interfere with the servlet container to which the war file is deployed.
|
||||
To do so, you need to mark the embedded servlet runtime dependency as being provided.
|
||||
|
||||
For Maven, you need to mark the embedded servlet container dependency as being `provided`.
|
||||
For example:
|
||||
If you use Maven, the following example marks the servlet runtime (Tomcat, in this case) as being provided:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
|
|
@ -43,21 +43,20 @@ For example:
|
|||
<!-- ... -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
<artifactId>spring-boot-tomcat-runtime</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- ... -->
|
||||
</dependencies>
|
||||
----
|
||||
|
||||
If you use Gradle, you need to move only the runtime dependencies into the `providedRuntime` configuration.
|
||||
For example:
|
||||
If you use Gradle, the following example marks the servlet runtime (Tomcat, in this case) as being provided:
|
||||
|
||||
[source,gradle]
|
||||
----
|
||||
dependencies {
|
||||
// ...
|
||||
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat-runtime'
|
||||
providedRuntime 'org.springframework.boot:spring-boot-tomcat-runtime'
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
|
|
|||
|
|
@ -37,8 +37,6 @@ NOTE: The CNB builder used for the images is `paketobuildpacks/builder-noble-jav
|
|||
It has a small footprint and reduced attack surface. It does not include a shell and contains a reduced set of system libraries.
|
||||
If you need more tools in the resulting image, you can use `paketobuildpacks/ubuntu-noble-run:latest` as the *run* image.
|
||||
|
||||
NOTE: You have to build your application with at least JDK 25, because Buildpacks use the same GraalVM native-image version as the Java version used for compilation.
|
||||
|
||||
|
||||
|
||||
[[howto.native-image.developing-your-first-application.buildpacks.system-requirements]]
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ For more detail, see the following sections:
|
|||
|
||||
Spring Boot installs a '`whitelabel`' error page that you see in a browser client if you encounter a server error (machine clients consuming JSON and other media types should see a sensible response with the right error code).
|
||||
|
||||
NOTE: Set configprop:spring.web.error.whitelabel.enabled[] to `false` to switch the default error page off.
|
||||
NOTE: Set `server.error.whitelabel.enabled=false` to switch the default error page off.
|
||||
Doing so restores the default of the servlet container that you are using.
|
||||
Note that Spring Boot still tries to resolve the error view, so you should probably add your own error page rather than disabling it completely.
|
||||
|
||||
|
|
|
|||
|
|
@ -18,18 +18,13 @@ Many Spring Boot starters include default embedded containers.
|
|||
When switching to a different HTTP server, you need to swap the default dependencies for those that you need instead.
|
||||
To help with this process, Spring Boot provides a separate starter for each of the supported HTTP servers.
|
||||
|
||||
The following example shows how to exclude Tomcat and include Jetty for Spring MVC:
|
||||
The following Maven example shows how to exclude Tomcat and include Jetty for Spring MVC:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
|
||||
Maven::
|
||||
+
|
||||
[source,xml]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<!-- Exclude the Tomcat dependency -->
|
||||
<exclusion>
|
||||
|
|
@ -44,70 +39,23 @@ Maven::
|
|||
<artifactId>spring-boot-starter-jetty</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
+
|
||||
|
||||
Gradle::
|
||||
+
|
||||
The following Gradle example configures the necessary dependencies and a {url-gradle-docs}/resolution_rules.html#sec:module_replacement[module replacement] to use Tomcat in place of Reactor Netty for Spring WebFlux:
|
||||
|
||||
[source,gradle]
|
||||
----
|
||||
dependencies {
|
||||
implementation('org.springframework.boot:spring-boot-starter-webmvc') {
|
||||
// Exclude the Tomcat dependency
|
||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
|
||||
implementation "org.springframework.boot:spring-boot-starter-tomcat"
|
||||
implementation "org.springframework.boot:spring-boot-starter-webflux"
|
||||
modules {
|
||||
module("org.springframework.boot:spring-boot-starter-reactor-netty") {
|
||||
replacedBy("org.springframework.boot:spring-boot-starter-tomcat", "Use Tomcat instead of Reactor Netty")
|
||||
}
|
||||
}
|
||||
// Use Jetty instead
|
||||
implementation "org.springframework.boot:spring-boot-starter-jetty"
|
||||
}
|
||||
----
|
||||
+
|
||||
======
|
||||
|
||||
If you are creating a war file, you can use a similar approach, but you must indicate provided dependencies:
|
||||
|
||||
|
||||
[tabs]
|
||||
======
|
||||
|
||||
Maven::
|
||||
+
|
||||
[source,xml]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||
<exclusions>
|
||||
<!-- Exclude the Tomcat dependency -->
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- Use Jetty instead -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jetty</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
----
|
||||
+
|
||||
|
||||
Gradle::
|
||||
+
|
||||
[source,gradle]
|
||||
----
|
||||
dependencies {
|
||||
implementation('org.springframework.boot:spring-boot-starter-webmvc') {
|
||||
// Exclude the Tomcat dependency
|
||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
|
||||
}
|
||||
// Use Jetty instead
|
||||
implementation "org.springframework.boot:spring-boot-starter-jetty"
|
||||
providedRuntime "org.springframework.boot:spring-boot-starter-jetty-runtime"
|
||||
}
|
||||
----
|
||||
+
|
||||
======
|
||||
NOTE: `spring-boot-starter-reactor-netty` is required to use the javadoc:org.springframework.web.reactive.function.client.WebClient[] class, so you may need to keep a dependency on Netty even when you need to include a different HTTP server.
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -383,17 +383,6 @@ management:
|
|||
TIP: See javadoc:org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties[] for a complete list of options.
|
||||
|
||||
|
||||
[[actuator.endpoints.json]]
|
||||
== JSON
|
||||
|
||||
When working with JSON, Jackson is used for serialization and deserialization.
|
||||
By default, an isolated javadoc:tools.jackson.databind.json.JsonMapper[] is used.
|
||||
This isolation means that it does not share the same configuration as the application's `JsonMapper` and it is not affected by `spring.jackson.*` properties.
|
||||
To disable this behavior and configure Actuator to use the application's `JsonMapper`, set configprop:management.endpoints.jackson.isolated-json-mapper[] to `false`.
|
||||
Alternatively, you can define your own javadoc:org.springframework.boot.actuate.endpoint.jackson.EndpointJsonMapper[] bean that produces a `JsonMapper` that meets your needs.
|
||||
Actuator will then use it for JSON processing.
|
||||
|
||||
|
||||
|
||||
[[actuator.endpoints.implementing-custom]]
|
||||
== Implementing Custom Endpoints
|
||||
|
|
@ -685,8 +674,8 @@ with the `key` listed in the following table:
|
|||
TIP: You can disable them all by setting the configprop:management.health.defaults.enabled[] property.
|
||||
|
||||
TIP: The `ssl` javadoc:org.springframework.boot.actuate.health.HealthIndicator[] has a "warning threshold" property named configprop:management.health.ssl.certificate-validity-warning-threshold[].
|
||||
You can use this threshold to give yourself enough lead time to rotate the soon-to-be-expired certificate.
|
||||
If an SSL certificate will become invalid within the period defined by this threshold, the javadoc:org.springframework.boot.actuate.health.HealthIndicator[] will report this in the details section of its response where `details.validChains.certificates.[*].validity.status` will have the value `WILL_EXPIRE_SOON`.
|
||||
If an SSL certificate will be invalid within the time span defined by this threshold, the javadoc:org.springframework.boot.actuate.health.HealthIndicator[] will warn you but it will still return HTTP 200 to not disrupt the application.
|
||||
You can use this threshold to give yourself enough lead time to rotate the soon to be expired certificate.
|
||||
|
||||
Additional javadoc:org.springframework.boot.actuate.health.HealthIndicator[] beans are enabled by default:
|
||||
|
||||
|
|
@ -1271,7 +1260,7 @@ The `info` endpoint publishes information about your process, see javadoc:org.sp
|
|||
[[actuator.endpoints.info.ssl-information]]
|
||||
=== SSL Information
|
||||
|
||||
The `info` endpoint publishes information about your SSL certificates (that are configured through xref:features/ssl.adoc#features.ssl.bundles[SSL Bundles]), see javadoc:org.springframework.boot.info.SslInfo[] for more details.
|
||||
The `info` endpoint publishes information about your SSL certificates (that are configured through xref:features/ssl.adoc#features.ssl.bundles[SSL Bundles]), see javadoc:org.springframework.boot.info.SslInfo[] for more details. This endpoint reuses the "warning threshold" property of javadoc:org.springframework.boot.health.info.SslHealthIndicator[]: if an SSL certificate will be invalid within the time span defined by this threshold, it will trigger a warning. See the `management.health.ssl.certificate-validity-warning-threshold` property.
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -29,11 +29,6 @@ Observability support relies on the https://github.com/micrometer-metrics/contex
|
|||
By default, javadoc:java.lang.ThreadLocal[] values are not automatically reinstated in reactive operators.
|
||||
This behavior is controlled with the configprop:spring.reactor.context-propagation[] property, which can be set to `auto` to enable automatic propagation.
|
||||
|
||||
If you're working with javadoc:org.springframework.scheduling.annotation.Async[format=annotation] methods or use an javadoc:org.springframework.core.task.AsyncTaskExecutor[], you have to register the javadoc:org.springframework.core.task.support.ContextPropagatingTaskDecorator[] on the executor, otherwise the observability context is lost when switching threads.
|
||||
This can be done using this configuration:
|
||||
|
||||
include-code::ContextPropagationConfiguration[]
|
||||
|
||||
For more details about observations please see the {url-micrometer-docs}/observation[Micrometer Observation documentation].
|
||||
|
||||
|
||||
|
|
@ -87,18 +82,6 @@ The preceding example will prevent all observations whose name contains "denied"
|
|||
|
||||
|
||||
|
||||
[[actuator.observability.annotations]]
|
||||
== Micrometer Observation Annotations support
|
||||
|
||||
To enable scanning of observability annotations like javadoc:io.micrometer.observation.annotation.Observed[format=annotation], javadoc:io.micrometer.core.annotation.Timed[format=annotation], javadoc:io.micrometer.core.annotation.Counted[format=annotation], javadoc:io.micrometer.core.aop.MeterTag[format=annotation] and javadoc:io.micrometer.tracing.annotation.NewSpan[format=annotation], you need to set the configprop:management.observations.annotations.enabled[] property to `true`.
|
||||
This feature is supported by Micrometer directly.
|
||||
Please refer to the {url-micrometer-docs-concepts}/timers.html#_the_timed_annotation[Micrometer], {url-micrometer-docs-observation}/components.html#micrometer-observation-annotations[Micrometer Observation] and {url-micrometer-tracing-docs}/api.html#_aspect_oriented_programming[Micrometer Tracing] reference docs.
|
||||
|
||||
NOTE: When you annotate methods or classes which are already instrumented (for example, xref:reference:actuator/metrics.adoc#actuator.metrics.supported.spring-data-repository[Spring Data repositories] or xref:reference:actuator/metrics.adoc#actuator.metrics.supported.spring-mvc[Spring MVC controllers]), you will get duplicate observations.
|
||||
In that case you can either disable the automatic instrumentation using xref:reference:actuator/observability.adoc#actuator.observability.preventing-observations[properties] or an javadoc:io.micrometer.observation.ObservationPredicate[] and rely on your annotations, or you can remove your annotations.
|
||||
|
||||
|
||||
|
||||
[[actuator.observability.opentelemetry]]
|
||||
== OpenTelemetry Support
|
||||
|
||||
|
|
@ -118,82 +101,24 @@ Auto-configured attributes will be merged with attributes from the `OTEL_RESOURC
|
|||
|
||||
If you have defined your own javadoc:io.opentelemetry.sdk.resources.Resource[] bean, this will no longer be the case.
|
||||
|
||||
NOTE: Spring Boot does not provide automatic exporting of OpenTelemetry metrics or logs.
|
||||
Exporting OpenTelemetry traces is only auto-configured when used together with xref:actuator/tracing.adoc[Micrometer Tracing].
|
||||
|
||||
|
||||
|
||||
[[actuator.observability.opentelemetry.environment-variables]]
|
||||
=== Environment variables
|
||||
|
||||
Spring Boot supports the following environment variables to configure the OpenTelemetry resource:
|
||||
|
||||
* https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration[`OTEL_RESOURCE_ATTRIBUTES`]
|
||||
* https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration[`OTEL_SERVICE_NAME`]
|
||||
NOTE: Spring Boot does not provide auto-configuration for OpenTelemetry metrics or logging.
|
||||
OpenTelemetry tracing is only auto-configured when used together with xref:actuator/tracing.adoc[Micrometer Tracing].
|
||||
|
||||
NOTE: The `OTEL_RESOURCE_ATTRIBUTES` environment variable consists of a list of key-value pairs.
|
||||
For example: `key1=value1,key2=value2,key3=spring%20boot`.
|
||||
All attribute values are treated as strings, and any characters outside the baggage-octet range must be **percent-encoded**.
|
||||
|
||||
Micrometer also supports the following environment variables to configure the metrics export over OTLP:
|
||||
|
||||
* https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_endpoint[`OTEL_EXPORTER_OTLP_ENDPOINT`]
|
||||
* https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_metrics_endpoint[`OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`]
|
||||
* https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_headers[`OTEL_EXPORTER_OTLP_HEADERS`]
|
||||
* https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_metrics_headers[`OTEL_EXPORTER_OTLP_METRICS_HEADERS`]
|
||||
|
||||
Other environment variables as described in https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/[the OpenTelemetry documentation] are not supported.
|
||||
|
||||
If you want all environment variables specified by OpenTelemetry's SDK to be effective, you have to supply your own `OpenTelemetry` bean.
|
||||
|
||||
WARNING: Doing this will switch off Spring Boot's OpenTelemetry auto-configuration and may break the built-in observability functionality.
|
||||
|
||||
First, add a dependency to `io.opentelemetry:opentelemetry-sdk-extension-autoconfigure` to get https://opentelemetry.io/docs/languages/java/configuration/#zero-code-sdk-autoconfigure[OpenTelemetry's zero-code SDK autoconfigure module], then add this configuration:
|
||||
|
||||
include-code::AutoConfiguredOpenTelemetrySdkConfiguration[]
|
||||
The next sections will provide more details about logging, metrics and traces.
|
||||
|
||||
|
||||
|
||||
[[actuator.observability.opentelemetry.logging]]
|
||||
=== Logging
|
||||
[[actuator.observability.annotations]]
|
||||
== Micrometer Observation Annotations support
|
||||
|
||||
The javadoc:org.springframework.boot.opentelemetry.autoconfigure.logging.OpenTelemetryLoggingAutoConfiguration[] configures OpenTelemetry's javadoc:io.opentelemetry.sdk.logs.SdkLoggerProvider[].
|
||||
Exporting logs via OTLP is supported through the javadoc:org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingAutoConfiguration[], which enables OTLP log exporting over HTTP or gRPC.
|
||||
To enable scanning of observability annotations like javadoc:io.micrometer.observation.annotation.Observed[format=annotation], javadoc:io.micrometer.core.annotation.Timed[format=annotation], javadoc:io.micrometer.core.annotation.Counted[format=annotation], javadoc:io.micrometer.core.aop.MeterTag[format=annotation] and javadoc:io.micrometer.tracing.annotation.NewSpan[format=annotation], you need to set the configprop:management.observations.annotations.enabled[] property to `true`.
|
||||
This feature is supported by Micrometer directly.
|
||||
Please refer to the {url-micrometer-docs-concepts}/timers.html#_the_timed_annotation[Micrometer], {url-micrometer-docs-observation}/components.html#micrometer-observation-annotations[Micrometer Observation] and {url-micrometer-tracing-docs}/api.html#_aspect_oriented_programming[Micrometer Tracing] reference docs.
|
||||
|
||||
However, while there is a `SdkLoggerProvider` bean, Spring Boot doesn't support bridging logs to this bean out of the box.
|
||||
This can be done with 3rd-party log bridges, as described in the xref:reference:actuator/loggers.adoc#actuator.loggers.opentelemetry[Logging with OpenTelemetry] section.
|
||||
|
||||
|
||||
|
||||
[[actuator.observability.opentelemetry.metrics]]
|
||||
=== Metrics
|
||||
|
||||
The choice of metrics in the Spring portfolio is Micrometer, which means that metrics are not collected and exported through the OpenTelemetry's javadoc:io.opentelemetry.sdk.metrics.SdkMeterProvider[].
|
||||
Spring Boot doesn't provide a `SdkMeterProvider` bean.
|
||||
|
||||
|
||||
However, Micrometer metrics can be exported via OTLP to any OpenTelemetry capable backend using the javadoc:io.micrometer.registry.otlp.OtlpMeterRegistry[], as described in the xref:reference:actuator/metrics.adoc#actuator.metrics.export.otlp[Metrics with OTLP] section.
|
||||
|
||||
NOTE: Micrometer's OTLP registry doesn't use the `Resource` bean, but setting `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_SERVICE_NAME` or configprop:management.opentelemetry.resource-attributes[] works.
|
||||
|
||||
[[actuator.observability.opentelemetry.metrics.api-and-sdk]]
|
||||
==== Metrics via the OpenTelemetry API and SDK
|
||||
|
||||
If you or a dependency you include make use of OpenTelemetry's javadoc:io.opentelemetry.api.metrics.MeterProvider[], those metrics are not exported.
|
||||
|
||||
We strongly recommend that you report your metrics with Micrometer.
|
||||
If a dependency you include uses OpenTelemetry's `MeterProvider`, you can include this configuration in your application to configure a `MeterProvider` bean, which you then have to wire into your dependency:
|
||||
|
||||
include-code::OpenTelemetryMetricsConfiguration[]
|
||||
|
||||
This configuration also enables metrics export via OTLP over HTTP.
|
||||
|
||||
|
||||
|
||||
[[actuator.observability.opentelemetry.tracing]]
|
||||
=== Tracing
|
||||
|
||||
If Micrometer tracing is used, the javadoc:org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.OpenTelemetryTracingAutoConfiguration[] configures OpenTelemetry's javadoc:io.opentelemetry.sdk.trace.SdkTracerProvider[].
|
||||
Exporting traces through OTLP is enabled by the javadoc:org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.OtlpTracingAutoConfiguration[], which supports exporting traces with OTLP over HTTP or gRPC.
|
||||
|
||||
We strongly recommend using the Micrometer Observation or Tracing API instead of using the OpenTelemetry API directly.
|
||||
NOTE: When you annotate methods or classes which are already instrumented (for example, xref:reference:actuator/metrics.adoc#actuator.metrics.supported.spring-data-repository[Spring Data repositories] or xref:reference:actuator/metrics.adoc#actuator.metrics.supported.spring-mvc[Spring MVC controllers]), you will get duplicate observations.
|
||||
In that case you can either disable the automatic instrumentation using xref:reference:actuator/observability.adoc#actuator.observability.preventing-observations[properties] or an javadoc:io.micrometer.observation.ObservationPredicate[] and rely on your annotations, or you can remove your annotations.
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@ To take full control over the client's configuration, define a javadoc:co.elasti
|
|||
|
||||
|
||||
|
||||
Additionally, a javadoc:co.elastic.clients.transport.rest5_client.low_level.sniffer.Sniffer[] can be auto-configured to automatically discover nodes from a running Elasticsearch cluster and set them on the javadoc:co.elastic.clients.transport.rest5_client.low_level.Rest5Client[] bean.
|
||||
Additionally, a javadoc:co.elastic.clients.transport.rest5_client.low_level.sniffer.Sniffer[] is auto-configured to automatically discover nodes from a running Elasticsearch cluster and set them on the javadoc:co.elastic.clients.transport.rest5_client.low_level.Rest5Client[] bean.
|
||||
You can further tune how javadoc:co.elastic.clients.transport.rest5_client.low_level.sniffer.Sniffer[] is configured, as shown in the following example:
|
||||
|
||||
[configprops,yaml]
|
||||
|
|
@ -350,17 +350,18 @@ spring:
|
|||
elasticsearch:
|
||||
restclient:
|
||||
sniffer:
|
||||
enabled: true
|
||||
interval: "10m"
|
||||
delay-after-failure: "30s"
|
||||
----
|
||||
|
||||
To disable auto-configuration of the Sniffer, set configprop:spring.elasticsearch.restclient.sniffer.enabled[] to `false`.
|
||||
|
||||
|
||||
|
||||
[[data.nosql.elasticsearch.connecting-using-rest.javaapiclient]]
|
||||
==== Connecting to Elasticsearch Using ElasticsearchClient
|
||||
|
||||
If you use the `spring-boot-starter-elasticsearch` or have added `co.elastic.clients:elasticsearch-java` to the classpath, Spring Boot will auto-configure and register an javadoc:co.elastic.clients.elasticsearch.ElasticsearchClient[] bean.
|
||||
If you have `co.elastic.clients:elasticsearch-java` on the classpath, Spring Boot will auto-configure and register an javadoc:co.elastic.clients.elasticsearch.ElasticsearchClient[] bean.
|
||||
|
||||
The javadoc:co.elastic.clients.elasticsearch.ElasticsearchClient[] uses a transport that depends upon the previously described javadoc:co.elastic.clients.transport.rest5_client.low_level.Rest5Client[].
|
||||
Therefore, the properties described previously can be used to configure the javadoc:co.elastic.clients.elasticsearch.ElasticsearchClient[].
|
||||
|
|
|
|||
|
|
@ -795,8 +795,12 @@ NOTE: The properties that map to javadoc:org.springframework.boot.context.proper
|
|||
Such arrangement relies on a default empty constructor and getters and setters are usually mandatory, since binding is through standard Java Beans property descriptors, just like in Spring MVC.
|
||||
A setter may be omitted in the following cases:
|
||||
|
||||
* Pre-initialized Maps and Collections, as long as they are initialized with a mutable implementation (like the `roles` field in the preceding example).
|
||||
* Pre-initialized nested POJOs (like the `Security` field in the preceding example).
|
||||
* Maps, as long as they are initialized, need a getter but not necessarily a setter, since they can be mutated by the binder.
|
||||
* Collections and arrays can be accessed either through an index (typically with YAML) or by using a single comma-separated value (properties).
|
||||
In the latter case, a setter is mandatory.
|
||||
We recommend to always add a setter for such types.
|
||||
If you initialize a collection, make sure it is not immutable (as in the preceding example).
|
||||
* If nested POJO properties are initialized (like the `Security` field in the preceding example), a setter is not required.
|
||||
If you want the binder to create the instance on the fly by using its default constructor, you need a setter.
|
||||
|
||||
Some people use Project Lombok to add getters and setters automatically.
|
||||
|
|
|
|||
|
|
@ -101,4 +101,4 @@ The preferred JSON-B implementation is Eclipse Yasson for which dependency manag
|
|||
|
||||
Auto-configuration for Kotlin Serialization is provided.
|
||||
When `kotlinx-serialization-json` is on the classpath a https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json/[Json] bean is automatically configured.
|
||||
Several `+spring.kotlinx.serialization.json.*+` configuration properties are provided for customizing the configuration.
|
||||
Several `+spring.kotlin.serialization.*+` configuration properties are provided for customizing the configuration.
|
||||
|
|
|
|||
|
|
@ -142,7 +142,10 @@ spring:
|
|||
Caches can be created on startup by setting the configprop:spring.cache.cache-names[] property.
|
||||
If a custom javadoc:org.infinispan.configuration.cache.ConfigurationBuilder[] bean is defined, it is used to customize the caches.
|
||||
|
||||
For more details, see https://infinispan.org/docs/stable/titles/spring/spring.html[the documentation].
|
||||
To be compatible with Spring Boot's Jakarta EE 9 baseline, Infinispan's `-jakarta` modules must be used.
|
||||
For every module with a `-jakarta` variant, the variant must be used in place of the standard module.
|
||||
For example, `infinispan-core-jakarta` and `infinispan-commons-jakarta` must be used in place of `infinispan-core` and `infinispan-commons` respectively.
|
||||
|
||||
|
||||
|
||||
[[io.caching.provider.couchbase]]
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ NOTE: Your executable jar must include AOT generated assets such as generated cl
|
|||
Spring Boot applications usually use Cloud Native Buildpacks through the Maven (`mvn spring-boot:build-image`) or Gradle (`gradle bootBuildImage`) integrations.
|
||||
You can, however, also use {url-buildpacks-docs}/for-platform-operators/how-to/integrate-ci/pack/[`pack`] to turn an AOT processed Spring Boot executable jar into a native container image.
|
||||
|
||||
NOTE: You have to build your application with at least JDK 25, because Buildpacks use the same GraalVM native-image version as the Java version used for compilation.
|
||||
|
||||
First, make sure that a Docker daemon is available (see https://docs.docker.com/installation/#installation[Get Docker] for more details).
|
||||
https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user[Configure it to allow non-root user] if you are on Linux.
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ Usually, you would define the properties in your `application.properties` or `ap
|
|||
Common server settings include:
|
||||
|
||||
* Network settings: Listen port for incoming HTTP requests (`server.port`), interface address to bind to (`server.address`), and so on.
|
||||
* Error management: Location of the error page (configprop:spring.web.error.path[]) and so on.
|
||||
* Error management: Location of the error page (`server.error.path`) and so on.
|
||||
* xref:how-to:webserver.adoc#howto.webserver.configure-ssl[SSL]
|
||||
* xref:how-to:webserver.adoc#howto.webserver.enable-response-compression[HTTP compression]
|
||||
|
||||
|
|
|
|||
|
|
@ -589,14 +589,6 @@ The single `onStartup` method provides access to the javadoc:jakarta.servlet.Ser
|
|||
|
||||
|
||||
|
||||
[[web.servlet.embedded-container.context-initializer.init-parameters]]
|
||||
==== Init Parameters
|
||||
|
||||
Init parameters can be configured on the javadoc:jakarta.servlet.ServletContext[] using `server.servlet.context-parameters.*` properties.
|
||||
For example, the property `server.servlet.context-parameters.com.example.parameter=example` will configure a `ServletContext` init parameter named `com.example.parameter` with the value `example`.
|
||||
|
||||
|
||||
|
||||
[[web.servlet.embedded-container.context-initializer.scanning]]
|
||||
==== Scanning for Servlets, Filters, and listeners
|
||||
|
||||
|
|
@ -636,7 +628,7 @@ Common server settings include:
|
|||
|
||||
* Network settings: Listen port for incoming HTTP requests (`server.port`), interface address to bind to (`server.address`), and so on.
|
||||
* Session settings: Whether the session is persistent (`server.servlet.session.persistent`), session timeout (`server.servlet.session.timeout`), location of session data (`server.servlet.session.store-dir`), and session-cookie configuration (`server.servlet.session.cookie.*`).
|
||||
* Error management: Location of the error page (configprop:spring.web.error.path[]) and so on.
|
||||
* Error management: Location of the error page (`server.error.path`) and so on.
|
||||
* xref:how-to:webserver.adoc#howto.webserver.configure-ssl[SSL]
|
||||
* xref:how-to:webserver.adoc#howto.webserver.enable-response-compression[HTTP compression]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.actuator.observability.contextpropagation;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class ContextPropagationConfiguration {
|
||||
|
||||
@Bean
|
||||
ContextPropagatingTaskDecorator contextPropagatingTaskDecorator() {
|
||||
return new ContextPropagatingTaskDecorator();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.actuator.observability.opentelemetry.environmentvariables;
|
||||
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class AutoConfiguredOpenTelemetrySdkConfiguration {
|
||||
|
||||
@Bean
|
||||
OpenTelemetry autoConfiguredOpenTelemetrySdk() {
|
||||
return AutoConfiguredOpenTelemetrySdk.initialize().getOpenTelemetrySdk();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.actuator.observability.opentelemetry.metrics.apiandsdk;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter;
|
||||
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
|
||||
import io.opentelemetry.sdk.metrics.export.MetricExporter;
|
||||
import io.opentelemetry.sdk.metrics.export.MetricReader;
|
||||
import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
|
||||
import io.opentelemetry.sdk.resources.Resource;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class OpenTelemetryMetricsConfiguration {
|
||||
|
||||
@Bean
|
||||
OtlpHttpMetricExporter metricExporter() {
|
||||
String endpoint = "http://localhost:4318/v1/metrics";
|
||||
return OtlpHttpMetricExporter.builder().setEndpoint(endpoint).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
PeriodicMetricReader metricReader(MetricExporter exporter) {
|
||||
Duration interval = Duration.ofMinutes(1);
|
||||
return PeriodicMetricReader.builder(exporter).setInterval(interval).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
SdkMeterProvider meterProvider(Resource resource, MetricReader metricReader) {
|
||||
return SdkMeterProvider.builder().registerMetricReader(metricReader).setResource(resource).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ public class MyProperties {
|
|||
}
|
||||
|
||||
@Deprecated
|
||||
@DeprecatedConfigurationProperty(replacement = "my.app.name", since = "1.2.0")
|
||||
@DeprecatedConfigurationProperty(replacement = "my.app.name")
|
||||
public String getTarget() {
|
||||
return this.name;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,11 +30,11 @@ public final class MyAutoConfiguration {
|
|||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(SomeService.class)
|
||||
static class SomeServiceConfiguration {
|
||||
public static class SomeServiceConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
SomeService someService() {
|
||||
public SomeService someService() {
|
||||
return new SomeService();
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue