Add custom-starter module: a Boot 4 auto-configuration starter with conditions, metadata and runner tests
Two-jar starter (autoconfigure + starter), a demo app, and 17 captured transcripts covering AutoConfiguration.imports, @ConditionalOn*, ordering, optional Jackson 3 integration, configuration metadata and the Boot 4 module split. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Uu7q8vPeREyT4218EJPzz1
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
# custom-starter
|
||||||
|
|
||||||
|
A Spring Boot 4 starter written from scratch, and the application that uses it. Companion to
|
||||||
|
[Write Your Own Spring Boot 4 Starter: Auto-Configuration, @Conditional and Properties](https://ankurm.com/write-your-own-spring-boot-4-starter-auto-configuration-conditional-properties/).
|
||||||
|
|
||||||
|
The starter provides one thing, a `Masker` that hides all but the last few characters of a value, plus an optional
|
||||||
|
Jackson integration that writes a `Sensitive` value masked. The subject is the packaging, not the masking.
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Spring Boot | 4.1.1 (parent pom, published 20 Aug 2026) |
|
||||||
|
| Spring Framework | 7.0.9 |
|
||||||
|
| Java | 25 |
|
||||||
|
| Maven | 3.9 |
|
||||||
|
|
||||||
|
There is **no `docs/` folder**. The explanations live in the article, in collapsible sections next to the paragraph
|
||||||
|
each one extends, and every console block in the article is quoted from a file in [`output/`](output).
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
| Module | What it is |
|
||||||
|
|---|---|
|
||||||
|
| [`masker-spring-boot-autoconfigure/`](masker-spring-boot-autoconfigure) | `MaskerAutoConfiguration`, `MaskerJacksonAutoConfiguration`, `MaskerProperties`, the `AutoConfiguration.imports` file, the configuration metadata, and the tests |
|
||||||
|
| [`masker-spring-boot-starter/`](masker-spring-boot-starter) | The dependency an application adds. No code, only a pom |
|
||||||
|
| [`masker-demo/`](masker-demo) | An application that adds the starter and nothing else |
|
||||||
|
|
||||||
|
`masker-spring-boot-autoconfigure/src/test/java/com/ankurm/traps/` holds auto-configurations that are wrong on
|
||||||
|
purpose. `src/broken/` holds a source file that is made not to compile on purpose. Neither is part of the shipped jar.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export JAVA_HOME=/path/to/jdk-25
|
||||||
|
./scripts/run-all.sh # tests, then the jar and failure captures: regenerates all of output/
|
||||||
|
mvn -B test # only the tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Captured output
|
||||||
|
|
||||||
|
| File | What it shows | Made by |
|
||||||
|
|---|---|---|
|
||||||
|
| [`01`](output/01-defaults-without-any-configuration.txt) | the auto-configuration with no properties and no user beans | `MaskerAutoConfigurationTests` |
|
||||||
|
| [`02`](output/02-properties-and-the-off-switch.txt) | `masker.*` properties and `masker.enabled=false` | `MaskerAutoConfigurationTests` |
|
||||||
|
| [`03`](output/03-conditional-on-missing-bean-and-registration-order.txt) | `@ConditionalOnMissingBean` backing off, and the registration order that decides it | `MaskerAutoConfigurationTests` |
|
||||||
|
| [`04`](output/04-jackson-integration-active.txt) | the Jackson module registered, a `Sensitive` written masked, the recorded condition outcomes | `JacksonIntegrationTests` |
|
||||||
|
| [`05`](output/05-jackson-integration-absent-without-error.txt) | Jackson hidden with `FilteredClassLoader`: no failure, no module | `JacksonIntegrationTests` |
|
||||||
|
| [`06`](output/06-conditional-on-bean-needs-an-ordering.txt) | `@ConditionalOnBean` with and without an ordering constraint, and the sorted order | `ConditionTrapTests` |
|
||||||
|
| [`07`](output/07-conditional-on-class-on-a-bean-method.txt) | `@ConditionalOnClass` on a `@Bean` method, on a classpath with no Jackson | `capture-jar-facts.sh` |
|
||||||
|
| [`08`](output/08-auto-configuration-missing-from-the-imports-file.txt) | an auto-configuration missing from the imports file: passes a runner test, absent everywhere | `ForgottenImportsEntryTests` |
|
||||||
|
| [`09`](output/09-reading-the-imports-file-in-a-test.txt) | `ImportCandidates`, the guard test for the omission | `ForgottenImportsEntryTests` |
|
||||||
|
| [`10`](output/10-demo-app-with-only-the-starter.txt) | an application with only the starter dependency, and Boot's recorded conditions | `DemoApplicationTests` |
|
||||||
|
| [`11`](output/11-demo-app-properties-and-off-switch.txt) | the same application with properties, then switched off | `DemoApplicationTests` |
|
||||||
|
| [`12`](output/12-jar-contents-and-dependency-trees.txt) | what is in the two jars, and what the starter pulls in | `capture-jar-facts.sh` |
|
||||||
|
| [`13`](output/13-configuration-metadata-in-the-jar.txt) | the generated and the hand-written configuration metadata, and the auto-configuration conditions metadata | `capture-jar-facts.sh` |
|
||||||
|
| [`14`](output/14-boot4-autoconfiguration-locations.txt) | Boot 4's split of `spring-boot-autoconfigure`, and the Boot 3 import that no longer compiles | `capture-jar-facts.sh` |
|
||||||
|
| [`15`](output/15-annotation-processor-version-trap.txt) | the processor path failing when the version is taken from your own parent | `capture-jar-facts.sh` |
|
||||||
|
| [`17`](output/17-where-the-classes-live.txt) | which Boot 4.1.1 jar holds each class this starter imports | `capture-jar-facts.sh` |
|
||||||
|
| [`16`](output/16-conditional-on-property-without-match-if-missing.txt) | `@ConditionalOnProperty` without `matchIfMissing` | `ConditionTrapTests` |
|
||||||
|
|
||||||
|
Transcripts are deterministic; only build-directory paths are rewritten, to `<custom-starter>`.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?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>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>com.ankurm.masker</groupId>
|
||||||
|
<artifactId>masker-parent</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>masker-demo</artifactId>
|
||||||
|
<name>masker-demo</name>
|
||||||
|
<description>An ordinary application that adds the starter and nothing else</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.ankurm.masker</groupId>
|
||||||
|
<artifactId>masker-spring-boot-starter</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-jackson</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.ankurm.demo;
|
||||||
|
|
||||||
|
import com.ankurm.masker.Sensitive;
|
||||||
|
|
||||||
|
/** An application type that carries a value which must not reach a log or a response in clear. */
|
||||||
|
public record Customer(String name, Sensitive card) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ankurm.demo;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ordinary application. Its only knowledge of the masker is the one dependency in its pom:
|
||||||
|
* no {@code @Import}, no {@code @Bean}, no {@code @ComponentScan} mentioning the starter.
|
||||||
|
*/
|
||||||
|
@SpringBootApplication
|
||||||
|
public class DemoApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(DemoApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.ankurm.demo;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import com.ankurm.masker.Masker;
|
||||||
|
import com.ankurm.masker.MaskerJacksonModule;
|
||||||
|
import com.ankurm.masker.MaskerProperties;
|
||||||
|
import com.ankurm.masker.Sensitive;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.Banner;
|
||||||
|
import org.springframework.boot.WebApplicationType;
|
||||||
|
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/** The consumer's view: a real application started with SpringApplication, the starter reached only through its pom. */
|
||||||
|
class DemoApplicationTests {
|
||||||
|
|
||||||
|
private static ConfigurableApplicationContext start(String... args) {
|
||||||
|
return new SpringApplicationBuilder(DemoApplication.class).web(WebApplicationType.NONE)
|
||||||
|
.bannerMode(Banner.Mode.OFF).properties("logging.level.root=WARN").run(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addingTheDependencyIsEnough() {
|
||||||
|
try (Transcript t = new Transcript("10-demo-app-with-only-the-starter.txt",
|
||||||
|
"An application that adds masker-spring-boot-starter and nothing else");
|
||||||
|
ConfigurableApplicationContext context = start()) {
|
||||||
|
assertThat(context).isNotNull();
|
||||||
|
t.line("Masker beans : %s", Arrays.toString(context.getBeanNamesForType(Masker.class)));
|
||||||
|
t.line("MaskerProperties beans : %s", Arrays.toString(context.getBeanNamesForType(MaskerProperties.class)));
|
||||||
|
t.line("Jackson module beans : %s", Arrays.toString(context.getBeanNamesForType(MaskerJacksonModule.class)));
|
||||||
|
String json = context.getBean(JsonMapper.class).writeValueAsString(
|
||||||
|
new Customer("Asha", new Sensitive("4111111111111111")));
|
||||||
|
t.line("JSON of a Customer : %s", json);
|
||||||
|
assertThat(json).isEqualTo("{\"name\":\"Asha\",\"card\":\"************1111\"}");
|
||||||
|
t.section("what Boot recorded (the same lines --debug prints)");
|
||||||
|
ReportSupport.print(t, context, "com.ankurm.masker.Masker");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void propertiesAndTheOffSwitchWorkFromTheApplication() {
|
||||||
|
try (Transcript t = new Transcript("11-demo-app-properties-and-off-switch.txt",
|
||||||
|
"The same application configured with masker.* properties, then switched off")) {
|
||||||
|
t.section("--masker.replacement=# --masker.visible-tail=6");
|
||||||
|
try (ConfigurableApplicationContext context = start("--masker.replacement=#", "--masker.visible-tail=6")) {
|
||||||
|
String json = context.getBean(JsonMapper.class).writeValueAsString(
|
||||||
|
new Customer("Asha", new Sensitive("4111111111111111")));
|
||||||
|
t.line("JSON of a Customer : %s", json);
|
||||||
|
assertThat(json).contains("##########111111");
|
||||||
|
}
|
||||||
|
|
||||||
|
t.section("--masker.enabled=false");
|
||||||
|
try (ConfigurableApplicationContext context = start("--masker.enabled=false")) {
|
||||||
|
t.line("Masker beans : %s", Arrays.toString(context.getBeanNamesForType(Masker.class)));
|
||||||
|
t.line("Jackson module beans : %s", Arrays.toString(context.getBeanNamesForType(MaskerJacksonModule.class)));
|
||||||
|
assertThat(context).isNotNull();
|
||||||
|
assertThat(context.getBeanNamesForType(Masker.class)).isEmpty();
|
||||||
|
t.section("what Boot recorded");
|
||||||
|
ReportSupport.print(t, context, "com.ankurm.masker.Masker");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ankurm.demo;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
|
||||||
|
/** Prints the condition outcomes Boot recorded for the classes whose name contains a filter, as {@code --debug} would. */
|
||||||
|
final class ReportSupport {
|
||||||
|
|
||||||
|
private ReportSupport() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print(Transcript t, ConfigurableApplicationContext context, String filter) {
|
||||||
|
ConditionEvaluationReport report = ConditionEvaluationReport.get(context.getBeanFactory());
|
||||||
|
report.getConditionAndOutcomesBySource().entrySet().stream()
|
||||||
|
.filter(e -> e.getKey().contains(filter))
|
||||||
|
.sorted(Map.Entry.comparingByKey())
|
||||||
|
.forEach(e -> {
|
||||||
|
t.line("%s [%s]", e.getKey(), e.getValue().isFullMatch() ? "matched" : "not matched");
|
||||||
|
e.getValue().forEach(o -> t.line(" %s %s",
|
||||||
|
o.getOutcome().isMatch() ? "matched:" : "did not match:", o.getOutcome().getMessage()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.ankurm.demo;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a numbered transcript under {@code custom-starter/output/} and echoes it to the console.
|
||||||
|
* Every console block quoted in the article comes out of one of these files verbatim.
|
||||||
|
*/
|
||||||
|
public final class Transcript implements AutoCloseable {
|
||||||
|
|
||||||
|
private static final Path ROOT =
|
||||||
|
Path.of(System.getProperty("basedir", ".")).toAbsolutePath().normalize().getParent();
|
||||||
|
|
||||||
|
private final Path path;
|
||||||
|
private final StringWriter buffer = new StringWriter();
|
||||||
|
private final PrintWriter out = new PrintWriter(buffer);
|
||||||
|
|
||||||
|
public Transcript(String fileName, String title) {
|
||||||
|
this.path = ROOT.resolve("output").resolve(fileName);
|
||||||
|
out.println("# " + title);
|
||||||
|
out.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Transcript line(String format, Object... args) {
|
||||||
|
out.println(args.length == 0 ? format : String.format(format, args));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Transcript section(String heading) {
|
||||||
|
out.println();
|
||||||
|
out.println("--- " + heading + " ---");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
out.flush();
|
||||||
|
// Absolute paths of whoever ran the build are environment noise, not a finding.
|
||||||
|
String text = buffer.toString().replace(ROOT.toString(), "<custom-starter>");
|
||||||
|
try {
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
Files.writeString(path, text);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException("could not write " + path, e);
|
||||||
|
}
|
||||||
|
System.out.print(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?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>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>com.ankurm.masker</groupId>
|
||||||
|
<artifactId>masker-parent</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>masker-spring-boot-autoconfigure</artifactId>
|
||||||
|
<name>masker-spring-boot-autoconfigure</name>
|
||||||
|
<description>The auto-configuration, the properties and the two optional integrations</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- the infrastructure every auto-configuration needs: @AutoConfiguration and the @Conditional* annotations -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- optional: the Jackson integration only activates when the application brings Jackson itself -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-jackson</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
</path>
|
||||||
|
<path>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-autoconfigure-processor</artifactId>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package broken;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
||||||
|
|
||||||
|
/** How a Boot 3 starter orders itself after Jackson. Not part of the build: scripts/capture-jar-facts.sh compiles it on purpose. */
|
||||||
|
@AutoConfiguration(after = JacksonAutoConfiguration.class)
|
||||||
|
public class OldJacksonImport {
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package com.ankurm.masker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The one thing this starter provides: turn a sensitive string into one that is safe to log or return.
|
||||||
|
*
|
||||||
|
* <p>An application that wants different behaviour declares its own {@code Masker} bean and the
|
||||||
|
* auto-configuration backs off (see {@link MaskerAutoConfiguration}).
|
||||||
|
*/
|
||||||
|
public interface Masker {
|
||||||
|
|
||||||
|
String mask(String value);
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ankurm.masker;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The starter's main auto-configuration. It is listed in
|
||||||
|
* {@code META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports}; that file, not this
|
||||||
|
* annotation, is what makes Boot load it.
|
||||||
|
*/
|
||||||
|
@AutoConfiguration
|
||||||
|
@ConditionalOnProperty(prefix = "masker", name = "enabled", matchIfMissing = true)
|
||||||
|
@EnableConfigurationProperties(MaskerProperties.class)
|
||||||
|
public class MaskerAutoConfiguration {
|
||||||
|
|
||||||
|
/** Backs off when the application defines its own {@link Masker}. */
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public Masker masker(MaskerProperties properties) {
|
||||||
|
return new TailMasker(properties.replacement(), properties.visibleTail());
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ankurm.masker;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers {@link MaskerJacksonModule} when Jackson is on the classpath and a {@link Masker} exists.
|
||||||
|
*
|
||||||
|
* <p>{@code @ConditionalOnClass} names {@link JsonMapper} in an annotation, which Spring reads from the class file
|
||||||
|
* without loading it, so this class is safe to have on a classpath that has no Jackson. It must therefore stay a
|
||||||
|
* separate auto-configuration: a nested class or a {@code @Bean} method signature that mentions Jackson would not be.
|
||||||
|
*/
|
||||||
|
@AutoConfiguration(after = MaskerAutoConfiguration.class)
|
||||||
|
@ConditionalOnClass(JsonMapper.class)
|
||||||
|
@ConditionalOnBean(Masker.class)
|
||||||
|
public class MaskerJacksonAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public MaskerJacksonModule maskerJacksonModule(Masker masker) {
|
||||||
|
return new MaskerJacksonModule(masker);
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ankurm.masker;
|
||||||
|
|
||||||
|
import tools.jackson.core.JsonGenerator;
|
||||||
|
import tools.jackson.databind.SerializationContext;
|
||||||
|
import tools.jackson.databind.module.SimpleModule;
|
||||||
|
import tools.jackson.databind.ser.std.StdSerializer;
|
||||||
|
|
||||||
|
/** Writes {@link Sensitive} values through a {@link Masker}. Boot registers any {@code JacksonModule} bean itself. */
|
||||||
|
public class MaskerJacksonModule extends SimpleModule {
|
||||||
|
|
||||||
|
public MaskerJacksonModule(Masker masker) {
|
||||||
|
super("masker");
|
||||||
|
addSerializer(Sensitive.class, new StdSerializer<>(Sensitive.class) {
|
||||||
|
@Override
|
||||||
|
public void serialize(Sensitive value, JsonGenerator generator, SerializationContext context) {
|
||||||
|
generator.writeString(masker.mask(value.value()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ankurm.masker;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings for the auto-configured {@link Masker}.
|
||||||
|
*
|
||||||
|
* @param enabled whether the starter configures anything at all
|
||||||
|
* @param replacement the text that stands in for each hidden character
|
||||||
|
* @param visibleTail how many characters at the end of the value stay readable
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties("masker")
|
||||||
|
public record MaskerProperties(
|
||||||
|
@DefaultValue("true") boolean enabled,
|
||||||
|
@DefaultValue("*") String replacement,
|
||||||
|
@DefaultValue("4") int visibleTail) {
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package com.ankurm.masker;
|
||||||
|
|
||||||
|
/** A value that must never be serialised in clear. With the Jackson integration active it is written masked. */
|
||||||
|
public record Sensitive(String value) {
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package com.ankurm.masker;
|
||||||
|
|
||||||
|
/** Replaces everything except the last {@code visibleTail} characters. */
|
||||||
|
public class TailMasker implements Masker {
|
||||||
|
|
||||||
|
private final String replacement;
|
||||||
|
private final int visibleTail;
|
||||||
|
|
||||||
|
public TailMasker(String replacement, int visibleTail) {
|
||||||
|
this.replacement = replacement;
|
||||||
|
this.visibleTail = visibleTail;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String mask(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int hidden = Math.max(0, value.length() - visibleTail);
|
||||||
|
return replacement.repeat(hidden) + value.substring(hidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"hints": [
|
||||||
|
{
|
||||||
|
"name": "masker.replacement",
|
||||||
|
"values": [
|
||||||
|
{ "value": "*", "description": "Asterisk (the default)." },
|
||||||
|
{ "value": "#", "description": "Hash sign." },
|
||||||
|
{ "value": "•", "description": "Bullet." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
com.ankurm.masker.MaskerAutoConfiguration
|
||||||
|
com.ankurm.masker.MaskerJacksonAutoConfiguration
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
package com.ankurm.consumer;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
/** A consumer: its own package, the starter on the classpath, nothing else. */
|
||||||
|
@SpringBootApplication
|
||||||
|
public class ConsumerApplication {
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
package com.ankurm.masker;
|
||||||
|
|
||||||
|
import com.ankurm.traps.DefaultOffAutoConfiguration;
|
||||||
|
import com.ankurm.traps.EarlyModuleAutoConfiguration;
|
||||||
|
import com.ankurm.traps.Marker;
|
||||||
|
import com.ankurm.traps.OrderedModuleAutoConfiguration;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
|
||||||
|
import org.springframework.boot.context.annotation.Configurations;
|
||||||
|
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
|
||||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/** Correct-looking conditions that are wrong: one runs too early, one defaults to off. */
|
||||||
|
class ConditionTrapTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aConditionalOnBeanNeedsAnOrderingOrItDependsOnTheClassName() {
|
||||||
|
try (Transcript t = new Transcript("06-conditional-on-bean-needs-an-ordering.txt",
|
||||||
|
"@ConditionalOnBean(JsonMapper) with and without an ordering constraint")) {
|
||||||
|
AutoConfigurations configurations = AutoConfigurations.of(
|
||||||
|
JacksonAutoConfiguration.class, OrderedModuleAutoConfiguration.class,
|
||||||
|
EarlyModuleAutoConfiguration.class, MaskerAutoConfiguration.class);
|
||||||
|
t.section("the order Boot evaluates them in");
|
||||||
|
for (Class<?> c : Configurations.getClasses(configurations)) {
|
||||||
|
t.line("%s", c.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
new ApplicationContextRunner().withInitializer(new ConditionEvaluationReportLoggingListener())
|
||||||
|
.withConfiguration(configurations).run(context -> {
|
||||||
|
t.section("the beans each one produced");
|
||||||
|
for (Marker marker : context.getBeansOfType(Marker.class).values()) {
|
||||||
|
t.line("Marker bean created by %s", marker.createdBy());
|
||||||
|
}
|
||||||
|
assertThat(context.getBeansOfType(Marker.class).values())
|
||||||
|
.extracting(Marker::createdBy).containsExactly("OrderedModuleAutoConfiguration");
|
||||||
|
t.section("what Boot recorded");
|
||||||
|
ReportSupport.print(t, (ConfigurableApplicationContext) context.getSourceApplicationContext(),
|
||||||
|
"ModuleAutoConfiguration");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aPropertyConditionWithoutMatchIfMissingIsOffUntilSomeoneSetsIt() {
|
||||||
|
try (Transcript t = new Transcript("16-conditional-on-property-without-match-if-missing.txt",
|
||||||
|
"@ConditionalOnProperty(prefix = \"traps\", name = \"enabled\") with no matchIfMissing")) {
|
||||||
|
var runner = new ApplicationContextRunner().withInitializer(new ConditionEvaluationReportLoggingListener())
|
||||||
|
.withConfiguration(AutoConfigurations.of(DefaultOffAutoConfiguration.class));
|
||||||
|
t.section("no property set");
|
||||||
|
runner.run(context -> {
|
||||||
|
t.line("Marker beans : %d", context.getBeansOfType(Marker.class).size());
|
||||||
|
assertThat(context).doesNotHaveBean(Marker.class);
|
||||||
|
ReportSupport.print(t, (ConfigurableApplicationContext) context.getSourceApplicationContext(),
|
||||||
|
"DefaultOffAutoConfiguration");
|
||||||
|
});
|
||||||
|
t.section("traps.enabled=true");
|
||||||
|
runner.withPropertyValues("traps.enabled=true").run(context -> {
|
||||||
|
t.line("Marker beans : %d", context.getBeansOfType(Marker.class).size());
|
||||||
|
assertThat(context).hasSingleBean(Marker.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package com.ankurm.masker;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import com.ankurm.consumer.ConsumerApplication;
|
||||||
|
import com.ankurm.masker.scanapp.ForgottenAutoConfiguration;
|
||||||
|
import com.ankurm.masker.scanapp.OwnersApplication;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.Banner;
|
||||||
|
import org.springframework.boot.WebApplicationType;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||||
|
import org.springframework.boot.context.annotation.ImportCandidates;
|
||||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/** An auto-configuration missing from the imports file passes its own tests and does nothing in any application. */
|
||||||
|
class ForgottenImportsEntryTests {
|
||||||
|
|
||||||
|
private static ConfigurableApplicationContext start(Class<?> application) {
|
||||||
|
return new SpringApplicationBuilder(application).web(WebApplicationType.NONE).bannerMode(Banner.Mode.OFF)
|
||||||
|
.properties("logging.level.root=WARN").run();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aRunnerTestNamesTheClassAndSoItPassesWhateverTheImportsFileSays() {
|
||||||
|
try (Transcript t = new Transcript("08-auto-configuration-missing-from-the-imports-file.txt",
|
||||||
|
"An auto-configuration that is not listed in AutoConfiguration.imports (it is listed in spring.factories, which Boot 4 ignores for this)")) {
|
||||||
|
t.section("ApplicationContextRunner: the test names the class itself");
|
||||||
|
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ForgottenAutoConfiguration.class))
|
||||||
|
.run(context -> {
|
||||||
|
t.line("forgottenMarker bean : %s", context.containsBean("forgottenMarker"));
|
||||||
|
assertThat(context).hasBean("forgottenMarker");
|
||||||
|
});
|
||||||
|
|
||||||
|
t.section("the starter author's application: @SpringBootApplication scanning the package that holds both classes");
|
||||||
|
try (ConfigurableApplicationContext context = start(OwnersApplication.class)) {
|
||||||
|
t.line("forgottenMarker (@AutoConfiguration) : %s", context.containsBean("forgottenMarker"));
|
||||||
|
t.line("forgottenPlainMarker (@Configuration) : %s", context.containsBean("forgottenPlainMarker"));
|
||||||
|
assertThat(context.containsBean("forgottenMarker")).isFalse();
|
||||||
|
assertThat(context.containsBean("forgottenPlainMarker")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
t.section("a consumer application: its own package, the starter on the classpath");
|
||||||
|
try (ConfigurableApplicationContext context = start(ConsumerApplication.class)) {
|
||||||
|
t.line("forgottenMarker (@AutoConfiguration) : %s", context.containsBean("forgottenMarker"));
|
||||||
|
t.line("forgottenPlainMarker (@Configuration) : %s", context.containsBean("forgottenPlainMarker"));
|
||||||
|
t.line("Masker beans : %s",
|
||||||
|
Arrays.toString(context.getBeanNamesForType(Masker.class)));
|
||||||
|
assertThat(context.containsBean("forgottenMarker")).isFalse();
|
||||||
|
assertThat(context.containsBean("forgottenPlainMarker")).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aTestThatReadsTheImportsFileCatchesTheOmission() {
|
||||||
|
try (Transcript t = new Transcript("09-reading-the-imports-file-in-a-test.txt",
|
||||||
|
"ImportCandidates: what Boot itself reads from META-INF/spring/...AutoConfiguration.imports")) {
|
||||||
|
var candidates = ImportCandidates.load(AutoConfiguration.class, getClass().getClassLoader()).getCandidates();
|
||||||
|
var ours = candidates.stream().filter(c -> c.startsWith("com.ankurm.masker.")).toList();
|
||||||
|
t.line("entries under com.ankurm.masker: %d", ours.size());
|
||||||
|
ours.forEach(c -> t.line(" %s", c));
|
||||||
|
t.line("ForgottenAutoConfiguration listed: %s", candidates.contains(ForgottenAutoConfiguration.class.getName()));
|
||||||
|
assertThat(ours).containsExactlyInAnyOrder(MaskerAutoConfiguration.class.getName(),
|
||||||
|
MaskerJacksonAutoConfiguration.class.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
package com.ankurm.masker;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
|
||||||
|
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
|
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/** The optional integration: present with Jackson, silently absent without it, and the order that matters. */
|
||||||
|
class JacksonIntegrationTests {
|
||||||
|
|
||||||
|
private final ApplicationContextRunner runner = new ApplicationContextRunner()
|
||||||
|
.withInitializer(new ConditionEvaluationReportLoggingListener())
|
||||||
|
.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class,
|
||||||
|
MaskerAutoConfiguration.class, MaskerJacksonAutoConfiguration.class));
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withJacksonASensitiveValueIsWrittenMasked() {
|
||||||
|
try (Transcript t = new Transcript("04-jackson-integration-active.txt",
|
||||||
|
"Jackson on the classpath: the module is registered and Sensitive is written masked")) {
|
||||||
|
runner.run(context -> {
|
||||||
|
assertThat(context).hasSingleBean(MaskerJacksonModule.class);
|
||||||
|
String json = context.getBean(JsonMapper.class)
|
||||||
|
.writeValueAsString(Map.of("card", new Sensitive("4111111111111111")));
|
||||||
|
assertThat(json).isEqualTo("{\"card\":\"************1111\"}");
|
||||||
|
t.line("module beans : %s", Arrays.toString(context.getBeanNamesForType(MaskerJacksonModule.class)));
|
||||||
|
t.line("JSON : %s", json);
|
||||||
|
t.section("what Boot recorded for the Jackson auto-configuration");
|
||||||
|
ReportSupport.print(t, (ConfigurableApplicationContext) context.getSourceApplicationContext(),
|
||||||
|
"MaskerJacksonAutoConfiguration");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withoutJacksonTheStarterStillStartsAndSaysWhy() {
|
||||||
|
try (Transcript t = new Transcript("05-jackson-integration-absent-without-error.txt",
|
||||||
|
"Jackson hidden with FilteredClassLoader: nothing fails, the integration just is not there")) {
|
||||||
|
runner.withClassLoader(new FilteredClassLoader("tools.jackson")).run(context -> {
|
||||||
|
assertThat(context).hasNotFailed().hasSingleBean(Masker.class).doesNotHaveBean(MaskerJacksonModule.class);
|
||||||
|
t.line("context failed : %s", context.getStartupFailure() != null);
|
||||||
|
t.line("Masker beans : %s", Arrays.toString(context.getBeanNamesForType(Masker.class)));
|
||||||
|
t.line("module beans : %s", Arrays.toString(context.getBeanNamesForType(MaskerJacksonModule.class)));
|
||||||
|
t.section("what Boot recorded for the Jackson auto-configuration");
|
||||||
|
ReportSupport.print(t, (ConfigurableApplicationContext) context.getSourceApplicationContext(),
|
||||||
|
"MaskerJacksonAutoConfiguration");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
package com.ankurm.masker;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||||
|
|
||||||
|
/** The auto-configuration exercised with {@link ApplicationContextRunner}: no application, no server, a few milliseconds. */
|
||||||
|
class MaskerAutoConfigurationTests {
|
||||||
|
|
||||||
|
private static final String CARD = "4111111111111111";
|
||||||
|
|
||||||
|
private final ApplicationContextRunner runner = new ApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(MaskerAutoConfiguration.class));
|
||||||
|
|
||||||
|
/** A user configuration. Deliberately not annotated: the runner registers whatever class it is given. */
|
||||||
|
static class CustomMasker {
|
||||||
|
@Bean
|
||||||
|
Masker customMasker() {
|
||||||
|
return value -> "<redacted>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void defaults() {
|
||||||
|
try (Transcript t = new Transcript("01-defaults-without-any-configuration.txt",
|
||||||
|
"The auto-configuration with no properties and no user beans")) {
|
||||||
|
runner.run(context -> {
|
||||||
|
assertThat(context).hasSingleBean(Masker.class).hasSingleBean(MaskerProperties.class);
|
||||||
|
MaskerProperties properties = context.getBean(MaskerProperties.class);
|
||||||
|
t.line("Masker beans : %s", Arrays.toString(context.getBeanNamesForType(Masker.class)));
|
||||||
|
t.line("Masker implementation : %s", context.getBean(Masker.class).getClass().getSimpleName());
|
||||||
|
t.line("defaults bound : enabled=%s replacement='%s' visibleTail=%d",
|
||||||
|
properties.enabled(), properties.replacement(), properties.visibleTail());
|
||||||
|
t.line("mask(\"%s\") = %s", CARD, context.getBean(Masker.class).mask(CARD));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void propertiesChangeTheBehaviourAndEnabledFalseRemovesEverything() {
|
||||||
|
try (Transcript t = new Transcript("02-properties-and-the-off-switch.txt",
|
||||||
|
"masker.* properties, and masker.enabled=false")) {
|
||||||
|
t.section("masker.replacement=# masker.visible-tail=2");
|
||||||
|
runner.withPropertyValues("masker.replacement=#", "masker.visible-tail=2").run(context -> {
|
||||||
|
assertThat(context.getBean(Masker.class).mask(CARD)).isEqualTo("##############11");
|
||||||
|
t.line("mask(\"%s\") = %s", CARD, context.getBean(Masker.class).mask(CARD));
|
||||||
|
});
|
||||||
|
|
||||||
|
t.section("masker.enabled=false");
|
||||||
|
runner.withPropertyValues("masker.enabled=false").run(context -> {
|
||||||
|
assertThat(context).doesNotHaveBean(Masker.class).doesNotHaveBean(MaskerProperties.class);
|
||||||
|
t.line("Masker beans : %s", Arrays.toString(context.getBeanNamesForType(Masker.class)));
|
||||||
|
t.line("MaskerProperties beans : %s", Arrays.toString(context.getBeanNamesForType(MaskerProperties.class)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aUserBeanMakesTheStarterBackOffOnlyWhenItIsRegisteredFirst() {
|
||||||
|
try (Transcript t = new Transcript("03-conditional-on-missing-bean-and-registration-order.txt",
|
||||||
|
"@ConditionalOnMissingBean: backing off, and the order that decides it")) {
|
||||||
|
t.section("user configuration + auto-configuration (the way Boot loads them)");
|
||||||
|
runner.withUserConfiguration(CustomMasker.class).run(context -> {
|
||||||
|
assertThat(context).hasSingleBean(Masker.class);
|
||||||
|
t.line("Masker beans : %s", Arrays.toString(context.getBeanNamesForType(Masker.class)));
|
||||||
|
t.line("getBean(Masker.class) : mask -> %s", context.getBean(Masker.class).mask(CARD));
|
||||||
|
});
|
||||||
|
|
||||||
|
t.section("the same two classes registered as ordinary configuration, starter first");
|
||||||
|
new ApplicationContextRunner().withUserConfiguration(MaskerAutoConfiguration.class, CustomMasker.class)
|
||||||
|
.run(context -> {
|
||||||
|
t.line("Masker beans : %s", Arrays.toString(context.getBeanNamesForType(Masker.class)));
|
||||||
|
Throwable failure = catchThrowable(() -> context.getBean(Masker.class));
|
||||||
|
assertThat(failure).isInstanceOf(NoUniqueBeanDefinitionException.class);
|
||||||
|
t.line("getBean(Masker.class) : %s", failure.getClass().getSimpleName());
|
||||||
|
});
|
||||||
|
|
||||||
|
t.section("the same two classes, user configuration first");
|
||||||
|
new ApplicationContextRunner().withUserConfiguration(CustomMasker.class, MaskerAutoConfiguration.class)
|
||||||
|
.run(context -> {
|
||||||
|
assertThat(context).hasSingleBean(Masker.class);
|
||||||
|
t.line("Masker beans : %s", Arrays.toString(context.getBeanNamesForType(Masker.class)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ankurm.masker;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
|
||||||
|
/** Prints the condition outcomes Boot recorded for the classes whose name contains a filter, as {@code --debug} would. */
|
||||||
|
final class ReportSupport {
|
||||||
|
|
||||||
|
private ReportSupport() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print(Transcript t, ConfigurableApplicationContext context, String filter) {
|
||||||
|
ConditionEvaluationReport report = ConditionEvaluationReport.get(context.getBeanFactory());
|
||||||
|
report.getConditionAndOutcomesBySource().entrySet().stream()
|
||||||
|
.filter(e -> e.getKey().contains(filter))
|
||||||
|
.sorted(Map.Entry.comparingByKey())
|
||||||
|
.forEach(e -> {
|
||||||
|
t.line("%s [%s]", e.getKey(), e.getValue().isFullMatch() ? "matched" : "not matched");
|
||||||
|
e.getValue().forEach(o -> t.line(" %s %s",
|
||||||
|
o.getOutcome().isMatch() ? "matched:" : "did not match:", o.getOutcome().getMessage()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
package com.ankurm.masker;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a numbered transcript under {@code custom-starter/output/} and echoes it to the console.
|
||||||
|
* Every console block quoted in the article comes out of one of these files verbatim.
|
||||||
|
*/
|
||||||
|
public final class Transcript implements AutoCloseable {
|
||||||
|
|
||||||
|
private static final Path ROOT =
|
||||||
|
Path.of(System.getProperty("basedir", ".")).toAbsolutePath().normalize().getParent();
|
||||||
|
|
||||||
|
private final Path path;
|
||||||
|
private final StringWriter buffer = new StringWriter();
|
||||||
|
private final PrintWriter out = new PrintWriter(buffer);
|
||||||
|
|
||||||
|
public Transcript(String fileName, String title) {
|
||||||
|
this.path = ROOT.resolve("output").resolve(fileName);
|
||||||
|
out.println("# " + title);
|
||||||
|
out.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Transcript line(String format, Object... args) {
|
||||||
|
out.println(args.length == 0 ? format : String.format(format, args));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Transcript section(String heading) {
|
||||||
|
out.println();
|
||||||
|
out.println("--- " + heading + " ---");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
out.flush();
|
||||||
|
// Absolute paths of whoever ran the build are environment noise, not a finding.
|
||||||
|
String text = buffer.toString().replace(ROOT.toString(), "<custom-starter>");
|
||||||
|
try {
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
Files.writeString(path, text);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException("could not write " + path, e);
|
||||||
|
}
|
||||||
|
System.out.print(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package com.ankurm.masker.scanapp;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An auto-configuration whose author never added it to {@code AutoConfiguration.imports}.
|
||||||
|
* It looks finished: annotated, conditional-free, and it compiles.
|
||||||
|
*/
|
||||||
|
@AutoConfiguration
|
||||||
|
public class ForgottenAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
String forgottenMarker() {
|
||||||
|
return "created by ForgottenAutoConfiguration";
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package com.ankurm.masker.scanapp;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/** The other way to forget: a plain {@code @Configuration}, also missing from the imports file. */
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
public class ForgottenPlainConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
String forgottenPlainMarker() {
|
||||||
|
return "created by ForgottenPlainConfiguration";
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package com.ankurm.masker.scanapp;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The starter author's own test application. It scans {@code com.ankurm.masker}, which is the starter's package,
|
||||||
|
* so it finds classes a real consumer's scan never will.
|
||||||
|
*/
|
||||||
|
@SpringBootApplication(scanBasePackages = "com.ankurm.masker.scanapp")
|
||||||
|
public class OwnersApplication {
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ankurm.traps;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
||||||
|
/** {@code @ConditionalOnProperty} without {@code matchIfMissing}: the property has to be set for anything to happen. */
|
||||||
|
@AutoConfiguration
|
||||||
|
@ConditionalOnProperty(prefix = "traps", name = "enabled")
|
||||||
|
public class DefaultOffAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
Marker defaultOffMarker() {
|
||||||
|
return new Marker("DefaultOffAutoConfiguration");
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ankurm.traps;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditional on a bean that another auto-configuration creates, and says nothing about ordering.
|
||||||
|
* Its name sorts before Boot's, so it is evaluated first.
|
||||||
|
*/
|
||||||
|
@AutoConfiguration
|
||||||
|
@ConditionalOnBean(JsonMapper.class)
|
||||||
|
public class EarlyModuleAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
Marker earlyMarker() {
|
||||||
|
return new Marker("EarlyModuleAutoConfiguration");
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package com.ankurm.traps;
|
||||||
|
|
||||||
|
/** A bean that only says which auto-configuration created it. */
|
||||||
|
public record Marker(String createdBy) {
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package com.ankurm.traps;
|
||||||
|
|
||||||
|
import com.ankurm.masker.Masker;
|
||||||
|
import com.ankurm.masker.MaskerJacksonModule;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The condition is on the {@code @Bean} method, but the method's return type needs Jackson to be loaded at all.
|
||||||
|
* Compare with {@code MaskerJacksonAutoConfiguration}, where the whole class carries the condition.
|
||||||
|
*/
|
||||||
|
@AutoConfiguration
|
||||||
|
public class MethodLevelConditionAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnClass(JsonMapper.class)
|
||||||
|
MaskerJacksonModule methodLevelModule(Masker masker) {
|
||||||
|
return new MaskerJacksonModule(masker);
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package com.ankurm.traps;
|
||||||
|
|
||||||
|
import com.ankurm.masker.MaskerAutoConfiguration;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Started by {@code scripts/capture-jar-facts.sh}, on classpaths with and without Jackson. A test cannot show this:
|
||||||
|
* the classes under test are already loaded by a loader that can see Jackson, whatever a filtered loader hides.
|
||||||
|
*
|
||||||
|
* <p>The argument picks the auto-configuration: {@code method} (condition on the {@code @Bean} method) or
|
||||||
|
* {@code nested} (condition on a nested class).
|
||||||
|
*/
|
||||||
|
public final class MethodLevelMain {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
boolean nested = args.length > 0 && args[0].equals("nested");
|
||||||
|
Class<?> configuration = nested ? NestedConditionAutoConfiguration.class : MethodLevelConditionAutoConfiguration.class;
|
||||||
|
String beanName = nested ? "nestedModule" : "methodLevelModule";
|
||||||
|
try (var context = new AnnotationConfigApplicationContext()) {
|
||||||
|
context.register(MaskerAutoConfiguration.class, configuration);
|
||||||
|
context.refresh();
|
||||||
|
System.out.println("context started; " + beanName + " bean present: " + context.containsBean(beanName));
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
StringBuilder chain = new StringBuilder();
|
||||||
|
Throwable root = failure;
|
||||||
|
for (Throwable t = failure; t != null; t = t.getCause()) {
|
||||||
|
chain.append(chain.isEmpty() ? "" : " -> ").append(t.getClass().getSimpleName());
|
||||||
|
root = t;
|
||||||
|
}
|
||||||
|
System.out.println("context failed");
|
||||||
|
System.out.println("exception chain: " + chain);
|
||||||
|
System.out.println("root cause : " + root.getClass().getName() + ": " + root.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ankurm.traps;
|
||||||
|
|
||||||
|
import com.ankurm.masker.Masker;
|
||||||
|
import com.ankurm.masker.MaskerJacksonModule;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
|
/** The repair: the condition sits on a nested class, so the outer class never has a Jackson type in any signature. */
|
||||||
|
@AutoConfiguration
|
||||||
|
public class NestedConditionAutoConfiguration {
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnClass(JsonMapper.class)
|
||||||
|
static class JacksonConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
MaskerJacksonModule nestedModule(Masker masker) {
|
||||||
|
return new MaskerJacksonModule(masker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package com.ankurm.traps;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
|
/** The same condition, with the ordering stated. {@code afterName} needs no compile-time dependency on Boot's Jackson module. */
|
||||||
|
@AutoConfiguration(afterName = "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration")
|
||||||
|
@ConditionalOnBean(JsonMapper.class)
|
||||||
|
public class OrderedModuleAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
Marker orderedMarker() {
|
||||||
|
return new Marker("OrderedModuleAutoConfiguration");
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
# The pre-Boot-2.7 way to register an auto-configuration. ForgottenImportsEntryTests shows that Boot 4 ignores it.
|
||||||
|
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.ankurm.masker.scanapp.ForgottenAutoConfiguration
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?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>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>com.ankurm.masker</groupId>
|
||||||
|
<artifactId>masker-parent</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>masker-spring-boot-starter</artifactId>
|
||||||
|
<name>masker-spring-boot-starter</name>
|
||||||
|
<description>The dependency an application adds. It has no code: it pulls in the auto-configuration module.</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.ankurm.masker</groupId>
|
||||||
|
<artifactId>masker-spring-boot-autoconfigure</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# The auto-configuration with no properties and no user beans
|
||||||
|
|
||||||
|
Masker beans : [masker]
|
||||||
|
Masker implementation : TailMasker
|
||||||
|
defaults bound : enabled=true replacement='*' visibleTail=4
|
||||||
|
mask("4111111111111111") = ************1111
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# masker.* properties, and masker.enabled=false
|
||||||
|
|
||||||
|
|
||||||
|
--- masker.replacement=# masker.visible-tail=2 ---
|
||||||
|
mask("4111111111111111") = ##############11
|
||||||
|
|
||||||
|
--- masker.enabled=false ---
|
||||||
|
Masker beans : []
|
||||||
|
MaskerProperties beans : []
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# @ConditionalOnMissingBean: backing off, and the order that decides it
|
||||||
|
|
||||||
|
|
||||||
|
--- user configuration + auto-configuration (the way Boot loads them) ---
|
||||||
|
Masker beans : [customMasker]
|
||||||
|
getBean(Masker.class) : mask -> <redacted>
|
||||||
|
|
||||||
|
--- the same two classes registered as ordinary configuration, starter first ---
|
||||||
|
Masker beans : [masker, customMasker]
|
||||||
|
getBean(Masker.class) : NoUniqueBeanDefinitionException
|
||||||
|
|
||||||
|
--- the same two classes, user configuration first ---
|
||||||
|
Masker beans : [customMasker]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Jackson on the classpath: the module is registered and Sensitive is written masked
|
||||||
|
|
||||||
|
module beans : [maskerJacksonModule]
|
||||||
|
JSON : {"card":"************1111"}
|
||||||
|
|
||||||
|
--- what Boot recorded for the Jackson auto-configuration ---
|
||||||
|
com.ankurm.masker.MaskerJacksonAutoConfiguration [matched]
|
||||||
|
matched: @ConditionalOnClass found required class 'tools.jackson.databind.json.JsonMapper'
|
||||||
|
matched: @ConditionalOnBean (types: com.ankurm.masker.Masker; SearchStrategy: all) found bean 'masker'
|
||||||
|
com.ankurm.masker.MaskerJacksonAutoConfiguration#maskerJacksonModule [matched]
|
||||||
|
matched: @ConditionalOnMissingBean (types: com.ankurm.masker.MaskerJacksonModule; SearchStrategy: all) did not find any beans
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Jackson hidden with FilteredClassLoader: nothing fails, the integration just is not there
|
||||||
|
|
||||||
|
context failed : false
|
||||||
|
Masker beans : [masker]
|
||||||
|
module beans : []
|
||||||
|
|
||||||
|
--- what Boot recorded for the Jackson auto-configuration ---
|
||||||
|
com.ankurm.masker.MaskerJacksonAutoConfiguration [not matched]
|
||||||
|
did not match: @ConditionalOnClass did not find required class 'tools.jackson.databind.json.JsonMapper'
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# @ConditionalOnBean(JsonMapper) with and without an ordering constraint
|
||||||
|
|
||||||
|
|
||||||
|
--- the order Boot evaluates them in ---
|
||||||
|
com.ankurm.masker.MaskerAutoConfiguration
|
||||||
|
com.ankurm.traps.EarlyModuleAutoConfiguration
|
||||||
|
org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration
|
||||||
|
com.ankurm.traps.OrderedModuleAutoConfiguration
|
||||||
|
|
||||||
|
--- the beans each one produced ---
|
||||||
|
Marker bean created by OrderedModuleAutoConfiguration
|
||||||
|
|
||||||
|
--- what Boot recorded ---
|
||||||
|
com.ankurm.traps.EarlyModuleAutoConfiguration [not matched]
|
||||||
|
did not match: @ConditionalOnBean (types: tools.jackson.databind.json.JsonMapper; SearchStrategy: all) did not find any beans of type tools.jackson.databind.json.JsonMapper
|
||||||
|
com.ankurm.traps.OrderedModuleAutoConfiguration [matched]
|
||||||
|
matched: @ConditionalOnBean (types: tools.jackson.databind.json.JsonMapper; SearchStrategy: all) found bean 'jacksonJsonMapper'
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# @ConditionalOnClass(JsonMapper.class) on a @Bean method, versus on a nested class, with and without Jackson
|
||||||
|
|
||||||
|
--- condition on the @Bean method, classpath without any Jackson jar ---
|
||||||
|
context failed
|
||||||
|
exception chain: BeanDefinitionStoreException -> IllegalStateException -> NoClassDefFoundError -> ClassNotFoundException
|
||||||
|
root cause : java.lang.ClassNotFoundException: tools.jackson.databind.module.SimpleModule
|
||||||
|
|
||||||
|
--- condition on a nested class, classpath without any Jackson jar ---
|
||||||
|
context started; nestedModule bean present: false
|
||||||
|
|
||||||
|
--- condition on the @Bean method, classpath with Jackson ---
|
||||||
|
context started; methodLevelModule bean present: true
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# An auto-configuration that is not listed in AutoConfiguration.imports (it is listed in spring.factories, which Boot 4 ignores for this)
|
||||||
|
|
||||||
|
|
||||||
|
--- ApplicationContextRunner: the test names the class itself ---
|
||||||
|
forgottenMarker bean : true
|
||||||
|
|
||||||
|
--- the starter author's application: @SpringBootApplication scanning the package that holds both classes ---
|
||||||
|
forgottenMarker (@AutoConfiguration) : false
|
||||||
|
forgottenPlainMarker (@Configuration) : true
|
||||||
|
|
||||||
|
--- a consumer application: its own package, the starter on the classpath ---
|
||||||
|
forgottenMarker (@AutoConfiguration) : false
|
||||||
|
forgottenPlainMarker (@Configuration) : false
|
||||||
|
Masker beans : [masker]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# ImportCandidates: what Boot itself reads from META-INF/spring/...AutoConfiguration.imports
|
||||||
|
|
||||||
|
entries under com.ankurm.masker: 2
|
||||||
|
com.ankurm.masker.MaskerAutoConfiguration
|
||||||
|
com.ankurm.masker.MaskerJacksonAutoConfiguration
|
||||||
|
ForgottenAutoConfiguration listed: false
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# An application that adds masker-spring-boot-starter and nothing else
|
||||||
|
|
||||||
|
Masker beans : [masker]
|
||||||
|
MaskerProperties beans : [masker-com.ankurm.masker.MaskerProperties]
|
||||||
|
Jackson module beans : [maskerJacksonModule]
|
||||||
|
JSON of a Customer : {"name":"Asha","card":"************1111"}
|
||||||
|
|
||||||
|
--- what Boot recorded (the same lines --debug prints) ---
|
||||||
|
com.ankurm.masker.MaskerAutoConfiguration [matched]
|
||||||
|
matched: @ConditionalOnProperty (masker.enabled) matched
|
||||||
|
com.ankurm.masker.MaskerAutoConfiguration#masker [matched]
|
||||||
|
matched: @ConditionalOnMissingBean (types: com.ankurm.masker.Masker; SearchStrategy: all) did not find any beans
|
||||||
|
com.ankurm.masker.MaskerJacksonAutoConfiguration [matched]
|
||||||
|
matched: @ConditionalOnClass found required class 'tools.jackson.databind.json.JsonMapper'
|
||||||
|
matched: @ConditionalOnBean (types: com.ankurm.masker.Masker; SearchStrategy: all) found bean 'masker'
|
||||||
|
com.ankurm.masker.MaskerJacksonAutoConfiguration#maskerJacksonModule [matched]
|
||||||
|
matched: @ConditionalOnMissingBean (types: com.ankurm.masker.MaskerJacksonModule; SearchStrategy: all) did not find any beans
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# The same application configured with masker.* properties, then switched off
|
||||||
|
|
||||||
|
|
||||||
|
--- --masker.replacement=# --masker.visible-tail=6 ---
|
||||||
|
JSON of a Customer : {"name":"Asha","card":"##########111111"}
|
||||||
|
|
||||||
|
--- --masker.enabled=false ---
|
||||||
|
Masker beans : []
|
||||||
|
Jackson module beans : []
|
||||||
|
|
||||||
|
--- what Boot recorded ---
|
||||||
|
com.ankurm.masker.MaskerAutoConfiguration [not matched]
|
||||||
|
did not match: @ConditionalOnProperty (masker.enabled) found different value in property 'enabled'
|
||||||
|
com.ankurm.masker.MaskerJacksonAutoConfiguration [not matched]
|
||||||
|
matched: @ConditionalOnClass found required class 'tools.jackson.databind.json.JsonMapper'
|
||||||
|
did not match: @ConditionalOnBean (types: com.ankurm.masker.Masker; SearchStrategy: all) did not find any beans of type com.ankurm.masker.Masker
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# What is inside the two jars, and what the starter drags in
|
||||||
|
|
||||||
|
--- masker-spring-boot-starter-1.0.0.jar ---
|
||||||
|
META-INF/MANIFEST.MF
|
||||||
|
META-INF/maven/com.ankurm.masker/masker-spring-boot-starter/pom.xml
|
||||||
|
META-INF/maven/com.ankurm.masker/masker-spring-boot-starter/pom.properties
|
||||||
|
|
||||||
|
--- masker-spring-boot-autoconfigure-1.0.0.jar (META-INF and the classes) ---
|
||||||
|
META-INF/spring-autoconfigure-metadata.properties
|
||||||
|
META-INF/additional-spring-configuration-metadata.json
|
||||||
|
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
|
||||||
|
META-INF/spring-configuration-metadata.json
|
||||||
|
com/ankurm/masker/MaskerJacksonAutoConfiguration.class
|
||||||
|
com/ankurm/masker/Sensitive.class
|
||||||
|
com/ankurm/masker/Masker.class
|
||||||
|
com/ankurm/masker/MaskerAutoConfiguration.class
|
||||||
|
com/ankurm/masker/TailMasker.class
|
||||||
|
com/ankurm/masker/MaskerJacksonModule.class
|
||||||
|
com/ankurm/masker/MaskerProperties.class
|
||||||
|
com/ankurm/masker/MaskerJacksonModule$1.class
|
||||||
|
|
||||||
|
--- META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ---
|
||||||
|
com.ankurm.masker.MaskerAutoConfiguration
|
||||||
|
com.ankurm.masker.MaskerJacksonAutoConfiguration
|
||||||
|
|
||||||
|
--- dependency tree of masker-spring-boot-starter (Jackson is not in it) ---
|
||||||
|
com.ankurm.masker:masker-spring-boot-starter:jar:1.0.0
|
||||||
|
+- org.springframework.boot:spring-boot-starter:jar:4.1.1:compile
|
||||||
|
| +- org.springframework.boot:spring-boot-starter-logging:jar:4.1.1:compile
|
||||||
|
| | +- ch.qos.logback:logback-classic:jar:1.5.38:compile
|
||||||
|
| | | +- ch.qos.logback:logback-core:jar:1.5.38:compile
|
||||||
|
| | | \- org.slf4j:slf4j-api:jar:2.0.18:compile
|
||||||
|
| | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.25.5:compile
|
||||||
|
| | | \- org.apache.logging.log4j:log4j-api:jar:2.25.5:compile
|
||||||
|
| | \- org.slf4j:jul-to-slf4j:jar:2.0.18:compile
|
||||||
|
| +- org.springframework.boot:spring-boot-autoconfigure:jar:4.1.1:compile
|
||||||
|
| | \- org.springframework.boot:spring-boot:jar:4.1.1:compile
|
||||||
|
| | +- org.springframework:spring-core:jar:7.0.9:compile
|
||||||
|
| | | +- commons-logging:commons-logging:jar:1.3.6:compile
|
||||||
|
| | | \- org.jspecify:jspecify:jar:1.0.1:compile
|
||||||
|
| | \- org.springframework:spring-context:jar:7.0.9:compile
|
||||||
|
| | +- org.springframework:spring-aop:jar:7.0.9:compile
|
||||||
|
| | +- org.springframework:spring-beans:jar:7.0.9:compile
|
||||||
|
| | +- org.springframework:spring-expression:jar:7.0.9:compile
|
||||||
|
| | \- io.micrometer:micrometer-observation:jar:1.17.1:compile
|
||||||
|
| | \- io.micrometer:micrometer-commons:jar:1.17.1:compile
|
||||||
|
| +- jakarta.annotation:jakarta.annotation-api:jar:3.0.0:compile
|
||||||
|
| \- org.yaml:snakeyaml:jar:2.6:compile
|
||||||
|
\- com.ankurm.masker:masker-spring-boot-autoconfigure:jar:1.0.0:compile
|
||||||
|
|
||||||
|
--- dependency tree of masker-spring-boot-autoconfigure (Jackson is there, marked optional in the pom) ---
|
||||||
|
com.ankurm.masker:masker-spring-boot-autoconfigure:jar:1.0.0
|
||||||
|
+- org.springframework.boot:spring-boot-autoconfigure:jar:4.1.1:compile
|
||||||
|
| \- org.springframework.boot:spring-boot:jar:4.1.1:compile
|
||||||
|
| \- org.springframework:spring-context:jar:7.0.9:compile
|
||||||
|
| +- org.springframework:spring-aop:jar:7.0.9:compile
|
||||||
|
| +- org.springframework:spring-beans:jar:7.0.9:compile
|
||||||
|
| +- org.springframework:spring-expression:jar:7.0.9:compile
|
||||||
|
| \- io.micrometer:micrometer-observation:jar:1.17.1:compile
|
||||||
|
| \- io.micrometer:micrometer-commons:jar:1.17.1:compile
|
||||||
|
\- org.springframework.boot:spring-boot-starter-jackson:jar:4.1.1:compile (optional)
|
||||||
|
+- org.springframework.boot:spring-boot-starter:jar:4.1.1:compile
|
||||||
|
| +- org.springframework.boot:spring-boot-starter-logging:jar:4.1.1:compile
|
||||||
|
| | +- ch.qos.logback:logback-classic:jar:1.5.38:compile
|
||||||
|
| | | \- ch.qos.logback:logback-core:jar:1.5.38:compile
|
||||||
|
| | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.25.5:compile
|
||||||
|
| | | \- org.apache.logging.log4j:log4j-api:jar:2.25.5:compile
|
||||||
|
| | \- org.slf4j:jul-to-slf4j:jar:2.0.18:compile
|
||||||
|
| +- jakarta.annotation:jakarta.annotation-api:jar:3.0.0:compile
|
||||||
|
| \- org.yaml:snakeyaml:jar:2.6:compile
|
||||||
|
\- org.springframework.boot:spring-boot-jackson:jar:4.1.1:compile (optional)
|
||||||
|
\- tools.jackson.core:jackson-databind:jar:3.1.5:compile (optional)
|
||||||
|
+- com.fasterxml.jackson.core:jackson-annotations:jar:2.21:compile (optional)
|
||||||
|
\- tools.jackson.core:jackson-core:jar:3.1.5:compile (optional)
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Metadata the two annotation processors write into the auto-configuration jar
|
||||||
|
|
||||||
|
--- META-INF/spring-configuration-metadata.json, written by spring-boot-configuration-processor ---
|
||||||
|
|
||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "masker",
|
||||||
|
"type": "com.ankurm.masker.MaskerProperties",
|
||||||
|
"sourceType": "com.ankurm.masker.MaskerProperties"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "masker.enabled",
|
||||||
|
"type": "java.lang.Boolean",
|
||||||
|
"description": "whether the starter configures anything at all",
|
||||||
|
"sourceType": "com.ankurm.masker.MaskerProperties",
|
||||||
|
"defaultValue": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "masker.replacement",
|
||||||
|
"type": "java.lang.String",
|
||||||
|
"description": "the text that stands in for each hidden character",
|
||||||
|
"sourceType": "com.ankurm.masker.MaskerProperties",
|
||||||
|
"defaultValue": "*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "masker.visible-tail",
|
||||||
|
"type": "java.lang.Integer",
|
||||||
|
"description": "how many characters at the end of the value stay readable",
|
||||||
|
"sourceType": "com.ankurm.masker.MaskerProperties",
|
||||||
|
"defaultValue": 4
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hints": [
|
||||||
|
{
|
||||||
|
"name": "masker.replacement",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "*",
|
||||||
|
"description": "Asterisk (the default)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#",
|
||||||
|
"description": "Hash sign."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "•",
|
||||||
|
"description": "Bullet."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ignored": {
|
||||||
|
"properties": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
--- META-INF/additional-spring-configuration-metadata.json, hand-written and kept as is ---
|
||||||
|
{
|
||||||
|
"hints": [
|
||||||
|
{
|
||||||
|
"name": "masker.replacement",
|
||||||
|
"values": [
|
||||||
|
{ "value": "*", "description": "Asterisk (the default)." },
|
||||||
|
{ "value": "#", "description": "Hash sign." },
|
||||||
|
{ "value": "•", "description": "Bullet." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
--- META-INF/spring-autoconfigure-metadata.properties, written by spring-boot-autoconfigure-processor ---
|
||||||
|
com.ankurm.masker.MaskerAutoConfiguration=
|
||||||
|
com.ankurm.masker.MaskerJacksonAutoConfiguration=
|
||||||
|
com.ankurm.masker.MaskerJacksonAutoConfiguration.AutoConfigureAfter=com.ankurm.masker.MaskerAutoConfiguration
|
||||||
|
com.ankurm.masker.MaskerJacksonAutoConfiguration.ConditionalOnBean=com.ankurm.masker.Masker
|
||||||
|
com.ankurm.masker.MaskerJacksonAutoConfiguration.ConditionalOnClass=tools.jackson.databind.json.JsonMapper
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Boot 4.1.1: where the auto-configuration classes a starter refers to now live
|
||||||
|
|
||||||
|
--- entries in spring-boot-autoconfigure's own AutoConfiguration.imports ---
|
||||||
|
12
|
||||||
|
|
||||||
|
--- JacksonAutoConfiguration: which jar, which package ---
|
||||||
|
spring-boot-autoconfigure-4.1.1.jar, classes named JacksonAutoConfiguration: 0
|
||||||
|
spring-boot-jackson-4.1.1.jar: org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.class
|
||||||
|
|
||||||
|
--- what @AutoConfiguration is made of (javap -v, class-level annotations) ---
|
||||||
|
org.springframework.context.annotation.Configuration(
|
||||||
|
proxyBeanMethods=false
|
||||||
|
org.springframework.boot.autoconfigure.AutoConfigureBefore
|
||||||
|
org.springframework.boot.autoconfigure.AutoConfigureAfter
|
||||||
|
|
||||||
|
--- AutoConfigurationExcludeFilter, the filter @SpringBootApplication's scan applies: the classes it tests for (javap -c) ---
|
||||||
|
ldc #26 // class org/springframework/context/annotation/Configuration
|
||||||
|
ldc #40 // class org/springframework/boot/autoconfigure/AutoConfiguration
|
||||||
|
ldc #40 // class org/springframework/boot/autoconfigure/AutoConfiguration
|
||||||
|
invokestatic #65 // Method org/springframework/boot/context/annotation/ImportCandidates.load:(Ljava/lang/Class;Ljava/lang/ClassLoader;)Lorg/springframework/boot/context/annotation/ImportCandidates;
|
||||||
|
|
||||||
|
--- the Boot 3 import, compiled against the 4.1.1 jars ---
|
||||||
|
masker-spring-boot-autoconfigure/src/broken/OldJacksonImport.java:4: error: package org.springframework.boot.autoconfigure.jackson does not exist
|
||||||
|
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
||||||
|
^
|
||||||
|
masker-spring-boot-autoconfigure/src/broken/OldJacksonImport.java:7: error: cannot find symbol
|
||||||
|
@AutoConfiguration(after = JacksonAutoConfiguration.class)
|
||||||
|
^
|
||||||
|
symbol: class JacksonAutoConfiguration
|
||||||
|
2 errors
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# The processor path with <version>${project.parent.version}</version>, when the module's parent is not Spring Boot's
|
||||||
|
|
||||||
|
Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.15.0:compile (default-compile) on project masker-spring-boot-autoconfigure: Resolution of annotationProcessorPath dependencies failed: The following artifacts could not be resolved: org.springframework.boot:spring-boot-configuration-processor:jar:1.0.0 (absent): Could not find artifact org.springframework.boot:spring-boot-configuration-processor:jar:1.0.0 in central (https://repo.maven.apache.org/maven2)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# @ConditionalOnProperty(prefix = "traps", name = "enabled") with no matchIfMissing
|
||||||
|
|
||||||
|
|
||||||
|
--- no property set ---
|
||||||
|
Marker beans : 0
|
||||||
|
com.ankurm.traps.DefaultOffAutoConfiguration [not matched]
|
||||||
|
did not match: @ConditionalOnProperty (traps.enabled) did not find property 'enabled'
|
||||||
|
|
||||||
|
--- traps.enabled=true ---
|
||||||
|
Marker beans : 1
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Which 4.1.1 jar holds each Boot class this starter and its tests import (searched in every Boot jar the build resolved)
|
||||||
|
|
||||||
|
spring-boot-autoconfigure-4.1.1.jar org/springframework/boot/autoconfigure/AutoConfiguration.class
|
||||||
|
spring-boot-autoconfigure-4.1.1.jar org/springframework/boot/autoconfigure/AutoConfigureAfter.class
|
||||||
|
spring-boot-autoconfigure-4.1.1.jar org/springframework/boot/autoconfigure/AutoConfigurations.class
|
||||||
|
spring-boot-autoconfigure-4.1.1.jar org/springframework/boot/autoconfigure/condition/ConditionalOnClass.class
|
||||||
|
spring-boot-autoconfigure-4.1.1.jar org/springframework/boot/autoconfigure/condition/ConditionalOnBean.class
|
||||||
|
spring-boot-autoconfigure-4.1.1.jar org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.class
|
||||||
|
spring-boot-autoconfigure-4.1.1.jar org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.class
|
||||||
|
spring-boot-autoconfigure-4.1.1.jar org/springframework/boot/autoconfigure/logging/ConditionEvaluationReportLoggingListener.class
|
||||||
|
spring-boot-4.1.1.jar org/springframework/boot/context/annotation/ImportCandidates.class
|
||||||
|
spring-boot-4.1.1.jar org/springframework/boot/context/annotation/Configurations.class
|
||||||
|
spring-boot-jackson-4.1.1.jar org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.class
|
||||||
|
spring-boot-test-4.1.1.jar org/springframework/boot/test/context/runner/ApplicationContextRunner.class
|
||||||
|
spring-boot-test-4.1.1.jar org/springframework/boot/test/context/FilteredClassLoader.class
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?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>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>4.1.1</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>com.ankurm.masker</groupId>
|
||||||
|
<artifactId>masker-parent</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
<name>masker-parent</name>
|
||||||
|
<description>A custom Spring Boot 4 starter: auto-configuration, @Conditional, properties metadata and tests</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>25</java.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<modules>
|
||||||
|
<module>masker-spring-boot-autoconfigure</module>
|
||||||
|
<module>masker-spring-boot-starter</module>
|
||||||
|
<module>masker-demo</module>
|
||||||
|
</modules>
|
||||||
|
</project>
|
||||||
Executable
+136
@@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Facts that a test cannot show, read from the built jars and from deliberately failing builds:
|
||||||
|
# 07 @ConditionalOnClass on a @Bean method versus on a nested class, on classpaths with and without Jackson
|
||||||
|
# 12 what is in the starter jar and the auto-configuration jar, and what the starter pulls in
|
||||||
|
# 13 the generated configuration metadata and auto-configuration metadata, as they sit in the jar
|
||||||
|
# 14 Boot 4's split of spring-boot-autoconfigure, and the Boot 3 import that no longer compiles
|
||||||
|
# 15 the annotation-processor version trap when the module has its own parent pom
|
||||||
|
# 17 which Boot 4.1.1 jar holds each class the starter imports
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
mkdir -p output
|
||||||
|
M2=~/.m2/repository/org/springframework/boot
|
||||||
|
AC=masker-spring-boot-autoconfigure
|
||||||
|
JP() { "$@" 2>&1 | grep -v -E '^(Picked up|WARNING)'; }
|
||||||
|
ROOT=$(pwd)
|
||||||
|
clean() { sed "s#$ROOT#<custom-starter>#g; s#$HOME#~#g"; }
|
||||||
|
|
||||||
|
echo "== build the jars"
|
||||||
|
mvn -B -q package -DskipTests dependency:tree -Dscope=compile -DoutputFile=target/tree.txt >/dev/null 2>&1 || mvn -B package -DskipTests dependency:tree -Dscope=compile -DoutputFile=target/tree.txt
|
||||||
|
|
||||||
|
echo "== 07"
|
||||||
|
mvn -B -q -pl "$AC" test-compile dependency:build-classpath -Dmdep.outputFile=/tmp/masker-cp.txt -Dmdep.includeScope=test >/dev/null 2>&1
|
||||||
|
FULL=$(cat /tmp/masker-cp.txt)
|
||||||
|
NOJACKSON=$(tr ':' '\n' < /tmp/masker-cp.txt | grep -v jackson | paste -sd:)
|
||||||
|
{
|
||||||
|
echo "# @ConditionalOnClass(JsonMapper.class) on a @Bean method, versus on a nested class, with and without Jackson"
|
||||||
|
echo
|
||||||
|
echo "--- condition on the @Bean method, classpath without any Jackson jar ---"
|
||||||
|
JP java -cp "$AC/target/test-classes:$AC/target/classes:$NOJACKSON" com.ankurm.traps.MethodLevelMain method | grep -v -E '^[0-9:.]+ \[main\]'
|
||||||
|
echo
|
||||||
|
echo "--- condition on a nested class, classpath without any Jackson jar ---"
|
||||||
|
JP java -cp "$AC/target/test-classes:$AC/target/classes:$NOJACKSON" com.ankurm.traps.MethodLevelMain nested | grep -v -E '^[0-9:.]+ \[main\]'
|
||||||
|
echo
|
||||||
|
echo "--- condition on the @Bean method, classpath with Jackson ---"
|
||||||
|
JP java -cp "$AC/target/test-classes:$AC/target/classes:$FULL" com.ankurm.traps.MethodLevelMain method | grep -v -E '^[0-9:.]+ \[main\]'
|
||||||
|
} | clean > output/07-conditional-on-class-on-a-bean-method.txt
|
||||||
|
|
||||||
|
echo "== 12"
|
||||||
|
{
|
||||||
|
echo "# What is inside the two jars, and what the starter drags in"
|
||||||
|
echo
|
||||||
|
echo "--- masker-spring-boot-starter-1.0.0.jar ---"
|
||||||
|
unzip -l masker-spring-boot-starter/target/masker-spring-boot-starter-1.0.0.jar | awk 'NR>3 && $4!="" {print $4}' | grep -v '/$'
|
||||||
|
echo
|
||||||
|
echo "--- masker-spring-boot-autoconfigure-1.0.0.jar (META-INF and the classes) ---"
|
||||||
|
unzip -l $AC/target/$AC-1.0.0.jar | awk 'NR>3 && $4!="" {print $4}' | grep -v '/$' | grep -v -E 'pom\.(xml|properties)|MANIFEST'
|
||||||
|
echo
|
||||||
|
echo "--- META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ---"
|
||||||
|
unzip -p $AC/target/$AC-1.0.0.jar META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
|
||||||
|
echo
|
||||||
|
echo "--- dependency tree of masker-spring-boot-starter (Jackson is not in it) ---"
|
||||||
|
cat masker-spring-boot-starter/target/tree.txt
|
||||||
|
echo
|
||||||
|
echo "--- dependency tree of masker-spring-boot-autoconfigure (Jackson is there, marked optional in the pom) ---"
|
||||||
|
cat $AC/target/tree.txt
|
||||||
|
} | clean > output/12-jar-contents-and-dependency-trees.txt
|
||||||
|
|
||||||
|
echo "== 13"
|
||||||
|
{
|
||||||
|
echo "# Metadata the two annotation processors write into the auto-configuration jar"
|
||||||
|
echo
|
||||||
|
echo "--- META-INF/spring-configuration-metadata.json, written by spring-boot-configuration-processor ---"
|
||||||
|
echo
|
||||||
|
unzip -p $AC/target/$AC-1.0.0.jar META-INF/spring-configuration-metadata.json
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
echo "--- META-INF/additional-spring-configuration-metadata.json, hand-written and kept as is ---"
|
||||||
|
unzip -p $AC/target/$AC-1.0.0.jar META-INF/additional-spring-configuration-metadata.json
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
echo "--- META-INF/spring-autoconfigure-metadata.properties, written by spring-boot-autoconfigure-processor ---"
|
||||||
|
unzip -p $AC/target/$AC-1.0.0.jar META-INF/spring-autoconfigure-metadata.properties
|
||||||
|
} > output/13-configuration-metadata-in-the-jar.txt
|
||||||
|
|
||||||
|
echo "== 14"
|
||||||
|
BOOT_AC=$M2/spring-boot-autoconfigure/4.1.1/spring-boot-autoconfigure-4.1.1.jar
|
||||||
|
BOOT_JACKSON=$M2/spring-boot-jackson/4.1.1/spring-boot-jackson-4.1.1.jar
|
||||||
|
{
|
||||||
|
echo "# Boot 4.1.1: where the auto-configuration classes a starter refers to now live"
|
||||||
|
echo
|
||||||
|
echo "--- entries in spring-boot-autoconfigure's own AutoConfiguration.imports ---"
|
||||||
|
unzip -p "$BOOT_AC" META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | grep -c .
|
||||||
|
echo
|
||||||
|
echo "--- JacksonAutoConfiguration: which jar, which package ---"
|
||||||
|
echo "spring-boot-autoconfigure-4.1.1.jar, classes named JacksonAutoConfiguration: $(unzip -l "$BOOT_AC" | grep -c 'JacksonAutoConfiguration' || true)"
|
||||||
|
unzip -l "$BOOT_JACKSON" | awk '/JacksonAutoConfiguration.class/ {print "spring-boot-jackson-4.1.1.jar: " $4}'
|
||||||
|
echo
|
||||||
|
X=$(mktemp -d)
|
||||||
|
unzip -q -o "$BOOT_AC" 'org/springframework/boot/autoconfigure/AutoConfiguration.class' \
|
||||||
|
'org/springframework/boot/autoconfigure/AutoConfigurationExcludeFilter.class' -d "$X"
|
||||||
|
echo "--- what @AutoConfiguration is made of (javap -v, class-level annotations) ---"
|
||||||
|
JP javap -v -cp "$X" org.springframework.boot.autoconfigure.AutoConfiguration \
|
||||||
|
| sed -n '/^RuntimeVisibleAnnotations/,$p' | grep -E '^ (org\.springframework)|proxyBeanMethods' | sed 's/^ *//'
|
||||||
|
echo
|
||||||
|
echo "--- AutoConfigurationExcludeFilter, the filter @SpringBootApplication's scan applies: the classes it tests for (javap -c) ---"
|
||||||
|
JP javap -p -c -cp "$X" org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter \
|
||||||
|
| grep -E 'ldc|ImportCandidates.load' | sed -E 's/^ *[0-9]+: //; s/ +/ /g'
|
||||||
|
rm -rf "$X"
|
||||||
|
echo
|
||||||
|
echo "--- the Boot 3 import, compiled against the 4.1.1 jars ---"
|
||||||
|
mkdir -p /tmp/masker-broken
|
||||||
|
JP javac -d /tmp/masker-broken -cp "$BOOT_AC:$BOOT_JACKSON" $AC/src/broken/OldJacksonImport.java | clean || true
|
||||||
|
} > output/14-boot4-autoconfiguration-locations.txt
|
||||||
|
|
||||||
|
echo "== 17"
|
||||||
|
{
|
||||||
|
echo "# Which 4.1.1 jar holds each Boot class this starter and its tests import (searched in every Boot jar the build resolved)"
|
||||||
|
echo
|
||||||
|
for c in AutoConfiguration AutoConfigureAfter AutoConfigurations ConditionalOnClass ConditionalOnBean ConditionalOnMissingBean ConditionalOnProperty \
|
||||||
|
ConditionEvaluationReportLoggingListener ImportCandidates Configurations JacksonAutoConfiguration ApplicationContextRunner FilteredClassLoader; do
|
||||||
|
for j in "$M2"/spring-boot*/4.1.1/spring-boot*-4.1.1.jar; do
|
||||||
|
unzip -l "$j" | awk -v c="/$c.class" -v j="$(basename "$j")" 'index($4, c) && $4 !~ /\$/ {printf "%-46s %s\n", j, $4}'
|
||||||
|
done
|
||||||
|
done
|
||||||
|
} > output/17-where-the-classes-live.txt
|
||||||
|
|
||||||
|
echo "== 15"
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
cp -r pom.xml "$AC" "$TMP/"
|
||||||
|
mkdir -p "$TMP/masker-spring-boot-starter" "$TMP/masker-demo"
|
||||||
|
cp masker-spring-boot-starter/pom.xml "$TMP/masker-spring-boot-starter/"; cp masker-demo/pom.xml "$TMP/masker-demo/"
|
||||||
|
rm -rf "$TMP/$AC/target"
|
||||||
|
python3 - "$TMP/$AC/pom.xml" <<'PY'
|
||||||
|
import sys
|
||||||
|
p=sys.argv[1]
|
||||||
|
s=open(p).read()
|
||||||
|
s=s.replace("<artifactId>spring-boot-configuration-processor</artifactId>","<artifactId>spring-boot-configuration-processor</artifactId>\n <version>${project.parent.version}</version>")
|
||||||
|
open(p,'w').write(s)
|
||||||
|
PY
|
||||||
|
{
|
||||||
|
echo "# The processor path with <version>\${project.parent.version}</version>, when the module's parent is not Spring Boot's"
|
||||||
|
echo
|
||||||
|
(cd "$TMP" && mvn -B -U -pl $AC compile 2>&1 | grep -E 'ERROR.*(annotationProcessorPath|Could not find artifact)' | head -1 | sed -E 's/^\[ERROR\] //; s/ -> \[Help 1\]//')
|
||||||
|
} > output/15-annotation-processor-version-trap.txt
|
||||||
|
rm -rf "$TMP"
|
||||||
|
echo "done"
|
||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerates every file under output/.
|
||||||
|
#
|
||||||
|
# ./scripts/run-all.sh
|
||||||
|
#
|
||||||
|
# Needs a JDK 25 and Maven 3.9. Transcripts 01-06, 08-11 and 16 come out of the test suite, which is the point:
|
||||||
|
# the figures in the article are assertions that fail the build if they stop being true.
|
||||||
|
# Transcripts 07 and 12-15 are read from the built jars and from builds that are made to fail on purpose.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
echo "== test suite (transcripts 01-06, 08-11, 16)"
|
||||||
|
mvn -B test
|
||||||
|
|
||||||
|
echo "== jar contents, metadata, Boot 4 locations and deliberate failures (07, 12-15, 17)"
|
||||||
|
./scripts/capture-jar-facts.sh
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "output:"
|
||||||
|
ls -1 output
|
||||||
Reference in New Issue
Block a user