1. Spring Boot seen from ASP.NET Core
Sections
Reading settings
No voice for this language in this browser.
More voices
Code and English terms
Off: the page’s voice reads everything, with no pause between two voices.
No voice for this language in this browser.
More voices
More voices: Microsoft Edge has natural online voices, and Windows (Settings › Time & language › Speech), macOS and Android install others.
Full example: code/spring-cloud-reactor/l01-mvc, a scales API on Spring MVC. Its tests compare every response, log excerpt and error report quoted below with the files in expected, and the .NET side is csharp/l01-di.cs.
The same application, two frameworks
Section titled “The same application, two frameworks”An ASP.NET Core application tells you what it contains: Program.cs registers services, reads configuration and maps endpoints, one line at a time. A Spring Boot application mostly doesn’t. It declares a class, annotates a few others, and the framework finds the rest on the class path. The pieces are the same; what changes is who writes the list.
| ASP.NET Core | Spring Boot | What it does |
|---|---|---|
dotnet new webapi |
Spring Initializr | generates a project |
Program.cs with WebApplication.CreateBuilder |
a class annotated @SpringBootApplication |
the entry point |
IServiceCollection, IServiceProvider |
the IoC container, an ApplicationContext |
creates and injects objects |
builder.Services.AddScoped<T>() |
component scanning of @Component classes, or @Bean methods |
registers them |
AddControllers(), AddHealthChecks()… |
auto-configuration | turns features on |
appsettings.json, appsettings.Development.json |
application.yml, application-dev.yml (profiles) |
configuration |
IOptions<T> |
@ConfigurationProperties |
typed configuration |
[ApiController], minimal APIs |
@RestController (Spring MVC) |
endpoints |
| Kestrel | Tomcat (embedded) | the HTTP server |
MapHealthChecks, IHealthCheck |
Actuator, HealthIndicator |
health and management endpoints |
WebApplicationFactory<Program> |
@SpringBootTest |
integration tests |
Generating a project
Section titled “Generating a project”Spring Initializr is a web page and an HTTP API. The page lets you tick dependencies; the API does the same from a terminal:
curl.exe https://start.spring.io/starter.zip -d type=maven-project -d bootVersion=4.1.1 -d javaVersion=25 ` -d groupId=dev.learn -d artifactId=scales -d packageName=dev.learn.scales ` -d dependencies=web,actuator,validation -o scales.zipExpand-Archive scales.zip -DestinationPath scalescd scales.\mvnw.cmd packagecurl https://start.spring.io/starter.zip -d type=maven-project -d bootVersion=4.1.1 -d javaVersion=25 \ -d groupId=dev.learn -d artifactId=scales -d packageName=dev.learn.scales \ -d dependencies=web,actuator,validation -o scales.zipunzip scales.zip -d scalescd scales./mvnw packagecurl https://start.spring.io/starter.zip -d type=maven-project -d bootVersion=4.1.1 -d javaVersion=25 \ -d groupId=dev.learn -d artifactId=scales -d packageName=dev.learn.scales \ -d dependencies=web,actuator,validation -o scales.zipunzip scales.zip -d scalescd scales./mvnw packageI ran the PowerShell commands and the Bash commands in Git Bash on Windows on 2026-09-14; on Linux and macOS they are to verify (unzip may need installing first). The archive holds a Maven project with the Maven Wrapper (mvnw, which downloads Maven 3.9.16 on first use), one application class and one test:
scales/├── .mvn/wrapper/maven-wrapper.properties├── mvnw, mvnw.cmd├── pom.xml└── src/ ├── main/java/dev/learn/scales/ScalesApplication.java ├── main/resources/application.properties └── test/java/dev/learn/scales/ScalesApplicationTests.javaThe generated pom.xml is short because its parent does the work, as lesson 10 of the Java course explained: spring-boot-starter-parent fixes the version of every Spring library and plugin, like Directory.Packages.props and the SDK’s defaults together.
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> <!-- lookup parent from repository --></parent>...<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> ... <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc-test</artifactId> <scope>test</scope> </dependency></dependencies>A starter is a dependency with no code of its own that brings a coherent set of libraries: spring-boot-starter-webmvc brings Spring MVC, Tomcat and Jackson. It plays the role of the framework reference that Microsoft.NET.Sdk.Web adds to a project. Two details are new in Spring Boot 4, according to the migration guide: the web starter I had known as spring-boot-starter-web is now spring-boot-starter-webmvc (the Initializr API still accepts the old identifier web, as the command above shows), and every technology has its own test starter, named spring-boot-starter-<technology>-test.
The course’s parent POM also imports the Spring Cloud BOM, spring-cloud-dependencies 2025.1.3, the release train that spring.io pairs with Spring Boot 4.0 and 4.1. Lessons 1 to 4 don’t use Spring Cloud yet; importing the BOM from the start means the later lessons add dependencies without versions.
The entry point
Section titled “The entry point”// Program.cs: @SpringBootApplication turns on component scanning and auto-configuration for this package and below.@SpringBootApplication@ConfigurationPropertiesScanpublic class ScalesApplication {
public static void main(String[] args) { SpringApplication.run(ScalesApplication.class, args); }}@SpringBootApplication combines three annotations: @SpringBootConfiguration (this class can declare beans), @EnableAutoConfiguration (configure what the class path suggests) and @ComponentScan (register every annotated class in this package and its subpackages). @ConfigurationPropertiesScan adds the typed configuration classes of the same packages. SpringApplication.run creates the container, starts the embedded server and returns once the application is ready. The log shows the steps; the test that captures it replaces the process ID, the port, the paths and the durations, which change from one run to the next:
INFO <pid> --- [scales] [ main] dev.learn.scales.ScalesApplication : Starting ScalesApplication using Java <version> with PID <pid> (<path>) INFO <pid> --- [scales] [ main] dev.learn.scales.ScalesApplication : No active profile set, falling back to 1 default profile: "default" INFO <pid> --- [scales] [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port <port> (http) INFO <pid> --- [scales] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] INFO <pid> --- [scales] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/<version>] INFO <pid> --- [scales] [ main] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in <n> ms INFO <pid> --- [scales] [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 2 endpoints beneath base path '/actuator' INFO <pid> --- [scales] [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port <port> (http) with context path '/' INFO <pid> --- [scales] [ main] dev.learn.scales.ScalesApplication : Started ScalesApplication in <n> seconds (process running for <n>)[scales] is spring.application.name, and main is the thread. On my machine the whole start took 1.5 seconds, most of it class loading.
Beans and the container
Section titled “Beans and the container”Spring calls the objects its container manages beans. There are two ways to declare one, and a Spring application uses both.
Component scanning. A class annotated @Component, or one of its specialisations @Service, @Repository, @Controller and @RestController, is registered because it is in a scanned package. There is no AddSingleton line anywhere:
// @Service is a @Component: component scanning registers it as a singleton, with no AddSingleton call.@Servicepublic class ScaleService {
private final NoteSpeller speller;
// A single constructor needs no @Autowired: Spring injects its parameters, like the .NET container. public ScaleService(NoteSpeller speller) { this.speller = speller; }@Bean methods. A method of a @Configuration class returns the object to register. It is the Spring form of services.AddSingleton<INoteSpeller>(sp => ...), for objects that need a decision or come from a library you can’t annotate:
// A @Configuration class holds factory methods, like services.AddSingleton<INoteSpeller>(sp => ...).@Configurationclass SpellingConfiguration {
@Bean NoteSpeller noteSpeller(ScalesProperties properties) { return switch (properties.spelling()) { case SHARPS -> NoteSpeller.sharps(); case FLATS -> NoteSpeller.flats(); }; }}The parameters of a @Bean method are injected like constructor parameters. NoteSpeller names a note with sharps (A#) or flats (Bb); the next section binds ScalesProperties from configuration.
Three differences from .NET matter every day:
- Singleton is the default. A .NET registration always states a lifetime. A Spring bean is a singleton unless it says otherwise with
@Scope:prototypeis .NET’s transient, andrequestis the closest thing to scoped in a web application. Most beans in a Spring service are stateless singletons. - Every bean has a name.
scaleServicefor the class above,noteSpellerfor the method. Names matter when several beans share a type, and they collide: in lesson 4, a@Beanmethod named like its own@Configurationclass stopped the application. - The container is checked at startup. Singletons are created eagerly when the context starts, so a missing dependency stops the application before it serves a request.
When the container can’t decide
Section titled “When the container can’t decide”What happens when a dependency is missing shows the last difference best. The test StartupTest starts a context that registers ScaleService but nothing that implements NoteSpeller. Spring Boot’s failure analyzers turn the exception into a report:
***************************APPLICATION FAILED TO START***************************
Description:
Parameter 0 of constructor in dev.learn.scales.ScaleService required a bean of type 'dev.learn.scales.NoteSpeller' that could not be found.
Action:
Consider defining a bean of type 'dev.learn.scales.NoteSpeller' in your configuration.The .NET container builds a provider without complaint and fails only when the service is resolved. ValidateOnBuild moves the check to startup, which is what Spring always does. The C# side prints:
built a provider without INoteSpellerresolve: Unable to resolve service for type 'INoteSpeller' while attempting to activate 'ScaleService'.ValidateOnBuild: 1 error(s): Error while validating the service descriptor 'ServiceType: ScaleService Lifetime: Singleton ImplementationType: ScaleService': Unable to resolve service for type 'INoteSpeller' while attempting to activate 'ScaleService'.ServiceProviderOptions.ValidateOnBuild is on by default in the Development environment of an ASP.NET Core host, so both frameworks catch this at startup during development; only Spring does it in every environment.
Two candidates are a harder case. Register two NoteSpeller beans and ask for one:
// Two NoteSpeller beans and a constructor that asks for one: .NET would silently take the last registration.@Configuration@Import(ScaleService.class)public class TwoSpellersApplication {
@Bean NoteSpeller sharpSpeller() { return NoteSpeller.sharps(); }
@Bean NoteSpeller flatSpeller() { return NoteSpeller.flats(); }}***************************APPLICATION FAILED TO START***************************
Description:
Parameter 0 of constructor in dev.learn.scales.ScaleService required a single bean, but 2 were found: - sharpSpeller: defined by method 'sharpSpeller' in dev.learn.broken.ambiguous.TwoSpellersApplication - flatSpeller: defined by method 'flatSpeller' in dev.learn.broken.ambiguous.TwoSpellersApplication
This may be due to missing parameter name information
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed.NET resolves the same situation silently, with the last registration:
two registrations, ScaleService uses FlatSpellerIEnumerable<INoteSpeller>: SharpSpeller, FlatSpellerkeyed "flats": FlatSpellerSpring’s three ways out map onto .NET’s:
| Spring | .NET |
|---|---|
@Primary on one bean |
register it last |
a List<NoteSpeller> parameter |
IEnumerable<INoteSpeller> |
@Qualifier("flatSpeller") on the parameter, or a Map<String, NoteSpeller> keyed by bean name |
keyed services, [FromKeyedServices("flats")] |
The line “This may be due to missing parameter name information” is noise here: when the parameter name matches a bean name, Spring uses it as a fallback qualifier, and it only needs -parameters, which the parent POM already passes to javac.
Auto-configuration
Section titled “Auto-configuration”Nothing in the example creates a Tomcat server, a JSON mapper or a DispatcherServlet (the front controller of Spring MVC). Auto-configuration classes, shipped in Spring Boot’s JARs, create them, each under conditions. Starting the application with --debug prints the conditions evaluation report. An excerpt of mine:
============================CONDITIONS EVALUATION REPORT============================
Positive matches:-----------------
DispatcherServletAutoConfiguration matched: - @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition) - found 'session' scope (OnWebApplicationCondition)
JacksonAutoConfiguration#jacksonJsonMapper matched: - @ConditionalOnMissingBean (types: tools.jackson.databind.json.JsonMapper; SearchStrategy: all) did not find any beans (OnBeanCondition)
TomcatServletWebServerAutoConfiguration matched: - @ConditionalOnClass found required classes 'jakarta.servlet.ServletRequest', 'org.apache.catalina.startup.Tomcat', 'org.apache.coyote.UpgradeProtocol', 'org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory' (OnClassCondition) - found 'session' scope (OnWebApplicationCondition)
Negative matches:-----------------
JacksonAutoConfiguration.XmlConfiguration: Did not match: - @ConditionalOnClass did not find required class 'tools.jackson.dataformat.xml.XmlMapper' (OnClassCondition)Two conditions do most of the work:
@ConditionalOnClass: configure Tomcat because Tomcat is on the class path. Adding a starter is how you turn a feature on, where ASP.NET Core needs both the package and anAdd…()call.@ConditionalOnMissingBean: create aJsonMapperonly if the application hasn’t declared one. Declaring your own bean of that type is how you replace the default, with noReplaceorRemoveAllon the service collection.
The tools.jackson package is also new: Spring Boot 4 uses Jackson 3, whose packages moved from com.fasterxml.jackson.
Configuration
Section titled “Configuration”application.yml and typed properties
Section titled “application.yml and typed properties”spring: application: name: scales
# Bound to ScalesProperties; SCALES_SPELLING=flats in the environment overrides itscales: default-mode: ionian spelling: sharps
management: endpoints: web: exposure: include: health,info endpoint: health: show-details: alwaysapplication.properties (flat key=value lines) and application.yml are equivalent; Initializr generates the first, many projects use the second. The scales section binds to a record, the Spring counterpart of an options class:
// The options pattern: IOptions<ScalesOptions> bound from the "Scales" section, validated at startup.@ConfigurationProperties("scales")@Validatedpublic record ScalesProperties(@NotBlank String defaultMode, @NotNull Spelling spelling) {
public enum Spelling { SHARPS, FLATS }}The record is itself a bean: ScaleController and the @Bean method above receive ScalesProperties directly, where .NET code injects IOptions<ScalesOptions> and reads .Value. @Validated with Jakarta Bean Validation annotations is ValidateDataAnnotations().ValidateOnStart(). An empty mode stops the application:
***************************APPLICATION FAILED TO START***************************
Description:
Binding to target dev.learn.scales.ScalesProperties failed:
Property: scales.defaultMode Value: "" Origin: "scales.default-mode" from property source "commandLineArgs" Reason: must not be blank
Action:
Update your application's configurationThe report names the property source, here the command line. This is Spring’s relaxed binding: scales.default-mode in a file, scales.defaultMode in Java, and SCALES_DEFAULTMODE or SCALES_DEFAULT_MODE in the environment all reach the same field. .NET’s convention for the same thing is Scales__DefaultMode.
Where a value comes from
Section titled “Where a value comes from”Both frameworks layer configuration sources, and a later source wins. In .NET the order is the order of the Add… calls; the C# side puts an in-memory source first and environment variables second, and prints Scales:Spelling = flats, Scales:DefaultMode = ionian. Spring Boot has a fixed order of about fifteen sources. The ones you meet daily, from weakest to strongest:
application.ymlpackaged in the JAR;application-<profile>.ymlpackaged in the JAR;- the same files next to the JAR, outside it;
- environment variables;
- Java system properties (
-Dscales.spelling=flats); - command-line arguments (
--scales.spelling=flats).
Test annotations sit above all of these: @SpringBootTest(properties = ...) wins over the files, which ProfilesTest checks.
Profiles
Section titled “Profiles”A profile is a named set of beans and properties, turned on with spring.profiles.active. It covers what .NET splits between ASPNETCORE_ENVIRONMENT and appsettings.{Environment}.json, except that several profiles can be active at once and their names are yours. application-flats.yml is loaded on top of application.yml when the flats profile is active:
# Loaded on top of application.yml when the "flats" profile is active, like appsettings.Development.jsonscales: spelling: flats default-mode: aeolianBeans can depend on a profile too. This runner exists only with the print profile, which gives a quick way to see the configuration without starting a web server:
// Runs once the context has started, only with the "print" profile: a quick way to see the configuration at work.@Component@Profile("print")class PrintScaleRunner implements ApplicationRunner {ApplicationRunner runs once after startup, like the start of an IHostedService that does its work and returns. Running the packaged JAR with an environment variable:
$env:SCALES_SPELLING = 'flats'java -jar target/scales.jar --spring.profiles.active=print --spring.main.web-application-type=noneRemove-Item Env:SCALES_SPELLINGSCALES_SPELLING=flats java -jar target/scales.jar --spring.profiles.active=print --spring.main.web-application-type=noneSCALES_SPELLING=flats java -jar target/scales.jar --spring.profiles.active=print --spring.main.web-application-type=nonespelling: FLATSScaleView[root=Bb, mode=ionian, notes=[Bb, C, D, Eb, F, G, A], chords=[Bb, Cm, Dm, Eb, F, Gm, Adim]]--spring.main.web-application-type=none starts the container without Tomcat. CI runs the same command through check.sh, with the log level raised to warn so that only these lines remain.
The controller
Section titled “The controller”// [ApiController] with [Route("scales")]: the return value is written as JSON.@RestController@RequestMapping("/scales")class ScaleController {
private final ScaleService scales; private final ScalesProperties properties;
ScaleController(ScaleService scales, ScalesProperties properties) { this.scales = scales; this.properties = properties; }
// GET /scales/D?mode=dorian, like [HttpGet("{root}")] with a [FromQuery] parameter. @GetMapping("/{root}") ScaleService.ScaleView scale(@PathVariable String root, @RequestParam Optional<String> mode) { return scales.describe(root, mode.orElse(properties.defaultMode())); }
// An RFC 9457 problem, like Results.Problem(...) in ASP.NET Core. @ExceptionHandler(IllegalArgumentException.class) ProblemDetail badName(IllegalArgumentException e) { ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage()); problem.setTitle("Unknown note or mode"); return problem; }}The mapping is close to an ASP.NET Core controller. @PathVariable and @RequestParam are [FromRoute] and [FromQuery]; an Optional parameter is optional, like a nullable one. @ExceptionHandler catches an exception thrown by this controller’s methods and returns a ProblemDetail, the RFC 9457 body that ASP.NET Core writes with Results.Problem. HttpTranscriptTest starts the application on a free port and records the responses:
GET /scales/D?mode=dorian200 application/json{"root":"D","mode":"dorian","notes":["D","E","F","G","A","B","C"],"chords":["Dm","Em","F","G","Am","Bdim","C"]}
GET /scales/Bb200 application/json{"root":"A#","mode":"ionian","notes":["A#","C","D","D#","F","G","A"],"chords":["A#","Cm","Dm","D#","F","Gm","Adim"]}
GET /scales/H400 application/problem+json{"detail":"unknown note: H","instance":"/scales/H","status":400,"title":"Unknown note or mode"}Bb comes back as A#: without a profile, the speller uses sharps. The record serialises with its component names, and Jackson writes the problem’s properties in alphabetical order, where ASP.NET Core starts with type and title.
Actuator
Section titled “Actuator”spring-boot-starter-actuator adds management endpoints under /actuator. Only health is exposed over HTTP by default; the configuration above adds info and shows the details. A custom check is a HealthIndicator bean, the counterpart of IHealthCheck:
// IHealthCheck: Actuator adds every HealthIndicator bean to /actuator/health, named after the bean.@Componentclass ScaleCatalogHealthIndicator implements HealthIndicator {
@Override public Health health() { int modes = Mode.values().length; return modes == 7 ? Health.up().withDetail("modes", modes).build() : Health.down().withDetail("modes", modes).build(); }}GET /actuator/health/scaleCatalog200 application/vnd.spring-boot.actuator.v3+json{"details":{"modes":7},"status":"UP"}
GET /actuator/health/liveness200 application/vnd.spring-boot.actuator.v3+json{"status":"UP"}The indicator is named scaleCatalog, its bean name without the HealthIndicator suffix, and each component has its own URL. The full /actuator/health also listed diskSpace, ping, ssl, livenessState and readinessState on my machine; the test doesn’t quote it, because disk space and paths differ from one machine to the next. The liveness and readiness groups exist for Kubernetes probes, as health check tags do in ASP.NET Core. HealthIndicator and Health moved in Spring Boot 4 to the package org.springframework.boot.health.contributor, so older examples import them from a package that no longer exists.
Integration tests
Section titled “Integration tests”@SpringBootTest starts the whole context, like WebApplicationFactory<Program>. With webEnvironment = RANDOM_PORT, it starts Tomcat on a free port as well, and @LocalServerPort injects the port:
// WebApplicationFactory<Program> with a real Kestrel: the application starts on a free port for the test class.@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)class HttpTranscriptTest {
@LocalServerPort int port;Spring caches the context between test classes that use the same configuration, so eight test classes don’t start eight servers. The test starter brings JUnit, AssertJ and Mockito, which lesson 11 of the Java course covered. Even though no test mocks anything, Spring’s test support initialises Mockito, and the JVM warned that a Java agent had been loaded dynamically until Surefire loaded Mockito with -javaagent, as that lesson does. Exercise 3 writes a test with RestTestClient, Spring Framework 7’s fluent client for tests.
Key takeaways
Section titled “Key takeaways”- A Spring Boot application declares less than
Program.cs: component scanning registers annotated classes, and auto-configuration turns on what the class path contains. - Beans are singletons by default, have names, and are created at startup, so a missing or ambiguous dependency stops the application with a readable report. .NET takes the last registration where Spring refuses to guess.
@Beanmethods in a@Configurationclass are the factory registrations;@Primary,@Qualifier,ListandMapparameters resolve several candidates.@ConditionalOnClassand@ConditionalOnMissingBeanexplain most of auto-configuration: add a starter to turn a feature on, declare your own bean to replace a default.--debugprints the report.application.yml, profiles and@ConfigurationPropertiesrecords replaceappsettings.json, environments andIOptions<T>. Environment variables beat packaged files; command-line arguments beat everything but tests.- Spring Boot 4 renamed the web starter to
spring-boot-starter-webmvc, split test starters by technology, moved to Jackson 3, and moved Actuator’s health classes.
Exercises
Section titled “Exercises”- With the
flatsprofile,application-flats.ymlsetsdefault-modetoaeolian. Predict what theprintrunner prints withSCALES_DEFAULT_MODE=dorianin the environment and--spring.profiles.active=print,flats, then check.
Solution
$ SCALES_DEFAULT_MODE=dorian java -jar target/scales.jar --spring.profiles.active=print,flatsspelling: FLATSScaleView[root=Bb, mode=dorian, notes=[Bb, C, Db, Eb, F, G, Ab], chords=[Bbm, Cm, Db, Eb, Fm, Gdim, Ab]]The spelling comes from the profile’s file, which nothing overrides. The mode comes from the environment variable: environment variables rank above every application*.yml file packaged in the JAR, profile-specific or not. In .NET, the same result depends on the order of the Add… calls; the default host adds environment variables after the JSON files, so the outcome matches. check.sh runs this command in CI and compares the output.
- .NET’s keyed services let a class choose an implementation by key at run time. Register two
NoteSpellerbeans namedsharpsandflats, and write aSpellingChooserthat receives all of them and names a note with the speller whose name it is given, without@Qualifier.
Solution
@Configurationstatic class Spellers {
@Bean NoteSpeller sharps() { return NoteSpeller.sharps(); }
@Bean NoteSpeller flats() { return NoteSpeller.flats(); }
@Bean SpellingChooser chooser(Map<String, NoteSpeller> byName, List<NoteSpeller> all) { return new SpellingChooser(byName, all); }}
// Map<String, T> is keyed by bean name, like [FromKeyedServices("flats")]; List<T> is IEnumerable<T>.record SpellingChooser(Map<String, NoteSpeller> byName, List<NoteSpeller> all) {
String name(Note note, String spelling) { NoteSpeller speller = byName.get(spelling); if (speller == null) { throw new IllegalArgumentException("unknown spelling: " + spelling + ", expected one of " + byName.keySet()); } return speller.name(note); }}A Map<String, NoteSpeller> parameter receives every bean of that type, keyed by bean name, in registration order: the test checks that the keys are [sharps, flats] and that A# becomes Bb with flats. The test creates the container itself with new AnnotationConfigApplicationContext(Spellers.class), which is all the IoC container needs; Spring Boot is not involved. @Qualifier("flats") would fix one choice at injection time, like [FromKeyedServices]; the map defers it to each call, like GetRequiredKeyedService.
- Write an integration test with
RestTestClientchecking thatGET /scales/C?mode=bluesanswers 400 with anapplication/problem+jsonbody whosetitle,detailandinstanceare the ones the controller sets.
Solution
@SpringBootTest(classes = ScalesApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT)@AutoConfigureRestTestClientclass Exercise3ProblemDetailsTest {
@Autowired RestTestClient client;
@Test void unknownModeIsABadRequestProblem() { client.get().uri("/scales/C?mode=blues") .exchange() .expectStatus().isBadRequest() .expectHeader().contentType(MediaType.APPLICATION_PROBLEM_JSON) .expectBody() .jsonPath("$.title").isEqualTo("Unknown note or mode") .jsonPath("$.detail").isEqualTo("unknown mode: blues") .jsonPath("$.instance").isEqualTo("/scales/C"); }}@AutoConfigureRestTestClient configures the client with the random port, so the URIs are relative, like the HttpClient that WebApplicationFactory.CreateClient() returns. classes = ScalesApplication.class is optional here: without it, @SpringBootTest searches the test’s package and its parents for a @SpringBootConfiguration, and finds ScalesApplication one level up. instance is the path without the query string.
Sources
Section titled “Sources”- Spring Boot reference: using
@SpringBootApplication, auto-configuration, externalized configuration, profiles, Actuator endpoints, testing Spring Boot applications - Spring Boot 4.0 migration guide
- Spring Framework reference: the IoC container, Java-based configuration, bean scopes, error responses
- .NET: dependency injection in ASP.NET Core, configuration, the options pattern, environments, health checks, integration tests