Workshop @ Spring I/O 2025 - Moritz Halbritter & Fabian Krüger
Getting Started
-
Checkout the workshop project from GitHub
cd ~
git clone https://github.com/fabapp2/spring-boot-magic-workshop.git
cd spring-boot-magic-workshop
git checkout exercise-1
|
Tip
|
Remember to reload your workspace after adding dependencies to make your IDE aware of the new types. |
Project Layout
The project has these modules following the app continuum layout.
-
app- The Spring Boot application -
library-autoconfigure- Spring Boot Auto-configuration for thelibrary -
library-api- Spring Boot independent library withGreetingServiceinterface -
library-stdout- Spring Boot independent library implementation logging to stdout -
library-slf4j- Spring Boot independent library implementation logging with SLF4J -
library-spring-boot-starter- A Spring Boot starter to make the auto-configuration and all dependencies easily available
Prerequisite
Start the application in app and understand how modules are wired.
Exercise 1: Auto Configured Bean
Make the StdOutGreetingService from library-stdout available as an auto-configured Spring bean for applications that rely on GreetingService.
Learnings
-
How does Spring Boot find auto-configuration classes
-
How does Spring Boot provide auto-configured beans
-
How to allow auto-configured beans to be overwritten
Task
-
Create an auto-configuration class
com.workshop.magic.config.GreetingAutoConfigurationin thelibrary-autoconfiguremodule. -
Add a Maven dependency to
org.springframework.boot:spring-boot-autoconfigurewhich brings the@AutoConfigurationannotation. -
In the
library-autoconfiguremodule, add a Maven dependency to other required modules (library-apiandlibrary-stdout). Make them optional, which is a best-practice for auto-configurations. -
Annotate the
GreetingAutoConfigurationwith@AutoConfiguration. -
Annotate the
GreetingAutoConfigurationwith@ConditionalOnClass(GreetingService.class)to only process the auto-config whenGreetingServiceis on the application classpath. -
This auto-configuration should provide
StdOutGreetingServiceas a Spring bean of typeGreetingService. Use the@Beanannotation for that. -
Auto-configuration classes must be declared in a classpath resource file named
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. This file contains fully qualified names of the auto-configuration so that Spring Boot can find them. -
Add the Maven dependency
library-autoconfigureandlibrary-stdouttoapp, making the auto-configuredGreetingServiceavailable. -
🤔 Starting the application should fail. Why is that?
-
To avoid conflicts and allow custom implementations, the
StdOutGreetingServiceshould only be created when no other bean of typeGreetingServiceexists. This can be achieved by annotating the bean declaration with@ConditionalOnMissingBean, which tells Spring Boot to back off when such a bean already exists. -
✅ Starting the application should now print: MyGreetingService: Hola Spring I/O Barcelona.
-
Modify the application to use the
StdOutGreetingServicenow. -
✅ Starting the application should now print: StdOutGreetingService: Hola Spring I/O Barcelona.
|
Note
|
Auto-configurations must be loaded only by being named in the imports file. Make sure that they are defined in a specific package space and that they are never the target of component scanning. Furthermore, auto-configuration classes should not enable component scanning to find additional components. Specific @Import annotations should be used instead. |
Detailed Steps
Detailed Steps
-
Create a new class
com.workshop.magic.config.GreetingAutoConfigurationin thelibrary-autoconfiguremodule. -
Add a Maven dependency to
org.springframework.boot:spring-boot-autoconfigurein thelibrary-autoconfiguremodule. -
Add a Maven dependency to
com.workshop:library-stdoutin thelibrary-autoconfiguremodule, with<optional>true</optional>. -
Create a new file
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsin thelibrary-autoconfiguremodule (see the reference documentation). -
Add the fully qualified classname of the
GreetingAutoConfigurationclass to the.importsfile. -
Annotate the
GreetingAutoConfigurationwith@AutoConfiguration. -
Create a new
GreetingServicebean inGreetingAutoConfigurationthat returns a new instance ofStdOutGreetingService.@Bean GreetingService stdOutGreetingService() { return new StdOutGreetingService(); } -
Add a Maven dependency to
com.workshop:library-autoconfigurein theappmodule. -
Add a Maven dependency to
com.workshop:library-stdoutin theappmodule. -
Starting the application fails. That’s because there are now two beans of type
GreetingService:MyGreetingService(annotated with@Service) from theappmodule and theStdOutGreetingServicefrom the auto-configuration. -
Use the
@ConditionalOnMissingBeanannotation on theGreetingServicebean method inGreetingAutoConfigurationto only load the bean when no other bean of typeGreetingServiceexists (see the reference documentation). -
The application now starts and uses the
MyGreetingService. -
Now, remove the
MyGreetingServiceclass from theappmodule, or comment out/remove the@Serviceannotation onMyGreetingService. -
The application now starts and uses the
StdOutGreetingService.
Conclusion
Think for a moment, when is this useful and where does Spring Boot use this concept?
Answer
Spring Boot’s auto-configuration simplifies application development by automatically configuring components based on the dependencies present on the classpath. This feature reduces the need for manual setup, allowing developers to focus on business logic rather than boilerplate code.
For example, adding spring-boot-starter-web sets up a whole webserver without manual configuration.
Solution
git checkout -f exercise-2
🥳 Fantastic, let’s move on to the next exercise
Exercise 2: Custom Spring Boot Starter
It’s a bit unfortunate that users of your auto-configuration need a dependency on com.workshop:library-autoconfigure and on com.workshop:library-stdout.
You will now package the library-autoconfigure and library-stdout modules into a reusable Spring Boot starter.
Learnings
-
How do Spring Boot Starters work
Task:
-
Add Maven dependencies to
library-autoconfigureandlibrary-stdoutin thelibrary-spring-boot-startermodule. -
Add a Maven dependency to
org.springframework.boot:spring-boot-starterin thelibrary-spring-boot-startermodule. -
Replace direct dependencies to
library-autoconfigureandlibrary-stdoutin theappmodule with the new starter. -
✅ Confirm that the app still works as expected and prints the greeting.
Conclusion
🤔 Why create a starter? When could that be useful?
Answer
A starter simplifies the integration of your library. It contains the auto-configuration and all the needed dependencies in one single dependency. In our case, the starter only contains two dependencies, but you can image starters for more complex scenarios, which bring dozens or more dependencies.
Solution
git checkout -f exercise-3
🥳 Awesome, let’s move on to the next exercise
Exercise 3: Custom Starter Without Default GreetingService
In this exercise, you will make the existing LoggerGreetingService available as an auto-configured bean — but only when the corresponding class is on the classpath. You will also adjust the fallback behavior of StdOutGreetingService so it is only used when the SLF4J-based implementation is not present.
This pattern mimics common practices in Spring Boot where auto-configured beans adapt to the available classpath.
Learnings
-
How to auto-configure beans conditionally based on classpath presence
-
How to combine
@ConditionalOnClassand@ConditionalOnMissingClass -
How to selectively expose features outside the default starter
Task
-
In the
library-autoconfiguremodule add an optional dependency tolibrary-slf4j. -
In the
GreetingAutoConfiguration, register an additionalGreetingServicebean that returns aLoggerGreetingServiceinstance. -
Annotate this method with:
-
@ConditionalOnClass(LoggerGreetingService.class)— to create a bean only ifLoggerGreetingServiceis on the classpath. -
@ConditionalOnMissingBean— to allow overriding by users.
-
-
Update the existing
StdOutGreetingServicebean:-
Add
@ConditionalOnMissingClass("com.workshop.magic.service.slf4j.LoggerGreetingService")— to create the bean only ifLoggerGreetingServiceis not on the classpath.
-
-
Ensure the module
library-slf4jis not included in thelibrary-spring-boot-startermodule. -
In the
appmodule, add a Maven dependency tolibrary-slf4j. -
✅ Start the app: You should see LoggerGreetingService: Hola Spring I/O Barcelona.
-
Remove the
library-slf4jMaven dependency again: -
✅ Start the app: You should now see StdOutGreetingService: Hola Spring I/O Barcelona.
Detailed Steps
Detailed Steps
-
In the
library-autoconfiguremodule add a dependency tolibrary-slf4jwith:<dependency> <groupId>com.workshop</groupId> <artifactId>library-slf4j</artifactId> <optional>true</optional> </dependency> -
In the
GreetingAutoConfigurationclass, add this bean method:@Bean @ConditionalOnMissingBean @ConditionalOnClass(LoggerGreetingService.class) GreetingService slf4jGreetingService() { return new LoggerGreetingService(); } -
On the existing
stdOutGreetingService()method, add:@ConditionalOnMissingClass("com.workshop.magic.service.slf4j.LoggerGreetingService") -
In the
library-spring-boot-startermodule, ensurelibrary-slf4jis not added as a dependency. Onlylibrary-api(not necessarily needed, as it comes transitively throughlibrary-stdout),library-stdout, andlibrary-autoconfigureshould be included.<dependency> <groupId>com.workshop</groupId> <artifactId>library-autoconfigure</artifactId> </dependency> <dependency> <groupId>com.workshop</groupId> <artifactId>library-api</artifactId> </dependency> <dependency> <groupId>com.workshop</groupId> <artifactId>library-stdout</artifactId> </dependency> -
Make sure the
appmodule declares a dependency tolibrary-slf4jwith:<dependency> <groupId>com.workshop</groupId> <artifactId>library-slf4j</artifactId> </dependency> -
Run the application.
LoggerGreetingServiceis now used, as it’s on the classpath. TheStdOutGreetingServicebean isn’t created, asLoggerGreetingServiceis on the classpath. -
Remove the
library-slf4jdependency from theappmodule and re-run it. -
StdOutGreetingServiceis now used, asLoggerGreetingServiceis not on the classpath.
Conclusion
This pattern of classpath-based behavior is common in real-world Spring Boot libraries. It allows default behavior that can be changed or enhanced by simply adding another dependency — without requiring configuration or code changes.
🤔 Can you think of an example where it is done this way?
Answer
Spring Boot uses classpath detection extensively to toggle features. For example, if Hibernate is on the classpath, JPA support is auto-configured. If it isn’t, Spring Boot silently skips it. This reduces configuration overhead and provides smart defaults that adapt to the environment.
The spring-boot-starter-data-jpa starter doesn’t include a database driver, because the Spring Boot team doesn’t want to force a database choice on you.
You’ll need to add one for yourself, for example adding org.postgresql:postgresql auto-configures a DataSource which can talk to PostgreSQL.
Solution
git checkout -f exercise-4
🥳 Superb, let’s move on to the next exercise
Exercise 4: Conditions Evaluation Report
In this exercise, you’ll learn how to leverage Spring Boot’s Conditions Evaluation Report to understand why certain auto-configurations are applied or not. This is especially useful when troubleshooting unexpected behavior in your application.
Learnings
-
How to enable and interpret the Conditions Evaluation Report
-
How to identify why certain beans are or aren’t loaded
Task
-
Enable debug mode in your application to view the Conditions Evaluation Report:
debug=trueThis can be added to your
application.propertiesfile or passed as a command-line argument using--debug. -
Start your application. Upon startup, you should see a detailed report in the console that looks like:
=========================== CONDITIONS EVALUATION REPORT =========================== Positive matches: ----------------- ... Negative matches: ----------------- ...This report lists all auto-configuration classes with their conditions, and they were applied or not.
-
Review the report in regard to
GreetingAutoConfigurationand understand which configurations were applied and which were not, along with the reasons. -
Use this information to troubleshoot any unexpected behavior or to verify that your custom configurations are being considered appropriately.
Conclusion
The Conditions Evaluation Report is a powerful tool for diagnosing configuration issues in Spring Boot applications. By understanding which conditions are met or not, you can gain insights into the auto-configuration process and ensure your application behaves as expected.
Solution
git checkout -f exercise-5
🥳 Great job! Let’s proceed to the next exercise.
Exercise 5: Property Configuration and @ConditionalOnProperty
Learnings
-
How to parametrize auto-configured beans (task A)
-
How to create auto-configured beans depending on properties (task B)
Task A
-
There’s a
GreetingPropertiesclass in thelibrary-autoconfiguremodule which should be filled with values from theapplication.properties. -
Annotate the
GreetingAutoConfigurationwith@EnableConfigurationProperties(GreetingProperties.class)to enable loading the values from theapplication.properties. -
Annotate
GreetingPropertieswith@ConfigurationPropertiesand bind it to theworkshop.greetingprefix. -
The
StdOutGreetingServiceand theLoggerGreetingServicehave constructors which allows you to customize the greeting. By default, it’s "Hola", but this should now be configurable via theapplication.propertiesby settingworkshop.greeting.text. -
Change the bean methods for
StdOutGreetingServiceandLoggerGreetingServiceto injectGreetingPropertiesand configure the greeting prefix. -
Add a property
workshop.greeting.text=Gudetoapplication.properties. -
✅ Start the application. It should now print LoggerGreetingService: Gude Spring I/0 Barcelona or StdOutGreetingService: Gude Spring I/0 Barcelona.
Detailed Steps A
Detailed Steps
-
In
library-autoconfigure, annotateGreetingAutoConfigurationwith:@EnableConfigurationProperties(GreetingProperties.class) -
In the same module open the
GreetingPropertiesclass and annotate it with:@ConfigurationProperties(prefix = "workshop.greeting") -
In
GreetingAutoConfiguration, injectGreetingPropertiesinto bothGreetingServicebean methods:GreetingService stdOutGreetingService(GreetingProperties properties) GreetingService slf4jGreetingService(GreetingProperties properties) -
Replace the constructor calls with:
new StdOutGreetingService(properties.getText()) new LoggerGreetingService(properties.getText()) -
In
application.propertiesset the following:workshop.greeting.text=Gude -
Run the application
-
✅ You should see LoggerGreetingService: Gude Spring I/0 Barcelona or StdOutGreetingService: Gude Spring I/0 Barcelona now.
Task B
-
Now, we want a property called
workshop.greeting.typewhich controls the type ofGreetingServicethat will be used:-
workshop.greeting.type=loggershould create aLoggerGreetingServicebean. -
workshop.greeting.type=stdoutshould create aStdOutGreetingServicebean.
-
-
You can use
@ConditionalOnPropertyfor that. Annotate both bean methods with@ConditionalOnPropertyand set the annotation attributes accordingly. -
Remove the
@ConditionalOnMissingClassfrom theStdOutGreetingServicebean method. -
Add
workshop.greeting.type=stdoutto yourapplication.properties -
✅ Start the application. It should now print StdOutGreetingService: Gude Spring I/0 Barcelona.
-
Change
workshop.greeting.typetologger. -
✅ Start the application. It should now print LoggerGreetingService: Gude Spring I/0 Barcelona.
-
Remove the
workshop.greeting.typefromapplication.properties -
🤔 Start the application. It now fails. Why is that?
-
Change the annotation attributes from
@ConditionalOnPropertyon theStdOutGreetingServiceto also match if the property is missing. -
✅ Start the application. It should now print StdOutGreetingService: Gude Spring I/0 Barcelona.
Detailed Steps
Detailed Steps
-
Annotate the
StdOutGreetingServicebean method with:@ConditionalOnProperty(name = "workshop.greeting.type", havingValue = "stdout") -
Remove the
@ConditionalOnMissingClassannotation from theStdOutGreetingServicebean method -
Annotate the
LoggerGreetingServicebean method with:@ConditionalOnProperty(name = "workshop.greeting.type", havingValue = "logger") -
In
application.propertiesset the following:workshop.greeting.type=stdout -
Run the application.
-
✅ You should see: StdOutGreetingService: Gude Spring I/0 Barcelona
-
In
application.propertiesset the following:workshop.greeting.type=logger -
Run the application.
-
✅ You should see: LoggerGreetingService: Gude Spring I/0 Barcelona
TipThe LoggerGreetingServicebean will only be created iflibrary-slf4jis on the classpath. If not, eventype=loggerwill not work. -
Remove the
workshop.greeting.typeline and restart the app. -
Startup of the app fails, because there’s no
GreetingServiceavailable. You can use the Conditions Evaluation Report to find out why. -
Change the annotation of the
StdOutGreetingServicebean method inGreetingAutoConfigurationto look like this:@ConditionalOnProperty(name = "workshop.greeting.type", havingValue = "stdout", matchIfMissing = true) -
Run the application.
-
✅ You should see: StdOutGreetingService: Gude Spring I/0 Barcelona
Conclusion
In this exercise, you learned how to read properties from application.properties and use the values to configure your beans.
This can not only be used to configure beans, but it can also be used to influence which beans are created at all.
Using @ConditionalOnProperty, you can activate specific beans based on the application’s configuration, enabling powerful runtime flexibility.
This allows users to influence the behavior using simple property values, without needing to write their own bean overrides.
🤔 Why is this useful in real-world Spring Boot applications?
Answer
It allows configuring beans provided through auto-configuration and changing their behavior without the need to change the bean declaration itself. This enables teams to toggle functionality through properties, and provides sensible defaults with the ability to override them.
An example in Spring Boot would be the management.server.port property. If set, an additional webserver is started on the management port which provides access to actuator, etc.
A lot of beans are created under the hood to make that happen, all controlled by a single user-visible property.
Solution
git checkout -f exercise-6
🥳 Superb, let’s move on to the next exercise
Exercise 6: Using Custom Conditions
It is also possible to create custom conditions like the existing @On… conditions from Spring Boot.
Let’s create a custom condition that checks the system property my.custom.condition - just because it’s simple.
But imagine you have a more sophisticated custom check here, e.g., infrastructure checks like the Kubernetes probes.
Or you could write a condition which triggers only on 1st of April.
Oh, the possibilities!
Learnings
-
How to create your own conditions
-
How to use that custom condition
Task
-
Create a new annotation
@MyCustomConditionin thelibrary-autoconfiguremodule. It must have a@TargetofTYPEandMETHODand a@RetentionofRUNTIME(you can also copy that from Spring Boot’s@ConditionalOnProperty). -
The newly created annotation must be annotated with
@Conditional({OnCustomCondition.class}). -
A new class,
OnCustomConditionmust be created. It should extend Spring Boot’sSpringBootCondition. -
The
getMatchOutcomemethod must be overriden and should check themy.custom.conditionsystem property. UseConditionOutcome.matchandConditionOutcome.noMatchto signal if the condition matches or not. -
Modify the
GreetingAutoConfigurationto use the new@MyCustomCondition. A bean of classBeepGreetingService(located in thelibrary-slf4jmodule) should be created if@MyCustomConditionmatches. -
Test that the application works by setting the system property
my.custom.conditionand verify that theBeepGreetingServicebean is used.NoteYou’ll have to set workshop.greeting.typeto something else thanloggerorstdout, because otherwise theLoggerGreetingServiceorStdOutGreetingServiceis also created. -
🤔 Also take a look at the conditions evaluation report. Do you see your condition in there?
Detailed Steps
Detailed Steps
-
Create a new annotation in the
library-autoconfiguremodule, calledMyCustomCondition -
Annotate the annotation with
@Target({ElementType.TYPE, ElementType.METHOD})and with@Retention(RetentionPolicy.RUNTIME) -
Annotate the annotation with
@Conditional({OnCustomCondition.class})@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Conditional({OnCustomCondition.class}) @interface MyCustomCondition { } -
Create a class called
OnCustomConditionand let it extendSpringBootCondition -
Implement the
getMatchOutcomemethod-
Use
System.getProperty("my.custom.condition")to read themy.custom.conditionsystem property -
If the value of that property is
true, returnConditionOutcome.matchto signal that the condition matches -
Otherwise, return
ConditionOutcome.noMatchto signal that the condition didn’t matchclass OnCustomCondition extends SpringBootCondition { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { String value = System.getProperty("my.custom.condition"); if (value == null) { return ConditionOutcome.noMatch("No 'my.custom.condition' system property found"); } if (value.toLowerCase(Locale.ROOT).equals("true")) { return ConditionOutcome.match("'my.custom.condition' system property is true"); } return ConditionOutcome.noMatch("'my.custom.condition' system property is '%s'".formatted(value)); } }
-
-
Add a new
@Beanmethod to theGreetingAutoConfigurationclass, call itbeepGreetingService, its return type isGreetingService-
Annotate this new method with
@MyCustomCondition,@ConditionalOnMissingBeanand@ConditionalOnClass(BeepGreetingService.class) -
Return a new instance of
BeepGreetingServicefrom that method@Bean @ConditionalOnMissingBean @MyCustomCondition @ConditionalOnClass(BeepGreetingService.class) GreetingService beepGreetingService(GreetingProperties properties) { return new BeepGreetingService(properties.getPrefix()); }
-
-
To test the custom condition, you can add
System.setProperty("my.custom.condition", "true");as first line in themainmethod, or you can set the system properties when starting with your IDE -
You’ll also need to add
workshop.greeting.type=noneto yourapplication.properties, because otherwise theLoggerGreetingServiceor theStdOutGreetingServicewould be created
Conclusion
Can you image why it is useful to create custom conditions?
Answer
Creating your own conditions is useful if the conditions from Spring Framework and Spring Boot don’t fit your needs. Custom conditions show the power of an extensible framework like the Spring Framework. There’s no "magic" behind the built-in Spring Boot conditions — they are built on the same foundations like your custom condition is.
|
Note
|
You can take a look at the @Profile annotation from Spring Framework: The logic is implemented in ProfileCondition, and it essentially returns true if the profile is activated and false if not.
|
Solution
git checkout -f exercise-7
🥳 Phenomenal, let’s move on to the next exercise
Exercise 7: Testing The Auto-Configuration
Create unit tests to ensure that the GreetingAutoConfiguration works as expected.
Task
-
A Maven dependency on
org.springframework.boot:spring-boot-starter-testwith scopetesthas to be added in thelibrary-autoconfiguremodule. -
A test class for the
GreetingAutoConfigurationclass must be created. -
Spring Boot’s
ApplicationContextRunnershould be used to test the auto-configuration (see the reference documentation). -
AssertJ assertions should be used to verify that the context contains a
StdOutGreetingServicebean if no property is set. -
Implement tests for these use cases:
-
The context contains a
StdOutGreetingServicebean if the propertyworkshop.greeting.typeis set tostdout. -
The context contains a
LoggerGreetingServicebean if the propertyworkshop.greeting.typeis set tologger. -
The context contains
BeepGreetingServicebean if the system propertymy.custom.conditionis set totrue. -
That user-defined beans take precedence over the auto-configured
GreetingServicebeans — essentially testing that@ConditionalOnMissingBeanworks.
-
Detailed Steps
Detailed Steps
-
Add a Maven dependency to
org.springframework.boot:spring-boot-starter-testwith scopetestto thelibrary-autoconfiguremodule. -
Create a class named
GreetingAutoConfigurationTestinlibrary-autoconfigure/src/test/javain the packagecom.workshop.magic.config. -
Create a field of type
ApplicationContextRunner, and use the fluent API to callwithConfigurationwithAutoConfigurations.of(GreetingAutoConfiguration.class).private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(GreetingAutoConfiguration.class)); -
Write a test case named
shouldProvideStdOutGreetingServiceByDefaultwhich uses therunmethod of theApplicationContextRunnerfield.-
Inside the lambda block of the
runmethod, use AssertJ’sassertThaton the context to callhasSingleBeanwith anStdOutGreetingService.classargument.@Test void shouldProvideStdOutGreetingServiceByDefault() { this.contextRunner.run(context -> { assertThat(context).hasSingleBean(StdOutGreetingService.class); }); }
-
-
Write a test case named
shouldProvideStdOutGreetingServiceWhenPropertyIsSetwhich uses thewithPropertyValuesof theApplicationContextRunnerfield to set the propertyworkshop.greeting.typetostdout.-
Inside the lambda block of the
runmethod, use AssertJ’sassertThaton the context to callhasSingleBeanwith anStdOutGreetingService.classargument.@Test void shouldProvideStdOutGreetingServiceWhenPropertyIsSet() { this.contextRunner .withPropertyValues("workshop.greeting.type=stdout") .run(context -> { assertThat(context).hasSingleBean(StdOutGreetingService.class); }); }
-
-
Write a test case named
shouldProvideLoggerGreetingServiceWhenPropertyIsSetwhich uses thewithPropertyValuesof theApplicationContextRunnerfield to set the propertyworkshop.greeting.typetologger.-
Inside the lambda block of the
runmethod, use AssertJ’sassertThaton the context to callhasSingleBeanwith anLoggerGreetingService.classargument.
-
-
Write a test case named
shouldProvideBeepGreetingServiceIfSystemPropertyIsSetwhich useswithPropertyValuesof theApplicationContextRunnerfield to set the propertyworkshop.greeting.typetonone.-
Additionally, it uses the
withSystemPropertiesmethod to setmy.custom.conditiontotrue. -
Inside the lambda block of the
runmethod, use AssertJ’sassertThaton the context to callhasSingleBeanwith anBeepGreetingService.classargument.@Test void shouldProvideBeepGreetingServiceIfSystemPropertyIsSet() { this.contextRunner .withPropertyValues("workshop.greeting.type=none") .withSystemProperties("my.custom.condition=true") .run(context -> { assertThat(context).hasSingleBean(BeepGreetingService.class); }); }
-
-
Write a test case named
shouldBackOffIfGreetingServiceIsDefinedByUserwhich uses thewithBeanmethod of theApplicationContextRunnerfield to define a bean of typeGreetingService. Create an inner static class or an anonymous class for the "user provided"GreetingService.-
Inside the lambda block of the
runmethod, use AssertJ’sassertThaton the context to callhasSingleBeanwith anGreetingService.classargument.@Test void shouldBackOffIfGreetingServiceIsDefinedByUser() { this.contextRunner .withBean(GreetingService.class, UserGreetingService::new) .run(context -> { assertThat(context).hasSingleBean(GreetingService.class); assertThat(context).hasSingleBean(UserGreetingService.class); }); } private static class UserGreetingService implements GreetingService { @Override public void greet(String name) { System.out.println("UserGreetingService: Hello " + name); } }
-
Conclusion
What’s the benefit of writing a unit test for an auto-configuration?
Answer
Auto-configurations can contain a lot of conditions, sometimes even custom ones. As this auto-configuration is part of your codebase,
you should also unit-test it to ensure that it behaves as designed, same as the rest of your code.
Spring Boot’s ApplicationContextRunner makes this easy.
Solution
git checkout -f exercise-8
🥳 Brilliant, let’s move on to the next exercise
Exercise 8: Adding properties metadata
Use the Spring Boot configuration processor to generate metadata for your configuration properties.
Task
-
Add the
org.springframework.boot:spring-boot-configuration-processorto thelibrary-autoconfiguremodule. -
Run a build and inspect the
components/library-autoconfigure/target/classes/META-INF/spring-configuration-metadata.jsonfile. -
🤔 Think about why that file could be useful.
-
The
textproperty inGreetingPropertiesshould be renamed toprefix, while deprecating thetextproperty. Use@Deprecatedand@DeprecatedConfigurationPropertyannotations to achieve this. -
Run a build and inspect the file
spring-configuration-metadata.jsonagain. -
🤔 What has changed? Why could that be useful?
-
🤔 Open the
application.propertiesin your IDE. Do you notice something? -
Add
org.springframework.boot:spring-boot-properties-migratorto theappmodule. -
Start the app and observe the console output.
Detailed Steps
Detailed Steps
-
Add
org.springframework.boot:spring-boot-configuration-processortocomponents/library-autoconfigure/pom.xml, withoptional = true.<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> -
Newer Java versions require an explicit configuration for annotation processors. Configure the
maven-compiler-pluginto includeorg.springframework.boot:spring-boot-configuration-processoras an annotation processor. You can take a look at the POM file generated by start.spring.io for an example.<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> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build> -
Run
./mvnw compileand inspectcomponents/library-autoconfigure/target/classes/META-INF/spring-configuration-metadata.json. -
Replace
private String textin theGreetingPropertiesclass withprivate String prefix. -
Annotate the
public String getText()method with@Deprecatedand with@DeprecatedConfigurationProperty(replacement = "workshop.greeting.prefix"). -
Return
this.prefixfrom thegetText()method. -
Assign
this.prefixin thesetText()method. -
Add a new getter and setter method for
private String prefix.private String prefix = "Hello"; @DeprecatedConfigurationProperty(replacement = "workshop.greeting.prefix") @Deprecated public String getText() { return this.prefix; } public void setText(String text) { this.prefix = text; } public String getPrefix() { return this.prefix; } public void setPrefix(String prefix) { this.prefix = prefix; } -
Run
./mvnw compileand inspectcomponents/library-autoconfigure/target/classes/META-INF/spring-configuration-metadata.json. -
Add
org.springframework.boot:spring-boot-properties-migratorwithscope = runtimetoapp/app/pom.xml. -
Run the application
Conclusion
Why is providing that netadata file beneficial? Who would use it?
Answer
This metadata file is read by IDEs to provide auto-completion for properties.
Additionally, deprecations and their replacement are also recorded in that file, which is also used by IDEs to guide users.
And the spring-boot-properties-migrator also uses this file to display deprecations on startup and to provide the automatic mapping from the old property to the new one.
Solution
git checkout -f main
🥳 Congrats, you finished all exercises! We hope you enjoyed the learnings.
Feedback
We’d love your feedback on this workshop! If you enjoyed it or have ideas for improvement, please reach out or connect with us on social media or via the conference app. Thanks for helping us get better! ❤️ ️
Connect With Us!
-
Moritz Halbritter
-
Fabian Krüger