1. Introduction
This material makes the transition from a creating and executing a simple Java main application to a Spring Boot application.
1.2. Objectives
At the conclusion of this lecture and related exercises, the student will be able to:
-
extend the standard Maven
jar
module packaging type to include core Spring Boot dependencies -
construct a basic Spring Boot application
-
build and execute an executable Spring Boot JAR
-
define a simple Spring component and inject that into the Spring Boot application
2. Spring Boot Maven Dependencies
Spring Boot provides a spring-boot-starter-parent
(gradle source,
pom.xml) pom that can be used as a parent pom for our Spring Boot modules.
[1]
This defines version information for dependencies and plugins for building Spring Boot artifacts — along with an opinionated view of how the module should be built.
spring-boot-starter-parent
inherits from a spring-boot-dependencies
(gradle source,
pom.xml)
pom that provides a definition of artifact versions without an opinionated view of how the module is built.
This pom can be imported by modules that already inherit from a local Maven parent — which would be common.
This is the demonstrated approach we will take here. We will also include demonstration of how the build constructs are commonly spread across parent and local poms.
Spring Boot has converted over to gradle and posts a pom version of the gradle artifact to Maven central repository as a part of their build process. |
3. Parent POM
We are likely to create multiple Spring Boot modules and would be well-advised to begin by creating a local parent pom construct to house the common passive definitions. By passive definitions (versus active declarations), I mean definitions for the child poms to use if needed versus mandated declarations for each child module. For example, a parent pom may define the JDBC driver to use when needed but not all child modules will need a JDBC driver nor a database for that matter. In that case, we do not want the parent pom to actively declare a dependency. We just want the parent to passively define the dependency that the child can optionally choose to actively declare. This construct promotes consistency among all the modules.
"Root"/parent poms should define dependencies and plugins for consistent re-use among child poms and use dependencyManagement and pluginManagement elements to do so. |
"Child"/concrete/leaf poms declare dependencies and plugins to be used when building that module and try to keep dependencies to a minimum. |
"Prototype" poms are a blend of root and child pom concepts. They are a nearly-concrete, parent pom that can be extended by child poms but actively declare a select set of dependencies and plugins to allow child poms to be as terse as possible. |
3.1. Define Version for Spring Boot artifacts
Define the version for Spring Boot artifacts to use. I am using a technique below of defining the value in a property so that it is easy to locate and change as well as re-use elsewhere if necessary.
# Place this declaration in an inherited parent pom
<properties>
<springboot.version>3.3.2</springboot.version> (1)
</properties>
1 | default value has been declared in imported ejava-build-bom |
Property values can be overruled at build time by supplying a system property on the command line "-D(name)=(value)" |
3.2. Import springboot-dependencies-plugin
Import springboot-dependencies-plugin
. This will define dependencyManagement
for us for many artifacts that are relevant to our Spring Boot development.
# Place this declaration in an inherited parent pom
<dependencyManagement> (1)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${springboot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
1 | import is within examples-root for class examples, which is a grandparent of this example |
4. Local Child/Leaf Module POM
The local child module pom.xml is where the module is physically built. Although Maven modules can have multiple levels of inheritance — where each level is a child of their parent — the child module I am referring to here is the leaf module where the artifacts are meant to be really built. Everything defined above it is primarily used as a common definition (through dependencyManagement and pluginManagement) to simplify the child pom.xml and to promote consistency among sibling modules. It is the job of the leaf module to activate these definitions that are appropriate for the type of module being built.
4.2. Declare dependency on artifacts used
Realize the parent definition of the spring-boot-starter
dependency by declaring
it within the child dependencies section.
For where we are in this introduction, only the above dependency will be necessary.
The imported spring-boot-dependencies
will take care of declaring the version#
# Place this declaration in the child/leaf pom building the JAR archive
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<!--version --> (1)
</dependency>
</dependencies>
1 | parent has defined (using import in this case) the version for all children to consistently use |
The figure below shows the parent poms being the source of the passive dependency definitions and the child being the source of the active dependency declarations.
-
the parent is responsible for defining the version# for dependencies used
-
the child is responsible for declaring what dependencies are needed and adopts the parent version definition
An upgrade to a future dependency version should not require a change of a child module declaration if this pattern is followed.
5. Simple Spring Boot Application Java Class
With the necessary dependencies added to our build classpath, we now have enough to begin defining a simple Spring Boot Application.
package info.ejava.springboot.examples.app.build.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication (3)
public class SpringBootApp {
public static void main(String... args) { (1)
System.out.println("Running SpringApplication");
SpringApplication.run(SpringBootApp.class, args); (2)
System.out.println("Done SpringApplication");
}
}
1 | Define a class with a static main() method |
2 | Initiate Spring application bootstrap by invoking SpringApplication.run() and passing a) application class and b) args passed into main() |
3 | Annotate the class with @SpringBootApplication |
Startup can, of course be customized (e.g., change the printed banner, registering event listeners) |
5.1. Module Source Tree
The source tree will look similar to our previous Java main example.
|-- pom.xml
`-- src
|-- main
| |-- java
| | `-- info
| | `-- ejava
| | `-- examples
| | `-- app
| | `-- build
| | `-- springboot
| | `-- SpringBootApp.java
| `-- resources
`-- test
|-- java
`-- resources
6. Spring Boot Executable JAR
At this point we can likely execute the Spring Boot Application within the IDE but instead, lets go back to the pom and construct a JAR file to be able to execute the application from the command line.
6.1. Building the Spring Boot Executable JAR
We saw earlier how we could build a standard executable JAR using the maven-jar-plugin
.
However, there were some limitations to that approach — especially the fact that a standard Java JAR cannot house dependencies to form a self-contained classpath and Spring Boot will need additional JARs to complete the application bootstrap.
Spring Boot uses a custom executable JAR format that can be built with the aid of the
spring-boot-maven-plugin.
Let’s extend our pom.xml file to enhance the standard JAR to be a Spring Boot executable JAR.
6.1.1. Declare spring-boot-maven-plugin
The following snippet shows the configuration for a spring-boot-maven-plugin
that defines a default execution to build the Spring Boot executable JAR for all child modules that declare using it.
In addition to building the Spring Boot executable JAR, we are setting up a standard in the parent for all children to have their follow-on JAR classified separately as a bootexec
.
classifier
is a core Maven construct and is meant to label sibling artifacts to the original Java JAR for the module.
Other types of classifiers
are source
, schema
, javadoc
, etc.
bootexec
is a value we made up.
bootexec is a value we made up.
|
By default, the repackage
goal would have replaced the Java JAR with the Spring Boot executable JAR.
That would have left an ambiguous JAR artifact in the repository — we would not easily know its JAR type.
This will help eliminate dependency errors during the semester when we layer N+1
assignments on top of layer N
.
Only standard Java JARs can be used in classpath dependencies.
<properties>
<spring-boot.classifier>bootexec</spring-boot.classifier>
</properties>
...
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>${spring-boot.classifier}</classifier> (4)
</configuration>
<executions>
<execution>
<id>build-app</id> (1)
<phase>package</phase> (2)
<goals>
<goal>repackage</goal> (3)
</goals>
</execution>
</executions>
</plugin>
...
</plugins>
</pluginManagement>
</build>
1 | id used to describe execution and required when having more than one |
2 | phase identifies the maven goal in which this plugin runs |
3 | repackage identifies the goal to execute within the spring-boot-maven-plugin |
4 | adds a -bootexec to the executable JAR’s name |
We can do much more with the spring-boot-maven-plugin
on a per-module basis (e.g., run the application from within Maven).
We are just starting at construction at this point.
6.1.2. Build the JAR
$ mvn clean package
[INFO] Scanning for projects...
...
[INFO] --- maven-jar-plugin:3.4.2:jar (default-jar) @ springboot-app-example ---
[INFO] Building jar: .../target/springboot-app-example-6.1.0-SNAPSHOT.jar (1)
[INFO]
[INFO] --- spring-boot-maven-plugin:3.3.2:repackage (build-app) @ springboot-app-example ---
[INFO] Attaching repackaged archive .../target/springboot-app-example-6.1.0-SNAPSHOT-bootexec.jar with classifier bootexec (2)
1 | standard Java JAR is built by the maven-jar-plugin |
2 | standard Java JAR is augmented by the spring-boot-maven-plugin |
6.2. Java MANIFEST.MF properties
The spring-boot-maven-plugin
augmented the standard JAR by adding a few properties to the MANIFEST.MF file
$ unzip -qc target/springboot-app-example-6.1.0-SNAPSHOT-bootexec.jar META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.2.2
Build-Jdk-Spec: 17
Main-Class: org.springframework.boot.loader.launch.JarLauncher (1)
Start-Class: info.ejava.examples.app.build.springboot.SpringBootApp (2)
Spring-Boot-Version: 3.3.2
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
1 | Main-Class was set to a Spring Boot launcher |
2 | Start-Class was set to the class we defined with @SpringBootApplication |
6.3. JAR size
Notice that the size of the Spring Boot executable JAR is significantly larger the original standard JAR.
$ ls -lh target/*jar* | grep -v sources | cut -d\ -f9-99
10M Aug 28 15:19 target/springboot-app-example-6.1.0-SNAPSHOT-bootexec.jar (2)
4.3K Aug 28 15:19 target/springboot-app-example-6.1.0-SNAPSHOT.jar (1)
1 | The original Java JAR with Spring Boot annotations was 4.3KB |
2 | The Spring Boot JAR is 10MB |
6.4. JAR Contents
Unlike WARs, a standard Java JAR does not provide a standard way to embed dependency JARs. Common approaches to embed dependencies within a single JAR include a "shaded" JAR where all dependency JAR are unwound and packaged as a single "uber" JAR
-
positives
-
works
-
follows standard Java JAR constructs
-
-
negatives
-
obscures contents of the application
-
problem if multiple source JARs use files with same path/name
-
Spring Boot creates a custom WAR-like structure
BOOT-INF/classes/info/ejava/examples/app/build/springboot/AppCommand.class
BOOT-INF/classes/info/ejava/examples/app/build/springboot/SpringBootApp.class (3)
BOOT-INF/lib/javax.annotation-api-2.1.1.jar (2)
...
BOOT-INF/lib/spring-boot-3.3.2.jar
BOOT-INF/lib/spring-context-6.1.11.jar
BOOT-INF/lib/spring-beans-6.1.11.jar
BOOT-INF/lib/spring-core-6.1.11.jar
...
META-INF/MANIFEST.MF
META-INF/maven/info.ejava.examples.app/springboot-app-example/pom.properties
META-INF/maven/info.ejava.examples.app/springboot-app-example/pom.xml
org/springframework/boot/loader/launch/ExecutableArchiveLauncher.class (1)
org/springframework/boot/loader/launch/JarLauncher.class
...
org/springframework/boot/loader/util/SystemPropertyUtils.class
1 | Spring Boot loader classes hosted at the root / |
2 | Local application classes hosted in /BOOT-INF/classes |
3 | Dependency JARs hosted in /BOOT-INF/lib |
Spring Boot can also use a standard WAR structure — to be deployed to a web server.
|
6.5. Execute Command Line
springboot-app-example$ java -jar target/springboot-app-example-6.1.0-SNAPSHOT-bootexec.jar (1)
Running SpringApplication (2)
. ____ _ __ _ _ (3)
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.3.2)
2019-12-04 09:01:03.014 INFO 1287 --- [main] i.e.e.a.build.springboot.SpringBootApp: \
Starting SpringBootApp on Jamess-MBP with PID 1287 (.../springboot-app-example/target/springboot-app-example-6.1.0-SNAPSHOT.jar \
started by jim in .../springboot-app-example)
2019-12-04 09:01:03.017 INFO 1287 --- [main] i.e.e.a.build.springboot.SpringBootApp: \
No active profile set, falling back to default profiles: default
2019-12-04 09:01:03.416 INFO 1287 --- [main] i.e.e.a.build.springboot.SpringBootApp: \
Started SpringBootApp in 0.745 seconds (JVM running for 1.13)
Done SpringApplication (4)
1 | Execute the JAR using the java -jar command |
2 | Main executes and passes control to SpringApplication |
3 | Spring Boot bootstrap is started |
4 | SpringApplication terminates and returns control to our main() |
7. Add a Component to Output Message and Args
We have a lot of capability embedded into our current Spring Boot executable JAR that is there to bootstrap the application by looking around for components to activate. Let’s explore this capability with a simple class that will take over the responsibility for the output of a message with the arguments to the program.
We want this class found by Spring’s application startup processing, so we will:
// AppCommand.java
package info.ejava.examples.app.build.springboot; (2)
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.List;
@Component (1)
public class AppCommand implements CommandLineRunner {
public void run(String... args) throws Exception {
System.out.println("Component code says Hello " + List.of(args));
}
}
1 | Add a @Component annotation on the class |
2 | Place the class in a Java package configured to be scanned |
7.1. @Component Annotation
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class AppCommand implements CommandLineRunner {
Classes can be configured to have their instances managed by Spring. Class annotations
can be used to express the purpose of a class and to trigger Spring into managing them
in specific ways. The most generic form of component annotation is @Component
.
Others will include @Repository
, @Controller
, etc. Classes directly annotated with
a @Component
(or other annotation) indicates that Spring can instantiate instances
of this class with no additional assistance from a @Bean
factory.
7.2. Interface: CommandLineRunner
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class AppCommand implements CommandLineRunner {
public void run(String... args) throws Exception {
}
}
-
Components implementing CommandLineRunner interface get called after application initialization
-
Program arguments are passed to the
run()
method -
Can be used to perform one-time initialization at start-up
-
Alternative Interface: ApplicationRunner
-
Components implementing ApplicationRunner are also called after application initialization
-
Program arguments are passed to its
run()
method have been wrapped in ApplicationArguments convenience class
-
Component startup can be ordered with the @Ordered Annotation. |
7.3. @ComponentScan Tree
By default, the @SpringBootApplication
annotation configured Spring to look at
and below the Java package for our SpringBootApp class. I chose to place this
component class in the same Java package as the application class
@SpringBootApplication
// @ComponentScan
// @SpringBootConfiguration
// @EnableAutoConfiguration
public class SpringBootApp {
}
src/main/java
`-- info
`-- ejava
`-- springboot
`-- examples
`-- app
|-- AppCommand.java
`-- SpringBootApp.java
8. Running the Spring Boot Application
$ java -jar target/springboot-app-example-6.1.0-SNAPSHOT-bootexec.jar
Running SpringApplication (1)
. ____ _ __ _ _ (2)
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.3.2)
2019-09-06 15:56:45.666 INFO 11480 --- [ main] i.e.s.examples.app.SpringBootApp
: Starting SpringBootApp on Jamess-MacBook-Pro.local with PID 11480 (.../target/springboot-app-example-6.1.0-SNAPSHOT.jar ...)
2019-09-06 15:56:45.668 INFO 11480 --- [ main] i.e.s.examples.app.SpringBootApp
: No active profile set, falling back to default profiles: default
2019-09-06 15:56:46.146 INFO 11480 --- [ main] i.e.s.examples.app.SpringBootApp
: Started SpringBootApp in 5.791 seconds (JVM running for 6.161) (3)
Hello [] (4) (5)
Done SpringApplication (6)
1 | Our SpringBootApp.main() is called and logs Running SpringApplication |
2 | SpringApplication.run() is called to execute the Spring Boot application |
3 | Our AppCommand component is found within the classpath at or under the package declaring @SpringBootApplication |
4 | The AppCommand component run() method is called, and it prints out a message |
5 | The Spring Boot application terminates |
6 | Our SpringBootApp.main() logs Done SpringApplication an exits |
8.1. Implementation Note
I added print statements directly in the Spring Boot Application’s main() method
to help illustrate when calls were made. This output could have been packaged into listener
callbacks to leave the main() method implementation free — except to register the callbacks.
If you happen to need more complex behavior to fire before the Spring context begins initialization,
then look to add
listeners
of the SpringApplication instead.
|
9. Configure pom.xml to Test
At this point we are again ready to set up an automated execution of our JAR as a part of the build.
We can do that by adding a separate goal execution of the spring-boot-maven-plugin
.
<build>
...
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>run-application</id> (1)
<phase>integration-test</phase>
<goals>
<goal>run</goal>
</goals>
<configuration> (2)
<arguments>Maven,plugin-supplied,args</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
1 | new execution of the run goal to be performed during the Maven integration-test phase |
2 | command line arguments passed to main
|
9.1. Execute JAR as part of the build
$ mvn clean verify
[INFO] Scanning for projects...
...
[INFO] --- spring-boot-maven-plugin:3.3.2:run (run-application) @ springboot-app-example ---
[INFO] Attaching agents: [] (1)
Running SpringApplication
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.3.2)
2022-07-02 14:11:46.110 INFO 48432 --- [ main] i.e.e.a.build.springboot.SpringBootApp : Starting SpringBootApp using Java 17.0.3 on Jamess-MacBook-Pro.local with PID 48432 (.../springboot-app-example/target/classes started by jim in .../springboot-app-example)
2022-07-02 14:11:46.112 INFO 48432 --- [ main] i.e.e.a.build.springboot.SpringBootApp : No active profile set, falling back to 1 default profile: "default"
2022-07-02 14:11:46.463 INFO 48432 --- [ main] i.e.e.a.build.springboot.SpringBootApp : Started SpringBootApp in 0.611 seconds (JVM running for 0.87)
Component code says Hello [Maven, plugin-supplied, args] (2)
Done SpringApplication
1 | Our plugin is executing |
2 | Our application was executed and the results displayed |
10. Summary
As a part of this material, the student has learned how to:
-
Add Spring Boot constructs and artifact dependencies to the Maven POM
-
Define Application class with a main() method
-
Annotate the application class with @SpringBootApplication (and optionally use lower-level annotations)
-
Place the application class in a Java package that is at or above the Java packages with beans that will make up the core of your application
-
Add component classes that are core to your application to your Maven module
-
Typically, define components in a Java package that is at or below the Java package for the
SpringBootApplication
-
Annotate components with @Component (or other special-purpose annotations used by Spring)
-
Execute application like a normal executable JAR