spring-boot-magic-workshop/workbook.html

3664 lines
144 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="Asciidoctor 2.0.20">
<meta name="author" content="Moritz Halbritter &amp; Fabian Krüger">
<title>Demystifying Spring Boot&#8217;s Auto-Configuration Magic</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700">
<style>
/*! Asciidoctor default stylesheet | MIT License | https://asciidoctor.org */
/* Uncomment the following line when using as a custom stylesheet */
/* @import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700"; */
html {
font-family: sans-serif;
-webkit-text-size-adjust: 100%
}
a {
background: none
}
a:focus {
outline: thin dotted
}
a:active, a:hover {
outline: 0
}
h1 {
font-size: 2em;
margin: .67em 0
}
b, strong {
font-weight: bold
}
abbr {
font-size: .9em
}
abbr[title] {
cursor: help;
border-bottom: 1px dotted #dddddf;
text-decoration: none
}
dfn {
font-style: italic
}
hr {
height: 0
}
mark {
background: #ff0;
color: #000
}
code, kbd, pre, samp {
font-family: monospace;
font-size: 1em
}
pre {
white-space: pre-wrap
}
q {
quotes: "\201C" "\201D" "\2018" "\2019"
}
small {
font-size: 80%
}
sub, sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline
}
sup {
top: -.5em
}
sub {
bottom: -.25em
}
img {
border: 0
}
svg:not(:root) {
overflow: hidden
}
figure {
margin: 0
}
audio, video {
display: inline-block
}
audio:not([controls]) {
display: none;
height: 0
}
fieldset {
border: 1px solid silver;
margin: 0 2px;
padding: .35em .625em .75em
}
legend {
border: 0;
padding: 0
}
button, input, select, textarea {
font-family: inherit;
font-size: 100%;
margin: 0
}
button, input {
line-height: normal
}
button, select {
text-transform: none
}
button, html input[type=button], input[type=reset], input[type=submit] {
-webkit-appearance: button;
cursor: pointer
}
button[disabled], html input[disabled] {
cursor: default
}
input[type=checkbox], input[type=radio] {
padding: 0
}
button::-moz-focus-inner, input::-moz-focus-inner {
border: 0;
padding: 0
}
textarea {
overflow: auto;
vertical-align: top
}
table {
border-collapse: collapse;
border-spacing: 0
}
*, ::before, ::after {
box-sizing: border-box
}
html, body {
font-size: 100%
}
body {
background: #fff;
color: rgba(0, 0, 0, .8);
padding: 0;
margin: 0;
font-family: "Noto Serif", "DejaVu Serif", serif;
line-height: 1;
position: relative;
cursor: auto;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
word-wrap: anywhere;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased
}
a:hover {
cursor: pointer
}
img, object, embed {
max-width: 100%;
height: auto
}
object, embed {
height: 100%
}
img {
-ms-interpolation-mode: bicubic
}
.left {
float: left !important
}
.right {
float: right !important
}
.text-left {
text-align: left !important
}
.text-right {
text-align: right !important
}
.text-center {
text-align: center !important
}
.text-justify {
text-align: justify !important
}
.hide {
display: none
}
img, object, svg {
display: inline-block;
vertical-align: middle
}
textarea {
height: auto;
min-height: 50px
}
select {
width: 100%
}
.subheader, .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title {
line-height: 1.45;
color: #7a2518;
font-weight: 400;
margin-top: 0;
margin-bottom: .25em
}
div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td {
margin: 0;
padding: 0
}
a {
color: #2156a5;
text-decoration: underline;
line-height: inherit
}
a:hover, a:focus {
color: #1d4b8f
}
a img {
border: 0
}
p {
line-height: 1.6;
margin-bottom: 1.25em;
text-rendering: optimizeLegibility
}
p aside {
font-size: .875em;
line-height: 1.35;
font-style: italic
}
h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 {
font-family: "Open Sans", "DejaVu Sans", sans-serif;
font-weight: 300;
font-style: normal;
color: #ba3925;
text-rendering: optimizeLegibility;
margin-top: 1em;
margin-bottom: .5em;
line-height: 1.0125em
}
h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small {
font-size: 60%;
color: #e99b8f;
line-height: 0
}
h1 {
font-size: 2.125em
}
h2 {
font-size: 1.6875em
}
h3, #toctitle, .sidebarblock > .content > .title {
font-size: 1.375em
}
h4, h5 {
font-size: 1.125em
}
h6 {
font-size: 1em
}
hr {
border: solid #dddddf;
border-width: 1px 0 0;
clear: both;
margin: 1.25em 0 1.1875em
}
em, i {
font-style: italic;
line-height: inherit
}
strong, b {
font-weight: bold;
line-height: inherit
}
small {
font-size: 60%;
line-height: inherit
}
code {
font-family: "Droid Sans Mono", "DejaVu Sans Mono", monospace;
font-weight: 400;
color: rgba(0, 0, 0, .9)
}
ul, ol, dl {
line-height: 1.6;
margin-bottom: 1.25em;
list-style-position: outside;
font-family: inherit
}
ul, ol {
margin-left: 1.5em
}
ul li ul, ul li ol {
margin-left: 1.25em;
margin-bottom: 0
}
ul.circle {
list-style-type: circle
}
ul.disc {
list-style-type: disc
}
ul.square {
list-style-type: square
}
ul.circle ul:not([class]), ul.disc ul:not([class]), ul.square ul:not([class]) {
list-style: inherit
}
ol li ul, ol li ol {
margin-left: 1.25em;
margin-bottom: 0
}
dl dt {
margin-bottom: .3125em;
font-weight: bold
}
dl dd {
margin-bottom: 1.25em
}
blockquote {
margin: 0 0 1.25em;
padding: .5625em 1.25em 0 1.1875em;
border-left: 1px solid #ddd
}
blockquote, blockquote p {
line-height: 1.6;
color: rgba(0, 0, 0, .85)
}
@media screen and (min-width: 768px) {
h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 {
line-height: 1.2
}
h1 {
font-size: 2.75em
}
h2 {
font-size: 2.3125em
}
h3, #toctitle, .sidebarblock > .content > .title {
font-size: 1.6875em
}
h4 {
font-size: 1.4375em
}
}
table {
background: #fff;
margin-bottom: 1.25em;
border: 1px solid #dedede;
word-wrap: normal
}
table thead, table tfoot {
background: #f7f8f7
}
table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td {
padding: .5em .625em .625em;
font-size: inherit;
color: rgba(0, 0, 0, .8);
text-align: left
}
table tr th, table tr td {
padding: .5625em .625em;
font-size: inherit;
color: rgba(0, 0, 0, .8)
}
table tr.even, table tr.alt {
background: #f8f8f7
}
table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td {
line-height: 1.6
}
h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 {
line-height: 1.2;
word-spacing: -.05em
}
h1 strong, h2 strong, h3 strong, #toctitle strong, .sidebarblock > .content > .title strong, h4 strong, h5 strong, h6 strong {
font-weight: 400
}
.center {
margin-left: auto;
margin-right: auto
}
.stretch {
width: 100%
}
.clearfix::before, .clearfix::after, .float-group::before, .float-group::after {
content: " ";
display: table
}
.clearfix::after, .float-group::after {
clear: both
}
:not(pre).nobreak {
word-wrap: normal
}
:not(pre).nowrap {
white-space: nowrap
}
:not(pre).pre-wrap {
white-space: pre-wrap
}
:not(pre):not([class^=L]) > code {
font-size: .9375em;
font-style: normal !important;
letter-spacing: 0;
padding: .1em .5ex;
word-spacing: -.15em;
background: #f7f7f8;
border-radius: 4px;
line-height: 1.45;
text-rendering: optimizeSpeed
}
pre {
color: rgba(0, 0, 0, .9);
font-family: "Droid Sans Mono", "DejaVu Sans Mono", monospace;
line-height: 1.45;
text-rendering: optimizeSpeed
}
pre code, pre pre {
color: inherit;
font-size: inherit;
line-height: inherit
}
pre > code {
display: block
}
pre.nowrap, pre.nowrap pre {
white-space: pre;
word-wrap: normal
}
em em {
font-style: normal
}
strong strong {
font-weight: 400
}
.keyseq {
color: rgba(51, 51, 51, .8)
}
kbd {
font-family: "Droid Sans Mono", "DejaVu Sans Mono", monospace;
display: inline-block;
color: rgba(0, 0, 0, .8);
font-size: .65em;
line-height: 1.45;
background: #f7f7f7;
border: 1px solid #ccc;
border-radius: 3px;
box-shadow: 0 1px 0 rgba(0, 0, 0, .2), inset 0 0 0 .1em #fff;
margin: 0 .15em;
padding: .2em .5em;
vertical-align: middle;
position: relative;
top: -.1em;
white-space: nowrap
}
.keyseq kbd:first-child {
margin-left: 0
}
.keyseq kbd:last-child {
margin-right: 0
}
.menuseq, .menuref {
color: #000
}
.menuseq b:not(.caret), .menuref {
font-weight: inherit
}
.menuseq {
word-spacing: -.02em
}
.menuseq b.caret {
font-size: 1.25em;
line-height: .8
}
.menuseq i.caret {
font-weight: bold;
text-align: center;
width: .45em
}
b.button::before, b.button::after {
position: relative;
top: -1px;
font-weight: 400
}
b.button::before {
content: "[";
padding: 0 3px 0 2px
}
b.button::after {
content: "]";
padding: 0 2px 0 3px
}
p a > code:hover {
color: rgba(0, 0, 0, .9)
}
#header, #content, #footnotes, #footer {
width: 100%;
margin: 0 auto;
max-width: 62.5em;
*zoom: 1;
position: relative;
padding-left: .9375em;
padding-right: .9375em
}
#header::before, #header::after, #content::before, #content::after, #footnotes::before, #footnotes::after, #footer::before, #footer::after {
content: " ";
display: table
}
#header::after, #content::after, #footnotes::after, #footer::after {
clear: both
}
#content {
margin-top: 1.25em
}
#content::before {
content: none
}
#header > h1:first-child {
color: rgba(0, 0, 0, .85);
margin-top: 2.25rem;
margin-bottom: 0
}
#header > h1:first-child + #toc {
margin-top: 8px;
border-top: 1px solid #dddddf
}
#header > h1:only-child, body.toc2 #header > h1:nth-last-child(2) {
border-bottom: 1px solid #dddddf;
padding-bottom: 8px
}
#header .details {
border-bottom: 1px solid #dddddf;
line-height: 1.45;
padding-top: .25em;
padding-bottom: .25em;
padding-left: .25em;
color: rgba(0, 0, 0, .6);
display: flex;
flex-flow: row wrap
}
#header .details span:first-child {
margin-left: -.125em
}
#header .details span.email a {
color: rgba(0, 0, 0, .85)
}
#header .details br {
display: none
}
#header .details br + span::before {
content: "\00a0\2013\00a0"
}
#header .details br + span.author::before {
content: "\00a0\22c5\00a0";
color: rgba(0, 0, 0, .85)
}
#header .details br + span#revremark::before {
content: "\00a0|\00a0"
}
#header #revnumber {
text-transform: capitalize
}
#header #revnumber::after {
content: "\00a0"
}
#content > h1:first-child:not([class]) {
color: rgba(0, 0, 0, .85);
border-bottom: 1px solid #dddddf;
padding-bottom: 8px;
margin-top: 0;
padding-top: 1rem;
margin-bottom: 1.25rem
}
#toc {
border-bottom: 1px solid #e7e7e9;
padding-bottom: .5em
}
#toc > ul {
margin-left: .125em
}
#toc ul.sectlevel0 > li > a {
font-style: italic
}
#toc ul.sectlevel0 ul.sectlevel1 {
margin: .5em 0
}
#toc ul {
font-family: "Open Sans", "DejaVu Sans", sans-serif;
list-style-type: none
}
#toc li {
line-height: 1.3334;
margin-top: .3334em
}
#toc a {
text-decoration: none
}
#toc a:active {
text-decoration: underline
}
#toctitle {
color: #7a2518;
font-size: 1.2em
}
@media screen and (min-width: 768px) {
#toctitle {
font-size: 1.375em
}
body.toc2 {
padding-left: 15em;
padding-right: 0
}
#toc.toc2 {
margin-top: 0 !important;
background: #f8f8f7;
position: fixed;
width: 15em;
left: 0;
top: 0;
border-right: 1px solid #e7e7e9;
border-top-width: 0 !important;
border-bottom-width: 0 !important;
z-index: 1000;
padding: 1.25em 1em;
height: 100%;
overflow: auto
}
#toc.toc2 #toctitle {
margin-top: 0;
margin-bottom: .8rem;
font-size: 1.2em
}
#toc.toc2 > ul {
font-size: .9em;
margin-bottom: 0
}
#toc.toc2 ul ul {
margin-left: 0;
padding-left: 1em
}
#toc.toc2 ul.sectlevel0 ul.sectlevel1 {
padding-left: 0;
margin-top: .5em;
margin-bottom: .5em
}
body.toc2.toc-right {
padding-left: 0;
padding-right: 15em
}
body.toc2.toc-right #toc.toc2 {
border-right-width: 0;
border-left: 1px solid #e7e7e9;
left: auto;
right: 0
}
}
@media screen and (min-width: 1280px) {
body.toc2 {
padding-left: 20em;
padding-right: 0
}
#toc.toc2 {
width: 20em
}
#toc.toc2 #toctitle {
font-size: 1.375em
}
#toc.toc2 > ul {
font-size: .95em
}
#toc.toc2 ul ul {
padding-left: 1.25em
}
body.toc2.toc-right {
padding-left: 0;
padding-right: 20em
}
}
#content #toc {
border: 1px solid #e0e0dc;
margin-bottom: 1.25em;
padding: 1.25em;
background: #f8f8f7;
border-radius: 4px
}
#content #toc > :first-child {
margin-top: 0
}
#content #toc > :last-child {
margin-bottom: 0
}
#footer {
max-width: none;
background: rgba(0, 0, 0, .8);
padding: 1.25em
}
#footer-text {
color: hsla(0, 0%, 100%, .8);
line-height: 1.44
}
#content {
margin-bottom: .625em
}
.sect1 {
padding-bottom: .625em
}
@media screen and (min-width: 768px) {
#content {
margin-bottom: 1.25em
}
.sect1 {
padding-bottom: 1.25em
}
}
.sect1:last-child {
padding-bottom: 0
}
.sect1 + .sect1 {
border-top: 1px solid #e7e7e9
}
#content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor {
position: absolute;
z-index: 1001;
width: 1.5ex;
margin-left: -1.5ex;
display: block;
text-decoration: none !important;
visibility: hidden;
text-align: center;
font-weight: 400
}
#content h1 > a.anchor::before, h2 > a.anchor::before, h3 > a.anchor::before, #toctitle > a.anchor::before, .sidebarblock > .content > .title > a.anchor::before, h4 > a.anchor::before, h5 > a.anchor::before, h6 > a.anchor::before {
content: "\00A7";
font-size: .85em;
display: block;
padding-top: .1em
}
#content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover {
visibility: visible
}
#content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link {
color: #ba3925;
text-decoration: none
}
#content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover {
color: #a53221
}
details, .audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .videoblock {
margin-bottom: 1.25em
}
details {
margin-left: 1.25rem
}
details > summary {
cursor: pointer;
display: block;
position: relative;
line-height: 1.6;
margin-bottom: .625rem;
outline: none;
-webkit-tap-highlight-color: transparent
}
details > summary::-webkit-details-marker {
display: none
}
details > summary::before {
content: "";
border: solid transparent;
border-left: solid;
border-width: .3em 0 .3em .5em;
position: absolute;
top: .5em;
left: -1.25rem;
transform: translateX(15%)
}
details[open] > summary::before {
border: solid transparent;
border-top: solid;
border-width: .5em .3em 0;
transform: translateY(15%)
}
details > summary::after {
content: "";
width: 1.25rem;
height: 1em;
position: absolute;
top: .3em;
left: -1.25rem
}
.admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title {
text-rendering: optimizeLegibility;
text-align: left;
font-family: "Noto Serif", "DejaVu Serif", serif;
font-size: 1rem;
font-style: italic
}
table.tableblock.fit-content > caption.title {
white-space: nowrap;
width: 0
}
.paragraph.lead > p, #preamble > .sectionbody > [class=paragraph]:first-of-type p {
font-size: 1.21875em;
line-height: 1.6;
color: rgba(0, 0, 0, .85)
}
.admonitionblock > table {
border-collapse: separate;
border: 0;
background: none;
width: 100%
}
.admonitionblock > table td.icon {
text-align: center;
width: 80px
}
.admonitionblock > table td.icon img {
max-width: none
}
.admonitionblock > table td.icon .title {
font-weight: bold;
font-family: "Open Sans", "DejaVu Sans", sans-serif;
text-transform: uppercase
}
.admonitionblock > table td.content {
padding-left: 1.125em;
padding-right: 1.25em;
border-left: 1px solid #dddddf;
color: rgba(0, 0, 0, .6);
word-wrap: anywhere
}
.admonitionblock > table td.content > :last-child > :last-child {
margin-bottom: 0
}
.exampleblock > .content {
border: 1px solid #e6e6e6;
margin-bottom: 1.25em;
padding: 1.25em;
background: #fff;
border-radius: 4px
}
.sidebarblock {
border: 1px solid #dbdbd6;
margin-bottom: 1.25em;
padding: 1.25em;
background: #f3f3f2;
border-radius: 4px
}
.sidebarblock > .content > .title {
color: #7a2518;
margin-top: 0;
text-align: center
}
.exampleblock > .content > :first-child, .sidebarblock > .content > :first-child {
margin-top: 0
}
.exampleblock > .content > :last-child, .exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child {
margin-bottom: 0
}
.literalblock pre, .listingblock > .content > pre {
border-radius: 4px;
overflow-x: auto;
padding: 1em;
font-size: .8125em
}
@media screen and (min-width: 768px) {
.literalblock pre, .listingblock > .content > pre {
font-size: .90625em
}
}
@media screen and (min-width: 1280px) {
.literalblock pre, .listingblock > .content > pre {
font-size: 1em
}
}
.literalblock pre, .listingblock > .content > pre:not(.highlight), .listingblock > .content > pre[class=highlight], .listingblock > .content > pre[class^="highlight "] {
background: #f7f7f8
}
.literalblock.output pre {
color: #f7f7f8;
background: rgba(0, 0, 0, .9)
}
.listingblock > .content {
position: relative
}
.listingblock code[data-lang]::before {
display: none;
content: attr(data-lang);
position: absolute;
font-size: .75em;
top: .425rem;
right: .5rem;
line-height: 1;
text-transform: uppercase;
color: inherit;
opacity: .5
}
.listingblock:hover code[data-lang]::before {
display: block
}
.listingblock.terminal pre .command::before {
content: attr(data-prompt);
padding-right: .5em;
color: inherit;
opacity: .5
}
.listingblock.terminal pre .command:not([data-prompt])::before {
content: "$"
}
.listingblock pre.highlightjs {
padding: 0
}
.listingblock pre.highlightjs > code {
padding: 1em;
border-radius: 4px
}
.listingblock pre.prettyprint {
border-width: 0
}
.prettyprint {
background: #f7f7f8
}
pre.prettyprint .linenums {
line-height: 1.45;
margin-left: 2em
}
pre.prettyprint li {
background: none;
list-style-type: inherit;
padding-left: 0
}
pre.prettyprint li code[data-lang]::before {
opacity: 1
}
pre.prettyprint li:not(:first-child) code[data-lang]::before {
display: none
}
table.linenotable {
border-collapse: separate;
border: 0;
margin-bottom: 0;
background: none
}
table.linenotable td[class] {
color: inherit;
vertical-align: top;
padding: 0;
line-height: inherit;
white-space: normal
}
table.linenotable td.code {
padding-left: .75em
}
table.linenotable td.linenos, pre.pygments .linenos {
border-right: 1px solid;
opacity: .35;
padding-right: .5em;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
pre.pygments span.linenos {
display: inline-block;
margin-right: .75em
}
.quoteblock {
margin: 0 1em 1.25em 1.5em;
display: table
}
.quoteblock:not(.excerpt) > .title {
margin-left: -1.5em;
margin-bottom: .75em
}
.quoteblock blockquote, .quoteblock p {
color: rgba(0, 0, 0, .85);
font-size: 1.15rem;
line-height: 1.75;
word-spacing: .1em;
letter-spacing: 0;
font-style: italic;
text-align: justify
}
.quoteblock blockquote {
margin: 0;
padding: 0;
border: 0
}
.quoteblock blockquote::before {
content: "\201c";
float: left;
font-size: 2.75em;
font-weight: bold;
line-height: .6em;
margin-left: -.6em;
color: #7a2518;
text-shadow: 0 1px 2px rgba(0, 0, 0, .1)
}
.quoteblock blockquote > .paragraph:last-child p {
margin-bottom: 0
}
.quoteblock .attribution {
margin-top: .75em;
margin-right: .5ex;
text-align: right
}
.verseblock {
margin: 0 1em 1.25em
}
.verseblock pre {
font-family: "Open Sans", "DejaVu Sans", sans-serif;
font-size: 1.15rem;
color: rgba(0, 0, 0, .85);
font-weight: 300;
text-rendering: optimizeLegibility
}
.verseblock pre strong {
font-weight: 400
}
.verseblock .attribution {
margin-top: 1.25rem;
margin-left: .5ex
}
.quoteblock .attribution, .verseblock .attribution {
font-size: .9375em;
line-height: 1.45;
font-style: italic
}
.quoteblock .attribution br, .verseblock .attribution br {
display: none
}
.quoteblock .attribution cite, .verseblock .attribution cite {
display: block;
letter-spacing: -.025em;
color: rgba(0, 0, 0, .6)
}
.quoteblock.abstract blockquote::before, .quoteblock.excerpt blockquote::before, .quoteblock .quoteblock blockquote::before {
display: none
}
.quoteblock.abstract blockquote, .quoteblock.abstract p, .quoteblock.excerpt blockquote, .quoteblock.excerpt p, .quoteblock .quoteblock blockquote, .quoteblock .quoteblock p {
line-height: 1.6;
word-spacing: 0
}
.quoteblock.abstract {
margin: 0 1em 1.25em;
display: block
}
.quoteblock.abstract > .title {
margin: 0 0 .375em;
font-size: 1.15em;
text-align: center
}
.quoteblock.excerpt > blockquote, .quoteblock .quoteblock {
padding: 0 0 .25em 1em;
border-left: .25em solid #dddddf
}
.quoteblock.excerpt, .quoteblock .quoteblock {
margin-left: 0
}
.quoteblock.excerpt blockquote, .quoteblock.excerpt p, .quoteblock .quoteblock blockquote, .quoteblock .quoteblock p {
color: inherit;
font-size: 1.0625rem
}
.quoteblock.excerpt .attribution, .quoteblock .quoteblock .attribution {
color: inherit;
font-size: .85rem;
text-align: left;
margin-right: 0
}
p.tableblock:last-child {
margin-bottom: 0
}
td.tableblock > .content {
margin-bottom: 1.25em;
word-wrap: anywhere
}
td.tableblock > .content > :last-child {
margin-bottom: -1.25em
}
table.tableblock, th.tableblock, td.tableblock {
border: 0 solid #dedede
}
table.grid-all > * > tr > * {
border-width: 1px
}
table.grid-cols > * > tr > * {
border-width: 0 1px
}
table.grid-rows > * > tr > * {
border-width: 1px 0
}
table.frame-all {
border-width: 1px
}
table.frame-ends {
border-width: 1px 0
}
table.frame-sides {
border-width: 0 1px
}
table.frame-none > colgroup + * > :first-child > *, table.frame-sides > colgroup + * > :first-child > * {
border-top-width: 0
}
table.frame-none > :last-child > :last-child > *, table.frame-sides > :last-child > :last-child > * {
border-bottom-width: 0
}
table.frame-none > * > tr > :first-child, table.frame-ends > * > tr > :first-child {
border-left-width: 0
}
table.frame-none > * > tr > :last-child, table.frame-ends > * > tr > :last-child {
border-right-width: 0
}
table.stripes-all > * > tr, table.stripes-odd > * > tr:nth-of-type(odd), table.stripes-even > * > tr:nth-of-type(even), table.stripes-hover > * > tr:hover {
background: #f8f8f7
}
th.halign-left, td.halign-left {
text-align: left
}
th.halign-right, td.halign-right {
text-align: right
}
th.halign-center, td.halign-center {
text-align: center
}
th.valign-top, td.valign-top {
vertical-align: top
}
th.valign-bottom, td.valign-bottom {
vertical-align: bottom
}
th.valign-middle, td.valign-middle {
vertical-align: middle
}
table thead th, table tfoot th {
font-weight: bold
}
tbody tr th {
background: #f7f8f7
}
tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p {
color: rgba(0, 0, 0, .8);
font-weight: bold
}
p.tableblock > code:only-child {
background: none;
padding: 0
}
p.tableblock {
font-size: 1em
}
ol {
margin-left: 1.75em
}
ul li ol {
margin-left: 1.5em
}
dl dd {
margin-left: 1.125em
}
dl dd:last-child, dl dd:last-child > :last-child {
margin-bottom: 0
}
li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, .olist .ulist {
margin-bottom: .625em
}
ul.checklist, ul.none, ol.none, ul.no-bullet, ol.no-bullet, ol.unnumbered, ul.unstyled, ol.unstyled {
list-style-type: none
}
ul.no-bullet, ol.no-bullet, ol.unnumbered {
margin-left: .625em
}
ul.unstyled, ol.unstyled {
margin-left: 0
}
li > p:empty:only-child::before {
content: "";
display: inline-block
}
ul.checklist > li > p:first-child {
margin-left: -1em
}
ul.checklist > li > p:first-child > .fa-square-o:first-child, ul.checklist > li > p:first-child > .fa-check-square-o:first-child {
width: 1.25em;
font-size: .8em;
position: relative;
bottom: .125em
}
ul.checklist > li > p:first-child > input[type=checkbox]:first-child {
margin-right: .25em
}
ul.inline {
display: flex;
flex-flow: row wrap;
list-style: none;
margin: 0 0 .625em -1.25em
}
ul.inline > li {
margin-left: 1.25em
}
.unstyled dl dt {
font-weight: 400;
font-style: normal
}
ol.arabic {
list-style-type: decimal
}
ol.decimal {
list-style-type: decimal-leading-zero
}
ol.loweralpha {
list-style-type: lower-alpha
}
ol.upperalpha {
list-style-type: upper-alpha
}
ol.lowerroman {
list-style-type: lower-roman
}
ol.upperroman {
list-style-type: upper-roman
}
ol.lowergreek {
list-style-type: lower-greek
}
.hdlist > table, .colist > table {
border: 0;
background: none
}
.hdlist > table > tbody > tr, .colist > table > tbody > tr {
background: none
}
td.hdlist1, td.hdlist2 {
vertical-align: top;
padding: 0 .625em
}
td.hdlist1 {
font-weight: bold;
padding-bottom: 1.25em
}
td.hdlist2 {
word-wrap: anywhere
}
.literalblock + .colist, .listingblock + .colist {
margin-top: -.5em
}
.colist td:not([class]):first-child {
padding: .4em .75em 0;
line-height: 1;
vertical-align: top
}
.colist td:not([class]):first-child img {
max-width: none
}
.colist td:not([class]):last-child {
padding: .25em 0
}
.thumb, .th {
line-height: 0;
display: inline-block;
border: 4px solid #fff;
box-shadow: 0 0 0 1px #ddd
}
.imageblock.left {
margin: .25em .625em 1.25em 0
}
.imageblock.right {
margin: .25em 0 1.25em .625em
}
.imageblock > .title {
margin-bottom: 0
}
.imageblock.thumb, .imageblock.th {
border-width: 6px
}
.imageblock.thumb > .title, .imageblock.th > .title {
padding: 0 .125em
}
.image.left, .image.right {
margin-top: .25em;
margin-bottom: .25em;
display: inline-block;
line-height: 0
}
.image.left {
margin-right: .625em
}
.image.right {
margin-left: .625em
}
a.image {
text-decoration: none;
display: inline-block
}
a.image object {
pointer-events: none
}
sup.footnote, sup.footnoteref {
font-size: .875em;
position: static;
vertical-align: super
}
sup.footnote a, sup.footnoteref a {
text-decoration: none
}
sup.footnote a:active, sup.footnoteref a:active {
text-decoration: underline
}
#footnotes {
padding-top: .75em;
padding-bottom: .75em;
margin-bottom: .625em
}
#footnotes hr {
width: 20%;
min-width: 6.25em;
margin: -.25em 0 .75em;
border-width: 1px 0 0
}
#footnotes .footnote {
padding: 0 .375em 0 .225em;
line-height: 1.3334;
font-size: .875em;
margin-left: 1.2em;
margin-bottom: .2em
}
#footnotes .footnote a:first-of-type {
font-weight: bold;
text-decoration: none;
margin-left: -1.05em
}
#footnotes .footnote:last-of-type {
margin-bottom: 0
}
#content #footnotes {
margin-top: -.625em;
margin-bottom: 0;
padding: .75em 0
}
div.unbreakable {
page-break-inside: avoid
}
.big {
font-size: larger
}
.small {
font-size: smaller
}
.underline {
text-decoration: underline
}
.overline {
text-decoration: overline
}
.line-through {
text-decoration: line-through
}
.aqua {
color: #00bfbf
}
.aqua-background {
background: #00fafa
}
.black {
color: #000
}
.black-background {
background: #000
}
.blue {
color: #0000bf
}
.blue-background {
background: #0000fa
}
.fuchsia {
color: #bf00bf
}
.fuchsia-background {
background: #fa00fa
}
.gray {
color: #606060
}
.gray-background {
background: #7d7d7d
}
.green {
color: #006000
}
.green-background {
background: #007d00
}
.lime {
color: #00bf00
}
.lime-background {
background: #00fa00
}
.maroon {
color: #600000
}
.maroon-background {
background: #7d0000
}
.navy {
color: #000060
}
.navy-background {
background: #00007d
}
.olive {
color: #606000
}
.olive-background {
background: #7d7d00
}
.purple {
color: #600060
}
.purple-background {
background: #7d007d
}
.red {
color: #bf0000
}
.red-background {
background: #fa0000
}
.silver {
color: #909090
}
.silver-background {
background: #bcbcbc
}
.teal {
color: #006060
}
.teal-background {
background: #007d7d
}
.white {
color: #bfbfbf
}
.white-background {
background: #fafafa
}
.yellow {
color: #bfbf00
}
.yellow-background {
background: #fafa00
}
span.icon > .fa {
cursor: default
}
a span.icon > .fa {
cursor: inherit
}
.admonitionblock td.icon [class^="fa icon-"] {
font-size: 2.5em;
text-shadow: 1px 1px 2px rgba(0, 0, 0, .5);
cursor: default
}
.admonitionblock td.icon .icon-note::before {
content: "\f05a";
color: #19407c
}
.admonitionblock td.icon .icon-tip::before {
content: "\f0eb";
text-shadow: 1px 1px 2px rgba(155, 155, 0, .8);
color: #111
}
.admonitionblock td.icon .icon-warning::before {
content: "\f071";
color: #bf6900
}
.admonitionblock td.icon .icon-caution::before {
content: "\f06d";
color: #bf3400
}
.admonitionblock td.icon .icon-important::before {
content: "\f06a";
color: #bf0000
}
.conum[data-value] {
display: inline-block;
color: #fff !important;
background: rgba(0, 0, 0, .8);
border-radius: 50%;
text-align: center;
font-size: .75em;
width: 1.67em;
height: 1.67em;
line-height: 1.67em;
font-family: "Open Sans", "DejaVu Sans", sans-serif;
font-style: normal;
font-weight: bold
}
.conum[data-value] * {
color: #fff !important
}
.conum[data-value] + b {
display: none
}
.conum[data-value]::after {
content: attr(data-value)
}
pre .conum[data-value] {
position: relative;
top: -.125em
}
b.conum * {
color: inherit !important
}
.conum:not([data-value]):empty {
display: none
}
dt, th.tableblock, td.content, div.footnote {
text-rendering: optimizeLegibility
}
h1, h2, p, td.content, span.alt, summary {
letter-spacing: -.01em
}
p strong, td.content strong, div.footnote strong {
letter-spacing: -.005em
}
p, blockquote, dt, td.content, td.hdlist1, span.alt, summary {
font-size: 1.0625rem
}
p {
margin-bottom: 1.25rem
}
.sidebarblock p, .sidebarblock dt, .sidebarblock td.content, p.tableblock {
font-size: 1em
}
.exampleblock > .content {
background: #fffef7;
border-color: #e0e0dc;
box-shadow: 0 1px 4px #e0e0dc
}
.print-only {
display: none !important
}
@page {
margin: 1.25cm .75cm
}
@media print {
* {
box-shadow: none !important;
text-shadow: none !important
}
html {
font-size: 80%
}
a {
color: inherit !important;
text-decoration: underline !important
}
a.bare, a[href^="#"], a[href^="mailto:"] {
text-decoration: none !important
}
a[href^="http:"]:not(.bare)::after, a[href^="https:"]:not(.bare)::after {
content: "(" attr(href) ")";
display: inline-block;
font-size: .875em;
padding-left: .25em
}
abbr[title] {
border-bottom: 1px dotted
}
abbr[title]::after {
content: " (" attr(title) ")"
}
pre, blockquote, tr, img, object, svg {
page-break-inside: avoid
}
thead {
display: table-header-group
}
svg {
max-width: 100%
}
p, blockquote, dt, td.content {
font-size: 1em;
orphans: 3;
widows: 3
}
h2, h3, #toctitle, .sidebarblock > .content > .title {
page-break-after: avoid
}
#header, #content, #footnotes, #footer {
max-width: none
}
#toc, .sidebarblock, .exampleblock > .content {
background: none !important
}
#toc {
border-bottom: 1px solid #dddddf !important;
padding-bottom: 0 !important
}
body.book #header {
text-align: center
}
body.book #header > h1:first-child {
border: 0 !important;
margin: 2.5em 0 1em
}
body.book #header .details {
border: 0 !important;
display: block;
padding: 0 !important
}
body.book #header .details span:first-child {
margin-left: 0 !important
}
body.book #header .details br {
display: block
}
body.book #header .details br + span::before {
content: none !important
}
body.book #toc {
border: 0 !important;
text-align: left !important;
padding: 0 !important;
margin: 0 !important
}
body.book #toc, body.book #preamble, body.book h1.sect0, body.book .sect1 > h2 {
page-break-before: always
}
.listingblock code[data-lang]::before {
display: block
}
#footer {
padding: 0 .9375em
}
.hide-on-print {
display: none !important
}
.print-only {
display: block !important
}
.hide-for-print {
display: none !important
}
.show-for-print {
display: inherit !important
}
}
@media amzn-kf8, print {
#header > h1:first-child {
margin-top: 1.25rem
}
.sect1 {
padding: 0 !important
}
.sect1 + .sect1 {
border: 0
}
#footer {
background: none
}
#footer-text {
color: rgba(0, 0, 0, .6);
font-size: .9em
}
}
@media amzn-kf8 {
#header, #content, #footnotes, #footer {
padding: 0
}
}
</style>
</head>
<body class="article">
<div id="header">
<h1>Demystifying Spring Boot&#8217;s Auto-Configuration Magic</h1>
<div class="details">
<span id="author" class="author">Moritz Halbritter &amp; Fabian Krüger</span><br>
</div>
<div id="toc" class="toc">
<div id="toctitle">Table of Contents</div>
<ul class="sectlevel1">
<li><a href="#_getting_started">Getting Started</a></li>
<li><a href="#_project_layout">Project Layout</a></li>
<li><a href="#_prerequisite">Prerequisite</a></li>
<li><a href="#_exercise_1_auto_configured_bean">Exercise 1: Auto Configured Bean</a></li>
<li><a href="#_exercise_2_custom_spring_boot_starter">Exercise 2: Custom Spring Boot Starter</a></li>
<li><a href="#_exercise_3_custom_starter_without_default_greetingservice">Exercise 3: Custom Starter Without Default GreetingService</a></li>
<li><a href="#_exercise_4_conditions_evaluation_report">Exercise 4: Conditions Evaluation Report</a></li>
<li><a href="#_exercise_5_property_configuration_and_conditionalonproperty">Exercise 5: Property Configuration and @ConditionalOnProperty</a></li>
<li><a href="#_exercise_6_using_custom_conditions">Exercise 6: Using Custom Conditions</a></li>
<li><a href="#testing">Exercise 7: Testing The Auto-Configuration</a></li>
<li><a href="#properties-metadata">Exercise 8: Adding properties metadata</a></li>
<li><a href="#feedback">Feedback</a></li>
<li><a href="#_resources">Resources</a></li>
</ul>
</div>
</div>
<div id="content">
<div id="preamble">
<div class="sectionbody">
<div class="paragraph">
<p>Workshop @ Spring I/O 2025 - Moritz Halbritter &amp; Fabian Krüger</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_getting_started">Getting Started</h2>
<div class="sectionbody">
<div class="ulist">
<ul>
<li>
<p>Checkout the workshop project from GitHub</p>
</li>
</ul>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">cd ~
git clone https://github.com/fabapp2/spring-boot-magic-workshop.git
cd spring-boot-magic-workshop
git checkout exercise-1</code></pre>
</div>
</div>
<div class="admonitionblock tip">
<table>
<tr>
<td class="icon">
<div class="title">Tip</div>
</td>
<td class="content">
Remember to reload your workspace after adding dependencies to make your IDE aware of the new types.
</td>
</tr>
</table>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_project_layout">Project Layout</h2>
<div class="sectionbody">
<div class="paragraph">
<p>The project has these modules following the <a href="https://www.appcontinuum.io/">app continuum layout</a>.</p>
</div>
<div class="ulist">
<ul>
<li>
<p><code>app</code> - The <strong>Spring Boot application</strong></p>
</li>
<li>
<p><code>library-autoconfigure</code> - <strong>Spring Boot Auto-configuration</strong> for the <code>library</code></p>
</li>
<li>
<p><code>library-api</code> - <strong>Spring Boot independent</strong> library with <code>GreetingService</code> interface</p>
</li>
<li>
<p><code>library-stdout</code> - <strong>Spring Boot independent</strong> library implementation logging to stdout</p>
</li>
<li>
<p><code>library-slf4j</code> - <strong>Spring Boot independent</strong> library implementation logging with SLF4J</p>
</li>
<li>
<p><code>library-spring-boot-starter</code> - A <strong>Spring Boot starter</strong> to make the auto-configuration and all dependencies easily available</p>
</li>
</ul>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_prerequisite">Prerequisite</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Start the application in <code>app</code> and understand how modules are wired.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_exercise_1_auto_configured_bean">Exercise 1: Auto Configured Bean</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Make the <code>StdOutGreetingService</code> from <code>library-stdout</code> available as an auto-configured Spring bean for applications that rely on <code>GreetingService</code>.</p>
</div>
<div class="sect2">
<h3 id="_learnings">Learnings</h3>
<div class="ulist">
<ul>
<li>
<p><strong>How does Spring Boot find auto-configuration classes</strong></p>
</li>
<li>
<p><strong>How does Spring Boot provide auto-configured beans</strong></p>
</li>
<li>
<p><strong>How to allow auto-configured beans to be overwritten</strong></p>
</li>
</ul>
</div>
</div>
<div class="sect2">
<h3 id="_task">Task</h3>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Create an auto-configuration class <code>com.workshop.magic.config.GreetingAutoConfiguration</code> in the <code>library-autoconfigure</code> module.</p>
</li>
<li>
<p>Add a Maven dependency to <code>org.springframework.boot:spring-boot-autoconfigure</code> which brings the <code>@AutoConfiguration</code> annotation.</p>
</li>
<li>
<p>In the <code>library-autoconfigure</code> module, add a Maven dependency to other required modules (<code>library-api</code> and <code>library-stdout</code>). Make them optional, which is a best-practice for auto-configurations.</p>
</li>
<li>
<p>Annotate the <code>GreetingAutoConfiguration</code> with <code>@AutoConfiguration</code>.</p>
</li>
<li>
<p>Annotate the <code>GreetingAutoConfiguration</code> with <code>@ConditionalOnClass(GreetingService.class)</code> to only process the auto-config when <code>GreetingService</code> is on the application classpath.</p>
</li>
<li>
<p>This auto-configuration should provide <code>StdOutGreetingService</code> as a Spring bean of type <code>GreetingService</code>. Use the <code>@Bean</code> annotation for that.</p>
</li>
<li>
<p>Auto-configuration classes must be declared in a classpath resource file named <code>META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports</code>. This file contains fully qualified names of the auto-configuration so that Spring Boot can find them.</p>
</li>
<li>
<p>Add the Maven dependency <code>library-autoconfigure</code> and <code>library-stdout</code> to <code>app</code>, making the auto-configured <code>GreetingService</code> available.</p>
</li>
<li>
<p>🤔 Starting the application should fail. Why is that?</p>
</li>
<li>
<p>To avoid conflicts and allow custom implementations, the <code>StdOutGreetingService</code> should only be created when no other bean of type <code>GreetingService</code> exists.
This can be achieved by annotating the bean declaration with <code>@ConditionalOnMissingBean</code>, which tells Spring Boot to back off when such a bean already exists.</p>
</li>
<li>
<p>✅ Starting the application should now print: <em>MyGreetingService: Hola Spring I/O Barcelona</em>.</p>
</li>
<li>
<p>Modify the application to use the <code>StdOutGreetingService</code> now.</p>
</li>
<li>
<p>✅ Starting the application should now print: <em>StdOutGreetingService: Hola Spring I/O Barcelona</em>.</p>
</li>
</ol>
</div>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
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.
</td>
</tr>
</table>
</div>
</div>
<div class="sect2">
<h3 id="_detailed_steps">Detailed Steps</h3>
<details>
<summary class="title">Detailed Steps</summary>
<div class="content">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Create a new class <code>com.workshop.magic.config.GreetingAutoConfiguration</code> in the <code>library-autoconfigure</code> module.</p>
</li>
<li>
<p>Add a Maven dependency to <code>org.springframework.boot:spring-boot-autoconfigure</code> in the <code>library-autoconfigure</code> module.</p>
</li>
<li>
<p>Add a Maven dependency to <code>com.workshop:library-stdout</code> in the <code>library-autoconfigure</code> module, with <code>&lt;optional&gt;true&lt;/optional&gt;</code>.</p>
</li>
<li>
<p>Create a new file <code>src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports</code> in the <code>library-autoconfigure</code> module (<a href="https://docs.spring.io/spring-boot/reference/features/developing-auto-configuration.html#features.developing-auto-configuration.locating-auto-configuration-candidates">see the reference documentation</a>).</p>
</li>
<li>
<p>Add the fully qualified classname of the <code>GreetingAutoConfiguration</code> class to the <code>.imports</code> file.</p>
</li>
<li>
<p>Annotate the <code>GreetingAutoConfiguration</code> with <code>@AutoConfiguration</code>.</p>
</li>
<li>
<p>Create a new <code>GreetingService</code> bean in <code>GreetingAutoConfiguration</code> that returns a new instance of <code>StdOutGreetingService</code>.</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-java" data-lang="java">@Bean
GreetingService stdOutGreetingService() {
return new StdOutGreetingService();
}</code></pre>
</div>
</div>
</li>
<li>
<p>Add a Maven dependency to <code>com.workshop:library-autoconfigure</code> in the <code>app</code> module.</p>
</li>
<li>
<p>Add a Maven dependency to <code>com.workshop:library-stdout</code> in the <code>app</code> module.</p>
</li>
<li>
<p>Starting the application fails. That&#8217;s because there are now two beans of type <code>GreetingService</code>: <code>MyGreetingService</code> (annotated with <code>@Service</code>) from the <code>app</code> module and the <code>StdOutGreetingService</code> from the auto-configuration.</p>
</li>
<li>
<p>Use the <code>@ConditionalOnMissingBean</code> annotation on the <code>GreetingService</code> bean method in <code>GreetingAutoConfiguration</code> to only load the bean when no other bean of type <code>GreetingService</code> exists (<a href="https://docs.spring.io/spring-boot/reference/features/developing-auto-configuration.html#features.developing-auto-configuration.condition-annotations.bean-conditions">see the reference documentation</a>).</p>
</li>
<li>
<p>The application now starts and uses the <code>MyGreetingService</code>.</p>
</li>
<li>
<p>Now, remove the <code>MyGreetingService</code> class from the <code>app</code> module, or comment out/remove the <code>@Service</code> annotation on <code>MyGreetingService</code>.</p>
</li>
<li>
<p>The application now starts and uses the <code>StdOutGreetingService</code>.</p>
</li>
</ol>
</div>
</div>
</details>
</div>
<div class="sect2">
<h3 id="_conclusion">Conclusion</h3>
<div class="paragraph">
<p>Think for a moment, when is this useful and where does Spring Boot use this concept?</p>
</div>
<details>
<summary class="title">Answer</summary>
<div class="content">
<div class="paragraph">
<p>Spring Boot&#8217;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.</p>
</div>
<div class="paragraph">
<p>For example, adding <code>spring-boot-starter-web</code> sets up a whole webserver without manual configuration.</p>
</div>
</div>
</details>
</div>
<div class="sect2">
<h3 id="_solution">Solution</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">git checkout -f exercise-2</code></pre>
</div>
</div>
<div class="paragraph">
<p>🥳 Fantastic, let&#8217;s move on to the next exercise</p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_exercise_2_custom_spring_boot_starter">Exercise 2: Custom Spring Boot Starter</h2>
<div class="sectionbody">
<div class="paragraph">
<p>It&#8217;s a bit unfortunate that users of your auto-configuration need a dependency on <code>com.workshop:library-autoconfigure</code> and on <code>com.workshop:library-stdout</code>.</p>
</div>
<div class="paragraph">
<p>You will now package the <code>library-autoconfigure</code> and <code>library-stdout</code> modules into a reusable Spring Boot starter.</p>
</div>
<div class="sect2">
<h3 id="_learnings_2">Learnings</h3>
<div class="ulist">
<ul>
<li>
<p><strong>How do Spring Boot Starters work</strong></p>
</li>
</ul>
</div>
</div>
<div class="sect2">
<h3 id="_task_2">Task:</h3>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Add Maven dependencies to <code>library-autoconfigure</code> and <code>library-stdout</code> in the <code>library-spring-boot-starter</code> module.</p>
</li>
<li>
<p>Add a Maven dependency to <code>org.springframework.boot:spring-boot-starter</code> in the <code>library-spring-boot-starter</code> module.</p>
</li>
<li>
<p>Replace direct dependencies to <code>library-autoconfigure</code> and <code>library-stdout</code> in the <code>app</code> module with the new starter.</p>
</li>
<li>
<p>✅ Confirm that the app still works as expected and prints the greeting.</p>
</li>
</ol>
</div>
</div>
<div class="sect2">
<h3 id="_conclusion_2">Conclusion</h3>
<div class="paragraph">
<p>🤔 Why create a starter? When could that be useful?</p>
</div>
<details>
<summary class="title">Answer</summary>
<div class="content">
<div class="paragraph">
<p>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.</p>
</div>
</div>
</details>
</div>
<div class="sect2">
<h3 id="_solution_2">Solution</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">git checkout -f exercise-3</code></pre>
</div>
</div>
<div class="paragraph">
<p>🥳 Awesome, let&#8217;s move on to the next exercise</p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_exercise_3_custom_starter_without_default_greetingservice">Exercise 3: Custom Starter Without Default GreetingService</h2>
<div class="sectionbody">
<div class="paragraph">
<p>In this exercise, you will make the existing <code>LoggerGreetingService</code> available as an auto-configured bean — but only when the corresponding class is on the classpath. You will also adjust the fallback behavior of <code>StdOutGreetingService</code> so it is only used when the SLF4J-based implementation is not present.</p>
</div>
<div class="paragraph">
<p>This pattern mimics common practices in Spring Boot where auto-configured beans adapt to the available classpath.</p>
</div>
<div class="sect2">
<h3 id="_learnings_3">Learnings</h3>
<div class="ulist">
<ul>
<li>
<p><strong>How to auto-configure beans conditionally based on classpath presence</strong></p>
</li>
<li>
<p><strong>How to combine <code>@ConditionalOnClass</code> and <code>@ConditionalOnMissingClass</code></strong></p>
</li>
<li>
<p><strong>How to selectively expose features outside the default starter</strong></p>
</li>
</ul>
</div>
</div>
<div class="sect2">
<h3 id="_task_3">Task</h3>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>In the <code>library-autoconfigure</code> module add an optional dependency to <code>library-slf4j</code>.</p>
</li>
<li>
<p>In the <code>GreetingAutoConfiguration</code>, register an additional <code>GreetingService</code> bean that returns a <code>LoggerGreetingService</code> instance.</p>
</li>
<li>
<p>Annotate this method with:</p>
<div class="ulist">
<ul>
<li>
<p><code>@ConditionalOnClass(LoggerGreetingService.class)</code> — to create a bean only if <code>LoggerGreetingService</code> is on the classpath.</p>
</li>
<li>
<p><code>@ConditionalOnMissingBean</code> — to allow overriding by users.</p>
</li>
</ul>
</div>
</li>
<li>
<p>Update the existing <code>StdOutGreetingService</code> bean:</p>
<div class="ulist">
<ul>
<li>
<p>Add <code>@ConditionalOnMissingClass("com.workshop.magic.service.slf4j.LoggerGreetingService")</code> — to create the bean only if <code>LoggerGreetingService</code> is not on the classpath.</p>
</li>
</ul>
</div>
</li>
<li>
<p>Ensure the module <code>library-slf4j</code> is <strong>not</strong> included in the <code>library-spring-boot-starter</code> module.</p>
</li>
<li>
<p>In the <code>app</code> module, add a Maven dependency to <code>library-slf4j</code>.</p>
</li>
<li>
<p>✅ Start the app: You should see <em>LoggerGreetingService: Hola Spring I/O Barcelona</em>.</p>
</li>
<li>
<p>Remove the <code>library-slf4j</code> Maven dependency again:</p>
</li>
<li>
<p>✅ Start the app: You should now see <em>StdOutGreetingService: Hola Spring I/O Barcelona</em>.</p>
</li>
</ol>
</div>
</div>
<div class="sect2">
<h3 id="_detailed_steps_2">Detailed Steps</h3>
<details>
<summary class="title">Detailed Steps</summary>
<div class="content">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>In the <code>library-autoconfigure</code> module add a dependency to <code>library-slf4j</code> with:</p>
<div class="literalblock">
<div class="content">
<pre>&lt;dependency&gt;
&lt;groupId&gt;com.workshop&lt;/groupId&gt;
&lt;artifactId&gt;library-slf4j&lt;/artifactId&gt;
&lt;optional&gt;true&lt;/optional&gt;
&lt;/dependency&gt;</pre>
</div>
</div>
</li>
<li>
<p>In the <code>GreetingAutoConfiguration</code> class, add this bean method:</p>
<div class="literalblock">
<div class="content">
<pre>@Bean
@ConditionalOnMissingBean
@ConditionalOnClass(LoggerGreetingService.class)
GreetingService slf4jGreetingService() {
return new LoggerGreetingService();
}</pre>
</div>
</div>
</li>
<li>
<p>On the existing <code>stdOutGreetingService()</code> method, add:</p>
<div class="literalblock">
<div class="content">
<pre>@ConditionalOnMissingClass("com.workshop.magic.service.slf4j.LoggerGreetingService")</pre>
</div>
</div>
</li>
<li>
<p>In the <code>library-spring-boot-starter</code> module, ensure <code>library-slf4j</code> is <strong>not</strong> added as a dependency. Only <code>library-api</code> (not necessarily needed, as it comes transitively through <code>library-stdout</code>), <code>library-stdout</code>, and <code>library-autoconfigure</code> should be included.</p>
<div class="literalblock">
<div class="content">
<pre>&lt;dependency&gt;
&lt;groupId&gt;com.workshop&lt;/groupId&gt;
&lt;artifactId&gt;library-autoconfigure&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
&lt;groupId&gt;com.workshop&lt;/groupId&gt;
&lt;artifactId&gt;library-api&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
&lt;groupId&gt;com.workshop&lt;/groupId&gt;
&lt;artifactId&gt;library-stdout&lt;/artifactId&gt;
&lt;/dependency&gt;</pre>
</div>
</div>
</li>
<li>
<p>Make sure the <code>app</code> module declares a dependency to <code>library-slf4j</code> with:</p>
<div class="literalblock">
<div class="content">
<pre>&lt;dependency&gt;
&lt;groupId&gt;com.workshop&lt;/groupId&gt;
&lt;artifactId&gt;library-slf4j&lt;/artifactId&gt;
&lt;/dependency&gt;</pre>
</div>
</div>
</li>
<li>
<p>Run the application. <code>LoggerGreetingService</code> is now used, as it&#8217;s on the classpath. The <code>StdOutGreetingService</code> bean isn&#8217;t created, as <code>LoggerGreetingService</code> is on the classpath.</p>
</li>
<li>
<p>Remove the <code>library-slf4j</code> dependency from the <code>app</code> module and re-run it.</p>
</li>
<li>
<p><code>StdOutGreetingService</code> is now used, as <code>LoggerGreetingService</code> is not on the classpath.</p>
</li>
</ol>
</div>
</div>
</details>
</div>
<div class="sect2">
<h3 id="_conclusion_3">Conclusion</h3>
<div class="paragraph">
<p>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.</p>
</div>
<div class="paragraph">
<p>🤔 Can you think of an example where it is done this way?</p>
</div>
<details>
<summary class="title">Answer</summary>
<div class="content">
<div class="paragraph">
<p>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&#8217;t, Spring Boot silently skips it.
This reduces configuration overhead and provides smart defaults that adapt to the environment.</p>
</div>
<div class="paragraph">
<p>The <code>spring-boot-starter-data-jpa</code> starter doesn&#8217;t include a database driver, because the Spring Boot team doesn&#8217;t want to force a database choice on you.
You&#8217;ll need to add one for yourself, for example adding <code>org.postgresql:postgresql</code> auto-configures a <code>DataSource</code> which can talk to PostgreSQL.</p>
</div>
</div>
</details>
</div>
<div class="sect2">
<h3 id="_solution_3">Solution</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">git checkout -f exercise-4</code></pre>
</div>
</div>
<div class="paragraph">
<p>🥳 Superb, let&#8217;s move on to the next exercise</p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_exercise_4_conditions_evaluation_report">Exercise 4: Conditions Evaluation Report</h2>
<div class="sectionbody">
<div class="paragraph">
<p>In this exercise, you&#8217;ll learn how to leverage Spring Boot&#8217;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.</p>
</div>
<div class="sect2">
<h3 id="_learnings_4">Learnings</h3>
<div class="ulist">
<ul>
<li>
<p><strong>How to enable and interpret the Conditions Evaluation Report</strong></p>
</li>
<li>
<p><strong>How to identify why certain beans are or aren&#8217;t loaded</strong></p>
</li>
</ul>
</div>
</div>
<div class="sect2">
<h3 id="_task_4">Task</h3>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Enable debug mode in your application to view the Conditions Evaluation Report:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-properties" data-lang="properties">debug=true</code></pre>
</div>
</div>
<div class="paragraph">
<p>This can be added to your <code>application.properties</code> file or passed as a command-line argument using <code>--debug</code>.</p>
</div>
</li>
<li>
<p>Start your application. Upon startup, you should see a detailed report in the console that looks like:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code>===========================
CONDITIONS EVALUATION REPORT
===========================
Positive matches:
-----------------
...
Negative matches:
-----------------
...</code></pre>
</div>
</div>
<div class="paragraph">
<p>This report lists all auto-configuration classes with their conditions, and they were applied or not.</p>
</div>
</li>
<li>
<p>Review the report in regard to <code>GreetingAutoConfiguration</code> and understand which configurations were applied and which were not, along with the reasons.</p>
</li>
<li>
<p>Use this information to troubleshoot any unexpected behavior or to verify that your custom configurations are being considered appropriately.</p>
</li>
</ol>
</div>
</div>
<div class="sect2">
<h3 id="_conclusion_4">Conclusion</h3>
<div class="paragraph">
<p>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.</p>
</div>
</div>
<div class="sect2">
<h3 id="_solution_4">Solution</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">git checkout -f exercise-5</code></pre>
</div>
</div>
<div class="paragraph">
<p>🥳 Great job! Let&#8217;s proceed to the next exercise.</p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_exercise_5_property_configuration_and_conditionalonproperty">Exercise 5: Property Configuration and @ConditionalOnProperty</h2>
<div class="sectionbody">
<div class="sect2">
<h3 id="_learnings_5">Learnings</h3>
<div class="ulist">
<ul>
<li>
<p><strong>How to parametrize auto-configured beans</strong> (task A)</p>
</li>
<li>
<p><strong>How to create auto-configured beans depending on properties</strong> (task B)</p>
</li>
</ul>
</div>
</div>
<div class="sect2">
<h3 id="_task_a">Task A</h3>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>There&#8217;s a <code>GreetingProperties</code> class in the <code>library-autoconfigure</code> module which should be filled with values from the <code>application.properties</code>.</p>
</li>
<li>
<p>Annotate the <code>GreetingAutoConfiguration</code> with <code>@EnableConfigurationProperties(GreetingProperties.class)</code> to enable loading the values from the <code>application.properties</code>.</p>
</li>
<li>
<p>Annotate <code>GreetingProperties</code> with <code>@ConfigurationProperties</code> and bind it to the <code>workshop.greeting</code> prefix.</p>
</li>
<li>
<p>The <code>StdOutGreetingService</code> and the <code>LoggerGreetingService</code> have constructors which allows you to customize the greeting. By default, it&#8217;s "Hola", but this should now be configurable via the <code>application.properties</code> by setting <code>workshop.greeting.text</code>.</p>
</li>
<li>
<p>Change the bean methods for <code>StdOutGreetingService</code> and <code>LoggerGreetingService</code> to inject <code>GreetingProperties</code> and configure the greeting prefix.</p>
</li>
<li>
<p>Add a property <code>workshop.greeting.text=Gude</code> to <code>application.properties</code>.</p>
</li>
<li>
<p>✅ Start the application. It should now print <em>LoggerGreetingService: Gude Spring I/0 Barcelona</em> or <em>StdOutGreetingService: Gude Spring I/0 Barcelona</em>.</p>
</li>
</ol>
</div>
</div>
<div class="sect2">
<h3 id="_detailed_steps_a">Detailed Steps A</h3>
<details>
<summary class="title">Detailed Steps</summary>
<div class="content">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>In <code>library-autoconfigure</code>, annotate <code>GreetingAutoConfiguration</code> with:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-java" data-lang="java">@EnableConfigurationProperties(GreetingProperties.class)</code></pre>
</div>
</div>
</li>
<li>
<p>In the same module open the <code>GreetingProperties</code> class and annotate it with:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-java" data-lang="java">@ConfigurationProperties(prefix = "workshop.greeting")</code></pre>
</div>
</div>
</li>
<li>
<p>In <code>GreetingAutoConfiguration</code>, inject <code>GreetingProperties</code> into both <code>GreetingService</code> bean methods:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-java" data-lang="java">GreetingService stdOutGreetingService(GreetingProperties properties)
GreetingService slf4jGreetingService(GreetingProperties properties)</code></pre>
</div>
</div>
</li>
<li>
<p>Replace the constructor calls with:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-java" data-lang="java">new StdOutGreetingService(properties.getText())
new LoggerGreetingService(properties.getText())</code></pre>
</div>
</div>
</li>
<li>
<p>In <code>application.properties</code> set the following:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-properties" data-lang="properties">workshop.greeting.text=Gude</code></pre>
</div>
</div>
</li>
<li>
<p>Run the application</p>
</li>
<li>
<p>✅ You should see <em>LoggerGreetingService: Gude Spring I/0 Barcelona</em> or <em>StdOutGreetingService: Gude Spring I/0 Barcelona</em> now.</p>
</li>
</ol>
</div>
</div>
</details>
</div>
<div class="sect2">
<h3 id="_task_b">Task B</h3>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Now, we want a property called <code>workshop.greeting.type</code> which controls the type of <code>GreetingService</code> that will be used:</p>
<div class="ulist">
<ul>
<li>
<p><code>workshop.greeting.type=logger</code> should create a <code>LoggerGreetingService</code> bean.</p>
</li>
<li>
<p><code>workshop.greeting.type=stdout</code> should create a <code>StdOutGreetingService</code> bean.</p>
</li>
</ul>
</div>
</li>
<li>
<p>You can use <code>@ConditionalOnProperty</code> for that. Annotate both bean methods with <code>@ConditionalOnProperty</code> and set the annotation attributes accordingly.</p>
</li>
<li>
<p>Remove the <code>@ConditionalOnMissingClass</code> from the <code>StdOutGreetingService</code> bean method.</p>
</li>
<li>
<p>Add <code>workshop.greeting.type=stdout</code> to your <code>application.properties</code></p>
</li>
<li>
<p>✅ Start the application. It should now print <em>StdOutGreetingService: Gude Spring I/0 Barcelona</em>.</p>
</li>
<li>
<p>Change <code>workshop.greeting.type</code> to <code>logger</code>.</p>
</li>
<li>
<p>✅ Start the application. It should now print <em>LoggerGreetingService: Gude Spring I/0 Barcelona</em>.</p>
</li>
<li>
<p>Remove the <code>workshop.greeting.type</code> from <code>application.properties</code></p>
</li>
<li>
<p>🤔 Start the application. It now fails. Why is that?</p>
</li>
<li>
<p>Change the annotation attributes from <code>@ConditionalOnProperty</code> on the <code>StdOutGreetingService</code> to also match if the property is missing.</p>
</li>
<li>
<p>✅ Start the application. It should now print <em>StdOutGreetingService: Gude Spring I/0 Barcelona</em>.</p>
</li>
</ol>
</div>
</div>
<div class="sect2">
<h3 id="_detailed_steps_3">Detailed Steps</h3>
<details>
<summary class="title">Detailed Steps</summary>
<div class="content">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Annotate the <code>StdOutGreetingService</code> bean method with:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-java" data-lang="java">@ConditionalOnProperty(name = "workshop.greeting.type", havingValue = "stdout")</code></pre>
</div>
</div>
</li>
<li>
<p>Remove the <code>@ConditionalOnMissingClass</code> annotation from the <code>StdOutGreetingService</code> bean method</p>
</li>
<li>
<p>Annotate the <code>LoggerGreetingService</code> bean method with:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-java" data-lang="java">@ConditionalOnProperty(name = "workshop.greeting.type", havingValue = "logger")</code></pre>
</div>
</div>
</li>
<li>
<p>In <code>application.properties</code> set the following:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-properties" data-lang="properties">workshop.greeting.type=stdout</code></pre>
</div>
</div>
</li>
<li>
<p>Run the application.</p>
</li>
<li>
<p>✅ You should see: <em>StdOutGreetingService: Gude Spring I/0 Barcelona</em></p>
</li>
<li>
<p>In <code>application.properties</code> set the following:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-properties" data-lang="properties">workshop.greeting.type=logger</code></pre>
</div>
</div>
</li>
<li>
<p>Run the application.</p>
</li>
<li>
<p>✅ You should see: <em>LoggerGreetingService: Gude Spring I/0 Barcelona</em></p>
<div class="admonitionblock tip">
<table>
<tr>
<td class="icon">
<div class="title">Tip</div>
</td>
<td class="content">
The <code>LoggerGreetingService</code> bean will only be created if <code>library-slf4j</code> is on the classpath. If not, even <code>type=logger</code> will not work.
</td>
</tr>
</table>
</div>
</li>
<li>
<p>Remove the <code>workshop.greeting.type</code> line and restart the app.</p>
</li>
<li>
<p>Startup of the app fails, because there&#8217;s no <code>GreetingService</code> available. You can use the Conditions Evaluation Report to find out why.</p>
</li>
<li>
<p>Change the annotation of the <code>StdOutGreetingService</code> bean method in <code>GreetingAutoConfiguration</code> to look like this:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-java" data-lang="java">@ConditionalOnProperty(name = "workshop.greeting.type", havingValue = "stdout", matchIfMissing = true)</code></pre>
</div>
</div>
</li>
<li>
<p>Run the application.</p>
</li>
<li>
<p>✅ You should see: <em>StdOutGreetingService: Gude Spring I/0 Barcelona</em></p>
</li>
</ol>
</div>
</div>
</details>
</div>
<div class="sect2">
<h3 id="_conclusion_5">Conclusion</h3>
<div class="paragraph">
<p>In this exercise, you learned how to read properties from <code>application.properties</code> 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.</p>
</div>
<div class="paragraph">
<p>Using <code>@ConditionalOnProperty</code>, you can activate specific beans based on the application&#8217;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.</p>
</div>
<div class="paragraph">
<p>🤔 Why is this useful in real-world Spring Boot applications?</p>
</div>
<details>
<summary class="title">Answer</summary>
<div class="content">
<div class="paragraph">
<p>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.</p>
</div>
<div class="paragraph">
<p>An example in Spring Boot would be the <code>management.server.port</code> 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.</p>
</div>
</div>
</details>
</div>
<div class="sect2">
<h3 id="_solution_5">Solution</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">git checkout -f exercise-6</code></pre>
</div>
</div>
<div class="paragraph">
<p>🥳 Superb, let&#8217;s move on to the next exercise</p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_exercise_6_using_custom_conditions">Exercise 6: Using Custom Conditions</h2>
<div class="sectionbody">
<div class="paragraph">
<p>It is also possible to create custom conditions like the existing <code>@On&#8230;&#8203;</code> conditions from Spring Boot.</p>
</div>
<div class="paragraph">
<p>Let&#8217;s create a custom condition that checks the system property <code>my.custom.condition</code> - just because it&#8217;s simple.
But imagine you have a more sophisticated custom check here, e.g., infrastructure checks like the <a href="https://docs.spring.io/spring-boot/reference/actuator/endpoints.html#actuator.endpoints.kubernetes-probes">Kubernetes probes</a>.
Or you could write a condition which triggers only on 1st of April.</p>
</div>
<div class="paragraph">
<p>Oh, the possibilities!</p>
</div>
<div class="sect2">
<h3 id="_learnings_6">Learnings</h3>
<div class="ulist">
<ul>
<li>
<p><strong>How to create your own conditions</strong></p>
</li>
<li>
<p><strong>How to use that custom condition</strong></p>
</li>
</ul>
</div>
</div>
<div class="sect2">
<h3 id="_task_5">Task</h3>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Create a new annotation <code>@MyCustomCondition</code> in the <code>library-autoconfigure</code> module. It must have a <code>@Target</code> of <code>TYPE</code> and <code>METHOD</code> and a <code>@Retention</code> of <code>RUNTIME</code> (you can also copy that from Spring Boot&#8217;s <code>@ConditionalOnProperty</code>).</p>
</li>
<li>
<p>The newly created annotation must be annotated with <code>@Conditional({OnCustomCondition.class})</code>.</p>
</li>
<li>
<p>A new class, <code>OnCustomCondition</code> must be created. It should extend Spring Boot&#8217;s <code>SpringBootCondition</code>.</p>
</li>
<li>
<p>The <code>getMatchOutcome</code> method must be overriden and should check the <code>my.custom.condition</code> system property. Use <code>ConditionOutcome.match</code> and <code>ConditionOutcome.noMatch</code> to signal if the condition matches or not.</p>
</li>
<li>
<p>Modify the <code>GreetingAutoConfiguration</code> to use the new <code>@MyCustomCondition</code>. A bean of class <code>BeepGreetingService</code> (located in the <code>library-slf4j</code> module) should be created if <code>@MyCustomCondition</code> matches.</p>
</li>
<li>
<p>Test that the application works by setting the system property <code>my.custom.condition</code> and verify that the <code>BeepGreetingService</code> bean is used.</p>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
You&#8217;ll have to set <code>workshop.greeting.type</code> to something else than <code>logger</code> or <code>stdout</code>, because otherwise the <code>LoggerGreetingService</code> or <code>StdOutGreetingService</code> is also created.
</td>
</tr>
</table>
</div>
</li>
<li>
<p>🤔 Also take a look at the conditions evaluation report. Do you see your condition in there?</p>
</li>
</ol>
</div>
</div>
<div class="sect2">
<h3 id="_detailed_steps_4">Detailed Steps</h3>
<details>
<summary class="title">Detailed Steps</summary>
<div class="content">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Create a new annotation in the <code>library-autoconfigure</code> module, called <code>MyCustomCondition</code></p>
</li>
<li>
<p>Annotate the annotation with <code>@Target({ElementType.TYPE, ElementType.METHOD})</code> and with <code>@Retention(RetentionPolicy.RUNTIME)</code></p>
</li>
<li>
<p>Annotate the annotation with <code>@Conditional({OnCustomCondition.class})</code></p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-java" data-lang="java">@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional({OnCustomCondition.class})
@interface MyCustomCondition {
}</code></pre>
</div>
</div>
</li>
<li>
<p>Create a class called <code>OnCustomCondition</code> and let it extend <code>SpringBootCondition</code></p>
</li>
<li>
<p>Implement the <code>getMatchOutcome</code> method</p>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>Use <code>System.getProperty("my.custom.condition")</code> to read the <code>my.custom.condition</code> system property</p>
</li>
<li>
<p>If the value of that property is <code>true</code>, return <code>ConditionOutcome.match</code> to signal that the condition matches</p>
</li>
<li>
<p>Otherwise, return <code>ConditionOutcome.noMatch</code> to signal that the condition didn&#8217;t match</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-java" data-lang="java">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));
}
}</code></pre>
</div>
</div>
</li>
</ol>
</div>
</li>
<li>
<p>Add a new <code>@Bean</code> method to the <code>GreetingAutoConfiguration</code> class, call it <code>beepGreetingService</code>, its return type is <code>GreetingService</code></p>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>Annotate this new method with <code>@MyCustomCondition</code>, <code>@ConditionalOnMissingBean</code> and <code>@ConditionalOnClass(BeepGreetingService.class)</code></p>
</li>
<li>
<p>Return a new instance of <code>BeepGreetingService</code> from that method</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-java" data-lang="java">@Bean
@ConditionalOnMissingBean
@MyCustomCondition
@ConditionalOnClass(BeepGreetingService.class)
GreetingService beepGreetingService(GreetingProperties properties) {
return new BeepGreetingService(properties.getPrefix());
}</code></pre>
</div>
</div>
</li>
</ol>
</div>
</li>
<li>
<p>To test the custom condition, you can add <code>System.setProperty("my.custom.condition", "true");</code> as first line in the <code>main</code> method, or you can set the system properties when starting with your IDE</p>
</li>
<li>
<p>You&#8217;ll also need to add <code>workshop.greeting.type=none</code> to your <code>application.properties</code>, because otherwise the <code>LoggerGreetingService</code> or the <code>StdOutGreetingService</code> would be created</p>
</li>
</ol>
</div>
</div>
</details>
</div>
<div class="sect2">
<h3 id="_conclusion_6">Conclusion</h3>
<div class="paragraph">
<p>Can you image why it is useful to create custom conditions?</p>
</div>
<details>
<summary class="title">Answer</summary>
<div class="content">
<div class="paragraph">
<p>Creating your own conditions is useful if the conditions from Spring Framework and Spring Boot don&#8217;t fit your needs.
Custom conditions show the power of an extensible framework like the Spring Framework.
There&#8217;s no "magic" behind the built-in Spring Boot conditions — they are built on the same foundations like your custom condition is.</p>
</div>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
You can take a look at the <code>@Profile</code> annotation from Spring Framework: The logic is implemented in <code>ProfileCondition</code>, and it essentially returns <code>true</code> if the profile is activated and <code>false</code> if not.
</td>
</tr>
</table>
</div>
</div>
</details>
</div>
<div class="sect2">
<h3 id="_solution_6">Solution</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">git checkout -f exercise-7</code></pre>
</div>
</div>
<div class="paragraph">
<p>🥳 Phenomenal, let&#8217;s move on to the next exercise</p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="testing">Exercise 7: Testing The Auto-Configuration</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Create unit tests to ensure that the <code>GreetingAutoConfiguration</code> works as expected.</p>
</div>
<div class="sect2">
<h3 id="_task_6">Task</h3>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>A Maven dependency on <code>org.springframework.boot:spring-boot-starter-test</code> with scope <code>test</code> has to be added in the <code>library-autoconfigure</code> module.</p>
</li>
<li>
<p>A test class for the <code>GreetingAutoConfiguration</code> class must be created.</p>
</li>
<li>
<p>Spring Boot&#8217;s <code>ApplicationContextRunner</code> should be used to test the auto-configuration (<a href="https://docs.spring.io/spring-boot/3.3/reference/features/developing-auto-configuration.html#features.developing-auto-configuration.testing">see the reference documentation</a>).</p>
</li>
<li>
<p>AssertJ assertions should be used to verify that the context contains a <code>StdOutGreetingService</code> bean if no property is set.</p>
</li>
<li>
<p>Implement tests for these use cases:</p>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>The context contains a <code>StdOutGreetingService</code> bean if the property <code>workshop.greeting.type</code> is set to <code>stdout</code>.</p>
</li>
<li>
<p>The context contains a <code>LoggerGreetingService</code> bean if the property <code>workshop.greeting.type</code> is set to <code>logger</code>.</p>
</li>
<li>
<p>The context contains <code>BeepGreetingService</code> bean if the system property <code>my.custom.condition</code> is set to <code>true</code>.</p>
</li>
<li>
<p>That user-defined beans take precedence over the auto-configured <code>GreetingService</code> beans — essentially testing that <code>@ConditionalOnMissingBean</code> works.</p>
</li>
</ol>
</div>
</li>
</ol>
</div>
</div>
<div class="sect2">
<h3 id="_detailed_steps_5">Detailed Steps</h3>
<details>
<summary class="title">Detailed Steps</summary>
<div class="content">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Add a Maven dependency to <code>org.springframework.boot:spring-boot-starter-test</code> with scope <code>test</code> to the <code>library-autoconfigure</code> module.</p>
</li>
<li>
<p>Create a class named <code>GreetingAutoConfigurationTest</code> in <code>library-autoconfigure/src/test/java</code> in the package <code>com.workshop.magic.config</code>.</p>
</li>
<li>
<p>Create a field of type <code>ApplicationContextRunner</code>, and use the fluent API to call <code>withConfiguration</code> with <code>AutoConfigurations.of(GreetingAutoConfiguration.class)</code>.</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-java" data-lang="java">private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GreetingAutoConfiguration.class));</code></pre>
</div>
</div>
</li>
<li>
<p>Write a test case named <code>shouldProvideStdOutGreetingServiceByDefault</code> which uses the <code>run</code> method of the <code>ApplicationContextRunner</code> field.</p>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>Inside the lambda block of the <code>run</code> method, use AssertJ&#8217;s <code>assertThat</code> on the context to call <code>hasSingleBean</code> with an <code>StdOutGreetingService.class</code> argument.</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-java" data-lang="java">@Test
void shouldProvideStdOutGreetingServiceByDefault() {
this.contextRunner.run(context -&gt; {
assertThat(context).hasSingleBean(StdOutGreetingService.class);
});
}</code></pre>
</div>
</div>
</li>
</ol>
</div>
</li>
<li>
<p>Write a test case named <code>shouldProvideStdOutGreetingServiceWhenPropertyIsSet</code> which uses the <code>withPropertyValues</code> of the <code>ApplicationContextRunner</code> field to set the property <code>workshop.greeting.type</code> to <code>stdout</code>.</p>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>Inside the lambda block of the <code>run</code> method, use AssertJ&#8217;s <code>assertThat</code> on the context to call <code>hasSingleBean</code> with an <code>StdOutGreetingService.class</code> argument.</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-java" data-lang="java">@Test
void shouldProvideStdOutGreetingServiceWhenPropertyIsSet() {
this.contextRunner
.withPropertyValues("workshop.greeting.type=stdout")
.run(context -&gt; {
assertThat(context).hasSingleBean(StdOutGreetingService.class);
});
}</code></pre>
</div>
</div>
</li>
</ol>
</div>
</li>
<li>
<p>Write a test case named <code>shouldProvideLoggerGreetingServiceWhenPropertyIsSet</code> which uses the <code>withPropertyValues</code> of the <code>ApplicationContextRunner</code> field to set the property <code>workshop.greeting.type</code> to <code>logger</code>.</p>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>Inside the lambda block of the <code>run</code> method, use AssertJ&#8217;s <code>assertThat</code> on the context to call <code>hasSingleBean</code> with an <code>LoggerGreetingService.class</code> argument.</p>
</li>
</ol>
</div>
</li>
<li>
<p>Write a test case named <code>shouldProvideBeepGreetingServiceIfSystemPropertyIsSet</code> which uses <code>withPropertyValues</code> of the <code>ApplicationContextRunner</code> field to set the property <code>workshop.greeting.type</code> to <code>none</code>.</p>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>Additionally, it uses the <code>withSystemProperties</code> method to set <code>my.custom.condition</code> to <code>true</code>.</p>
</li>
<li>
<p>Inside the lambda block of the <code>run</code> method, use AssertJ&#8217;s <code>assertThat</code> on the context to call <code>hasSingleBean</code> with an <code>BeepGreetingService.class</code> argument.</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-java" data-lang="java">@Test
void shouldProvideBeepGreetingServiceIfSystemPropertyIsSet() {
this.contextRunner
.withPropertyValues("workshop.greeting.type=none")
.withSystemProperties("my.custom.condition=true")
.run(context -&gt; {
assertThat(context).hasSingleBean(BeepGreetingService.class);
});
}</code></pre>
</div>
</div>
</li>
</ol>
</div>
</li>
<li>
<p>Write a test case named <code>shouldBackOffIfGreetingServiceIsDefinedByUser</code> which uses the <code>withBean</code> method of the <code>ApplicationContextRunner</code> field to define a bean of type <code>GreetingService</code>. Create an inner static class or an anonymous class for the "user provided" <code>GreetingService</code>.</p>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>Inside the lambda block of the <code>run</code> method, use AssertJ&#8217;s <code>assertThat</code> on the context to call <code>hasSingleBean</code> with an <code>GreetingService.class</code> argument.</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-java" data-lang="java">@Test
void shouldBackOffIfGreetingServiceIsDefinedByUser() {
this.contextRunner
.withBean(GreetingService.class, UserGreetingService::new)
.run(context -&gt; {
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);
}
}</code></pre>
</div>
</div>
</li>
</ol>
</div>
</li>
</ol>
</div>
</div>
</details>
</div>
<div class="sect2">
<h3 id="_conclusion_7">Conclusion</h3>
<div class="paragraph">
<p>Whats the benefit of writing a unit test for an auto-configuration?</p>
</div>
<details>
<summary class="title">Answer</summary>
<div class="content">
<div class="paragraph">
<p>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&#8217;s <code>ApplicationContextRunner</code> makes this easy.</p>
</div>
</div>
</details>
</div>
<div class="sect2">
<h3 id="_solution_7">Solution</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">git checkout -f exercise-8</code></pre>
</div>
</div>
<div class="paragraph">
<p>🥳 Brilliant, let&#8217;s move on to the next exercise</p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="properties-metadata">Exercise 8: Adding properties metadata</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Use the Spring Boot configuration processor to generate metadata for your configuration properties.</p>
</div>
<div class="sect2">
<h3 id="_task_7">Task</h3>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Add the <code>org.springframework.boot:spring-boot-configuration-processor</code> to the <code>library-autoconfigure</code> module.</p>
</li>
<li>
<p>Run a build and inspect the <code>components/library-autoconfigure/target/classes/META-INF/spring-configuration-metadata.json</code> file.</p>
</li>
<li>
<p>🤔 Think about why that file could be useful.</p>
</li>
<li>
<p>The <code>text</code> property in <code>GreetingProperties</code> should be renamed to <code>prefix</code>, while deprecating the <code>text</code> property. Use <code>@Deprecated</code> and <code>@DeprecatedConfigurationProperty</code> annotations to achieve this.</p>
</li>
<li>
<p>Run a build and inspect the file <code>spring-configuration-metadata.json</code> again.</p>
</li>
<li>
<p>🤔 What has changed? Why could that be useful?</p>
</li>
<li>
<p>🤔 Open the <code>application.properties</code> in your IDE. Do you notice something?</p>
</li>
<li>
<p>Add <code>org.springframework.boot:spring-boot-properties-migrator</code> to the <code>app</code> module.</p>
</li>
<li>
<p>Start the app and observe the console output.</p>
</li>
</ol>
</div>
</div>
<div class="sect2">
<h3 id="_detailed_steps_6">Detailed Steps</h3>
<details>
<summary class="title">Detailed Steps</summary>
<div class="content">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Add <code>org.springframework.boot:spring-boot-configuration-processor</code> to <code>components/library-autoconfigure/pom.xml</code>, with <code>optional = true</code>.</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-xml" data-lang="xml">&lt;dependency&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-configuration-processor&lt;/artifactId&gt;
&lt;optional&gt;true&lt;/optional&gt;
&lt;/dependency&gt;</code></pre>
</div>
</div>
</li>
<li>
<p>Newer Java versions require an explicit configuration for annotation processors. Configure the <code>maven-compiler-plugin</code> to include <code>org.springframework.boot:spring-boot-configuration-processor</code> as an annotation processor.
You can take a look at <a href="https://start.spring.io/#!type=maven-project&amp;language=java&amp;platformVersion=3.4.5&amp;packaging=jar&amp;jvmVersion=24&amp;groupId=com.example&amp;artifactId=demo&amp;name=demo&amp;description=Demo%20project%20for%20Spring%20Boot&amp;packageName=com.example.demo&amp;dependencies=configuration-processor">the POM file generated by start.spring.io</a> for an example.</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-xml" data-lang="xml">&lt;build&gt;
&lt;plugins&gt;
&lt;plugin&gt;
&lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
&lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;
&lt;configuration&gt;
&lt;annotationProcessorPaths&gt;
&lt;path&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-configuration-processor&lt;/artifactId&gt;
&lt;/path&gt;
&lt;/annotationProcessorPaths&gt;
&lt;/configuration&gt;
&lt;/plugin&gt;
&lt;/plugins&gt;
&lt;/build&gt;</code></pre>
</div>
</div>
</li>
<li>
<p>Run <code>./mvnw compile</code> and inspect <code>components/library-autoconfigure/target/classes/META-INF/spring-configuration-metadata.json</code>.</p>
</li>
<li>
<p>Replace <code>private String text</code> in the <code>GreetingProperties</code> class with <code>private String prefix</code>.</p>
</li>
<li>
<p>Annotate the <code>public String getText()</code> method with <code>@Deprecated</code> and with <code>@DeprecatedConfigurationProperty(replacement = "workshop.greeting.prefix")</code>.</p>
</li>
<li>
<p>Return <code>this.prefix</code> from the <code>getText()</code> method.</p>
</li>
<li>
<p>Assign <code>this.prefix</code> in the <code>setText()</code> method.</p>
</li>
<li>
<p>Add a new getter and setter method for <code>private String prefix</code>.</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-java" data-lang="java">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;
}</code></pre>
</div>
</div>
</li>
<li>
<p>Run <code>./mvnw compile</code> and inspect <code>components/library-autoconfigure/target/classes/META-INF/spring-configuration-metadata.json</code>.</p>
</li>
<li>
<p>Add <code>org.springframework.boot:spring-boot-properties-migrator</code> with <code>scope = runtime</code> to <code>app/app/pom.xml</code>.</p>
</li>
<li>
<p>Run the application</p>
</li>
</ol>
</div>
</div>
</details>
</div>
<div class="sect2">
<h3 id="_conclusion_8">Conclusion</h3>
<div class="paragraph">
<p>Why is providing that netadata file beneficial? Who would use it?</p>
</div>
<details>
<summary class="title">Answer</summary>
<div class="content">
<div class="paragraph">
<p>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 <code>spring-boot-properties-migrator</code> also uses this file to display deprecations on startup and to provide the automatic mapping from the old property to the new one.</p>
</div>
</div>
</details>
</div>
<div class="sect2">
<h3 id="_solution_8">Solution</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">git checkout -f main</code></pre>
</div>
</div>
<div class="paragraph">
<p>🥳 Congrats, you finished all exercises! We hope you enjoyed the learnings.</p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="feedback">Feedback</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Wed 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! ❤️ </p>
</div>
<div class="sect2">
<h3 id="_connect_with_us">Connect With Us!</h3>
<div class="ulist">
<ul>
<li>
<p><strong>Moritz Halbritter</strong></p>
<div class="ulist">
<ul>
<li>
<p><a href="https://mhalbritter.github.io/">Website</a></p>
</li>
</ul>
</div>
</li>
<li>
<p><strong>Fabian Krüger</strong></p>
<div class="ulist">
<ul>
<li>
<p><a href="https://www.linkedin.com/in/fabiankrueger/">on Linkedin</a></p>
</li>
<li>
<p><a href="https://x.com/hiiamfabian">on X</a></p>
</li>
</ul>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_resources">Resources</h2>
<div class="sectionbody">
<div class="sect2">
<h3 id="_related_talk">Related Talk</h3>
<div class="paragraph">
<p><a href="https://2025.springio.net/sessions/demystifying-spring-boot-magic/">Demystifying Spring Boot Magic by Patrick Baumgartner</a></p>
</div>
</div>
<div class="sect2">
<h3 id="_literature">Literature</h3>
<div class="ulist">
<ul>
<li>
<p><a href="https://docs.spring.io/spring-boot/reference/using/auto-configuration.html">Auto-configuration</a></p>
</li>
<li>
<p><a href="https://docs.spring.io/spring-boot/reference/features/developing-auto-configuration.html#page-title">Creating Your Own Auto-configuration</a></p>
</li>
<li>
<p><a href="https://docs.spring.io/spring-boot/3.3/reference/features/developing-auto-configuration.html#features.developing-auto-configuration.testing">Testing your Auto-configuration</a></p>
</li>
<li>
<p><a href="https://docs.spring.io/spring-boot/specification/configuration-metadata/annotation-processor.html">Generating Your Own Metadata by Using the Annotation Processor</a></p>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div id="footer">
<div id="footer-text">
Last updated 2025-05-20 14:57:24 +0200
</div>
</div>
</body>
</html>