Compare commits

..

1 Commits

Author SHA1 Message Date
Andy Wilkinson 104fe6e638 Release v4.0.0-M3 2025-09-18 15:42:10 +01:00
2363 changed files with 22049 additions and 22890 deletions

View File

@ -23,7 +23,7 @@ inputs:
java-version:
description: 'Java version to use for the build'
required: false
default: '25'
default: '24'
runs:
using: composite
steps:
@ -42,12 +42,12 @@ runs:
${{ inputs.java-toolchain == 'true' && '24' || '' }}
- name: Set Up Gradle With Read/Write Cache
if: ${{ inputs.cache-read-only == 'false' }}
uses: gradle/actions/setup-gradle@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # v4.4.4
uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3
with:
cache-read-only: false
develocity-access-key: ${{ inputs.develocity-access-key }}
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # v4.4.4
uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3
with:
develocity-access-key: ${{ inputs.develocity-access-key }}
develocity-token-expiry: 4

View File

@ -21,7 +21,7 @@ runs:
using: composite
steps:
- name: Set Up JFrog CLI
uses: jfrog/setup-jfrog-cli@c32bf10843e4071112c4ea3abf622d3b27cd8c17 # v4.7.0
uses: jfrog/setup-jfrog-cli@88e9eba31c07e31beefa4cef5c0e93d1af9535d7 # v4.6.1
env:
JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }}
- name: Download Artifacts

View File

@ -17,7 +17,7 @@ runs:
using: composite
steps:
- name: Set Up JFrog CLI
uses: jfrog/setup-jfrog-cli@c32bf10843e4071112c4ea3abf622d3b27cd8c17 # v4.7.0
uses: jfrog/setup-jfrog-cli@88e9eba31c07e31beefa4cef5c0e93d1af9535d7 # v4.6.1
env:
JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }}
- name: Download Release Artifacts

View File

@ -23,13 +23,16 @@ jobs:
toolchain: true
- version: 21
toolchain: true
- version: 25
- version: 24
toolchain: false
- version: 25
early-access: true
toolchain: true
exclude:
- os:
name: Linux
java:
version: 25
version: 24
- os:
name: ${{ github.repository == 'spring-projects/spring-boot-commercial' && 'Windows' }}
steps:

View File

@ -75,7 +75,7 @@ jobs:
runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }}
steps:
- name: Set up JFrog CLI
uses: jfrog/setup-jfrog-cli@c32bf10843e4071112c4ea3abf622d3b27cd8c17 # v4.7.0
uses: jfrog/setup-jfrog-cli@88e9eba31c07e31beefa4cef5c0e93d1af9535d7 # v4.6.1
env:
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
- name: Promote build

View File

@ -86,7 +86,7 @@ jobs:
runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }}
steps:
- name: Set up JFrog CLI
uses: jfrog/setup-jfrog-cli@c32bf10843e4071112c4ea3abf622d3b27cd8c17 # v4.7.0
uses: jfrog/setup-jfrog-cli@88e9eba31c07e31beefa4cef5c0e93d1af9535d7 # v4.6.1
env:
JF_ENV_SPRING: ${{ vars.COMMERCIAL && secrets.COMMERCIAL_JF_ARTIFACTORY_SPRING || secrets.JF_ARTIFACTORY_SPRING }}
- name: Promote open source build

View File

@ -59,7 +59,7 @@ jobs:
with:
stable: true
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # v4.4.4
uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3
with:
cache-read-only: false
- name: Configure Gradle Properties

View File

@ -1,3 +1,3 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=25-librca
java=24.0.2-librca

View File

@ -14,7 +14,7 @@
"@springio/antora-xref-extension": "1.0.0-alpha.4",
"@springio/antora-zip-contents-collector-extension": "1.0.0-alpha.8",
"@springio/asciidoctor-extensions": "1.0.0-alpha.17",
"patch-package": "^8.0.1"
"patch-package": "^8.0.0"
}
},
"node_modules/@antora/asciidoc-loader": {
@ -575,6 +575,14 @@
"resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz",
"integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="
},
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
@ -1398,17 +1406,17 @@
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"license": "MIT",
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
"dependencies": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
"node": ">=10"
}
},
"node_modules/fs-mkdirp-stream": {
@ -1985,10 +1993,9 @@
}
},
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"license": "MIT",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dependencies": {
"universalify": "^2.0.0"
},
@ -2324,30 +2331,38 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"node_modules/patch-package": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz",
"integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==",
"license": "MIT",
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz",
"integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==",
"dependencies": {
"@yarnpkg/lockfile": "^1.1.0",
"chalk": "^4.1.2",
"ci-info": "^3.7.0",
"cross-spawn": "^7.0.3",
"find-yarn-workspace-root": "^2.0.0",
"fs-extra": "^10.0.0",
"fs-extra": "^9.0.0",
"json-stable-stringify": "^1.0.2",
"klaw-sync": "^6.0.0",
"minimist": "^1.2.6",
"open": "^7.4.2",
"rimraf": "^2.6.3",
"semver": "^7.5.3",
"slash": "^2.0.0",
"tmp": "^0.2.4",
"tmp": "^0.0.33",
"yaml": "^2.2.2"
},
"bin": {
@ -2735,6 +2750,18 @@
"node": ">= 0.10"
}
},
"node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -3091,12 +3118,14 @@
}
},
"node_modules/tmp": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"license": "MIT",
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"dependencies": {
"os-tmpdir": "~1.0.2"
},
"engines": {
"node": ">=14.14"
"node": ">=0.6.0"
}
},
"node_modules/to-absolute-glob": {
@ -3209,7 +3238,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}

View File

@ -12,7 +12,7 @@
"@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",
"patch-package": "^8.0.1"
"patch-package": "^8.0.0"
},
"config": {
"ui-bundle-url": "https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip"

View File

@ -80,6 +80,7 @@ tasks.register("integrationTest") {
ant.propertyref(name: "ivy.class.path")
}
plainlistener()
file(layout.buildDirectory.dir("test-results/integrationTest")).mkdirs()
xmllistener(toDir: resultsDir)
fileset(dir: layout.buildDirectory.dir("it").get().asFile.toString(), includes: "**/build.xml")
}

View File

@ -27,7 +27,7 @@ tasks.named("bootRun") {
tasks.register("configuredSystemProperties") {
doLast {
bootRun.systemProperties.each { k, v ->
bootRun.systemProperties.each { k, v ->
println "$k = $v"
}
}

View File

@ -141,13 +141,9 @@ final class ApplicationPluginAction implements PluginApplicationAction {
}
}
@SuppressWarnings("deprecation")
private void configureFileMode(CopySpec copySpec, int mode) {
try {
copySpec.getClass().getMethod("setFileMode", Integer.class).invoke(copySpec, Integer.valueOf(mode));
}
catch (Exception ex) {
throw new RuntimeException("Failed to set file mode on CopySpec", ex);
}
copySpec.setFileMode(mode);
}
}

View File

@ -17,6 +17,7 @@
package org.springframework.boot.gradle.plugin;
import java.io.File;
import java.util.Collections;
import java.util.Set;
import java.util.jar.JarFile;
@ -32,7 +33,7 @@ import org.gradle.api.specs.Spec;
*/
class JarTypeFileSpec implements Spec<File> {
private static final Set<String> EXCLUDED_JAR_TYPES = Set.of("dependencies-starter", "development-tool");
private static final Set<String> EXCLUDED_JAR_TYPES = Collections.singleton("dependencies-starter");
@Override
public boolean isSatisfiedBy(File file) {

View File

@ -285,6 +285,7 @@ final class JavaPluginAction implements PluginApplicationAction {
private void configureProductionRuntimeClasspathConfiguration(Project project) {
Configuration productionRuntimeClasspath = project.getConfigurations()
.create(SpringBootPlugin.PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME);
productionRuntimeClasspath.setVisible(false);
Configuration runtimeClasspath = project.getConfigurations()
.getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME);
productionRuntimeClasspath.attributes((attributes) -> {

View File

@ -104,6 +104,7 @@ class WarPluginAction implements PluginApplicationAction {
.set(project.provider(() -> javaPluginExtension(project).getTargetCompatibility()));
bootWar.resolvedArtifacts(runtimeClasspath.getIncoming().getArtifacts().getResolvedArtifacts());
});
bootWarProvider.map(War::getClasspath);
return bootWarProvider;
}

View File

@ -43,7 +43,7 @@ import org.springframework.util.Assert;
* @since 3.0.0
*/
@CacheableTask
public abstract class ProcessTestAot extends AbstractAot {
public class ProcessTestAot extends AbstractAot {
private @Nullable FileCollection classpathRoots;

View File

@ -34,6 +34,8 @@ import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.loader.tools.LoaderImplementation;
/**
* A Spring Boot "fat" archive task.
*
@ -135,6 +137,15 @@ public interface BootArchive extends Task {
*/
void resolvedArtifacts(Provider<Set<ResolvedArtifactResult>> resolvedArtifacts);
/**
* The loader implementation that should be used with the archive.
* @return the loader implementation
* @since 3.2.0
*/
@Input
@Optional
Property<LoaderImplementation> getLoaderImplementation();
/**
* Returns whether the JAR tools should be included as a dependency in the layered
* archive.

View File

@ -22,24 +22,32 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import org.gradle.api.file.ConfigurableFilePermissions;
import org.gradle.api.file.CopySpec;
import org.gradle.api.file.FileCopyDetails;
import org.gradle.api.file.FileTreeElement;
import org.gradle.api.file.RelativePath;
import org.gradle.api.internal.file.copy.CopyAction;
import org.gradle.api.internal.file.copy.CopyActionProcessingStream;
import org.gradle.api.internal.file.copy.FileCopyDetailsInternal;
import org.gradle.api.java.archives.Attributes;
import org.gradle.api.java.archives.Manifest;
import org.gradle.api.provider.Property;
import org.gradle.api.specs.Spec;
import org.gradle.api.specs.Specs;
import org.gradle.api.tasks.WorkResult;
import org.gradle.api.tasks.bundling.Jar;
import org.gradle.api.tasks.util.PatternSet;
import org.gradle.util.GradleVersion;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.loader.tools.LoaderImplementation;
/**
* Support class for implementations of {@link BootArchive}.
*
@ -115,11 +123,13 @@ class BootArchiveSupport {
return (version != null) ? version : "unknown";
}
CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, boolean supportsSignatureFile) {
return createCopyAction(jar, resolvedDependencies, supportsSignatureFile, null, null);
CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies,
LoaderImplementation loaderImplementation, boolean supportsSignatureFile) {
return createCopyAction(jar, resolvedDependencies, loaderImplementation, supportsSignatureFile, null, null);
}
CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, boolean supportsSignatureFile,
CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies,
LoaderImplementation loaderImplementation, boolean supportsSignatureFile,
@Nullable LayerResolver layerResolver, @Nullable String jarmodeToolsLocation) {
File output = jar.getArchiveFile().get().getAsFile();
Manifest manifest = jar.getManifest();
@ -135,8 +145,9 @@ class BootArchiveSupport {
String encoding = jar.getMetadataCharset();
CopyAction action = new BootZipCopyAction(output, manifest, preserveFileTimestamps, dirPermissions,
filePermissions, includeDefaultLoader, jarmodeToolsLocation, requiresUnpack, exclusions, launchScript,
librarySpec, compressionResolver, encoding, resolvedDependencies, supportsSignatureFile, layerResolver);
return action;
librarySpec, compressionResolver, encoding, resolvedDependencies, supportsSignatureFile, layerResolver,
loaderImplementation);
return jar.isReproducibleFileOrder() ? new ReproducibleOrderingCopyAction(action) : action;
}
private @Nullable Integer getUnixNumericDirPermissions(CopySpec copySpec) {
@ -153,22 +164,14 @@ class BootArchiveSupport {
return permissions.isPresent() ? permissions.get().toUnixNumeric() : null;
}
@SuppressWarnings("deprecation")
private @Nullable Integer getDirMode(CopySpec copySpec) {
try {
return (Integer) copySpec.getClass().getMethod("getDirMode").invoke(copySpec);
}
catch (Exception ex) {
throw new RuntimeException("Failed to get dir mode from CopySpec", ex);
}
return copySpec.getDirMode();
}
@SuppressWarnings("deprecation")
private @Nullable Integer getFileMode(CopySpec copySpec) {
try {
return (Integer) copySpec.getClass().getMethod("getFileMode").invoke(copySpec);
}
catch (Exception ex) {
throw new RuntimeException("Failed to get file mode from CopySpec", ex);
}
return copySpec.getFileMode();
}
private boolean isUsingDefaultLoader(Jar jar) {
@ -231,4 +234,26 @@ class BootArchiveSupport {
details.setRelativePath(details.getRelativeSourcePath());
}
/**
* {@link CopyAction} variant that sorts entries to ensure reproducible ordering.
*/
private static final class ReproducibleOrderingCopyAction implements CopyAction {
private final CopyAction delegate;
private ReproducibleOrderingCopyAction(CopyAction delegate) {
this.delegate = delegate;
}
@Override
public WorkResult execute(CopyActionProcessingStream stream) {
return this.delegate.execute((action) -> {
Map<RelativePath, FileCopyDetailsInternal> detailsByPath = new TreeMap<>();
stream.process((details) -> detailsByPath.put(details.getRelativePath(), details));
detailsByPath.values().forEach(action::processFile);
});
}
}
}

View File

@ -38,6 +38,8 @@ import org.gradle.api.tasks.bundling.Jar;
import org.gradle.work.DisableCachingByDefault;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.loader.tools.LoaderImplementation;
/**
* A custom {@link Jar} task that produces a Spring Boot executable jar.
*
@ -143,12 +145,13 @@ public abstract class BootJar extends Jar implements BootArchive {
@Override
protected CopyAction createCopyAction() {
LoaderImplementation loaderImplementation = getLoaderImplementation().getOrElse(LoaderImplementation.DEFAULT);
LayerResolver layerResolver = null;
if (!isLayeredDisabled()) {
layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary);
}
String jarmodeToolsLocation = isIncludeJarmodeTools() ? LIB_DIRECTORY : null;
return this.support.createCopyAction(this, this.resolvedDependencies, true, layerResolver,
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, true, layerResolver,
jarmodeToolsLocation);
}

View File

@ -38,6 +38,8 @@ import org.gradle.api.tasks.bundling.War;
import org.gradle.work.DisableCachingByDefault;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.loader.tools.LoaderImplementation;
/**
* A custom {@link War} task that produces a Spring Boot executable war.
*
@ -117,13 +119,14 @@ public abstract class BootWar extends War implements BootArchive {
@Override
protected CopyAction createCopyAction() {
LoaderImplementation loaderImplementation = getLoaderImplementation().getOrElse(LoaderImplementation.DEFAULT);
LayerResolver layerResolver = null;
if (!isLayeredDisabled()) {
layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary);
}
String jarmodeToolsLocation = isIncludeJarmodeTools() ? LIB_DIRECTORY : null;
return this.support.createCopyAction(this, this.resolvedDependencies, false, layerResolver,
jarmodeToolsLocation);
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, false,
layerResolver, jarmodeToolsLocation);
}
private boolean isIncludeJarmodeTools() {

View File

@ -23,10 +23,13 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Collection;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@ -60,6 +63,7 @@ import org.springframework.boot.loader.tools.JarModeLibrary;
import org.springframework.boot.loader.tools.Layer;
import org.springframework.boot.loader.tools.LayersIndex;
import org.springframework.boot.loader.tools.LibraryCoordinates;
import org.springframework.boot.loader.tools.LoaderImplementation;
import org.springframework.boot.loader.tools.NativeImageArgFile;
import org.springframework.boot.loader.tools.ReachabilityMetadataProperties;
import org.springframework.util.Assert;
@ -116,13 +120,15 @@ class BootZipCopyAction implements CopyAction {
private final @Nullable LayerResolver layerResolver;
private final LoaderImplementation loaderImplementation;
BootZipCopyAction(File output, Manifest manifest, boolean preserveFileTimestamps, @Nullable Integer dirMode,
@Nullable Integer fileMode, boolean includeDefaultLoader, @Nullable String jarmodeToolsLocation,
Spec<FileTreeElement> requiresUnpack, Spec<FileTreeElement> exclusions,
@Nullable LaunchScriptConfiguration launchScript, Spec<FileCopyDetails> librarySpec,
Function<FileCopyDetails, ZipCompression> compressionResolver, @Nullable String encoding,
ResolvedDependencies resolvedDependencies, boolean supportsSignatureFile,
@Nullable LayerResolver layerResolver) {
@Nullable LayerResolver layerResolver, LoaderImplementation loaderImplementation) {
this.output = output;
this.manifest = manifest;
this.preserveFileTimestamps = preserveFileTimestamps;
@ -139,6 +145,7 @@ class BootZipCopyAction implements CopyAction {
this.resolvedDependencies = resolvedDependencies;
this.supportsSignatureFile = supportsSignatureFile;
this.layerResolver = layerResolver;
this.loaderImplementation = loaderImplementation;
}
@Override
@ -322,7 +329,8 @@ class BootZipCopyAction implements CopyAction {
// Always write loader entries after META-INF directory (see gh-16698)
return;
}
LoaderZipEntries loaderEntries = new LoaderZipEntries(getTime(), getDirMode(), getFileMode());
LoaderZipEntries loaderEntries = new LoaderZipEntries(getTime(), getDirMode(), getFileMode(),
BootZipCopyAction.this.loaderImplementation);
this.writtenLoaderEntries = loaderEntries.writeTo(this.out);
if (BootZipCopyAction.this.layerResolver != null) {
for (String name : this.writtenLoaderEntries.getFiles()) {
@ -504,13 +512,9 @@ class BootZipCopyAction implements CopyAction {
? details.getPermissions().toUnixNumeric() : getMode(details);
}
@SuppressWarnings("deprecation")
private int getMode(FileCopyDetails details) {
try {
return (int) details.getClass().getMethod("getMode").invoke(details);
}
catch (Exception ex) {
throw new RuntimeException("Failed to get mode from FileCopyDetails", ex);
}
return details.getMode();
}
}
@ -586,24 +590,36 @@ class BootZipCopyAction implements CopyAction {
private static final int BUFFER_SIZE = 32 * 1024;
private final boolean unpack;
private final @Nullable MessageDigest messageDigest;
private final CRC32 crc = new CRC32();
private long size;
StoredEntryPreparator(InputStream inputStream, boolean unpack) throws IOException {
this.unpack = unpack;
this.messageDigest = (unpack) ? sha1Digest() : null;
try (inputStream) {
load(inputStream);
}
}
private static MessageDigest sha1Digest() {
try {
return MessageDigest.getInstance("SHA-1");
}
catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException(ex);
}
}
private void load(InputStream inputStream) throws IOException {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
this.crc.update(buffer, 0, bytesRead);
if (this.messageDigest != null) {
this.messageDigest.update(buffer, 0, bytesRead);
}
this.size += bytesRead;
}
}
@ -613,8 +629,8 @@ class BootZipCopyAction implements CopyAction {
entry.setCompressedSize(this.size);
entry.setCrc(this.crc.getValue());
entry.setMethod(ZipEntry.STORED);
if (this.unpack) {
entry.setComment("UNPACK");
if (this.messageDigest != null) {
entry.setComment("UNPACK:" + HexFormat.of().formatHex(this.messageDigest.digest()));
}
}

View File

@ -29,6 +29,7 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.gradle.api.file.FileTreeElement;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.loader.tools.LoaderImplementation;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
@ -41,22 +42,27 @@ import org.springframework.util.StreamUtils;
*/
class LoaderZipEntries {
private final LoaderImplementation loaderImplementation;
private final @Nullable Long entryTime;
private final int dirMode;
private final int fileMode;
LoaderZipEntries(@Nullable Long entryTime, int dirMode, int fileMode) {
LoaderZipEntries(@Nullable Long entryTime, int dirMode, int fileMode,
@Nullable LoaderImplementation loaderImplementation) {
this.entryTime = entryTime;
this.dirMode = dirMode;
this.fileMode = fileMode;
this.loaderImplementation = (loaderImplementation != null) ? loaderImplementation
: LoaderImplementation.DEFAULT;
}
WrittenEntries writeTo(ZipArchiveOutputStream out) throws IOException {
WrittenEntries written = new WrittenEntries();
try (ZipInputStream loaderJar = new ZipInputStream(
getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) {
getResourceAsStream("/" + this.loaderImplementation.getJarResourceName()))) {
java.util.zip.ZipEntry entry = loaderJar.getNextEntry();
while (entry != null) {
if (entry.isDirectory() && !entry.getName().equals("META-INF/")) {

View File

@ -124,7 +124,7 @@ class PackagingDocumentationTests {
try (JarFile jar = new JarFile(file)) {
JarEntry entry = jar.getJarEntry("BOOT-INF/lib/jruby-complete-1.7.25.jar");
assertThat(entry).isNotNull();
assertThat(entry.getComment()).isEqualTo("UNPACK");
assertThat(entry.getComment()).startsWith("UNPACK:");
}
}

View File

@ -24,6 +24,8 @@ import java.time.format.DateTimeFormatter;
import java.util.Properties;
import org.gradle.api.Project;
import org.gradle.api.internal.project.ProjectInternal;
import org.gradle.initialization.GradlePropertiesController;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@ -171,7 +173,11 @@ class BuildInfoTests {
private Project createProject(String projectName) {
File projectDir = new File(this.temp, projectName);
return GradleProjectBuilder.builder().withProjectDir(projectDir).withName(projectName).build();
Project project = GradleProjectBuilder.builder().withProjectDir(projectDir).withName(projectName).build();
((ProjectInternal) project).getServices()
.get(GradlePropertiesController.class)
.loadGradlePropertiesFrom(projectDir, false);
return project;
}
private BuildInfo createTask(Project project) {

View File

@ -108,6 +108,16 @@ abstract class AbstractBootArchiveIntegrationTests {
assertThat(firstHash).isEqualTo(secondHash);
}
@TestTemplate
void classicLoader() throws IOException {
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome())
.isEqualTo(TaskOutcome.SUCCESS);
File jar = new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0];
try (JarFile jarFile = new JarFile(jar)) {
assertThat(jarFile.getEntry("org/springframework/boot/loader/LaunchedURLClassLoader.class")).isNotNull();
}
}
@TestTemplate
void upToDateWhenBuiltTwice() {
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome())
@ -233,8 +243,14 @@ abstract class AbstractBootArchiveIntegrationTests {
.filter((entry) -> !entry.isDirectory())
.map(JarEntry::getName)
.filter((name) -> name.startsWith(this.libPath));
assertThat(libEntryNames).containsExactly(this.libPath + "two-1.0.jar",
this.libPath + "commons-io-2.19.0.jar");
if (this.gradleBuild.gradleVersionIsLessThan("9.0.0-rc-1")) {
assertThat(libEntryNames).containsExactly(this.libPath + "two-1.0.jar",
this.libPath + "commons-io-2.19.0.jar");
}
else {
assertThat(libEntryNames).containsExactly(this.libPath + "commons-io-2.19.0.jar",
this.libPath + "two-1.0.jar");
}
}
}
@ -277,7 +293,6 @@ abstract class AbstractBootArchiveIntegrationTests {
void jarTypeFilteringIsApplied() throws IOException {
File flatDirRepository = new File(this.gradleBuild.getProjectDir(), "repository");
createDependenciesStarterJar(new File(flatDirRepository, "starter.jar"));
createDependenciesDeveloperToolsJar(new File(flatDirRepository, "devonly.jar"));
createStandardJar(new File(flatDirRepository, "standard.jar"));
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome())
.isEqualTo(TaskOutcome.SUCCESS);
@ -654,10 +669,6 @@ abstract class AbstractBootArchiveIntegrationTests {
createJar(location, (attributes) -> attributes.putValue("Spring-Boot-Jar-Type", "dependencies-starter"));
}
private void createDependenciesDeveloperToolsJar(File location) throws IOException {
createJar(location, (attributes) -> attributes.putValue("Spring-Boot-Jar-Type", "development-tool"));
}
private void createJar(File location, Consumer<Attributes> attributesConfigurer) throws IOException {
location.getParentFile().mkdirs();
Manifest manifest = new Manifest();

View File

@ -27,8 +27,6 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFilePermission;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
@ -37,7 +35,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
@ -68,6 +65,7 @@ import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.gradle.junit.GradleProjectBuilder;
import org.springframework.boot.loader.tools.DefaultLaunchScript;
import org.springframework.boot.loader.tools.JarModeLibrary;
import org.springframework.boot.loader.tools.LoaderImplementation;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
@ -285,6 +283,17 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
}
}
@Test
void loaderIsWrittenToTheRootOfTheJarWhenUsingClassicLoader() throws IOException {
this.task.getMainClass().set("com.example.Main");
this.task.getLoaderImplementation().set(LoaderImplementation.CLASSIC);
executeTask();
try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) {
assertThat(jarFile.getEntry("org/springframework/boot/loader/LaunchedURLClassLoader.class")).isNotNull();
assertThat(jarFile.getEntry("org/springframework/boot/loader/")).isNotNull();
}
}
@Test
void unpackCommentIsAddedToEntryIdentifiedByAPattern() throws IOException {
this.task.getMainClass().set("com.example.Main");
@ -292,7 +301,7 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
this.task.requiresUnpack("**/one.jar");
executeTask();
try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) {
assertThat(jarFile.getEntry(this.libPath + "one.jar").getComment()).isEqualTo("UNPACK");
assertThat(jarFile.getEntry(this.libPath + "one.jar").getComment()).startsWith("UNPACK:");
assertThat(jarFile.getEntry(this.libPath + "two.jar").getComment()).isNull();
}
}
@ -304,7 +313,7 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
this.task.requiresUnpack((element) -> element.getName().endsWith("two.jar"));
executeTask();
try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) {
assertThat(jarFile.getEntry(this.libPath + "two.jar").getComment()).isEqualTo("UNPACK");
assertThat(jarFile.getEntry(this.libPath + "two.jar").getComment()).startsWith("UNPACK:");
assertThat(jarFile.getEntry(this.libPath + "one.jar").getComment()).isNull();
}
}
@ -410,46 +419,23 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
}
@Test
void archiveIsReproducibleByDefault() throws IOException {
void reproducibleOrderingCanBeEnabled() throws IOException {
this.task.getMainClass().set("com.example.Main");
this.task.from(newFiles("files/b/bravo.txt", "files/a/alpha.txt", "files/c/charlie.txt"));
this.task.from(newFile("bravo.txt"), newFile("alpha.txt"), newFile("charlie.txt"));
this.task.setReproducibleFileOrder(true);
executeTask();
assertThat(this.task.getArchiveFile().get().getAsFile()).exists();
List<String> files = new ArrayList<>();
List<String> textFiles = new ArrayList<>();
try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
assertThat(entry.getLastModifiedTime().toMillis())
.isEqualTo(ZipEntryConstants.CONSTANT_TIME_FOR_ZIP_ENTRIES);
if (entry.getName().startsWith("files/")) {
files.add(entry.getName());
}
}
}
assertThat(files).containsExactly("files/", "files/a/", "files/a/alpha.txt", "files/b/", "files/b/bravo.txt",
"files/c/", "files/c/charlie.txt");
}
@Test
void archiveReproducibilityCanBeDisabled() throws IOException {
this.task.getMainClass().set("com.example.Main");
this.task.from(newFiles("files/b/bravo.txt", "files/a/alpha.txt", "files/c/charlie.txt"));
this.task.setPreserveFileTimestamps(true);
this.task.setReproducibleFileOrder(false);
executeTask();
assertThat(this.task.getArchiveFile().get().getAsFile()).exists();
try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().endsWith(".txt") || entry.getName().startsWith("BOOT-INF/lib/")) {
OffsetDateTime lastModifiedTime = entry.getLastModifiedTime().toInstant().atOffset(ZoneOffset.UTC);
assertThat(lastModifiedTime)
.isNotEqualTo(OffsetDateTime.of(1980, 2, 1, 0, 0, 0, 0, ZoneOffset.UTC));
if (entry.getName().endsWith(".txt")) {
textFiles.add(entry.getName());
}
}
}
assertThat(textFiles).containsExactly("alpha.txt", "bravo.txt", "charlie.txt");
}
@Test
@ -689,19 +675,6 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
return entryNames;
}
protected File newFiles(String... names) throws IOException {
File dir = new File(this.temp, UUID.randomUUID().toString());
dir.mkdir();
List<File> files = new ArrayList<>();
for (String name : names) {
File file = new File(dir, name);
file.getParentFile().mkdirs();
file.createNewFile();
files.add(file);
}
return dir;
}
protected File newFile(String name) throws IOException {
File file = new File(this.temp, name);
file.createNewFile();

View File

@ -30,8 +30,8 @@ import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.buildpack.platform.build.BuildRequest;
import org.springframework.boot.buildpack.platform.build.BuildpackReference;
import org.springframework.boot.buildpack.platform.build.PullPolicy;
import org.springframework.boot.buildpack.platform.docker.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.gradle.junit.GradleProjectBuilder;

View File

@ -66,10 +66,18 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
copyClasspathApplication();
BuildResult result = this.gradleBuild.build("launch");
String output = result.getOutput();
assertThat(output).containsPattern("1\\. .*classes");
assertThat(output).containsPattern("2\\. .*library-1.0-SNAPSHOT.jar");
assertThat(output).containsPattern("3\\. .*commons-lang3-3.9.jar");
assertThat(output).containsPattern("4\\. .*spring-boot-jarmode-tools.*.jar");
if (this.gradleBuild.gradleVersionIsLessThan("9.0.0-rc-1")) {
assertThat(output).containsPattern("1\\. .*classes");
assertThat(output).containsPattern("2\\. .*library-1.0-SNAPSHOT.jar");
assertThat(output).containsPattern("3\\. .*commons-lang3-3.9.jar");
assertThat(output).containsPattern("4\\. .*spring-boot-jarmode-tools.*.jar");
}
else {
assertThat(output).containsPattern("1\\. .*classes");
assertThat(output).containsPattern("2\\. .*commons-lang3-3.9.jar");
assertThat(output).containsPattern("3\\. .*library-1.0-SNAPSHOT.jar");
assertThat(output).containsPattern("4\\. .*spring-boot-jarmode-tools.*.jar");
}
assertThat(output).doesNotContain("5. ");
}
@ -78,10 +86,18 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
copyClasspathApplication();
BuildResult result = this.gradleBuild.build("launch");
String output = result.getOutput();
assertThat(output).containsPattern("1\\. .*classes");
assertThat(output).containsPattern("2\\. .*spring-boot-jarmode-tools.*.jar");
assertThat(output).containsPattern("3\\. .*library-1.0-SNAPSHOT.jar");
assertThat(output).containsPattern("4\\. .*commons-lang3-3.9.jar");
if (this.gradleBuild.gradleVersionIsLessThan("9.0.0-rc-1")) {
assertThat(output).containsPattern("1\\. .*classes");
assertThat(output).containsPattern("2\\. .*spring-boot-jarmode-tools.*.jar");
assertThat(output).containsPattern("3\\. .*library-1.0-SNAPSHOT.jar");
assertThat(output).containsPattern("4\\. .*commons-lang3-3.9.jar");
}
else {
assertThat(output).containsPattern("1\\. .*classes");
assertThat(output).containsPattern("2\\. .*spring-boot-jarmode-tools.*.jar");
assertThat(output).containsPattern("3\\. .*commons-lang3-3.9.jar");
assertThat(output).containsPattern("4\\. .*library-1.0-SNAPSHOT.jar");
}
assertThat(output).doesNotContain("5. ");
}

View File

@ -29,7 +29,7 @@ bootJar {
baseName = 'foo'
}
else {
archiveBaseName = 'foo'
archiveBaseName = 'foo'
}
}

View File

@ -29,7 +29,7 @@ bootWar {
baseName = 'foo'
}
else {
archiveBaseName = 'foo'
archiveBaseName = 'foo'
}
}

View File

@ -42,7 +42,7 @@ dependencies {
}
task('processTestAotClasspath') {
dependsOn configurations.processTestAotClasspath
dependsOn configurations.processTestAotClasspath
doFirst {
configurations.processTestAotClasspath.files.each { println it }
}

View File

@ -42,7 +42,7 @@ dependencies {
}
task('processTestAotClasspath') {
dependsOn configurations.processTestAotClasspath
dependsOn configurations.processTestAotClasspath
doFirst {
configurations.processTestAotClasspath.files.each { println it }
}

View File

@ -15,11 +15,11 @@
*/
plugins {
id "org.springframework.boot.starter"
id 'java'
id 'org.springframework.boot' version '{version}'
}
description = "Starter for testing aspect-oriented programming with AspectJ"
dependencies {
api(project(":starter:spring-boot-starter-test"))
bootJar {
mainClass = 'com.example.Application'
loaderImplementation = org.springframework.boot.loader.tools.LoaderImplementation.CLASSIC
}

View File

@ -33,7 +33,7 @@ bootJar {
include "*:*:*SNAPSHOT"
}
intoLayer("commons-dependencies") {
include "org.apache.commons:*"
include "org.apache.commons:*"
}
intoLayer("dependencies")
}

View File

@ -48,7 +48,7 @@ bootJar {
includeProjectDependencies()
}
intoLayer("commons-dependencies") {
include "org.apache.commons:*"
include "org.apache.commons:*"
}
intoLayer("dependencies")
}

View File

@ -21,8 +21,6 @@ plugins {
bootJar {
mainClass = 'com.example.Application'
if (GradleVersion.current().compareTo(GradleVersion.version("9.0.0-rc-1")) < 0) {
preserveFileTimestamps = false
reproducibleFileOrder = true
}
preserveFileTimestamps = false
reproducibleFileOrder = true
}

View File

@ -15,11 +15,11 @@
*/
plugins {
id "org.springframework.boot.starter"
id 'war'
id 'org.springframework.boot' version '{version}'
}
description = ""
dependencies {
api(project(":starter:spring-boot-starter-test"))
bootWar {
mainClass = 'com.example.Application'
loaderImplementation = org.springframework.boot.loader.tools.LoaderImplementation.CLASSIC
}

View File

@ -21,8 +21,6 @@ plugins {
bootWar {
mainClass = 'com.example.Application'
if (GradleVersion.current().compareTo(GradleVersion.version("9.0.0-rc-1")) < 0) {
preserveFileTimestamps = false
reproducibleFileOrder = true
}
preserveFileTimestamps = false
reproducibleFileOrder = true
}

View File

@ -6,7 +6,7 @@
<!-- tag::different-versions[] -->
<properties>
<slf4j.version>1.7.30</slf4j.version>
<spring-data-bom.version>2024.1.10</spring-data-bom.version>
<spring-data-releasetrain.version>Moore-SR6</spring-data-releasetrain.version>
</properties>
<!-- end::different-versions[] -->

View File

@ -24,8 +24,8 @@
<!-- Override Spring Data release train provided by Spring Boot -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-bom</artifactId>
<version>2024.1.10</version>
<artifactId>spring-data-releasetrain</artifactId>
<version>2020.0.0-SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>

View File

@ -67,9 +67,6 @@ include::example$using/different-versions-pom.xml[tags=different-versions]
Browse the xref:appendix:dependency-versions/properties.adoc[Dependency Versions Properties] section in the Spring Boot reference for a complete list of dependency version properties.
WARNING: Each Spring Boot release is designed and tested against a specific set of third-party dependencies.
Overriding versions may cause compatibility issues and should be done with care.
[[using.import]]

View File

@ -159,7 +159,7 @@ abstract class AbstractArchiveIntegrationTests {
Optional<JarEntry> match = entries.filter((entry) -> entry.getName().startsWith(prefix))
.findFirst();
assertThat(match).as("Name starting with %s", prefix)
.hasValueSatisfying((entry) -> assertThat(entry.getComment()).isEqualTo("UNPACK"));
.hasValueSatisfying((entry) -> assertThat(entry.getComment()).startsWith("UNPACK:"));
});
});
return this;

View File

@ -76,6 +76,26 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests {
});
}
@TestTemplate
void whenJarWithClassicLoaderIsRepackagedInPlaceOnlyRepackagedJarIsInstalled(MavenBuild mavenBuild) {
mavenBuild.project("jar-with-classic-loader").goals("install").execute((project) -> {
File original = new File(project, "target/jar-with-classic-loader-0.0.1.BUILD-SNAPSHOT.jar.original");
assertThat(original).isFile();
File repackaged = new File(project, "target/jar-with-classic-loader-0.0.1.BUILD-SNAPSHOT.jar");
assertThat(launchScript(repackaged)).isEmpty();
assertThat(jar(repackaged)).manifest((manifest) -> {
manifest.hasMainClass("org.springframework.boot.loader.launch.JarLauncher");
manifest.hasStartClass("some.random.Main");
manifest.hasAttribute("Not-Used", "Foo");
}).hasEntryWithName("org/springframework/boot/loader/launch/JarLauncher.class");
assertThat(buildLog(project))
.contains("Replacing main artifact " + repackaged + " with repackaged archive,")
.contains("The original artifact has been renamed to " + original)
.contains("Installing " + repackaged + " to")
.doesNotContain("Installing " + original + " to");
});
}
@TestTemplate
void whenAttachIsDisabledOnlyTheOriginalJarIsInstalled(MavenBuild mavenBuild) {
mavenBuild.project("jar-attach-disabled").goals("install").execute((project) -> {
@ -176,40 +196,7 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests {
.hasEntryWithNameStartingWith("BOOT-INF/lib/spring-context")
.hasEntryWithNameStartingWith("BOOT-INF/lib/spring-core")
.hasEntryWithNameStartingWith("BOOT-INF/lib/commons-logging")
.doesNotHaveEntryWithNameStartingWith("BOOT-INF/lib/servlet-api-");
});
}
@TestTemplate
void whenAnEntryIsOptionalByDefaultDoesNotAppearInTheRepackagedJar(MavenBuild mavenBuild) {
mavenBuild.project("jar-optional-default").goals("install").execute((project) -> {
File repackaged = new File(project, "target/jar-optional-default-0.0.1.BUILD-SNAPSHOT.jar");
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/")
.hasEntryWithNameStartingWith("BOOT-INF/lib/spring-context")
.hasEntryWithNameStartingWith("BOOT-INF/lib/spring-core")
.doesNotHaveEntryWithNameStartingWith("BOOT-INF/lib/log4j-api-");
});
}
@TestTemplate
void whenAnEntryIsOptionalAndOptionalsIncludedAppearsInTheRepackagedJar(MavenBuild mavenBuild) {
mavenBuild.project("jar-optional-include").goals("install").execute((project) -> {
File repackaged = new File(project, "target/jar-optional-include-0.0.1.BUILD-SNAPSHOT.jar");
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/")
.hasEntryWithNameStartingWith("BOOT-INF/lib/spring-context")
.hasEntryWithNameStartingWith("BOOT-INF/lib/spring-core")
.hasEntryWithNameStartingWith("BOOT-INF/lib/log4j-api-");
});
}
@TestTemplate
void whenAnEntryIsOptionalAndOptionalsExcludedDoesNotAppearInTheRepackagedJar(MavenBuild mavenBuild) {
mavenBuild.project("jar-optional-exclude").goals("install").execute((project) -> {
File repackaged = new File(project, "target/jar-optional-exclude-0.0.1.BUILD-SNAPSHOT.jar");
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/")
.hasEntryWithNameStartingWith("BOOT-INF/lib/spring-context")
.hasEntryWithNameStartingWith("BOOT-INF/lib/spring-core")
.doesNotHaveEntryWithNameStartingWith("BOOT-INF/lib/log4j-api-");
.doesNotHaveEntryWithName("BOOT-INF/lib/servlet-api-2.5.jar");
});
}
@ -261,8 +248,9 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests {
File repackaged = new File(project, "target/jar-exclude-group-0.0.1.BUILD-SNAPSHOT.jar");
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/")
.hasEntryWithNameStartingWith("BOOT-INF/lib/spring-context")
.hasEntryWithNameStartingWith("BOOT-INF/lib/spring-core")
.hasEntryWithNameStartingWith("BOOT-INF/lib/commons-logging")
.doesNotHaveEntryWithName("BOOT-INF/lib/log4j-api-");
.doesNotHaveEntryWithName("BOOT-INF/lib/log4j-api-2.4.1.jar");
});
}
@ -400,7 +388,7 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests {
assertThat(layerIndex.get("application")).contains("BOOT-INF/lib/jar-release-0.0.1.RELEASE.jar",
"BOOT-INF/lib/jar-snapshot-0.0.1.BUILD-SNAPSHOT.jar");
assertThat(layerIndex.get("dependencies"))
.anyMatch((dependency) -> dependency.startsWith("BOOT-INF/lib/log4j-api-"));
.anyMatch((dependency) -> dependency.startsWith("BOOT-INF/lib/log4j-api-2"));
}
catch (IOException ex) {
// Ignore

View File

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-optional-default</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>@maven-jar-plugin.version@</version>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>@spring-framework.version@</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>@log4j2.version@</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-optional-include</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<includeOptional>true</includeOptional>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>@maven-jar-plugin.version@</version>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>@spring-framework.version@</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>@log4j2.version@</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -3,7 +3,7 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-optional-exclude</artifactId>
<artifactId>jar-with-classic-loader</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@ -22,7 +22,7 @@
<goal>repackage</goal>
</goals>
<configuration>
<includeOptional>false</includeOptional>
<loaderImplementation>CLASSIC</loaderImplementation>
</configuration>
</execution>
</executions>
@ -31,6 +31,16 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>@maven-jar-plugin.version@</version>
<configuration>
<archive>
<manifest>
<mainClass>some.random.Main</mainClass>
</manifest>
<manifestEntries>
<Not-Used>Foo</Not-Used>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
@ -41,10 +51,10 @@
<version>@spring-framework.version@</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>@log4j2.version@</version>
<optional>true</optional>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>@jakarta-servlet.version@</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -47,6 +47,7 @@ import org.springframework.boot.loader.tools.Layouts.Jar;
import org.springframework.boot.loader.tools.Layouts.None;
import org.springframework.boot.loader.tools.Layouts.War;
import org.springframework.boot.loader.tools.Libraries;
import org.springframework.boot.loader.tools.LoaderImplementation;
import org.springframework.boot.loader.tools.Packager;
import org.springframework.boot.loader.tools.layer.CustomLayers;
@ -113,13 +114,6 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
@Parameter(defaultValue = "false")
public boolean includeSystemScope;
/**
* Include optional dependencies.
* @since 3.5.7
*/
@Parameter(defaultValue = "false")
public boolean includeOptional;
/**
* Include JAR tools.
* @since 3.3.0
@ -148,6 +142,15 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
return null;
}
/**
* Return the loader implementation that should be used.
* @return the loader implementation or {@code null}
* @since 3.2.0
*/
protected @Nullable LoaderImplementation getLoaderImplementation() {
return null;
}
/**
* Return the layout factory that will be used to determine the {@link LayoutType} if
* no explicit layout is set.
@ -165,6 +168,7 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
*/
protected <P extends Packager> P getConfiguredPackager(Supplier<P> supplier) {
P packager = supplier.get();
packager.setLoaderImplementation(getLoaderImplementation());
packager.setLayoutFactory(getLayoutFactory());
packager.addMainClassTimeoutWarningListener(new LoggingMainClassTimeoutWarningListener(this::getLog));
packager.setMainClass(this.mainClass);
@ -227,9 +231,6 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
if (!this.includeSystemScope) {
filters.add(new ScopeFilter(null, Artifact.SCOPE_SYSTEM));
}
if (!this.includeOptional) {
filters.add(DependencyFilter.exclude(Artifact::isOptional));
}
return filters.toArray(new ArtifactsFilter[0]);
}

View File

@ -50,6 +50,7 @@ import org.springframework.boot.loader.tools.EntryWriter;
import org.springframework.boot.loader.tools.ImagePackager;
import org.springframework.boot.loader.tools.LayoutFactory;
import org.springframework.boot.loader.tools.Libraries;
import org.springframework.boot.loader.tools.LoaderImplementation;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@ -207,6 +208,13 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo {
@Parameter
private @Nullable LayoutType layout;
/**
* The loader implementation that should be used.
* @since 3.2.0
*/
@Parameter
private @Nullable LoaderImplementation loaderImplementation;
/**
* The layout factory that will be used to create the executable archive if no
* explicit layout is set. Alternative layouts implementations can be provided by 3rd
@ -230,6 +238,11 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo {
return this.layout;
}
@Override
protected @Nullable LoaderImplementation getLoaderImplementation() {
return this.loaderImplementation;
}
/**
* Return the layout factory that will be used to determine the
* {@link AbstractPackagerMojo.LayoutType} if no explicit layout is set.

View File

@ -16,11 +16,9 @@
package org.springframework.boot.maven;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.shared.artifact.filter.collection.AbstractArtifactsFilter;
@ -83,22 +81,4 @@ public abstract class DependencyFilter extends AbstractArtifactsFilter {
return this.filters;
}
/**
* Return a new {@link DependencyFilter} the excludes artifacts based on the given
* predicate.
* @param filter the predicate used to filter the artifacts.
* @return a new {@link DependencyFilter} instance
* @since 3.5.7
*/
public static DependencyFilter exclude(Predicate<Artifact> filter) {
return new DependencyFilter(Collections.emptyList()) {
@Override
protected boolean filter(Artifact artifact) {
return filter.test(artifact);
}
};
}
}

View File

@ -34,8 +34,8 @@ import org.apache.maven.artifact.Artifact;
*/
class JarTypeFilter extends DependencyFilter {
private static final Set<String> EXCLUDED_JAR_TYPES = Collections.unmodifiableSet(
new HashSet<>(Arrays.asList("annotation-processor", "dependencies-starter", "development-tool")));
private static final Set<String> EXCLUDED_JAR_TYPES = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList("annotation-processor", "dependencies-starter")));
JarTypeFilter() {
super(Collections.emptyList());

View File

@ -40,6 +40,7 @@ import org.springframework.boot.loader.tools.DefaultLaunchScript;
import org.springframework.boot.loader.tools.LaunchScript;
import org.springframework.boot.loader.tools.LayoutFactory;
import org.springframework.boot.loader.tools.Libraries;
import org.springframework.boot.loader.tools.LoaderImplementation;
import org.springframework.boot.loader.tools.Repackager;
import org.springframework.lang.Contract;
import org.springframework.util.StringUtils;
@ -169,6 +170,13 @@ public class RepackageMojo extends AbstractPackagerMojo {
@Parameter(property = "spring-boot.repackage.layout")
private @Nullable LayoutType layout;
/**
* The loader implementation that should be used.
* @since 3.2.0
*/
@Parameter
private @Nullable LoaderImplementation loaderImplementation;
/**
* The layout factory that will be used to create the executable archive if no
* explicit layout is set. Alternative layouts implementations can be provided by 3rd
@ -193,6 +201,11 @@ public class RepackageMojo extends AbstractPackagerMojo {
return this.layout;
}
@Override
protected @Nullable LoaderImplementation getLoaderImplementation() {
return this.loaderImplementation;
}
/**
* Return the layout factory that will be used to determine the
* {@link AbstractPackagerMojo.LayoutType} if no explicit layout is set.

View File

@ -1,50 +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.maven;
import java.util.Set;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DefaultArtifact;
import org.apache.maven.artifact.handler.ArtifactHandler;
import org.apache.maven.artifact.handler.DefaultArtifactHandler;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.shared.artifact.filter.collection.ArtifactFilterException;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DependencyFilter}.
*
* @author Phillip Webb
*/
class DependencyFilterTests {
@Test
void excludeFiltersBasedOnPredicate() throws ArtifactFilterException {
DependencyFilter filter = DependencyFilter.exclude(Artifact::isOptional);
ArtifactHandler ah = new DefaultArtifactHandler();
VersionRange v = VersionRange.createFromVersion("1.0.0");
DefaultArtifact a1 = new DefaultArtifact("com.example", "a1", v, "compile", "jar", null, ah, false);
DefaultArtifact a2 = new DefaultArtifact("com.example", "a2", v, "compile", "jar", null, ah, true);
DefaultArtifact a3 = new DefaultArtifact("com.example", "a3", v, "compile", "jar", null, ah, false);
Set<Artifact> filtered = filter.filter(Set.of(a1, a2, a3));
assertThat(filtered).containsExactlyInAnyOrder(a1, a3);
}
}

View File

@ -31,8 +31,8 @@ import org.springframework.boot.buildpack.platform.build.BuildRequest;
import org.springframework.boot.buildpack.platform.build.BuildpackReference;
import org.springframework.boot.buildpack.platform.build.Cache;
import org.springframework.boot.buildpack.platform.build.PullPolicy;
import org.springframework.boot.buildpack.platform.docker.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.boot.buildpack.platform.io.TarArchive;

View File

@ -60,11 +60,6 @@ class JarTypeFilterTests {
assertThat(new JarTypeFilter().filter(createArtifact("annotation-processor"))).isTrue();
}
@Test
void whenArtifactHasDevelopmentToolJarTypeThenItIsExcluded() {
assertThat(new JarTypeFilter().filter(createArtifact("development-tool"))).isTrue();
}
@Test
void whenArtifactHasNoManifestFileThenItIsIncluded() {
assertThat(new JarTypeFilter().filter(createArtifactWithNoManifest())).isFalse();

View File

@ -52,7 +52,7 @@ dependencies {
implementation("commons-codec:commons-codec:${commonsCodecVersion}")
implementation("de.undercouch.download:de.undercouch.download.gradle.plugin:5.5.0")
implementation("dev.adamko.dokkatoo:dokkatoo-plugin:2.3.1")
implementation("dev.detekt:detekt-gradle-plugin:2.0.0-alpha.0")
implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.8")
implementation("io.spring.gradle.antora:spring-antora-plugin:0.0.1")
implementation("io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}")
implementation("io.spring.nohttp:nohttp-gradle:0.0.11")
@ -133,6 +133,10 @@ gradlePlugin {
id = "org.springframework.boot.integration-test"
implementationClass = "org.springframework.boot.build.test.IntegrationTestPlugin"
}
systemTestPlugin {
id = "org.springframework.boot.system-test"
implementationClass = "org.springframework.boot.build.test.SystemTestPlugin"
}
mavenPluginPlugin {
id = "org.springframework.boot.maven-plugin"
implementationClass = "org.springframework.boot.build.mavenplugin.MavenPluginPlugin"
@ -149,22 +153,10 @@ gradlePlugin {
id = "org.springframework.boot.starter"
implementationClass = "org.springframework.boot.build.starters.StarterPlugin"
}
systemTestPlugin {
id = "org.springframework.boot.system-test"
implementationClass = "org.springframework.boot.build.test.SystemTestPlugin"
}
testAutoConfigurationPlugin {
id = "org.springframework.boot.test-auto-configuration"
implementationClass = "org.springframework.boot.build.test.autoconfigure.TestAutoConfigurationPlugin"
}
testFailuresPlugin {
id = "org.springframework.boot.test-failures"
implementationClass = "org.springframework.boot.build.testing.TestFailuresPlugin"
}
testSlicePlugin {
id = "org.springframework.boot.test-slice"
implementationClass = "org.springframework.boot.build.test.autoconfigure.TestSlicePlugin"
}
}
}

View File

@ -20,9 +20,9 @@ import java.net.URI;
import dev.adamko.dokkatoo.DokkatooExtension;
import dev.adamko.dokkatoo.formats.DokkatooHtmlPlugin;
import dev.detekt.gradle.Detekt;
import dev.detekt.gradle.extensions.DetektExtension;
import dev.detekt.gradle.plugin.DetektPlugin;
import io.gitlab.arturbosch.detekt.Detekt;
import io.gitlab.arturbosch.detekt.DetektPlugin;
import io.gitlab.arturbosch.detekt.extensions.DetektExtension;
import org.gradle.api.Project;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer;
@ -76,7 +76,6 @@ class KotlinConventions {
private void configureDokkatoo(Project project) {
DokkatooExtension dokkatoo = project.getExtensions().getByType(DokkatooExtension.class);
dokkatoo.getVersions().getJetbrainsDokka().set("2.1.0-Beta");
dokkatoo.getDokkatooSourceSets().configureEach((sourceSet) -> {
if (SourceSet.MAIN_SOURCE_SET_NAME.equals(sourceSet.getName())) {
sourceSet.getSourceRoots().setFrom(project.file("src/main/kotlin"));
@ -104,9 +103,7 @@ class KotlinConventions {
project.getPlugins().apply(DetektPlugin.class);
DetektExtension detekt = project.getExtensions().getByType(DetektExtension.class);
detekt.getConfig().setFrom(project.getRootProject().file("config/detekt/config.yml"));
project.getTasks()
.withType(Detekt.class)
.configureEach((task) -> task.getJvmTarget().set(JVM_TARGET.getTarget()));
project.getTasks().withType(Detekt.class).configureEach((task) -> task.setJvmTarget(JVM_TARGET.getTarget()));
}
}

View File

@ -80,13 +80,6 @@ final class ArchitectureRules {
private static final String AUTOCONFIGURATION_ANNOTATION = "org.springframework.boot.autoconfigure.AutoConfiguration";
private static final String TEST_AUTOCONFIGURATION_ANNOTATION = "org.springframework.boot.test.autoconfigure.TestAutoConfiguration";
private static final Predicate<JavaPackage> NULL_MARKED_PACKAGE_FILTER = (candidate) -> !List
.of("org.springframework.boot.cli.json", "org.springframework.boot.configurationmetadata.json",
"org.springframework.boot.configurationprocessor.json")
.contains(candidate.getName());
private ArchitectureRules() {
}
@ -112,14 +105,13 @@ final class ArchitectureRules {
rules.add(noClassesShouldCallStringToUpperCaseWithoutLocale());
rules.add(noClassesShouldCallStringToLowerCaseWithoutLocale());
rules.add(conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodReturnType());
rules.add(enumSourceShouldNotHaveValueThatIsTheSameAsTypeOfMethodsFirstParameter());
rules.add(enumSourceShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodParameterType());
rules.add(classLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute());
rules.add(methodLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute());
rules.add(conditionsShouldNotBePublic());
rules.add(allConfigurationPropertiesBindingBeanMethodsShouldBeStatic());
rules.add(autoConfigurationClassesShouldBePublicAndFinal());
rules.add(autoConfigurationClassesShouldHaveNoPublicMembers());
rules.add(testAutoConfigurationClassesShouldBePackagePrivateAndFinal());
return List.copyOf(rules);
}
@ -138,12 +130,7 @@ final class ArchitectureRules {
}
private static ArchRule allPackagesShouldBeFreeOfTangles() {
return SlicesRuleDefinition.slices()
.matching("(**)")
.should()
.beFreeOfCycles()
.ignoreDependency("org.springframework.boot.env.EnvironmentPostProcessor",
"org.springframework.boot.SpringApplication");
return SlicesRuleDefinition.slices().matching("(**)").should().beFreeOfCycles();
}
private static ArchRule allBeanPostProcessorBeanMethodsShouldBeStaticAndNotCausePrematureInitialization() {
@ -263,9 +250,7 @@ final class ArchitectureRules {
}
static ArchRule packagesShouldBeAnnotatedWithNullMarked() {
return ArchRuleDefinition.all(packages(NULL_MARKED_PACKAGE_FILTER))
.should(beAnnotatedWithNullMarked())
.allowEmptyShould(true);
return ArchRuleDefinition.all(packages()).should(beAnnotatedWithNullMarked()).allowEmptyShould(true);
}
private static ArchCondition<? super JavaMethod> notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType() {
@ -273,7 +258,7 @@ final class ArchitectureRules {
JavaAnnotation<JavaMethod> conditionalAnnotation = item
.getAnnotationOfType("org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean");
Map<String, Object> properties = conditionalAnnotation.getProperties();
if (!hasProperty("type", properties) && !hasProperty("name", properties)) {
if (!properties.containsKey("type") && !properties.containsKey("name")) {
conditionalAnnotation.get("value").ifPresent((value) -> {
if (containsOnlySingleType((JavaType[]) value, item.getReturnType())) {
addViolation(events, item, conditionalAnnotation.getDescription()
@ -284,24 +269,16 @@ final class ArchitectureRules {
});
}
private static boolean hasProperty(String name, Map<String, Object> properties) {
Object property = properties.get(name);
if (property == null) {
return false;
}
return !property.getClass().isArray() || ((Object[]) property).length > 0;
}
private static ArchRule enumSourceShouldNotHaveValueThatIsTheSameAsTypeOfMethodsFirstParameter() {
private static ArchRule enumSourceShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodParameterType() {
return ArchRuleDefinition.methods()
.that()
.areAnnotatedWith("org.junit.jupiter.params.provider.EnumSource")
.should(notHaveValueThatIsTheSameAsTheTypeOfTheMethodsFirstParameter())
.should(notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType())
.allowEmptyShould(true);
}
private static ArchCondition<? super JavaMethod> notHaveValueThatIsTheSameAsTheTypeOfTheMethodsFirstParameter() {
return check("not have a value that is the same as the type of the method's first parameter",
private static ArchCondition<? super JavaMethod> notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType() {
return check("not specify only a type that is the same as the method's parameter type",
ArchitectureRules::notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType);
}
@ -309,13 +286,15 @@ final class ArchitectureRules {
ConditionEvents events) {
JavaAnnotation<JavaMethod> enumSourceAnnotation = item
.getAnnotationOfType("org.junit.jupiter.params.provider.EnumSource");
enumSourceAnnotation.get("value").ifPresent((value) -> {
JavaType parameterType = item.getParameterTypes().get(0);
if (value.equals(parameterType)) {
addViolation(events, item, enumSourceAnnotation.getDescription()
+ " should not specify a value that is the same as the type of the method's first parameter");
}
});
Map<String, Object> properties = enumSourceAnnotation.getProperties();
if (properties.size() == 1 && item.getParameterTypes().size() == 1) {
enumSourceAnnotation.get("value").ifPresent((value) -> {
if (value.equals(item.getParameterTypes().get(0))) {
addViolation(events, item, enumSourceAnnotation.getDescription()
+ " should not specify only a value that is the same as the method's parameter type");
}
});
}
}
private static ArchRule classLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute() {
@ -374,7 +353,8 @@ final class ArchitectureRules {
private static ArchRule autoConfigurationClassesShouldBePublicAndFinal() {
return ArchRuleDefinition.classes()
.that(areRegularAutoConfiguration())
.that()
.areAnnotatedWith(AUTOCONFIGURATION_ANNOTATION)
.should()
.bePublic()
.andShould()
@ -385,7 +365,8 @@ final class ArchitectureRules {
private static ArchRule autoConfigurationClassesShouldHaveNoPublicMembers() {
return ArchRuleDefinition.members()
.that()
.areDeclaredInClassesThat(areRegularAutoConfiguration())
.areDeclaredInClassesThat()
.areAnnotatedWith(AUTOCONFIGURATION_ANNOTATION)
.and(areNotDefaultConstructors())
.and(areNotConstants())
.and(dontOverridePublicMethods())
@ -394,24 +375,6 @@ final class ArchitectureRules {
.allowEmptyShould(true);
}
private static ArchRule testAutoConfigurationClassesShouldBePackagePrivateAndFinal() {
return ArchRuleDefinition.classes()
.that()
.areAnnotatedWith(TEST_AUTOCONFIGURATION_ANNOTATION)
.should()
.bePackagePrivate()
.andShould()
.haveModifier(JavaModifier.FINAL)
.allowEmptyShould(true);
}
private static DescribedPredicate<JavaClass> areRegularAutoConfiguration() {
return DescribedPredicate.describe("Regular @AutoConfiguration",
(javaClass) -> javaClass.isMetaAnnotatedWith(AUTOCONFIGURATION_ANNOTATION)
&& !javaClass.isMetaAnnotatedWith(TEST_AUTOCONFIGURATION_ANNOTATION)
&& !javaClass.isAnnotation());
}
private static DescribedPredicate<? super JavaMember> dontOverridePublicMethods() {
OverridesPublicMethod<JavaMember> predicate = new OverridesPublicMethod<>();
return DescribedPredicate.describe("don't override public methods", (member) -> !predicate.test(member));
@ -517,11 +480,11 @@ final class ArchitectureRules {
return string + " should be used instead";
}
static ClassesTransformer<JavaPackage> packages(Predicate<JavaPackage> filter) {
static ClassesTransformer<JavaPackage> packages() {
return new AbstractClassesTransformer<>("packages") {
@Override
public Iterable<JavaPackage> doTransform(JavaClasses collection) {
return collection.stream().map(JavaClass::getPackage).filter(filter).collect(Collectors.toSet());
return collection.stream().map(JavaClass::getPackage).collect(Collectors.toSet());
}
};
}

View File

@ -19,7 +19,6 @@ package org.springframework.boot.build.autoconfigure;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Collections;
@ -55,8 +54,8 @@ public record AutoConfigurationClass(String name, List<String> before, List<Stri
attributes.getOrDefault("afterName", Collections.emptyList()));
}
public static AutoConfigurationClass of(InputStream input) {
try {
static AutoConfigurationClass of(File classFile) {
try (FileInputStream input = new FileInputStream(classFile)) {
ClassReader classReader = new ClassReader(input);
AutoConfigurationClassVisitor visitor = new AutoConfigurationClassVisitor();
classReader.accept(visitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES);
@ -67,15 +66,6 @@ public record AutoConfigurationClass(String name, List<String> before, List<Stri
}
}
static AutoConfigurationClass of(File classFile) {
try (InputStream input = new FileInputStream(classFile)) {
return of(input);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private static final class AutoConfigurationClassVisitor extends ClassVisitor {
private AutoConfigurationClass autoConfigurationClass;

View File

@ -38,10 +38,7 @@ import org.gradle.api.tasks.SkipWhenEmpty;
*/
public abstract class AutoConfigurationImportsTask extends DefaultTask {
/**
* The path of the {@code AutoConfiguration.imports} file.
*/
public static final String IMPORTS_FILE = "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports";
static final String IMPORTS_FILE = "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports";
private FileCollection sourceFiles = getProject().getObjects().fileCollection();

View File

@ -65,7 +65,7 @@ public class ConfigurationPropertiesPlugin implements Plugin<Project> {
public static final String CHECK_ADDITIONAL_SPRING_CONFIGURATION_METADATA_TASK_NAME = "checkAdditionalSpringConfigurationMetadata";
/**
* Name of the {@link CheckSpringConfigurationMetadata} task.
* Name of the {@link CheckAdditionalSpringConfigurationMetadata} task.
*/
public static final String CHECK_SPRING_CONFIGURATION_METADATA_TASK_NAME = "checkSpringConfigurationMetadata";

View File

@ -41,7 +41,7 @@ public class OptionalDependenciesPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
Configuration optional = project.getConfigurations().create("optional");
optional.setCanBeConsumed(true);
optional.setCanBeConsumed(false);
optional.setCanBeResolved(false);
project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> {
SourceSetContainer sourceSets = project.getExtensions()

View File

@ -1,233 +0,0 @@
/*
* Copyright 2025-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.test.autoconfigure;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Consumer;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileTree;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.SkipWhenEmpty;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.VerificationException;
import org.gradle.language.base.plugins.LifecycleBasePlugin;
import org.springframework.boot.build.autoconfigure.AutoConfigurationClass;
/**
* Task to check the contents of a project's
* {@code META-INF/spring/*.AutoConfigure*.imports} files.
*
* @author Andy Wilkinson
*/
public abstract class CheckAutoConfigureImports extends DefaultTask {
private FileCollection sourceFiles = getProject().getObjects().fileCollection();
private FileCollection classpath = getProject().getObjects().fileCollection();
public CheckAutoConfigureImports() {
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
}
@InputFiles
@SkipWhenEmpty
@PathSensitive(PathSensitivity.RELATIVE)
public FileTree getSource() {
return this.sourceFiles.getAsFileTree()
.matching((filter) -> filter.include("META-INF/spring/*.AutoConfigure*.imports"));
}
public void setSource(Object source) {
this.sourceFiles = getProject().getObjects().fileCollection().from(source);
}
@Classpath
public FileCollection getClasspath() {
return this.classpath;
}
public void setClasspath(Object classpath) {
this.classpath = getProject().getObjects().fileCollection().from(classpath);
}
@OutputDirectory
public abstract DirectoryProperty getOutputDirectory();
@TaskAction
void execute() {
Map<String, List<String>> allProblems = new TreeMap<>();
for (AutoConfigureImports autoConfigureImports : loadImports()) {
List<String> problems = new ArrayList<>();
if (!find(autoConfigureImports.annotationName)) {
problems.add("Annotation '%s' was not found".formatted(autoConfigureImports.annotationName));
}
for (String imported : autoConfigureImports.imports) {
String importedClassName = imported;
if (importedClassName.startsWith("optional:")) {
importedClassName = importedClassName.substring("optional:".length());
}
boolean found = find(importedClassName, (input) -> {
if (!correctlyAnnotated(input)) {
problems.add("Imported auto-configuration '%s' is not annotated with @AutoConfiguration"
.formatted(imported));
}
});
if (!found) {
problems.add("Imported auto-configuration '%s' was not found".formatted(importedClassName));
}
}
List<String> sortedValues = new ArrayList<>(autoConfigureImports.imports);
Collections.sort(sortedValues, (i1, i2) -> {
boolean imported1 = i1.startsWith("optional:");
boolean imported2 = i2.startsWith("optional:");
int comparison = Boolean.compare(imported1, imported2);
if (comparison != 0) {
return comparison;
}
return i1.compareTo(i2);
});
if (!sortedValues.equals(autoConfigureImports.imports)) {
File sortedOutputFile = getOutputDirectory().file("sorted-" + autoConfigureImports.fileName)
.get()
.getAsFile();
writeString(sortedOutputFile, sortedValues.stream().collect(Collectors.joining(System.lineSeparator()))
+ System.lineSeparator());
problems.add(
"Entries should be required then optional, each sorted alphabetically (expected content written to '%s')"
.formatted(sortedOutputFile.getAbsolutePath()));
}
if (!problems.isEmpty()) {
allProblems.computeIfAbsent(autoConfigureImports.fileName, (unused) -> new ArrayList<>())
.addAll(problems);
}
}
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
writeReport(allProblems, outputFile);
if (!allProblems.isEmpty()) {
throw new VerificationException(
"AutoConfigure….imports checks failed. See '%s' for details".formatted(outputFile));
}
}
private List<AutoConfigureImports> loadImports() {
return getSource().getFiles().stream().map((file) -> {
String fileName = file.getName();
String annotationName = fileName.substring(0, fileName.length() - ".imports".length());
return new AutoConfigureImports(annotationName, loadImports(file), fileName);
}).toList();
}
private List<String> loadImports(File importsFile) {
try {
return Files.readAllLines(importsFile.toPath());
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private boolean find(String className) {
return find(className, (input) -> {
});
}
private boolean find(String className, Consumer<InputStream> handler) {
for (File root : this.classpath.getFiles()) {
String classFilePath = className.replace(".", "/") + ".class";
if (root.isDirectory()) {
File classFile = new File(root, classFilePath);
if (classFile.isFile()) {
try (InputStream input = new FileInputStream(classFile)) {
handler.accept(input);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
return true;
}
}
else {
try (JarFile jar = new JarFile(root)) {
ZipEntry entry = jar.getEntry(classFilePath);
if (entry != null) {
try (InputStream input = jar.getInputStream(entry)) {
handler.accept(input);
}
return true;
}
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}
return false;
}
private boolean correctlyAnnotated(InputStream classFile) {
return AutoConfigurationClass.of(classFile) != null;
}
private void writeReport(Map<String, List<String>> allProblems, File outputFile) {
outputFile.getParentFile().mkdirs();
StringBuilder report = new StringBuilder();
if (!allProblems.isEmpty()) {
allProblems.forEach((fileName, problems) -> {
report.append("Found problems in '%s':%n".formatted(fileName));
problems.forEach((problem) -> report.append(" - %s%n".formatted(problem)));
});
}
writeString(outputFile, report.toString());
}
private void writeString(File file, String content) {
try {
Files.writeString(file.toPath(), content);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
record AutoConfigureImports(String annotationName, List<String> imports, String fileName) {
}
}

View File

@ -17,14 +17,17 @@
package org.springframework.boot.build.test.autoconfigure;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.io.Reader;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.Enumeration;
import java.util.Properties;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.gradle.api.DefaultTask;
import org.gradle.api.Task;
@ -35,9 +38,9 @@ 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 tools.jackson.databind.json.JsonMapper;
import org.springframework.boot.build.test.autoconfigure.TestSliceMetadata.TestSlice;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* {@link Task} used to document test slices.
@ -46,16 +49,16 @@ import org.springframework.boot.build.test.autoconfigure.TestSliceMetadata.TestS
*/
public abstract class DocumentTestSlices extends DefaultTask {
private FileCollection testSliceMetadata;
private FileCollection testSlices;
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
public FileCollection getTestSlices() {
return this.testSliceMetadata;
return this.testSlices;
}
public void setTestSlices(FileCollection testSlices) {
this.testSliceMetadata = testSlices;
this.testSlices = testSlices;
}
@OutputFile
@ -63,42 +66,61 @@ public abstract class DocumentTestSlices extends DefaultTask {
@TaskAction
void documentTestSlices() throws IOException {
Map<String, List<TestSlice>> testSlices = readTestSlices();
Set<TestSlice> testSlices = readTestSlices();
writeTable(testSlices);
}
private Map<String, List<TestSlice>> readTestSlices() {
Map<String, List<TestSlice>> testSlices = new TreeMap<>();
for (File metadataFile : this.testSliceMetadata) {
JsonMapper mapper = JsonMapper.builder().build();
TestSliceMetadata metadata = mapper.readValue(metadataFile, TestSliceMetadata.class);
List<TestSlice> slices = new ArrayList<>(metadata.testSlices());
Collections.sort(slices, (s1, s2) -> s1.annotation().compareTo(s2.annotation()));
testSlices.put(metadata.module(), slices);
@SuppressWarnings("unchecked")
private Set<TestSlice> readTestSlices() throws IOException {
Set<TestSlice> testSlices = new TreeSet<>();
for (File metadataFile : this.testSlices) {
Properties metadata = new Properties();
try (Reader reader = new FileReader(metadataFile)) {
metadata.load(reader);
}
for (String name : Collections.list((Enumeration<String>) metadata.propertyNames())) {
testSlices.add(new TestSlice(name,
new TreeSet<>(StringUtils.commaDelimitedListToSet(metadata.getProperty(name)))));
}
}
return testSlices;
}
private void writeTable(Map<String, List<TestSlice>> testSlicesByModule) throws IOException {
private void writeTable(Set<TestSlice> testSlices) throws IOException {
File outputFile = getOutputFile().getAsFile().get();
outputFile.getParentFile().mkdirs();
try (PrintWriter writer = new PrintWriter(new FileWriter(outputFile))) {
writer.println("[cols=\"d,d,a\"]");
writer.println("[cols=\"d,a\"]");
writer.println("|===");
writer.println("|Module | Test slice | Imported auto-configuration");
testSlicesByModule.forEach((module, testSlices) -> {
testSlices.forEach((testSlice) -> {
writer.println();
writer.printf("| `%s`%n", module);
writer.printf("| javadoc:%s[format=annotation]%n", testSlice.annotation());
writer.println("| ");
for (String importedAutoConfiguration : testSlice.importedAutoConfigurations()) {
writer.printf("`%s`%n", importedAutoConfiguration);
}
});
});
writer.println("| Test slice | Imported auto-configuration");
for (TestSlice testSlice : testSlices) {
writer.println();
writer.printf("| `@%s`%n", testSlice.className);
writer.println("| ");
for (String importedAutoConfiguration : testSlice.importedAutoConfigurations) {
writer.printf("`%s`%n", importedAutoConfiguration);
}
}
writer.println("|===");
}
}
private static final class TestSlice implements Comparable<TestSlice> {
private final String className;
private final SortedSet<String> importedAutoConfigurations;
private TestSlice(String className, SortedSet<String> importedAutoConfigurations) {
this.className = ClassUtils.getShortName(className);
this.importedAutoConfigurations = importedAutoConfigurations;
}
@Override
public int compareTo(TestSlice other) {
return this.className.compareTo(other.className);
}
}
}

View File

@ -1,238 +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.test.autoconfigure;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.inject.Inject;
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.model.ObjectFactory;
import org.gradle.api.tasks.Classpath;
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.SourceSet;
import org.gradle.api.tasks.TaskAction;
import org.springframework.boot.build.test.autoconfigure.TestSliceMetadata.TestSlice;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
import org.springframework.util.StringUtils;
/**
* A {@link Task} for generating metadata describing a project's test slices.
*
* @author Andy Wilkinson
*/
public abstract class GenerateTestSliceMetadata extends DefaultTask {
private final ObjectFactory objectFactory;
private FileCollection classpath;
private FileCollection importsFiles;
private FileCollection classesDirs;
@Inject
public GenerateTestSliceMetadata(ObjectFactory objectFactory) {
this.objectFactory = objectFactory;
}
public void setSourceSet(SourceSet sourceSet) {
this.classpath = sourceSet.getRuntimeClasspath();
this.importsFiles = this.objectFactory.fileTree()
.from(new File(sourceSet.getOutput().getResourcesDir(), "META-INF/spring"));
this.importsFiles.filter((file) -> file.getName().endsWith(".imports"));
getSpringFactories().set(new File(sourceSet.getOutput().getResourcesDir(), "META-INF/spring.factories"));
this.classesDirs = sourceSet.getOutput().getClassesDirs();
}
@OutputFile
public abstract RegularFileProperty getOutputFile();
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
abstract RegularFileProperty getSpringFactories();
@Classpath
FileCollection getClasspath() {
return this.classpath;
}
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
FileCollection getImportFiles() {
return this.importsFiles;
}
@Classpath
FileCollection getClassesDirs() {
return this.classesDirs;
}
@TaskAction
void generateTestSliceMetadata() throws IOException {
TestSliceMetadata metadata = readTestSlices();
File outputFile = getOutputFile().getAsFile().get();
outputFile.getParentFile().mkdirs();
metadata.writeTo(outputFile);
}
private TestSliceMetadata readTestSlices() throws IOException {
List<TestSlice> testSlices = new ArrayList<>();
try (URLClassLoader classLoader = new URLClassLoader(
StreamSupport.stream(this.classpath.spliterator(), false).map(this::toURL).toArray(URL[]::new))) {
MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(classLoader);
Properties springFactories = readSpringFactories(getSpringFactories().getAsFile().getOrNull());
readImportsFiles(springFactories, this.importsFiles);
for (File classesDir : this.classesDirs) {
testSlices.addAll(readTestSlices(classesDir, metadataReaderFactory, springFactories));
}
}
return new TestSliceMetadata(getProject().getName(), testSlices);
}
/**
* Reads the given imports files and puts them in springFactories. The key is the file
* name, the value is the file contents, split by line, delimited with a comma. This
* is done to mimic the spring.factories structure.
* @param springFactories spring.factories parsed as properties
* @param importsFiles the imports files to read
*/
private void readImportsFiles(Properties springFactories, FileCollection importsFiles) {
for (File file : importsFiles.getFiles()) {
try {
List<String> lines = removeComments(Files.readAllLines(file.toPath()));
String fileNameWithoutExtension = file.getName()
.substring(0, file.getName().length() - ".imports".length());
springFactories.setProperty(fileNameWithoutExtension,
StringUtils.collectionToCommaDelimitedString(lines));
}
catch (IOException ex) {
throw new UncheckedIOException("Failed to read file " + file, ex);
}
}
}
private List<String> removeComments(List<String> lines) {
List<String> result = new ArrayList<>();
for (String line : lines) {
int commentIndex = line.indexOf('#');
if (commentIndex > -1) {
line = line.substring(0, commentIndex);
}
line = line.trim();
if (!line.isEmpty()) {
result.add(line);
}
}
return result;
}
private URL toURL(File file) {
try {
return file.toURI().toURL();
}
catch (MalformedURLException ex) {
throw new RuntimeException(ex);
}
}
private Properties readSpringFactories(File file) throws IOException {
Properties springFactories = new Properties();
if (file.isFile()) {
try (Reader in = new FileReader(file)) {
springFactories.load(in);
}
}
return springFactories;
}
private List<TestSlice> readTestSlices(File classesDir, MetadataReaderFactory metadataReaderFactory,
Properties springFactories) throws IOException {
try (Stream<Path> classes = Files.walk(classesDir.toPath())) {
return classes.filter((path) -> path.toString().endsWith("Test.class"))
.map((path) -> getMetadataReader(path, metadataReaderFactory))
.filter((metadataReader) -> metadataReader.getClassMetadata().isAnnotation())
.map((metadataReader) -> readTestSlice(metadataReader, springFactories))
.toList();
}
}
private MetadataReader getMetadataReader(Path path, MetadataReaderFactory metadataReaderFactory) {
try {
return metadataReaderFactory.getMetadataReader(new FileSystemResource(path));
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
private TestSlice readTestSlice(MetadataReader metadataReader, Properties springFactories) {
String annotationName = metadataReader.getClassMetadata().getClassName();
List<String> importedAutoConfiguration = getImportedAutoConfiguration(springFactories,
metadataReader.getAnnotationMetadata());
return new TestSlice(annotationName, importedAutoConfiguration);
}
private List<String> getImportedAutoConfiguration(Properties springFactories,
AnnotationMetadata annotationMetadata) {
Stream<String> importers = findMetaImporters(annotationMetadata);
if (annotationMetadata.isAnnotated("org.springframework.boot.autoconfigure.ImportAutoConfiguration")) {
importers = Stream.concat(importers, Stream.of(annotationMetadata.getClassName()));
}
return importers
.flatMap((importer) -> StringUtils.commaDelimitedListToSet(springFactories.getProperty(importer)).stream())
.toList();
}
private Stream<String> findMetaImporters(AnnotationMetadata annotationMetadata) {
return annotationMetadata.getAnnotationTypes()
.stream()
.filter((annotationType) -> isAutoConfigurationImporter(annotationType, annotationMetadata));
}
private boolean isAutoConfigurationImporter(String annotationType, AnnotationMetadata metadata) {
return metadata.getMetaAnnotationTypes(annotationType)
.contains("org.springframework.boot.autoconfigure.ImportAutoConfiguration");
}
}

View File

@ -1,55 +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.test.autoconfigure;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.tasks.SourceSet;
/**
* {@link Plugin} for projects that define test auto-configuration. When the
* {@link JavaPlugin} is applied it:
*
* <ul>
* <li>Add checks to ensure AutoConfigure*.import files and related annotations are
* correct</li>
* </ul>
*
* @author Andy Wilkinson
*/
public class TestAutoConfigurationPlugin implements Plugin<Project> {
@Override
public void apply(Project target) {
target.getPlugins().withType(JavaPlugin.class, (plugin) -> {
target.getTasks().register("checkAutoConfigureImports", CheckAutoConfigureImports.class, (task) -> {
SourceSet mainSourceSet = target.getExtensions()
.getByType(JavaPluginExtension.class)
.getSourceSets()
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
task.setSource(mainSourceSet.getResources());
ConfigurableFileCollection classpath = target.files(mainSourceSet.getRuntimeClasspath(),
target.getConfigurations().getByName(mainSourceSet.getRuntimeClasspathConfigurationName()));
task.setClasspath(classpath);
});
});
}
}

View File

@ -17,30 +17,229 @@
package org.springframework.boot.build.test.autoconfigure;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.json.JsonMapper;
import javax.inject.Inject;
import org.gradle.api.DefaultTask;
import org.gradle.api.Task;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.InputFile;
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.SourceSet;
import org.gradle.api.tasks.TaskAction;
import org.springframework.core.CollectionFactory;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
import org.springframework.util.StringUtils;
/**
* Metadata describing a module's test slices.
* A {@link Task} for generating metadata describing a project's test slices.
*
* @param module the module's name
* @param testSlices the module's test slices
* @author Andy Wilkinson
*/
record TestSliceMetadata(String module, List<TestSlice> testSlices) {
public abstract class TestSliceMetadata extends DefaultTask {
static TestSliceMetadata readFrom(File file) {
return JsonMapper.builder().build().readValue(file, TestSliceMetadata.class);
private final ObjectFactory objectFactory;
private FileCollection classpath;
private FileCollection importsFiles;
private FileCollection classesDirs;
@Inject
public TestSliceMetadata(ObjectFactory objectFactory) {
this.objectFactory = objectFactory;
Configuration testSliceMetadata = getProject().getConfigurations().maybeCreate("testSliceMetadata");
getProject().afterEvaluate((evaluated) -> evaluated.getArtifacts()
.add(testSliceMetadata.getName(), getOutputFile(), (artifact) -> artifact.builtBy(this)));
}
void writeTo(File file) {
JsonMapper.builder().enable(SerializationFeature.INDENT_OUTPUT).build().writeValue(file, this);
public void setSourceSet(SourceSet sourceSet) {
this.classpath = sourceSet.getRuntimeClasspath();
this.importsFiles = this.objectFactory.fileTree()
.from(new File(sourceSet.getOutput().getResourcesDir(), "META-INF/spring"));
this.importsFiles.filter((file) -> file.getName().endsWith(".imports"));
getSpringFactories().set(new File(sourceSet.getOutput().getResourcesDir(), "META-INF/spring.factories"));
this.classesDirs = sourceSet.getOutput().getClassesDirs();
}
record TestSlice(String annotation, List<String> importedAutoConfigurations) {
@OutputFile
public abstract RegularFileProperty getOutputFile();
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
abstract RegularFileProperty getSpringFactories();
@Classpath
FileCollection getClasspath() {
return this.classpath;
}
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
FileCollection getImportFiles() {
return this.importsFiles;
}
@Classpath
FileCollection getClassesDirs() {
return this.classesDirs;
}
@TaskAction
void documentTestSlices() throws IOException {
Properties testSlices = readTestSlices();
File outputFile = getOutputFile().getAsFile().get();
outputFile.getParentFile().mkdirs();
try (FileWriter writer = new FileWriter(outputFile)) {
testSlices.store(writer, null);
}
}
private Properties readTestSlices() throws IOException {
Properties testSlices = CollectionFactory.createSortedProperties(true);
try (URLClassLoader classLoader = new URLClassLoader(
StreamSupport.stream(this.classpath.spliterator(), false).map(this::toURL).toArray(URL[]::new))) {
MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(classLoader);
Properties springFactories = readSpringFactories(getSpringFactories().getAsFile().get());
readImportsFiles(springFactories, this.importsFiles);
for (File classesDir : this.classesDirs) {
addTestSlices(testSlices, classesDir, metadataReaderFactory, springFactories);
}
}
return testSlices;
}
/**
* Reads the given imports files and puts them in springFactories. The key is the file
* name, the value is the file contents, split by line, delimited with a comma. This
* is done to mimic the spring.factories structure.
* @param springFactories spring.factories parsed as properties
* @param importsFiles the imports files to read
*/
private void readImportsFiles(Properties springFactories, FileCollection importsFiles) {
for (File file : importsFiles.getFiles()) {
try {
List<String> lines = removeComments(Files.readAllLines(file.toPath()));
String fileNameWithoutExtension = file.getName()
.substring(0, file.getName().length() - ".imports".length());
springFactories.setProperty(fileNameWithoutExtension,
StringUtils.collectionToCommaDelimitedString(lines));
}
catch (IOException ex) {
throw new UncheckedIOException("Failed to read file " + file, ex);
}
}
}
private List<String> removeComments(List<String> lines) {
List<String> result = new ArrayList<>();
for (String line : lines) {
int commentIndex = line.indexOf('#');
if (commentIndex > -1) {
line = line.substring(0, commentIndex);
}
line = line.trim();
if (!line.isEmpty()) {
result.add(line);
}
}
return result;
}
private URL toURL(File file) {
try {
return file.toURI().toURL();
}
catch (MalformedURLException ex) {
throw new RuntimeException(ex);
}
}
private Properties readSpringFactories(File file) throws IOException {
Properties springFactories = new Properties();
try (Reader in = new FileReader(file)) {
springFactories.load(in);
}
return springFactories;
}
private void addTestSlices(Properties testSlices, File classesDir, MetadataReaderFactory metadataReaderFactory,
Properties springFactories) throws IOException {
try (Stream<Path> classes = Files.walk(classesDir.toPath())) {
classes.filter((path) -> path.toString().endsWith("Test.class"))
.map((path) -> getMetadataReader(path, metadataReaderFactory))
.filter((metadataReader) -> metadataReader.getClassMetadata().isAnnotation())
.forEach((metadataReader) -> addTestSlice(testSlices, springFactories, metadataReader));
}
}
private MetadataReader getMetadataReader(Path path, MetadataReaderFactory metadataReaderFactory) {
try {
return metadataReaderFactory.getMetadataReader(new FileSystemResource(path));
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
private void addTestSlice(Properties testSlices, Properties springFactories, MetadataReader metadataReader) {
testSlices.setProperty(metadataReader.getClassMetadata().getClassName(),
StringUtils.collectionToCommaDelimitedString(
getImportedAutoConfiguration(springFactories, metadataReader.getAnnotationMetadata())));
}
private SortedSet<String> getImportedAutoConfiguration(Properties springFactories,
AnnotationMetadata annotationMetadata) {
Stream<String> importers = findMetaImporters(annotationMetadata);
if (annotationMetadata.isAnnotated("org.springframework.boot.autoconfigure.ImportAutoConfiguration")) {
importers = Stream.concat(importers, Stream.of(annotationMetadata.getClassName()));
}
return importers
.flatMap((importer) -> StringUtils.commaDelimitedListToSet(springFactories.getProperty(importer)).stream())
.collect(Collectors.toCollection(TreeSet::new));
}
private Stream<String> findMetaImporters(AnnotationMetadata annotationMetadata) {
return annotationMetadata.getAnnotationTypes()
.stream()
.filter((annotationType) -> isAutoConfigurationImporter(annotationType, annotationMetadata));
}
private boolean isAutoConfigurationImporter(String annotationType, AnnotationMetadata metadata) {
return metadata.getMetaAnnotationTypes(annotationType)
.contains("org.springframework.boot.autoconfigure.ImportAutoConfiguration");
}
}

View File

@ -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.test.autoconfigure;
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.plugins.PluginContainer;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskProvider;
/**
* {@link Plugin} for projects that define one or more test slices. When applied, it:
*
* <ul>
* <li>Applies the {@link TestAutoConfigurationPlugin}
* </ul>
* Additionally, when the {@link JavaPlugin} is applied it:
*
* <ul>
* <li>Defines a task that produces metadata describing the test slices. The metadata is
* made available as an artifact in the {@code testSliceMetadata} configuration
* </ul>
*
* @author Andy Wilkinson
*/
public class TestSlicePlugin implements Plugin<Project> {
private static final String TEST_SLICE_METADATA_CONFIGURATION_NAME = "testSliceMetadata";
@Override
public void apply(Project target) {
PluginContainer plugins = target.getPlugins();
plugins.apply(TestAutoConfigurationPlugin.class);
plugins.withType(JavaPlugin.class, (plugin) -> {
TaskProvider<GenerateTestSliceMetadata> generateTestSliceMetadata = target.getTasks()
.register("generateTestSliceMetadata", GenerateTestSliceMetadata.class, (task) -> {
SourceSet mainSourceSet = target.getExtensions()
.getByType(JavaPluginExtension.class)
.getSourceSets()
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
task.setSourceSet(mainSourceSet);
task.getOutputFile().set(target.getLayout().getBuildDirectory().file("test-slice-metadata.json"));
});
addMetadataArtifact(target, generateTestSliceMetadata);
});
}
private void addMetadataArtifact(Project project, TaskProvider<GenerateTestSliceMetadata> task) {
project.getConfigurations().consumable(TEST_SLICE_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, "test-slice-metadata"));
});
});
project.getArtifacts().add(TEST_SLICE_METADATA_CONFIGURATION_NAME, task);
}
}

View File

@ -116,18 +116,6 @@ apiref-openjdk=https://docs.oracle.com/en/java/javase/17/docs/api
# === Code Links ===
code-spring-boot=https://github.com/{github-repo}/tree/{github-ref}
code-spring-boot-autoconfigure-src={code-spring-boot}/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure
code-spring-boot-batch-jdbc-src={code-spring-boot}/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc
code-spring-boot-batch-src={code-spring-boot}/module/spring-boot-batch/src/main/java/org/springframework/boot/batch
code-spring-boot-freemarker-src={code-spring-boot}/module/spring-boot-freemarker/src/main/java/org/springframework/boot/freemarker
code-spring-boot-groovy-templates-src={code-spring-boot}/module/spring-boot-groovy-templates/src/main/java/org/springframework/boot/groovy/template
code-spring-boot-hibernate-src={code-spring-boot}/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate
code-spring-boot-integration-src={code-spring-boot}/module/spring-boot-integration/src/main/java/org/springframework/boot/integration
code-spring-boot-jdbc-src={code-spring-boot}/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc
code-spring-boot-jooq-src={code-spring-boot}/module/spring-boot-jooq/src/main/java/org/springframework/boot/jooq
code-spring-boot-jpa-src={code-spring-boot}/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa
code-spring-boot-autoconfigure-src={code-spring-boot}/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure
code-spring-boot-latest=https://github.com/{github-repo}/tree/main
code-spring-boot-servlet-src={code-spring-boot}/module/spring-boot-servlet/src/main/java/org/springframework/boot/servlet
code-spring-boot-thymeleaf-src={code-spring-boot}/module/spring-boot-thymeleaf/src/main/java/org/springframework/boot/thymeleaf
code-spring-boot-webmvc-src={code-spring-boot}/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc

View File

@ -77,9 +77,7 @@ class ConventionsPluginTests {
out.println(" id 'org.springframework.boot.conventions'");
out.println("}");
out.println("version = '1.2.3'");
out.println("java {");
out.println(" sourceCompatibility = '17'");
out.println("}");
out.println("sourceCompatibility = '17'");
out.println("description 'Test project for manifest customization'");
out.println("jar.archiveFileName = 'test.jar'");
}
@ -109,9 +107,7 @@ class ConventionsPluginTests {
out.println(" id 'org.springframework.boot.conventions'");
out.println("}");
out.println("version = '1.2.3'");
out.println("java {");
out.println(" sourceCompatibility = '17'");
out.println("}");
out.println("sourceCompatibility = '17'");
out.println("description 'Test'");
}
runGradle("assemble");
@ -140,9 +136,7 @@ class ConventionsPluginTests {
out.println(" id 'org.springframework.boot.conventions'");
out.println("}");
out.println("version = '1.2.3'");
out.println("java {");
out.println(" sourceCompatibility = '17'");
out.println("}");
out.println("sourceCompatibility = '17'");
out.println("description 'Test'");
}
runGradle("assemble");

View File

@ -57,8 +57,6 @@ class ArchitectureCheckTests {
private static final String SPRING_CONTEXT = "org.springframework:spring-context:6.2.9";
private static final String JUNIT_JUPITER = "org.junit.jupiter:junit-jupiter:5.12.0";
private static final String SPRING_INTEGRATION_JMX = "org.springframework.integration:spring-integration-jmx:6.5.1";
private GradleBuild gradleBuild;
@ -285,29 +283,6 @@ class ArchitectureCheckTests {
build(this.gradleBuild.withNullMarked(true), Task.CHECK_ARCHITECTURE_TEST);
}
@Test
void whenEnumSourceValueIsInferredShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_TEST, "junit/enumsource/inferredfromparametertype");
build(this.gradleBuild.withDependencies(JUNIT_JUPITER), Task.CHECK_ARCHITECTURE_TEST);
}
@Test
void whenEnumSourceValueIsNotTheSameAsTypeOfMethodsFirstParameterShouldSucceedAndWriteEmptyReport()
throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_TEST, "junit/enumsource/valuenecessary");
build(this.gradleBuild.withDependencies(JUNIT_JUPITER), Task.CHECK_ARCHITECTURE_TEST);
}
@Test
void whenEnumSourceValueIsSameAsTypeOfMethodsFirstParameterShouldFailAndWriteReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_TEST, "junit/enumsource/sameasparametertype");
buildAndFail(this.gradleBuild.withDependencies(JUNIT_JUPITER), Task.CHECK_ARCHITECTURE_TEST,
"method <org.springframework.boot.build.architecture.junit.enumsource.sameasparametertype"
+ ".EnumSourceSameAsParameterType.exampleMethod(org.springframework.boot.build."
+ "architecture.junit.enumsource.sameasparametertype.EnumSourceSameAsParameterType$Example)>",
"should not have a value that is the same as the type of the method's first parameter");
}
private void prepareTask(Task task, String... sourceDirectories) throws IOException {
for (String sourceDirectory : sourceDirectories) {
FileSystemUtils.copyRecursively(

View File

@ -1,34 +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.junit.enumsource.inferredfromparametertype;
import org.junit.jupiter.params.provider.EnumSource;
class EnumSourceInferredFromParameterType {
@EnumSource
void exampleMethod(Example example) {
}
enum Example {
ONE, TWO, THREE
}
}

View File

@ -1,34 +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.junit.enumsource.sameasparametertype;
import org.junit.jupiter.params.provider.EnumSource;
class EnumSourceSameAsParameterType {
@EnumSource(Example.class)
void exampleMethod(Example example) {
}
enum Example {
ONE, TWO, THREE
}
}

View File

@ -1,34 +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.junit.enumsource.valuenecessary;
import org.junit.jupiter.params.provider.EnumSource;
class EnumSourceValueNecessary {
@EnumSource(Example.class)
void exampleMethod(String thing, Example example) {
}
enum Example {
ONE, TWO, THREE
}
}

View File

@ -31,7 +31,6 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.junit.jupiter.params.support.ParameterDeclarations;
import static org.assertj.core.api.Assertions.assertThat;
@ -245,8 +244,7 @@ class DependencyVersionUpgradeTests {
static class InputProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ParameterDeclarations parameterDeclarations,
ExtensionContext context) {
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
Method testMethod = context.getRequiredTestMethod();
Stream<Arguments> artifactVersions = artifactVersions(testMethod)
.map((artifactVersion) -> Arguments.of(VersionType.ARTIFACT_VERSION.parse(artifactVersion.current()),

View File

@ -1,50 +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.test.autoconfigure;
import java.io.File;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.build.test.autoconfigure.TestSliceMetadata.TestSlice;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link TestSliceMetadata}.
*
* @author Andy Wilkinson
*/
public class TestSliceMetadataTests {
@TempDir
private File temp;
@Test
void roundtripJson() {
TestSliceMetadata source = new TestSliceMetadata("example",
List.of(new TestSlice("ExampleOneTest", List.of("com.example.OneAutoConfiguration")),
new TestSlice("ExampleTwoTest", List.of("com.example.TwoAutoConfiguration"))));
File metadataFile = new File(this.temp, "metadata.json");
source.writeTo(metadataFile);
TestSliceMetadata readBack = TestSliceMetadata.readFrom(metadataFile);
assertThat(source).isEqualTo(readBack);
}
}

View File

@ -166,7 +166,6 @@ class TestFailuresPluginIntegrationTests {
writer.println("dependencies {");
writer.println(" testImplementation 'org.junit.jupiter:junit-jupiter:5.6.0'");
writer.println(" testImplementation 'org.assertj:assertj-core:3.11.1'");
writer.println(" testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.6.0'");
writer.println("}");
writer.println();
writer.println("test {");

View File

@ -21,11 +21,11 @@ import java.util.function.Consumer;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.buildpack.platform.docker.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent;
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;

View File

@ -21,7 +21,7 @@ import java.util.stream.IntStream;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.buildpack.platform.docker.ApiVersion;
import org.springframework.boot.buildpack.platform.docker.type.ApiVersion;
import org.springframework.util.StringUtils;
/**

View File

@ -21,11 +21,11 @@ import java.util.function.Consumer;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.buildpack.platform.docker.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent;
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;

View File

@ -28,8 +28,8 @@ import java.util.function.Function;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.buildpack.platform.docker.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.boot.buildpack.platform.io.TarArchive;

View File

@ -24,7 +24,6 @@ import org.jspecify.annotations.Nullable;
import org.springframework.boot.buildpack.platform.docker.DockerApi;
import org.springframework.boot.buildpack.platform.docker.DockerLog;
import org.springframework.boot.buildpack.platform.docker.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener;
import org.springframework.boot.buildpack.platform.docker.TotalProgressPushListener;
@ -35,6 +34,7 @@ import org.springframework.boot.buildpack.platform.docker.transport.DockerEngine
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageArchive;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.IOBiConsumer;
import org.springframework.boot.buildpack.platform.io.TarArchive;

View File

@ -28,10 +28,10 @@ import com.sun.jna.Platform;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.buildpack.platform.build.Cache.Bind;
import org.springframework.boot.buildpack.platform.docker.ApiVersion;
import org.springframework.boot.buildpack.platform.docker.DockerApi;
import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent;
import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost;
import org.springframework.boot.buildpack.platform.docker.type.ApiVersion;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig;
import org.springframework.boot.buildpack.platform.docker.type.ContainerContent;

View File

@ -34,12 +34,14 @@ import org.springframework.boot.buildpack.platform.docker.PushImageUpdateEvent.E
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConnectionConfiguration;
import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport;
import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response;
import org.springframework.boot.buildpack.platform.docker.type.ApiVersion;
import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig;
import org.springframework.boot.buildpack.platform.docker.type.ContainerContent;
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageArchive;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
import org.springframework.boot.buildpack.platform.io.IOBiConsumer;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.docker;
package org.springframework.boot.buildpack.platform.docker.type;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -28,7 +28,7 @@ import org.springframework.util.Assert;
*
* @author Phillip Webb
* @author Scott Frederick
* @since 4.0.0
* @since 3.4.0
*/
public final class ApiVersion {

View File

@ -14,20 +14,19 @@
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.docker;
package org.springframework.boot.buildpack.platform.docker.type;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.util.Assert;
/**
* A platform specification for a Docker image.
*
* @author Scott Frederick
* @since 4.0.0
* @since 3.4.0
*/
public class ImagePlatform {

View File

@ -20,7 +20,7 @@ import java.util.stream.IntStream;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.ApiVersion;
import org.springframework.boot.buildpack.platform.docker.type.ApiVersion;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;

View File

@ -37,9 +37,9 @@ import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.buildpack.platform.docker.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ImageName;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.boot.buildpack.platform.io.TarArchive;

View File

@ -31,7 +31,6 @@ import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
import org.springframework.boot.buildpack.platform.docker.DockerLog;
import org.springframework.boot.buildpack.platform.docker.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication;
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
@ -40,6 +39,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ContainerReferenc
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageArchive;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.TarArchive;

View File

@ -41,7 +41,6 @@ import org.springframework.boot.buildpack.platform.docker.DockerApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
import org.springframework.boot.buildpack.platform.docker.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConnectionConfiguration;
import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
@ -49,6 +48,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig;
import org.springframework.boot.buildpack.platform.docker.type.ContainerContent;
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
import org.springframework.boot.buildpack.platform.io.IOConsumer;

View File

@ -25,10 +25,10 @@ import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent;
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
import org.springframework.util.FileCopyUtils;

View File

@ -45,12 +45,14 @@ import org.springframework.boot.buildpack.platform.docker.DockerApi.SystemApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport;
import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response;
import org.springframework.boot.buildpack.platform.docker.type.ApiVersion;
import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig;
import org.springframework.boot.buildpack.platform.docker.type.ContainerContent;
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageArchive;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
import org.springframework.boot.buildpack.platform.io.Content;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.docker;
package org.springframework.boot.buildpack.platform.docker.type;
import java.util.Arrays;

View File

@ -14,13 +14,12 @@
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.docker;
package org.springframework.boot.buildpack.platform.docker.type;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
import static org.assertj.core.api.Assertions.assertThat;
@ -65,7 +64,7 @@ class ImagePlatformTests extends AbstractJsonTests {
}
private Image getImage() throws IOException {
return Image.of(getContent("type/image.json"));
return Image.of(getContent("image.json"));
}
}

View File

@ -79,7 +79,4 @@
<suppress files="EntityManagerFactoryBuilder\.java" checks="NoWhitespaceBefore" message="'...' is preceded with whitespace"/>
<suppress files="DockerApi\.java" checks="NoWhitespaceBefore" message="'...' is preceded with whitespace"/>
<suppress files="InvocationContext\.java" checks="NoWhitespaceBefore" message="'...' is preceded with whitespace"/>
<!-- https://github.com/apache/logging-log4j2/issues/2769#issuecomment-3049020222 -->
<suppress files="SpringProfileArbiter\.java" checks="SpringMethodVisibility"/>
<suppress files="StructuredLogLayout\.java" checks="SpringMethodVisibility"/>
</suppressions>

View File

@ -6,10 +6,7 @@
<module name="SuppressionFilter">
<property name="file" value="${config_loc}/checkstyle-suppressions.xml"/>
</module>
<module name="io.spring.javaformat.checkstyle.SpringChecks">
<property name="avoidStaticImportExcludes"
value="org.springframework.boot.autoconfigure.AutoConfigurationImportedCondition.importedAutoConfiguration" />
</module>
<module name="io.spring.javaformat.checkstyle.SpringChecks" />
<module name="io.spring.javaformat.checkstyle.check.SpringHeaderCheck">
<property name="headerFile" value="${config_loc}/checkstyle-header.txt"/>
</module>

View File

@ -17,7 +17,7 @@
<subpackage name="ansi">
<disallow pkg="org.springframework.boot" exact-match="true"/>
</subpackage>
<subpackage name="availability">
<subpackage name="avilability">
<disallow pkg="org.springframework.boot" exact-match="true"/>
</subpackage>
<subpackage name="bootstrap">
@ -91,6 +91,8 @@
<disallow pkg="jakarta.servlet" />
<!-- Common -->
<subpackage name="client">
</subpackage>
<subpackage name="context">
<allow pkg="org.springframework.context" />
<subpackage name="servlet">
@ -109,10 +111,6 @@
<allow pkg="org.springframework.boot.web.servlet" />
</subpackage>
</subpackage>
<subpackage name="client">
<allow pkg="jakarta.servlet" />
<allow pkg="org.springframework.context" />
</subpackage>
<subpackage name="context">
<allow pkg="org.springframework.context" />
</subpackage>

View File

@ -19,7 +19,7 @@ style:
active: true
NewLineAtEndOfFile:
active: true
UnusedImport:
UnusedImports:
active: true
WildcardImport:
active: true

View File

@ -136,25 +136,20 @@ def dependenciesOf(String version) {
modules += [
"spring-boot-jersey"
]
}
if (version.equals("4.0.0-M1")) {
modules += [
"spring-boot-metrics",
"spring-boot-observation",
"spring-boot-tracing"
]
}
else {
modules += [
"spring-boot-micrometer-metrics",
"spring-boot-micrometer-observation",
"spring-boot-micrometer-tracing"
]
}
if (version.equals("4.0.0-RC1")) {
modules += [
"spring-boot-batch-jdbc"
]
if (version.equals("4.0.0-M1")) {
modules += [
"spring-boot-metrics",
"spring-boot-observation",
"spring-boot-tracing"
]
}
else {
modules += [
"spring-boot-micrometer-metrics",
"spring-boot-micrometer-observation",
"spring-boot-micrometer-tracing"
]
}
}
return modules
}

Some files were not shown because too many files have changed in this diff Show More