commit bd98dab58877760fa9794a8f8bd17515251e3548 Author: Moritz Halbritter Date: Tue May 13 14:40:20 2025 +0200 Exercise 1 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ad5c660 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 1000 +tab_width = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84b70cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Project exclude paths +**/target/ +.idea/ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..d58dfb7 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/README.md b/README.md new file mode 100644 index 0000000..d2001fa --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Demystifying Spring Boot’s Auto-Configuration Magic + +See the [workbook](https://htmlpreview.github.io/?https://github.com/fabapp2/spring-boot-magic-workshop/blob/main/workbook.html). diff --git a/app/app/pom.xml b/app/app/pom.xml new file mode 100644 index 0000000..9dd8170 --- /dev/null +++ b/app/app/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + + com.workshop + spring-boot-magic + 1.0.0-SNAPSHOT + ../../pom.xml + + + app + jar + + + + com.workshop + library-api + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/app/app/src/main/java/com/workshop/magic/app/MyCommandLineRunner.java b/app/app/src/main/java/com/workshop/magic/app/MyCommandLineRunner.java new file mode 100644 index 0000000..5f360a2 --- /dev/null +++ b/app/app/src/main/java/com/workshop/magic/app/MyCommandLineRunner.java @@ -0,0 +1,20 @@ +package com.workshop.magic.app; + +import com.workshop.magic.service.GreetingService; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +class MyCommandLineRunner implements CommandLineRunner { + private final GreetingService greetingService; + + MyCommandLineRunner(GreetingService gs) { + this.greetingService = gs; + } + + @Override + public void run(String... args) { + this.greetingService.greet("Spring I/O Barcelona"); + } +} diff --git a/app/app/src/main/java/com/workshop/magic/app/MyGreetingService.java b/app/app/src/main/java/com/workshop/magic/app/MyGreetingService.java new file mode 100644 index 0000000..c7e6824 --- /dev/null +++ b/app/app/src/main/java/com/workshop/magic/app/MyGreetingService.java @@ -0,0 +1,15 @@ +package com.workshop.magic.app; + +import com.workshop.magic.service.AbstractGreetingService; + +import org.springframework.stereotype.Service; + +@Service +class MyGreetingService extends AbstractGreetingService { + + @Override + public void greet(String name) { + System.out.println(buildGreeting(name)); + } + +} diff --git a/app/app/src/main/java/com/workshop/magic/app/WorkshopApplication.java b/app/app/src/main/java/com/workshop/magic/app/WorkshopApplication.java new file mode 100644 index 0000000..2c9880b --- /dev/null +++ b/app/app/src/main/java/com/workshop/magic/app/WorkshopApplication.java @@ -0,0 +1,12 @@ +package com.workshop.magic.app; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class WorkshopApplication { + + public static void main(String[] args) { + SpringApplication.run(WorkshopApplication.class, args); + } +} diff --git a/app/app/src/main/resources/application.properties b/app/app/src/main/resources/application.properties new file mode 100644 index 0000000..aab92eb --- /dev/null +++ b/app/app/src/main/resources/application.properties @@ -0,0 +1 @@ +debug=false \ No newline at end of file diff --git a/components/library-api/pom.xml b/components/library-api/pom.xml new file mode 100644 index 0000000..5ee9a37 --- /dev/null +++ b/components/library-api/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + com.workshop + spring-boot-magic + 1.0.0-SNAPSHOT + ../../pom.xml + + + library-api + + diff --git a/components/library-api/src/main/java/com/workshop/magic/service/AbstractGreetingService.java b/components/library-api/src/main/java/com/workshop/magic/service/AbstractGreetingService.java new file mode 100644 index 0000000..d7ed740 --- /dev/null +++ b/components/library-api/src/main/java/com/workshop/magic/service/AbstractGreetingService.java @@ -0,0 +1,24 @@ +package com.workshop.magic.service; + +public abstract class AbstractGreetingService implements GreetingService { + + private final String prefix; + + protected AbstractGreetingService() { + this("Hola"); + } + + protected AbstractGreetingService(String prefix) { + this.prefix = prefix; + } + + @Override + public abstract void greet(String name); + + protected String buildGreeting(String name) { + if (this.prefix != null && !this.prefix.isEmpty()) { + return "%s: %s %s".formatted(getClass().getSimpleName(), this.prefix, name); + } + return "%s: %s".formatted(getClass().getSimpleName(), name); + } +} diff --git a/components/library-api/src/main/java/com/workshop/magic/service/GreetingService.java b/components/library-api/src/main/java/com/workshop/magic/service/GreetingService.java new file mode 100644 index 0000000..c896acd --- /dev/null +++ b/components/library-api/src/main/java/com/workshop/magic/service/GreetingService.java @@ -0,0 +1,5 @@ +package com.workshop.magic.service; + +public interface GreetingService { + void greet(String name); +} diff --git a/components/library-autoconfigure/pom.xml b/components/library-autoconfigure/pom.xml new file mode 100644 index 0000000..bf945db --- /dev/null +++ b/components/library-autoconfigure/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + + com.workshop + spring-boot-magic + 1.0.0-SNAPSHOT + ../../pom.xml + + + library-autoconfigure + + diff --git a/components/library-autoconfigure/src/main/java/com/workshop/magic/config/GreetingProperties.java b/components/library-autoconfigure/src/main/java/com/workshop/magic/config/GreetingProperties.java new file mode 100644 index 0000000..08c800a --- /dev/null +++ b/components/library-autoconfigure/src/main/java/com/workshop/magic/config/GreetingProperties.java @@ -0,0 +1,28 @@ +package com.workshop.magic.config; + +public class GreetingProperties { + private String text = "Hello"; + private Type type = Type.STDOUT; + + public String getText() { + return this.text; + } + + public void setText(String text) { + this.text = text; + } + + public Type getType() { + return this.type; + } + + public void setType(Type type) { + this.type = type; + } + + public enum Type { + STDOUT, + LOGGER, + NONE + } +} diff --git a/components/library-autoconfigure/src/main/resources/.keep b/components/library-autoconfigure/src/main/resources/.keep new file mode 100644 index 0000000..e69de29 diff --git a/components/library-autoconfigure/src/test/java/.keep b/components/library-autoconfigure/src/test/java/.keep new file mode 100644 index 0000000..e69de29 diff --git a/components/library-slf4j/pom.xml b/components/library-slf4j/pom.xml new file mode 100644 index 0000000..0620e4e --- /dev/null +++ b/components/library-slf4j/pom.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + + com.workshop + spring-boot-magic + 1.0.0-SNAPSHOT + ../../pom.xml + + + library-slf4j + + + + com.workshop + library-api + + + org.slf4j + slf4j-api + + + + \ No newline at end of file diff --git a/components/library-slf4j/src/main/java/com/workshop/magic/service/slf4j/BeepGreetingService.java b/components/library-slf4j/src/main/java/com/workshop/magic/service/slf4j/BeepGreetingService.java new file mode 100644 index 0000000..b2642c4 --- /dev/null +++ b/components/library-slf4j/src/main/java/com/workshop/magic/service/slf4j/BeepGreetingService.java @@ -0,0 +1,27 @@ +package com.workshop.magic.service.slf4j; + +import java.awt.*; + +import com.workshop.magic.service.AbstractGreetingService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BeepGreetingService extends AbstractGreetingService { + + private static final Logger LOGGER = LoggerFactory.getLogger(BeepGreetingService.class); + + public BeepGreetingService() { + super(); + } + + public BeepGreetingService(String prefix) { + super(prefix); + } + + @Override + public void greet(String name) { + LOGGER.info(buildGreeting(name)); + Toolkit.getDefaultToolkit().beep(); + } + +} diff --git a/components/library-slf4j/src/main/java/com/workshop/magic/service/slf4j/LoggerGreetingService.java b/components/library-slf4j/src/main/java/com/workshop/magic/service/slf4j/LoggerGreetingService.java new file mode 100644 index 0000000..558773b --- /dev/null +++ b/components/library-slf4j/src/main/java/com/workshop/magic/service/slf4j/LoggerGreetingService.java @@ -0,0 +1,22 @@ +package com.workshop.magic.service.slf4j; + +import com.workshop.magic.service.AbstractGreetingService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LoggerGreetingService extends AbstractGreetingService { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerGreetingService.class); + + public LoggerGreetingService() { + super(); + } + + public LoggerGreetingService(String prefix) { + super(prefix); + } + + @Override + public void greet(String name) { + LOGGER.info(buildGreeting(name)); + } +} diff --git a/components/library-spring-boot-starter/pom.xml b/components/library-spring-boot-starter/pom.xml new file mode 100644 index 0000000..b3de236 --- /dev/null +++ b/components/library-spring-boot-starter/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + com.workshop + spring-boot-magic + 1.0.0-SNAPSHOT + ../../pom.xml + + + library-spring-boot-starter + + \ No newline at end of file diff --git a/components/library-stdout/pom.xml b/components/library-stdout/pom.xml new file mode 100644 index 0000000..c25dbc2 --- /dev/null +++ b/components/library-stdout/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + com.workshop + spring-boot-magic + 1.0.0-SNAPSHOT + ../../pom.xml + + + library-stdout + + + + com.workshop + library-api + + + + \ No newline at end of file diff --git a/components/library-stdout/src/main/java/com/workshop/magic/service/stdout/StdOutGreetingService.java b/components/library-stdout/src/main/java/com/workshop/magic/service/stdout/StdOutGreetingService.java new file mode 100644 index 0000000..e341a41 --- /dev/null +++ b/components/library-stdout/src/main/java/com/workshop/magic/service/stdout/StdOutGreetingService.java @@ -0,0 +1,18 @@ +package com.workshop.magic.service.stdout; + +import com.workshop.magic.service.AbstractGreetingService; + +public class StdOutGreetingService extends AbstractGreetingService { + public StdOutGreetingService() { + super(); + } + + public StdOutGreetingService(String prefix) { + super(prefix); + } + + @Override + public void greet(String name) { + System.out.println(buildGreeting(name)); + } +} diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..19529dd --- /dev/null +++ b/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..249bdf3 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c7b93e7 --- /dev/null +++ b/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.4.5 + + + + com.workshop + spring-boot-magic + 1.0.0-SNAPSHOT + pom + + + app/app + components/library-autoconfigure + components/library-api + components/library-slf4j + components/library-stdout + components/library-spring-boot-starter + + + + 17 + + + + + + com.workshop + app + ${project.version} + + + com.workshop + library-api + ${project.version} + + + com.workshop + library-autoconfigure + ${project.version} + + + com.workshop + library-slf4j + ${project.version} + + + com.workshop + library-stdout + ${project.version} + + + com.workshop + library-spring-boot-starter + ${project.version} + + + + diff --git a/workbook.html b/workbook.html new file mode 100644 index 0000000..2ae3595 --- /dev/null +++ b/workbook.html @@ -0,0 +1,3663 @@ + + + + + + + + + Demystifying Spring Boot’s Auto-Configuration Magic + + + + + +
+
+
+
+

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 the library

    +
  • +
  • +

    library-api - Spring Boot independent library with GreetingService interface

    +
  • +
  • +

    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

+
+
    +
  1. +

    Create an auto-configuration class com.workshop.magic.config.GreetingAutoConfiguration in the library-autoconfigure module.

    +
  2. +
  3. +

    Add a Maven dependency to org.springframework.boot:spring-boot-autoconfigure which brings the @AutoConfiguration annotation.

    +
  4. +
  5. +

    In the library-autoconfigure module, add a Maven dependency to other required modules (library-api and library-stdout). Make them optional, which is a best-practice for auto-configurations.

    +
  6. +
  7. +

    Annotate the GreetingAutoConfiguration with @AutoConfiguration.

    +
  8. +
  9. +

    Annotate the GreetingAutoConfiguration with @ConditionalOnClass(GreetingService.class) to only process the auto-config when GreetingService is on the application classpath.

    +
  10. +
  11. +

    This auto-configuration should provide StdOutGreetingService as a Spring bean of type GreetingService. Use the @Bean annotation for that.

    +
  12. +
  13. +

    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.

    +
  14. +
  15. +

    Add the Maven dependency library-autoconfigure and library-stdout to app, making the auto-configured GreetingService available.

    +
  16. +
  17. +

    🤔 Starting the application should fail. Why is that?

    +
  18. +
  19. +

    To avoid conflicts and allow custom implementations, the StdOutGreetingService should only be created when no other bean of type GreetingService exists. + This can be achieved by annotating the bean declaration with @ConditionalOnMissingBean, which tells Spring Boot to back off when such a bean already exists.

    +
  20. +
  21. +

    ✅ Starting the application should now print: MyGreetingService: Hola Spring I/O Barcelona.

    +
  22. +
  23. +

    Modify the application to use the StdOutGreetingService now.

    +
  24. +
  25. +

    ✅ Starting the application should now print: StdOutGreetingService: Hola Spring I/O Barcelona.

    +
  26. +
+
+
+ + + + + +
+
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 +
+
+
    +
  1. +

    Create a new class com.workshop.magic.config.GreetingAutoConfiguration in the library-autoconfigure module.

    +
  2. +
  3. +

    Add a Maven dependency to org.springframework.boot:spring-boot-autoconfigure in the library-autoconfigure module.

    +
  4. +
  5. +

    Add a Maven dependency to com.workshop:library-stdout in the library-autoconfigure module, with <optional>true</optional>.

    +
  6. +
  7. +

    Create a new file src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports in the library-autoconfigure module (see the reference documentation).

    +
  8. +
  9. +

    Add the fully qualified classname of the GreetingAutoConfiguration class to the .imports file.

    +
  10. +
  11. +

    Annotate the GreetingAutoConfiguration with @AutoConfiguration.

    +
  12. +
  13. +

    Create a new GreetingService bean in GreetingAutoConfiguration that returns a new instance of StdOutGreetingService.

    +
    +
    +
    @Bean
    +GreetingService stdOutGreetingService() {
    +    return new StdOutGreetingService();
    +}
    +
    +
    +
  14. +
  15. +

    Add a Maven dependency to com.workshop:library-autoconfigure in the app module.

    +
  16. +
  17. +

    Add a Maven dependency to com.workshop:library-stdout in the app module.

    +
  18. +
  19. +

    Starting the application fails. That’s because there are now two beans of type GreetingService: MyGreetingService (annotated with @Service) from the app module and the StdOutGreetingService from the auto-configuration.

    +
  20. +
  21. +

    Use the @ConditionalOnMissingBean annotation on the GreetingService bean method in GreetingAutoConfiguration to only load the bean when no other bean of type GreetingService exists (see the reference documentation).

    +
  22. +
  23. +

    The application now starts and uses the MyGreetingService.

    +
  24. +
  25. +

    Now, remove the MyGreetingService class from the app module, or comment out/remove the @Service annotation on MyGreetingService.

    +
  26. +
  27. +

    The application now starts and uses the StdOutGreetingService.

    +
  28. +
+
+
+
+
+
+

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:

+
+
    +
  1. +

    Add Maven dependencies to library-autoconfigure and library-stdout in the library-spring-boot-starter module.

    +
  2. +
  3. +

    Add a Maven dependency to org.springframework.boot:spring-boot-starter in the library-spring-boot-starter module.

    +
  4. +
  5. +

    Replace direct dependencies to library-autoconfigure and library-stdout in the app module with the new starter.

    +
  6. +
  7. +

    ✅ Confirm that the app still works as expected and prints the greeting.

    +
  8. +
+
+
+
+

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 @ConditionalOnClass and @ConditionalOnMissingClass

    +
  • +
  • +

    How to selectively expose features outside the default starter

    +
  • +
+
+
+
+

Task

+
+
    +
  1. +

    In the library-autoconfigure module add an optional dependency to library-slf4j.

    +
  2. +
  3. +

    In the GreetingAutoConfiguration, register an additional GreetingService bean that returns a LoggerGreetingService instance.

    +
  4. +
  5. +

    Annotate this method with:

    +
    +
      +
    • +

      @ConditionalOnClass(LoggerGreetingService.class) — to create a bean only if LoggerGreetingService is on the classpath.

      +
    • +
    • +

      @ConditionalOnMissingBean — to allow overriding by users.

      +
    • +
    +
    +
  6. +
  7. +

    Update the existing StdOutGreetingService bean:

    +
    +
      +
    • +

      Add @ConditionalOnMissingClass("com.workshop.magic.service.slf4j.LoggerGreetingService") — to create the bean only if LoggerGreetingService is not on the classpath.

      +
    • +
    +
    +
  8. +
  9. +

    Ensure the module library-slf4j is not included in the library-spring-boot-starter module.

    +
  10. +
  11. +

    In the app module, add a Maven dependency to library-slf4j.

    +
  12. +
  13. +

    ✅ Start the app: You should see LoggerGreetingService: Hola Spring I/O Barcelona.

    +
  14. +
  15. +

    Remove the library-slf4j Maven dependency again:

    +
  16. +
  17. +

    ✅ Start the app: You should now see StdOutGreetingService: Hola Spring I/O Barcelona.

    +
  18. +
+
+
+
+

Detailed Steps

+
+ Detailed Steps +
+
+
    +
  1. +

    In the library-autoconfigure module add a dependency to library-slf4j with:

    +
    +
    +
    <dependency>
    +    <groupId>com.workshop</groupId>
    +    <artifactId>library-slf4j</artifactId>
    +    <optional>true</optional>
    +</dependency>
    +
    +
    +
  2. +
  3. +

    In the GreetingAutoConfiguration class, add this bean method:

    +
    +
    +
    @Bean
    +@ConditionalOnMissingBean
    +@ConditionalOnClass(LoggerGreetingService.class)
    +GreetingService slf4jGreetingService() {
    +    return new LoggerGreetingService();
    +}
    +
    +
    +
  4. +
  5. +

    On the existing stdOutGreetingService() method, add:

    +
    +
    +
    @ConditionalOnMissingClass("com.workshop.magic.service.slf4j.LoggerGreetingService")
    +
    +
    +
  6. +
  7. +

    In the library-spring-boot-starter module, ensure library-slf4j is not added as a dependency. Only library-api (not necessarily needed, as it comes transitively through library-stdout), library-stdout, and library-autoconfigure should 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>
    +
    +
    +
  8. +
  9. +

    Make sure the app module declares a dependency to library-slf4j with:

    +
    +
    +
    <dependency>
    +    <groupId>com.workshop</groupId>
    +    <artifactId>library-slf4j</artifactId>
    +</dependency>
    +
    +
    +
  10. +
  11. +

    Run the application. LoggerGreetingService is now used, as it’s on the classpath. The StdOutGreetingService bean isn’t created, as LoggerGreetingService is on the classpath.

    +
  12. +
  13. +

    Remove the library-slf4j dependency from the app module and re-run it.

    +
  14. +
  15. +

    StdOutGreetingService is now used, as LoggerGreetingService is not on the classpath.

    +
  16. +
+
+
+
+
+
+

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

+
+
    +
  1. +

    Enable debug mode in your application to view the Conditions Evaluation Report:

    +
    +
    +
    debug=true
    +
    +
    +
    +

    This can be added to your application.properties file or passed as a command-line argument using --debug.

    +
    +
  2. +
  3. +

    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.

    +
    +
  4. +
  5. +

    Review the report in regard to GreetingAutoConfiguration and understand which configurations were applied and which were not, along with the reasons.

    +
  6. +
  7. +

    Use this information to troubleshoot any unexpected behavior or to verify that your custom configurations are being considered appropriately.

    +
  8. +
+
+
+
+

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

+
+
    +
  1. +

    There’s a GreetingProperties class in the library-autoconfigure module which should be filled with values from the application.properties.

    +
  2. +
  3. +

    Annotate the GreetingAutoConfiguration with @EnableConfigurationProperties(GreetingProperties.class) to enable loading the values from the application.properties.

    +
  4. +
  5. +

    Annotate GreetingProperties with @ConfigurationProperties and bind it to the workshop.greeting prefix.

    +
  6. +
  7. +

    The StdOutGreetingService and the LoggerGreetingService have constructors which allows you to customize the greeting. By default, it’s "Hola", but this should now be configurable via the application.properties by setting workshop.greeting.text.

    +
  8. +
  9. +

    Change the bean methods for StdOutGreetingService and LoggerGreetingService to inject GreetingProperties and configure the greeting prefix.

    +
  10. +
  11. +

    Add a property workshop.greeting.text=Gude to application.properties.

    +
  12. +
  13. +

    ✅ Start the application. It should now print LoggerGreetingService: Gude Spring I/0 Barcelona or StdOutGreetingService: Gude Spring I/0 Barcelona.

    +
  14. +
+
+
+
+

Detailed Steps A

+
+ Detailed Steps +
+
+
    +
  1. +

    In library-autoconfigure, annotate GreetingAutoConfiguration with:

    +
    +
    +
    @EnableConfigurationProperties(GreetingProperties.class)
    +
    +
    +
  2. +
  3. +

    In the same module open the GreetingProperties class and annotate it with:

    +
    +
    +
    @ConfigurationProperties(prefix = "workshop.greeting")
    +
    +
    +
  4. +
  5. +

    In GreetingAutoConfiguration, inject GreetingProperties into both GreetingService bean methods:

    +
    +
    +
    GreetingService stdOutGreetingService(GreetingProperties properties)
    +
    +GreetingService slf4jGreetingService(GreetingProperties properties)
    +
    +
    +
  6. +
  7. +

    Replace the constructor calls with:

    +
    +
    +
    new StdOutGreetingService(properties.getText())
    +
    +new LoggerGreetingService(properties.getText())
    +
    +
    +
  8. +
  9. +

    In application.properties set the following:

    +
    +
    +
    workshop.greeting.text=Gude
    +
    +
    +
  10. +
  11. +

    Run the application

    +
  12. +
  13. +

    ✅ You should see LoggerGreetingService: Gude Spring I/0 Barcelona or StdOutGreetingService: Gude Spring I/0 Barcelona now.

    +
  14. +
+
+
+
+
+
+

Task B

+
+
    +
  1. +

    Now, we want a property called workshop.greeting.type which controls the type of GreetingService that will be used:

    +
    +
      +
    • +

      workshop.greeting.type=logger should create a LoggerGreetingService bean.

      +
    • +
    • +

      workshop.greeting.type=stdout should create a StdOutGreetingService bean.

      +
    • +
    +
    +
  2. +
  3. +

    You can use @ConditionalOnProperty for that. Annotate both bean methods with @ConditionalOnProperty and set the annotation attributes accordingly.

    +
  4. +
  5. +

    Remove the @ConditionalOnMissingClass from the StdOutGreetingService bean method.

    +
  6. +
  7. +

    Add workshop.greeting.type=stdout to your application.properties

    +
  8. +
  9. +

    ✅ Start the application. It should now print StdOutGreetingService: Gude Spring I/0 Barcelona.

    +
  10. +
  11. +

    Change workshop.greeting.type to logger.

    +
  12. +
  13. +

    ✅ Start the application. It should now print LoggerGreetingService: Gude Spring I/0 Barcelona.

    +
  14. +
  15. +

    Remove the workshop.greeting.type from application.properties

    +
  16. +
  17. +

    🤔 Start the application. It now fails. Why is that?

    +
  18. +
  19. +

    Change the annotation attributes from @ConditionalOnProperty on the StdOutGreetingService to also match if the property is missing.

    +
  20. +
  21. +

    ✅ Start the application. It should now print StdOutGreetingService: Gude Spring I/0 Barcelona.

    +
  22. +
+
+
+
+

Detailed Steps

+
+ Detailed Steps +
+
+
    +
  1. +

    Annotate the StdOutGreetingService bean method with:

    +
    +
    +
    @ConditionalOnProperty(name = "workshop.greeting.type", havingValue = "stdout")
    +
    +
    +
  2. +
  3. +

    Remove the @ConditionalOnMissingClass annotation from the StdOutGreetingService bean method

    +
  4. +
  5. +

    Annotate the LoggerGreetingService bean method with:

    +
    +
    +
    @ConditionalOnProperty(name = "workshop.greeting.type", havingValue = "logger")
    +
    +
    +
  6. +
  7. +

    In application.properties set the following:

    +
    +
    +
    workshop.greeting.type=stdout
    +
    +
    +
  8. +
  9. +

    Run the application.

    +
  10. +
  11. +

    ✅ You should see: StdOutGreetingService: Gude Spring I/0 Barcelona

    +
  12. +
  13. +

    In application.properties set the following:

    +
    +
    +
    workshop.greeting.type=logger
    +
    +
    +
  14. +
  15. +

    Run the application.

    +
  16. +
  17. +

    ✅ You should see: LoggerGreetingService: Gude Spring I/0 Barcelona

    +
    + + + + + +
    +
    Tip
    +
    + The LoggerGreetingService bean will only be created if library-slf4j is on the classpath. If not, even type=logger will not work. +
    +
    +
  18. +
  19. +

    Remove the workshop.greeting.type line and restart the app.

    +
  20. +
  21. +

    Startup of the app fails, because there’s no GreetingService available. You can use the Conditions Evaluation Report to find out why.

    +
  22. +
  23. +

    Change the annotation of the StdOutGreetingService bean method in GreetingAutoConfiguration to look like this:

    +
    +
    +
    @ConditionalOnProperty(name = "workshop.greeting.type", havingValue = "stdout", matchIfMissing = true)
    +
    +
    +
  24. +
  25. +

    Run the application.

    +
  26. +
  27. +

    ✅ You should see: StdOutGreetingService: Gude Spring I/0 Barcelona

    +
  28. +
+
+
+
+
+
+

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

+
+
    +
  1. +

    Create a new annotation @MyCustomCondition in the library-autoconfigure module. It must have a @Target of TYPE and METHOD and a @Retention of RUNTIME (you can also copy that from Spring Boot’s @ConditionalOnProperty).

    +
  2. +
  3. +

    The newly created annotation must be annotated with @Conditional({OnCustomCondition.class}).

    +
  4. +
  5. +

    A new class, OnCustomCondition must be created. It should extend Spring Boot’s SpringBootCondition.

    +
  6. +
  7. +

    The getMatchOutcome method must be overriden and should check the my.custom.condition system property. Use ConditionOutcome.match and ConditionOutcome.noMatch to signal if the condition matches or not.

    +
  8. +
  9. +

    Modify the GreetingAutoConfiguration to use the new @MyCustomCondition. A bean of class BeepGreetingService (located in the library-slf4j module) should be created if @MyCustomCondition matches.

    +
  10. +
  11. +

    Test that the application works by setting the system property my.custom.condition and verify that the BeepGreetingService bean is used.

    +
    + + + + + +
    +
    Note
    +
    + You’ll have to set workshop.greeting.type to something else than logger or stdout, because otherwise the LoggerGreetingService or StdOutGreetingService is also created. +
    +
    +
  12. +
  13. +

    🤔 Also take a look at the conditions evaluation report. Do you see your condition in there?

    +
  14. +
+
+
+
+

Detailed Steps

+
+ Detailed Steps +
+
+
    +
  1. +

    Create a new annotation in the library-autoconfigure module, called MyCustomCondition

    +
  2. +
  3. +

    Annotate the annotation with @Target({ElementType.TYPE, ElementType.METHOD}) and with @Retention(RetentionPolicy.RUNTIME)

    +
  4. +
  5. +

    Annotate the annotation with @Conditional({OnCustomCondition.class})

    +
    +
    +
    @Target({ElementType.TYPE, ElementType.METHOD})
    +@Retention(RetentionPolicy.RUNTIME)
    +@Conditional({OnCustomCondition.class})
    +@interface MyCustomCondition {
    +}
    +
    +
    +
  6. +
  7. +

    Create a class called OnCustomCondition and let it extend SpringBootCondition

    +
  8. +
  9. +

    Implement the getMatchOutcome method

    +
    +
      +
    1. +

      Use System.getProperty("my.custom.condition") to read the my.custom.condition system property

      +
    2. +
    3. +

      If the value of that property is true, return ConditionOutcome.match to signal that the condition matches

      +
    4. +
    5. +

      Otherwise, return ConditionOutcome.noMatch to signal that the condition didn’t match

      +
      +
      +
      class 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));
      +    }
      +}
      +
      +
      +
    6. +
    +
    +
  10. +
  11. +

    Add a new @Bean method to the GreetingAutoConfiguration class, call it beepGreetingService, its return type is GreetingService

    +
    +
      +
    1. +

      Annotate this new method with @MyCustomCondition, @ConditionalOnMissingBean and @ConditionalOnClass(BeepGreetingService.class)

      +
    2. +
    3. +

      Return a new instance of BeepGreetingService from that method

      +
      +
      +
      @Bean
      +@ConditionalOnMissingBean
      +@MyCustomCondition
      +@ConditionalOnClass(BeepGreetingService.class)
      +GreetingService beepGreetingService(GreetingProperties properties) {
      +    return new BeepGreetingService(properties.getPrefix());
      +}
      +
      +
      +
    4. +
    +
    +
  12. +
  13. +

    To test the custom condition, you can add System.setProperty("my.custom.condition", "true"); as first line in the main method, or you can set the system properties when starting with your IDE

    +
  14. +
  15. +

    You’ll also need to add workshop.greeting.type=none to your application.properties, because otherwise the LoggerGreetingService or the StdOutGreetingService would be created

    +
  16. +
+
+
+
+
+
+

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

+
+
    +
  1. +

    A Maven dependency on org.springframework.boot:spring-boot-starter-test with scope test has to be added in the library-autoconfigure module.

    +
  2. +
  3. +

    A test class for the GreetingAutoConfiguration class must be created.

    +
  4. +
  5. +

    Spring Boot’s ApplicationContextRunner should be used to test the auto-configuration (see the reference documentation).

    +
  6. +
  7. +

    AssertJ assertions should be used to verify that the context contains a StdOutGreetingService bean if no property is set.

    +
  8. +
  9. +

    Implement tests for these use cases:

    +
    +
      +
    1. +

      The context contains a StdOutGreetingService bean if the property workshop.greeting.type is set to stdout.

      +
    2. +
    3. +

      The context contains a LoggerGreetingService bean if the property workshop.greeting.type is set to logger.

      +
    4. +
    5. +

      The context contains BeepGreetingService bean if the system property my.custom.condition is set to true.

      +
    6. +
    7. +

      That user-defined beans take precedence over the auto-configured GreetingService beans — essentially testing that @ConditionalOnMissingBean works.

      +
    8. +
    +
    +
  10. +
+
+
+
+

Detailed Steps

+
+ Detailed Steps +
+
+
    +
  1. +

    Add a Maven dependency to org.springframework.boot:spring-boot-starter-test with scope test to the library-autoconfigure module.

    +
  2. +
  3. +

    Create a class named GreetingAutoConfigurationTest in library-autoconfigure/src/test/java in the package com.workshop.magic.config.

    +
  4. +
  5. +

    Create a field of type ApplicationContextRunner, and use the fluent API to call withConfiguration with AutoConfigurations.of(GreetingAutoConfiguration.class).

    +
    +
    +
    private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
    +    .withConfiguration(AutoConfigurations.of(GreetingAutoConfiguration.class));
    +
    +
    +
  6. +
  7. +

    Write a test case named shouldProvideStdOutGreetingServiceByDefault which uses the run method of the ApplicationContextRunner field.

    +
    +
      +
    1. +

      Inside the lambda block of the run method, use AssertJ’s assertThat on the context to call hasSingleBean with an StdOutGreetingService.class argument.

      +
      +
      +
      @Test
      +void shouldProvideStdOutGreetingServiceByDefault() {
      +    this.contextRunner.run(context -> {
      +        assertThat(context).hasSingleBean(StdOutGreetingService.class);
      +    });
      +}
      +
      +
      +
    2. +
    +
    +
  8. +
  9. +

    Write a test case named shouldProvideStdOutGreetingServiceWhenPropertyIsSet which uses the withPropertyValues of the ApplicationContextRunner field to set the property workshop.greeting.type to stdout.

    +
    +
      +
    1. +

      Inside the lambda block of the run method, use AssertJ’s assertThat on the context to call hasSingleBean with an StdOutGreetingService.class argument.

      +
      +
      +
      @Test
      +void shouldProvideStdOutGreetingServiceWhenPropertyIsSet() {
      +    this.contextRunner
      +            .withPropertyValues("workshop.greeting.type=stdout")
      +            .run(context -> {
      +                assertThat(context).hasSingleBean(StdOutGreetingService.class);
      +            });
      +}
      +
      +
      +
    2. +
    +
    +
  10. +
  11. +

    Write a test case named shouldProvideLoggerGreetingServiceWhenPropertyIsSet which uses the withPropertyValues of the ApplicationContextRunner field to set the property workshop.greeting.type to logger.

    +
    +
      +
    1. +

      Inside the lambda block of the run method, use AssertJ’s assertThat on the context to call hasSingleBean with an LoggerGreetingService.class argument.

      +
    2. +
    +
    +
  12. +
  13. +

    Write a test case named shouldProvideBeepGreetingServiceIfSystemPropertyIsSet which uses withPropertyValues of the ApplicationContextRunner field to set the property workshop.greeting.type to none.

    +
    +
      +
    1. +

      Additionally, it uses the withSystemProperties method to set my.custom.condition to true.

      +
    2. +
    3. +

      Inside the lambda block of the run method, use AssertJ’s assertThat on the context to call hasSingleBean with an BeepGreetingService.class argument.

      +
      +
      +
      @Test
      +void shouldProvideBeepGreetingServiceIfSystemPropertyIsSet() {
      +    this.contextRunner
      +            .withPropertyValues("workshop.greeting.type=none")
      +            .withSystemProperties("my.custom.condition=true")
      +            .run(context -> {
      +                assertThat(context).hasSingleBean(BeepGreetingService.class);
      +            });
      +}
      +
      +
      +
    4. +
    +
    +
  14. +
  15. +

    Write a test case named shouldBackOffIfGreetingServiceIsDefinedByUser which uses the withBean method of the ApplicationContextRunner field to define a bean of type GreetingService. Create an inner static class or an anonymous class for the "user provided" GreetingService.

    +
    +
      +
    1. +

      Inside the lambda block of the run method, use AssertJ’s assertThat on the context to call hasSingleBean with an GreetingService.class argument.

      +
      +
      +
      @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);
      +    }
      +}
      +
      +
      +
    2. +
    +
    +
  16. +
+
+
+
+
+
+

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

+
+
    +
  1. +

    Add the org.springframework.boot:spring-boot-configuration-processor to the library-autoconfigure module.

    +
  2. +
  3. +

    Run a build and inspect the components/library-autoconfigure/target/classes/META-INF/spring-configuration-metadata.json file.

    +
  4. +
  5. +

    🤔 Think about why that file could be useful.

    +
  6. +
  7. +

    The text property in GreetingProperties should be renamed to prefix, while deprecating the text property. Use @Deprecated and @DeprecatedConfigurationProperty annotations to achieve this.

    +
  8. +
  9. +

    Run a build and inspect the file spring-configuration-metadata.json again.

    +
  10. +
  11. +

    🤔 What has changed? Why could that be useful?

    +
  12. +
  13. +

    🤔 Open the application.properties in your IDE. Do you notice something?

    +
  14. +
  15. +

    Add org.springframework.boot:spring-boot-properties-migrator to the app module.

    +
  16. +
  17. +

    Start the app and observe the console output.

    +
  18. +
+
+
+
+

Detailed Steps

+
+ Detailed Steps +
+
+
    +
  1. +

    Add org.springframework.boot:spring-boot-configuration-processor to components/library-autoconfigure/pom.xml, with optional = true.

    +
    +
    +
    <dependency>
    +    <groupId>org.springframework.boot</groupId>
    +    <artifactId>spring-boot-configuration-processor</artifactId>
    +    <optional>true</optional>
    +</dependency>
    +
    +
    +
  2. +
  3. +

    Newer Java versions require an explicit configuration for annotation processors. Configure the maven-compiler-plugin to include org.springframework.boot:spring-boot-configuration-processor as 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>
    +
    +
    +
  4. +
  5. +

    Run ./mvnw compile and inspect components/library-autoconfigure/target/classes/META-INF/spring-configuration-metadata.json.

    +
  6. +
  7. +

    Replace private String text in the GreetingProperties class with private String prefix.

    +
  8. +
  9. +

    Annotate the public String getText() method with @Deprecated and with @DeprecatedConfigurationProperty(replacement = "workshop.greeting.prefix").

    +
  10. +
  11. +

    Return this.prefix from the getText() method.

    +
  12. +
  13. +

    Assign this.prefix in the setText() method.

    +
  14. +
  15. +

    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;
    +}
    +
    +
    +
  16. +
  17. +

    Run ./mvnw compile and inspect components/library-autoconfigure/target/classes/META-INF/spring-configuration-metadata.json.

    +
  18. +
  19. +

    Add org.springframework.boot:spring-boot-properties-migrator with scope = runtime to app/app/pom.xml.

    +
  20. +
  21. +

    Run the application

    +
  22. +
+
+
+
+
+
+

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!

+
+ +
+
+
+
+ +
+ + +