jim stafford
The student will learn:
to decouple an application through the separation of interface and implementation
to configure an application using dependency injection and factory methods of a configuration class
At the conclusion of this lecture and related exercises, the student will be able to:
implement a service interface and implementation component
package a service within a Maven module separate from the application module
implement a Maven module dependency to make the component class available to the application module
use a @Bean
factory method of a @Configuration
class to instantiate a
Spring-managed component
create a sample Hello
service
implement an interface
a single implementation right off the bat
two separate modules
hello-service-api
hello-service-stdout
We will start out by creating two separate module directories.
The Hello Service API module will contain a single interface and pom.xml.
hello-service-api/
|-- pom.xml
`-- src
`-- main
`-- java
`-- info
`-- ejava
`-- examples
`-- app
`-- hello
`-- Hello.java (1)
1 | Service interface |
The Hello Service StdOut module will contain a single implementation class and pom.xml.
hello-service-stdout/
|-- pom.xml
`-- src
`-- main
`-- java
`-- info
`-- ejava
`-- examples
`-- app
`-- hello
`-- stdout
`-- StdOutHello.java (1)
1 | Service implementation |
Building normal Java JAR
No direct dependencies on Spring Boot or Spring
#pom.xml
...
<groupId>info.ejava.examples.app</groupId>
<version>6.0.1-SNAPSHOT</version>
<artifactId>hello-service-api</artifactId>
<packaging>jar</packaging>
...
The implementation will be similar to the interface’s pom.xml except it requires a dependency on the interface module.
#pom.xml
...
<groupId>info.ejava.examples.app</groupId>
<version>6.0.1-SNAPSHOT</version>
<artifactId>hello-service-stdout</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId> (1)
<artifactId>hello-service-api</artifactId>
<version>${project.version}</version> (1)
</dependency>
</dependencies>
...
1 | Dependency references leveraging ${project} variables module shares with dependency |
Since we are using the same source tree, we can leverage ${project} variables.
This will not be the case when declaring dependencies on external modules. |
Quite simple
Pass in the String name to say hello to
package info.ejava.examples.app.hello;
public interface Hello {
void sayHello(String name);
}
The service instance will be responsible for
the greeting
the implementation — how we say hello
package info.ejava.examples.app.hello.stdout; (1)
public class StdOutHello implements Hello {
private final String greeting; (2)
public StdOutHello(String greeting) { (3)
this.greeting = greeting;
}
@Override (4)
public void sayHello(String name) {
System.out.println(greeting + " " + name);
}
}
1 | Implementation defined within own package |
2 | greeting will hold our phrase for saying hello and is made final
to highlight it is required and will not change during the lifetime
of the class instance |
3 | A single constructor is provided to define a means to initialize the
instance. Remember — the greeting is final and must be set during
class instantiation and not later during a setter. |
4 | The sayHello() method provides implementation of method defined in interface |
final requires the value set when the instance is created and never change. |
Constructor injection makes required attributes marked final easier to set during testing |
$ mvn clean install -f hello-service-api
[INFO] Scanning for projects...
[INFO]
[INFO] -------------< info.ejava.examples.app:hello-service-api >--------------
[INFO] Building App::Config::Hello Service API 6.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:3.1.0:clean (default-clean) @ hello-service-api ---
[INFO]
[INFO] --- maven-resources-plugin:3.1.0:resources (default-resources) @ hello-service-api ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory .../app-config/hello-service-api/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ hello-service-api ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to .../app-config/hello-service-api/target/classes
[INFO]
[INFO] --- maven-resources-plugin:3.1.0:testResources (default-testResources) @ hello-service-api ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory .../app-config/hello-service-api/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ hello-service-api ---
[INFO] No sources to compile
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ hello-service-api ---
[INFO] No tests to run.
[INFO]
[INFO] --- maven-jar-plugin:3.1.2:jar (default-jar) @ hello-service-api ---
[INFO] Building jar: .../app-config/hello-service-api/target/hello-service-api-6.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- maven-install-plugin:3.0.0-M1:install (default-install) @ hello-service-api ---
[INFO] Installing .../app-config/hello-service-api/target/hello-service-api-6.0.1-SNAPSHOT.jar to .../.m2/repository/info/ejava/examples/app/hello-service-api/6.0.1-SNAPSHOT/hello-service-api-6.0.1-SNAPSHOT.jar
[INFO] Installing .../app-config/hello-service-api/pom.xml to .../.m2/repository/info/ejava/examples/app/hello-service-api/6.0.1-SNAPSHOT/hello-service-api-6.0.1-SNAPSHOT.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.070 s
$ mvn clean install -f hello-service-stdout
[INFO] Scanning for projects...
[INFO]
[INFO] ------------< info.ejava.examples.app:hello-service-stdout >------------
[INFO] Building App::Config::Hello Service StdOut 6.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:3.1.0:clean (default-clean) @ hello-service-stdout ---
[INFO]
[INFO] --- maven-resources-plugin:3.1.0:resources (default-resources) @ hello-service-stdout ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory .../app-config/hello-service-stdout/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ hello-service-stdout ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to .../app-config/hello-service-stdout/target/classes
[INFO]
[INFO] --- maven-resources-plugin:3.1.0:testResources (default-testResources) @ hello-service-stdout ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory .../app-config/hello-service-stdout/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ hello-service-stdout ---
[INFO] No sources to compile
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ hello-service-stdout ---
[INFO] No tests to run.
[INFO]
[INFO] --- maven-jar-plugin:3.1.2:jar (default-jar) @ hello-service-stdout ---
[INFO] Building jar: .../app-config/hello-service-stdout/target/hello-service-stdout-6.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- maven-install-plugin:3.0.0-M1:install (default-install) @ hello-service-stdout ---
[INFO] Installing .../app-config/hello-service-stdout/target/hello-service-stdout-6.0.1-SNAPSHOT.jar to .../.m2/repository/info/ejava/examples/app/hello-service-stdout/6.0.1-SNAPSHOT/hello-service-stdout-6.0.1-SNAPSHOT.jar
[INFO] Installing .../app-config/hello-service-stdout/pom.xml to .../.m2/repository/info/ejava/examples/app/hello-service-stdout/6.0.1-SNAPSHOT/hello-service-stdout-6.0.1-SNAPSHOT.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.658 s
We now move on to developing our application within its own module containing two (2) classes similar to earlier examples.
|-- pom.xml
`-- src
``-- main
`-- java
`-- info
`-- ejava
`-- examples
`-- app
`-- config
`-- beanfactory
|-- AppCommand.java (2)
`-- SelfConfiguredApp.java (1)
1 | Class with Java main() that starts Spring |
2 | Class containing our first component that will be the focus of our injection |
<groupId>info.ejava.examples.app</groupId>
<artifactId>appconfig-beanfactory-example</artifactId>
<name>App::Config::Bean Factory Example</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>hello-service-stdout</artifactId> (1)
<version>${project.version}</version>
</dependency>
</dependencies>
1 | Dependency on implementation creates dependency on both implementation and interface |
In this case, the module we are depending upon is in the
same groupId and shares the same version . For simplicity of
reference and versioning, I used the ${project} variables to
reference it. That will not always be the case. |
You can verify the dependencies exist using the tree
goal of the dependency
plugin.
$ mvn dependency:tree -f hello-service-stdout
...
[INFO] --- maven-dependency-plugin:3.1.1:tree (default-cli) @ hello-service-stdout ---
[INFO] info.ejava.examples.app:hello-service-stdout:jar:6.0.1-SNAPSHOT
[INFO] \- info.ejava.examples.app:hello-service-api:jar:6.0.1-SNAPSHOT:compile
package info.ejava.examples.app.config.beanfactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import info.ejava.examples.app.hello.Hello;
@Component
public class AppCommand implements CommandLineRunner {
private final Hello greeter; (1)
public AppCommand(Hello greeter) { (2)
this.greeter = greeter;
}
public void run(String... args) throws Exception {
greeter.sayHello("World");
}
}
1 | Add a reference to the Hello interface. Java attribute defined as final
to help assure that the value is assigned during the constructor. |
2 | Using contructor injection where the instance is supplied to the class through a parameter to the constructor |
Our AppCommand
class has been defined only with the interface to Hello
and not a
specific implementation.
we want our code broken into separate modular areas of concern
modules (JARs)
packages
classes
methods
helps improve modularity, testability, reuse, and many other desirable features
we do this using encapsulation (modularity) and information hiding (well defined interfaces)
improves simplicity and understandability
But how do does our client class (AppCommand
) get an instance of the implementation (StdOutHello
)?
client class directly instantiates the implementation
public AppCommand() {
this.greeter = new StdOutHello("World");
}
client becomes coupled to that specific implementation.
client class procedurally delegates to a factory
public AppCommand() {
this.greeter = BeanFactory.makeGreeter();
}
client runs the risk of violating Separation of Concerns by adding complex initialization code to its primary business purpose
Most frameworks, including Spring, implement dependency injection through a form of inversion of control (IoC)
traditional procedural code
normally makes calls to libraries in order to perform a specific purpose
inverted control
application code is part of a framework that calls the application code when it is time to do something
In this case the framework is for application assembly.
We defined the dependency using the Hello
interface and have three primary ways
to have dependencies injected into an instance.
import org.springframework.beans.factory.annotation.Autowired;
public class AppCommand implements CommandLineRunner {
//@Autowired -- FIELD injection (3)
private Hello greeter;
@Autowired //-- Constructor injection (1)
public AppCommand(Hello greeter) {
this.greeter = greeter;
}
//@Autowired -- PROPERTY injection (2)
public void setGreeter(Hello hello) {
this.greeter = hello;
}
1 | constructor injection - injected values required prior to instance being created |
2 | field injection - value injected directly into attribute |
3 | setter or property injection - setter() called with value |
may be applied to fields, methods, constructors
@Autowired(required=true)
- default value for required
attribute
successful injection mandatory when applied to a property
specific constructor use required when applied to a constructor
only a single constructor per class may have this annotation
@Autowired(required=false)
injected bean not required to exist when applied to a property
specific constructor an option for container to use
multiple constructors may have this annotation applied
container will determine best based on number of matches
single constructor has an implied @Autowired(required=false)
- making annotation optional
I selected constructor injection since the dependency is required for component to be valid |
In our example:
Spring will detect the AppCommand component and look for ways to instantiate it
The only constructor requires a Hello instance
Spring will then look for a way to instantiate an instance of Hello
When we go to run the application, we get the following error
$ mvn clean package
...
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in AppCommand required a bean of type 'Hello' that could not be found.
Action:
Consider defining a bean of type 'Hello' in your configuration.
Container has no knowledge of any beans that can satisfy the only available constructor
StdOutHello
class is not defined in a way that
allows Spring to use it
We can solve this in at least two (2) ways.
Add @Component to the StdOutHello class. This will trigger Spring to directly instantiate the class.
@Component
public class StdOutHello implements Hello {
problem: It may be one of many implementations of Hello
Define what is needed using a @Bean
factory method of a @Configuration
class.
This will trigger Spring to call a method that is in charge of instantiating
an object of the type identified in the method return signature.
@Configuration
public class AConfigurationClass {
@Bean
public Hello hello() {
return new StdOutHello("...");
}
}
classes that Spring expects to have one or more @Bean factory methods
@SpringBootApplication (1)
//==> wraps @SpringBootConfiguration (2)
// ==> wraps @Configuration
public class SelfConfiguredApp {
public static final void main(String...args) {
SpringApplication.run(SelfConfiguredApp.class, args);
}
//...
}
1 | @SpringBootApplication is a wrapper around a few annotations including
@SpringBootConfiguration |
2 | @SpringBootConfiguration is an alternative annotation to using @Configuration
with the caveat that there be only one @SpringBootConfiguration per application |
Therefore, we have the option to use our Spring Boot application class to host the configuration and
the @Bean
factory.
@SpringBootApplication (4) (5)
public class SelfConfiguredApp {
public static final void main(String...args) {
SpringApplication.run(SelfConfiguredApp.class, args);
}
@Bean (1)
public Hello hello() { (2)
return new StdOutHello("Application @Bean says Hey"); (3)
}
}
1 | method annotated with @Bean implementation |
2 | method returns Hello type required by container |
3 | method returns a fully instantiated instance. |
4 | method hosted within class with @Configuration annotation |
5 | @SpringBootConfiguration annotation included the capability defined for
@Configuration |
Anything missing to create instance gets declared as an input to the method and it will get created in the same manner and passed as a parameter. |
$ java -jar target/appconfig-beanfactory-example-*-SNAPSHOT-bootexec.jar
...
Application @Bean says Hey World
the container
obtained an instance of a Hello
bean
passed that bean to the AppCommand
class' constructor
to instantiate that @Component
the @Bean
factory method
chose the implementation of the Hello
service (StdOutHello
)
chose the greeting to be used ("Application @Bean says Hey")
return new StdOutHello("Application @Bean says Hey");
the AppCommand CommandLineRunner determined who to say hello to ("World")
greeter.sayHello("World");
import org.springframework.context.annotation.ImportResource;
@SpringBootApplication
@ImportResource({"classpath:contexts/applicationContext.xml"}) (1)
public class XmlConfiguredApp {
public static final void main(String...args) {
SpringApplication.run(XmlConfiguredApp.class, args);
}
}
1 | @ImportResource will enact the contents of context/applicationContext.xml |
|-- pom.xml
`-- src
`-- main
|-- java
| `-- info
| `-- ejava
| `-- examples
| `-- app
| `-- config
| `-- xmlconfig
| |-- AppCommand.java
| `-- XmlConfiguredApp.java
`-- resources
`-- contexts
`-- applicationContext.xml
$ jar tf target/appconfig-xmlconfig-example-*-SNAPSHOT-bootexec.jar | grep applicationContext.xml
BOOT-INF/classes/contexts/applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="info.ejava.examples.app.hello.stdout.StdOutHello"> (1)
<constructor-arg value="Xml @Bean says Hey" /> (2)
</bean>
</beans>
1 | A specific implementation of ther Hello interface is defined |
2 | Text is injected into the constructor when container instantiates |
Output:
$ java -jar target/appconfig-xmlconfig-example-*-SNAPSHOT-bootexec.jar
...
Xml @Bean says Hey World
In this module we
decoupled part of our application into three Maven modules (app, iface, and impl1)
decoupled the implementation details (StdOutHello
) of a service from the
caller (AppCommand
) of that service
injected the implementation of the service into a component using constructor injection
defined a @Bean
factory method to make the determination of what to inject
showed an alternative using XML-based configuration and @ImportResource