Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01HhW8LD8r3EigoBaCGrQZx5 Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01HhW8LD8r3EigoBaCGrQZx5
96 lines
5.2 KiB
Java
96 lines
5.2 KiB
Java
package com.ankurm.structure;
|
|
|
|
import com.ankurm.structure.scan.broken.app.BrokenApp;
|
|
import com.ankurm.structure.scan.fixed.FixedApp;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.extension.ExtendWith;
|
|
import org.springframework.boot.WebApplicationType;
|
|
import org.springframework.boot.builder.SpringApplicationBuilder;
|
|
import org.springframework.boot.test.system.CapturedOutput;
|
|
import org.springframework.boot.test.system.OutputCaptureExtension;
|
|
import org.springframework.context.ConfigurableApplicationContext;
|
|
|
|
import javax.tools.*;
|
|
import java.net.URI;
|
|
import java.net.URL;
|
|
import java.net.URLClassLoader;
|
|
import java.net.http.HttpClient;
|
|
import java.net.http.HttpRequest;
|
|
import java.net.http.HttpResponse;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.util.List;
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
|
|
/** The two ways a project layout silently breaks component scanning. */
|
|
@ExtendWith(OutputCaptureExtension.class)
|
|
class ScanTrapTests {
|
|
|
|
@Test
|
|
void appClassInTheWrongPackage() throws Exception {
|
|
HttpClient http = HttpClient.newHttpClient();
|
|
try (Transcript t = new Transcript("05-scan-starts-at-the-app-class.txt",
|
|
"The controller is found only if it is in or below the @SpringBootApplication class's package")) {
|
|
for (var c : List.of(
|
|
new Object[]{"broken", BrokenApp.class, "app class in scan.broken.app, controller in scan.broken.web (a sibling)"},
|
|
new Object[]{"fixed", FixedApp.class, "app class in scan.fixed, controller in scan.fixed.web (below it)"})) {
|
|
try (ConfigurableApplicationContext ctx = new SpringApplicationBuilder((Class<?>) c[1])
|
|
.properties("server.port=0", "spring.main.banner-mode=off", "logging.level.root=WARN").run()) {
|
|
String base = "http://localhost:" + ctx.getEnvironment().getProperty("local.server.port");
|
|
HttpResponse<String> r = http.send(HttpRequest.newBuilder(URI.create(base + "/hello")).GET().build(),
|
|
HttpResponse.BodyHandlers.ofString());
|
|
t.section((String) c[2]);
|
|
t.line("GET /hello -> %d", r.statusCode());
|
|
t.line("HelloController registered as a bean: %s", ctx.getBeanNamesForType(Object.class).length > 0
|
|
&& java.util.Arrays.stream(ctx.getBeanDefinitionNames()).anyMatch(n -> n.equals("helloController")));
|
|
if (c[0].equals("broken")) assertThat(r.statusCode()).isEqualTo(404);
|
|
else assertThat(r.statusCode()).isEqualTo(200);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void appClassInTheDefaultPackage(CapturedOutput output) throws Exception {
|
|
String source = "import org.springframework.boot.autoconfigure.SpringBootApplication;\n"
|
|
+ "@SpringBootApplication\n"
|
|
+ "public class DefaultPackageApp {\n"
|
|
+ "}\n";
|
|
Path out = Files.createTempDirectory("defaultpkg");
|
|
JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
|
|
JavaFileObject file = new SimpleJavaFileObject(URI.create("string:///DefaultPackageApp.java"), JavaFileObject.Kind.SOURCE) {
|
|
@Override public CharSequence getCharContent(boolean ignore) { return source; }
|
|
};
|
|
boolean compiled = javac.getTask(null, null, null,
|
|
List.of("-proc:none", "-d", out.toString(), "-classpath", System.getProperty("java.class.path")), null, List.of(file)).call();
|
|
assertThat(compiled).isTrue();
|
|
|
|
try (Transcript t = new Transcript("06-default-package.txt", "A @SpringBootApplication class in the default package (no package statement)")) {
|
|
t.line("source compiled:");
|
|
for (String l : source.split("\n")) t.line(" %s", l);
|
|
t.blank();
|
|
Class<?> app;
|
|
try (URLClassLoader cl = new URLClassLoader(new URL[]{out.toUri().toURL()}, getClass().getClassLoader())) {
|
|
app = Class.forName("DefaultPackageApp", true, cl);
|
|
String result;
|
|
try (ConfigurableApplicationContext ctx = new SpringApplicationBuilder(app)
|
|
.web(WebApplicationType.NONE).properties("spring.main.banner-mode=off").run()) {
|
|
result = "context started with " + ctx.getBeanDefinitionCount() + " bean definitions";
|
|
} catch (Throwable e) {
|
|
Throwable root = e;
|
|
while (root.getCause() != null) root = root.getCause();
|
|
result = "context FAILED to start: " + e.getClass().getSimpleName() + " / root cause "
|
|
+ root.getClass().getSimpleName() + ": " + String.valueOf(root.getMessage()).lines().findFirst().orElse("");
|
|
}
|
|
t.section("what Spring Boot logged about the default package");
|
|
output.getAll().lines().filter(l -> l.contains("default package") || l.contains("WARNING"))
|
|
.map(l -> l.replaceFirst("^\\S+\\s+", "")).forEach(t::line);
|
|
t.section("result");
|
|
t.line(result);
|
|
}
|
|
assertThat(output.getAll()).contains("default package");
|
|
}
|
|
}
|
|
}
|