What is Spring?
Spring is a lightweight framework for creating Java applications. This definition is very generic because Spring
is divided into submodules that allow you to create applications of different types: standalone, web, data access, event driven, etc...
Lightweight, not because of the size
of the applications created, but for the philosophy it uses: the purpose of the framework is to facilitate the programmer's development,
making sure that integration with it does not impact the original application code (I don't have to modify the application structure to use Spring).
But Spring was originally born as an IoC framework.
The Core Module
The core module is based on the functionality of the IoC (Inversion of Control).
The IoC is a pattern that allows the automated management of dependencies:
it will be the framework that will take care of enhancing class dependencies, not the programmer. For example, given these classes:
public interface UserService {
void existsById(Long id);
}
public interface UserDao {
void existsById(Long id);
String getUrl();
void setUrl(String url);
}
public class UserServiceImpl implements UserService {
private UserDao userDao;
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
public void existsById(Long id) {
userDao.existsById(id);
}
}
public class UserJdbcDaoImpl implements UserDao {
private String url;
public void existsById(Long id) {
System.out.println("existsById from JDBC");
}
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
}
public class UserJpaDaoImpl implements UserDao {
private String url;
public void existsById(Long id) {
System.out.println("existsById from JPA");
}
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
}
we notice that the class UserServiceImpl "depends" on an implementation of UserDao. UserDao has two possible implementations:
one that uses JDBC and one that uses JPA.
Therefore, the developer, in order to instantiate the UserServiceImpl class, must first instantiate an implementation of UserDao:
public class Main {
public static void main(String... args) {
UserDao userDao = new UserJpaDaoImpl();
userDao.setUrl("http://localhost:3306");
UserService userService = new UserServiceImpl(userDao);
userService.existsById(1L); //"existsById from JPA"
}
}
Also, if we no longer wanted to use the UserJpaDaoImpl implementation but UserJdbcDaoImpl, we would have to change our source code (in this example a trivial main).
A possible solution: apply the pattern factory
A possible solution to not having to worry about changing dependencies is to use a Factory class:
public class UserFactory {
private static UserFactory instance;
private UserDao userDao;
private UserService userService;
private Properties properties;
private UserFactory() {
try {
properties = new Properties();
properties.load(this.getClass()
.getResourceAsStream("/user.properties"));
userDao = (UserDao) Class.forName(properties.getProperty("userDao"))
.getDeclaredConstructor().newInstance();
userDao.setUrl("http://localhost:3306");
userService = (UserService) Class.forName(properties.getProperty("userService"))
.getDeclaredConstructor(UserDao.class).newInstance(userDao);
}
catch (Exception e) {
System.err.println("Unable to create the context");
}
}
static {
instance = new UserFactory();
}
public static UserFactory getInstance() {
return instance;
}
public UserDao getUserDao() {
return userDao;
}
public UserService getUserService() {
return userService;
}
}
which reads a properties file like this one:
#user.properties
userDao=com.vincenzoracca.intro.UserJpaDaoImpl
userService=com.vincenzoracca.intro.UserServiceImpl
The Main at this point would become:
public static void main(String... args) {
UserService userService = UserFactory.getInstance().getUserService();
userService.existsById(1L); //"existsById from JPA"
}
This way, to instantiate the UserService class, we don't have to create its UserDao dependency every time. We have improved things, but this method is very expensive if we wanted to apply it for all the classes in our application.
Solution using Spring
public class Main {
public static void main(String... args) {
ApplicationContext ctx =
new AnnotationConfigApplicationContext(Config.class);
UserService userService = ctx.getBean(UserService.class);
userService.existsById(1L); //"existsById from JPA"
}
}
With the IoC instead, the development flow changes: the developer through configurations (in this case the Config class), indicates how dependencies should be resolved. It will then be the responsibility of the framework to instantiate the classes such as UserServiceImpl, taking care of create an instance of UserDao with the right implementation first and providing the developer with an instance of UserServiceImpl ready to use.
This is why it is called Inversion of Control: the workflow is reversed. We have not to create the dependencies first and then the parent class, but we use the class directly because the dependencies are already provided at the startup behind the scenes.
In particular, IoC is a technique that can have various implementations (kind of like the JPA specification that can have various implementations, like Hibernate). Spring implements the IoC via Dependency Injection.
The Dependency Injection
Dependency injection is the technique whereby Spring automatically injects the required dependencies. DI in Spring can be applied via constructor, setter methods or via fields. So, dependencies are provided via constructor, setters or fields. Spring will use them to automatically inject dependencies. Compared to dependency lookup (another IoC technique), the developer does not have to "pull" the dependency using a lookup method (as was done long ago with early EJBs).
All the classes that Spring has to manage are called beans (not to be confused with JavaBean). The Spring container that contains all beans is called IoC Container. Spring beans, by default, have the singleton scope and are eagger. Singleton in the sense that you will always have the same instance for the same bean. Eagger in the sense that at context startup, Spring tries to resolve all dependencies specified by the by the configuration.
How to declare a bean in Spring?
We have 3 ways to declare a bean in Spring.
First way: via xml configuration file
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userDao" class="com.vincenzoracca.intro.UserJpaDaoImpl">
<property name="url" value="http://localhost:3306" />
</bean>
<bean id="userService" class="com.vincenzoracca.intro.UserServiceImpl">
<constructor-arg name="userDao" ref="userDao" />
</bean>
</beans>
Let's analyze the file:
- The set of beans is declared inside the beans tag.
- Each bean is declared inside the bean tag.
- With the id attribute, we indicate the name of the bean, which must be unique within the Spring context.
- With class, we indicate the implementation of the bean's class.
- With property, we are saying that the url field should be valued with the contents of the value attribute. We are using setter dependency injection.
- With constructor-args, we're applying constructor dependency injection instead: specifically, we're saying that the constructor parameter named userDao, must be valued with the bean named userDao.
So ref is used when we want to valorize a field with another bean, value is used when we want to valorize the field with a constant value.
The main class will be:
public class Main {
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("spring/app-context-xml.xml");
ctx.refresh();
UserService userService = ctx.getBean(UserService.class);
userService.existsById(1L); //"existsById from JPA"
ctx.close();
}
}
The GenericXmlApplicationContext class, from Spring, reads an xml configuration file where beans are declared. With the getBean method we are able to get the bean of type UserService.
Second way: by annotation stereotype
The UserServiceImpl and UserJpaDaoImpl classes will be:
@Service
public class UserServiceImpl implements UserService {
private UserDao userDao;
@Autowired
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
public void existsById(Long id) {
userDao.existsById(id);
}
}
@Component
public class UserJpaDaoImpl implements UserDao {
private String url;
public void existsById(Long id) {
System.out.println("existsById from JPA");
}
public String getUrl() { return url; }
@Autowired
public void setUrl(@Value("http://localhost:3306") String url) { this.url = url; }
}
We analyze the two classes:
- With @Component and @Service we indicate to Spring that it must create the beans of these two classes. In particular, there is no difference between @Component and @Service at functionality level. The difference is only conceptual: we annotate with @Service a more complex bean, maybe because it contains other beans.
- With @Autowired, we are indicating to Spring to perform the constructor DI for UserServiceImpl, valuing the bean of type UserDao, while for the UserDao class we tell the framework to perform the DI through setter. In particular, the value of the url parameter must not be a bean, but a constant value (and we indicate it with @Value).
We need a xml configuration file that will read the given beans:
<?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 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.vincenzoracca.intro.stereotype" />
</beans>
With context:component-scan we are indicating to Spring that our beans, i.e. our classes annotated with @Component and @Service,
are inside the indicated package. Thanks to this information, Spring will be able to inject the beans annotated with @Autowired.
The main is the same as the previous one, so we omit it.
Third way: via Java Configuration
Our UserServiceImpl and UserJpaDaoImpl classes will not contain any Spring annotations. We will however create a configuration file, this time not in xml but in Java!
@Configuration
public class AppConfig {
@Bean
UserDao userDao() {
UserDao userDao = new UserJpaDaoImpl();
userDao.setUrl("http://localhost:3306");
return userDao;
}
@Bean
UserService userService() {
return new UserServiceImpl(userDao());
}
}
Let's analyze the AppConfonfig class:
- With @Configuration, we indicate to Spring that this class configures beans (i.e., it contains methods annotated with @Bean).
- With @Bean, we create the beans for our two classes. In particular, by default, the name of the bean will be equal to the name of the annotated method.
Pretty simple right? Let's look at the Main class:
public class Main {
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(
AppConfig.class);
UserService userService = ctx.getBean(UserService.class);
userService.existsById(1L); //"existsById from JPA"
}
}
This time, instead of using the GenericXmlApplicationContext class, we use AnnotationConfigApplicationContext where we can point to one or more Spring configuration classes (i.e. classes annotated with @Configuration).
Of course, nothing prevents us from mixing configurations.
For example, we could use Spring's stereotype annotations with the java configuration instead of the xml file, like this:
@Configuration
@ComponentScan("com.vincenzoracca.intro.both")
public class AppConfig {
}
The UserServiceImpl and UserDaoImpl classes will be like the ones shown in the second way, i.e. annotated with @Service and @Component respectively. The main class will be the same as just seen previously.
Field Dependency Injection Considerations
We could have @Autowired the individual fields instead of the constructor and setter method to use the field dependency injection.
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
public void existsById(Long id) {
userDao.existsById(id);
}
}
@Component
public class UserJpaDaoImpl implements UserDao {
@Autowired
@Value("http://localhost:3306")
private String url;
public void existsById(Long id) {
System.out.println("existsById from JPA");
}
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
}
This type of injection, although very used, is not recommended because it has some disadvantages:
The developer might not write the constructor or the setters, so it is much easier not to realize the dependencies that are added as you go. A constructor with a lot of parameters would immediately jump out at you, rather than inserting multiple annotated fields simply with @Autowired.
With the libraries and Spring itself, it's not hard to inject a mock class as a dependency, even if the user class doesn't have a constructor or a setter to be able to inject it. We don't need to write a constructor or setter if we use the field DI.
But this goes against the principle of lightness of the framework: the programmer should write the classes without thinking about the use of the framework.
Dependency Injection Considerations
Some benefits of DI are:
- Reduces code writing.
- It simplifies application configurations. You can easily change bean implementation so that it is transparent to the user class.
- Improves testability. By leveraging DI, you can easily replace real dependencies with mocked classes, completely transparent to the user class.
- It pushes the programmer to improve the design of the application. DI pushes to create interfaces, so that the framework can then create the bean with the implementation. Working with interfaces leads to the benefit of low coupling.
Conclusions
We took a first look at Spring, talking about Dependency Injection and how it is applied in the Core module.
You can find the code for the examples on my github at this link: Spring Framework Tutorial
Articles about Spring: Spring
Recommended books about Spring:
- Pro Spring 5 (Spring from scratch a hero): https://amzn.to/3KvfWWO
- Pivotal Certified Professional Core Spring 5 Developer Exam: A Study Guide Using Spring Framework 5 (for Spring certification): https://amzn.to/3KxbJSC
- Pro Spring Boot 2: An Authoritative Guide to Building Microservices, Web and Enterprise Applications, and Best Practices (Spring Boot of the detail): https://amzn.to/3TrIZic