Cos'è Spring?
Spring è un framework leggero per creare applicazioni Java. Questa definizione è molto generica poichè Spring
è diviso in submodules che permettono di creare applicazioni di diverso tipo: standalone, web, data access, event driven, etc...
Leggero, non per le dimensioni
delle applicazioni create, ma per la filosofia che usa: lo scopo del framework è facilitare lo sviluppo del programmatore,
facendo sì che l'integrazione con esso non impatti sul codice originale dell'applicazione (non devo modificare la struttura dell'applicazione per usare Spring).
Ma Spring nacque originariamente come framework di IoC.
Il modulo Core
Il modulo core è basato sulla funzionalità dell'IoC (Inversion of Control).
L'IoC è un pattern che permette la gestione automatizzata delle dipendenze:
sarà il framework ad occuparsi di valorizzare le dipendenze delle classi e non il programmarore. Ad esempio, date queste classi:
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; }
}
notiamo che la classe UserServiceImpl "dipende" da una implementazione di UserDao. UserDao ha due possibili implementazioni:
una che usa JDBC e una che usa JPA.
Lo sviluppatore quindi, per istanziare la classe UserServiceImpl, deve preoccuaparsi prima di istanziare un'implementazione di 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"
}
}
Inoltre, se non volessimo più usare l'implementazione UserJpaDaoImpl ma UserJdbcDaoImpl, dovremmo cambiare il nostro codice sorgente (in questo esempio un banale main).
Una possibile soluzione: applicare il pattern factory
Una possibile soluzione per non doverci preoccupare di un eventuale cambio delle dipendenze è usare una classe Factory:
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;
}
}
che legge un file di properties come questo:
#user.properties
userDao=com.vincenzoracca.intro.UserJpaDaoImpl
userService=com.vincenzoracca.intro.UserServiceImpl
Il Main a questo punto diventerebbe:
public static void main(String... args) {
UserService userService = UserFactory.getInstance().getUserService();
userService.existsById(1L); //"existsById from JPA"
}
In questo modo, per istanziare la classe UserService, non siamo costretti a creare la sua dipendenza UserDao ogni volta. Abbiamo migliorato le cose, ma questo metodo è molto dispendioso se lo volessimo applicare per tutte le classi della nostra applicazione.
Soluzione utilizzando 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"
}
}
Con l'IoC invece, il flusso di sviluppo cambia: lo sviluppatore tramite delle configurazioni (in questo caso la classe Config), indica come devono essere risolte le dipendenze. Sarà poi responsabilità del framework istanziare le classi come UserServiceImpl, preoccupandosi di valorizzare prima UserDao con la giusta implementazione e fornire allo sviluppatore una istanza di UserServiceImpl pronta all'uso.
Per questo motivo viene chiamato Inversion of Control: il flusso di lavoro viene invertito. Non eseguiamo le istruzioni per creare prima le dipendenze e poi la classe utilizzatrice, ma utlizziamo direttamente la classe poichè le dipendenze vengono fornire già allo startup dietro le quinte.
In particolare, l'IoC è una tecnica che può avere varie implementazioni (un po' come la specifica JPA che può avere varie implementazioni, come Hibernate). Spring implementa l'IoC tramite la Dependency Injection.
La Dependency Injection
La dependency injection è la tecnica per cui Spring inietta automaticamente le dipendenze richieste. La DI in Spring può essere applicata tramite Costruttore, metodi setters o tramite fields. Quindi, le dipendenze sono fornite tramite costruttore, setters o fields. Spring poi utilizzarà questi ultimi per iniettare automaticamente le dipendenze. Rispetto alla dependency lookup (un'altra tecnica di IoC), lo sviluppatore non deve fare "pull" della dipendenza utilizzando un metodo di lookup (come si faceva tempo fa con i primi EJB).
Tutte le classi che Spring deve gestire, vengono chiamate bean (da non confondere con i JavaBean). Il container di Spring che contiene tutti i bean viene chiamato IoC Container. I bean di Spring, di default, hanno lo scope singleton e sono eagger. Singleton nel senso che si avrà sempre la stessa istanza per lo stesso bean. Eagger, nel senso che allo startup del contesto, Spring tenta di risolvere tutte le dipendenze indicate dalla configurazione.
Come si dichiara un bean in Spring?
Abbiamo 3 modi per dichiarare un bean in Spring.
Primo modo: tramite file di configurazione 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"
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>
Analizziamo il file:
- L'insieme dei bean si dichiara dentro il tag beans.
- Ogni bean è dichiarato con il tag bean.
- Con l'attributo id, indichiamo il nome del bean, che deve essere univoco all'interno del contesto Spring.
- Con class, indichiamo l'implementazione della classe del bean.
- Con property, stiamo dicendo che il campo url dovrà essere valorizzato con il contenuto dell'attributo value. Stiamo utilizzando il setter dependency injection.
- Con constructor-args, stiamo applicando invece il constructor dependency injection: in particolare stiamo dicendo che il parametro del costruttore chiamato userDao, deve essere valorizzato con il bean di nome userDao.
Quindi ref si usa quando vogliamo valorizzare un campo tramite un altro bean, value invece si usa quando vogliamo valorizzare il campo con un valore costante.
La classe main sarà:
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();
}
}
La classe GenericXmlApplicationContext, di Spring, legge un file di configurazione xml dove sono dichiarati i bean. Col metodo getBean siamo in grado di ricavare il bean di tipo UserService.
Secondo modo: tramite annotation stereotype
Le classi UserServiceImpl e UserJpaDaoImpl saranno:
@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; }
}
Analizziamo le due classi:
- Con @Component e @Service indichiamo a Spring che deve creare i bean di queste due classi. In particolare, non c'è nessuna differenza tra @Component e @Service a livello di funzionalità. La differenza è solo concettuale: annotiamo con @Service un bean più complesso, magari perchè a sua volta contiene altri bean.
- Con @Autowired, stiamo indicando a Spring di effettuare la constructor DI per UserServiceImpl, valorizzando il bean di tipo UserDao, mentre per la classe UserDao indichiamo al framework di realizzare la DI tramite setter. In particolare, il valore del parametro url non deve essere a sua volta un bean, ma un valore costante (e lo indichiamo con @Value).
Abbiamo bisogno di un file di configurazione xml che leggerà i bean indicati:
<?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>
Con context:component-scan stiamo indicando a Spring che i nostri bean, cioè le nostre classi annotate con @Component e @Service,
si trovano dentro il package indicato. Grazie a questa informazione, Spring sarà capace di iniettare i bean annotati con @Autowired.
Il main è uguale al precedente, quindi lo omettiamo.
Terzo modo: tramite Java Configuration
Le nostre classi UserServiceImpl e UserJpaDaoImpl non conterranno nessuna annotation di Spring. Creeremo però un file di configurazione, stavolta non in xml ma 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());
}
}
Analizziamo la classe AppConfonfig:
- Con @Configuration, indichiamo a Spring che questa classe configura dei bean (cioè contiene dei metodi annotati con @Bean).
- Con @Bean, creiamo i bean per le nostre due classi. In particolare, di default, il nome del bean sarà uguale al nome del metodo annotato.
Molto semplice vero? Vediamo la classe Main:
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"
}
}
Questa volta, invece di utilizzare la classe GenericXmlApplicationContext, usiamo AnnotationConfigApplicationContext dove possiamo indicare una o più classi di configurazione di Spring (cioè classi annotate con @Configuration).
Ovviamente, nulla ci vieta di mischiare le configurazioni.
Ad esempio potremmo utilizzare le annotazioni stereotype di Spring con la java configuration invece che con il file xml, in questo modo:
@Configuration
@ComponentScan("com.vincenzoracca.intro.both")
public class AppConfig {
}
Le classi UserServiceImpl e UserDaoImpl saranno come quelle indicate nel secondo modo, cioè annotate rispettivamente con @Service e @Component. Il main sarà uguale a quello appena visto precedentemente.
Considerazioni sulla Field Dependency Injection
Avremmo potuto annotare con @Autowired i singoli campi invece che il costruttore e il metodo setter per utlizzare la 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; }
}
Questo tipo di injection però, seppur molto utilizzata, non è consigliata poichè ha degli svantaggi:
Lo svilupparte potrebbe non scrivere il costruttore o i setter, in questo modo è molto più facile non rendersi conto delle dipenze che si aggiungono man mano. Saltarebbe subito all'occhio un costruttore con molti parametri, piuttosto che inserire più campi annotati semplicemente con @Autowired.
Con le librerie e con Spring stesso, non è difficile poter iniettare una classe mock come dipendenza, anche se la classe utilizzatrice non ha un costruttore o un settere per poterla iniettare. Non abbiamo bisogno, d'altronde, di scrivere il costruttore o il setter se utlizziamo la field DI.
Ma questo va contro il principio di leggerezza del framework: il programmatore dovrebbe scrivere le classi senza pensare all'ultilizzo del framework.
Considerazioni sulla Dependency Injection
Alcuni benefici della DI sono:
- Riduce la scrittura di codice.
- Semplifica le configurazioni dell'applicazione. Puoi facilmente cambiare implementazione del bean in modo tale che per la classe utilizzatrice sia trasparente.
- Migliora la testabilità. Sfruttando la DI, puoi facilmente sostituire le dipendenze reali con classi mockate, in modo del tutto trasparente alla classe utilizzatrice.
- Spinge il programmatore a migliorare il design dell'applicazione. La DI spinge a creare interfacce, in modo tale che il framework possa poi creare il bean con l'implementazione indicatogli. Lavorare con le interfacce porta al beneficio del low coupling.
Conclusioni
Abbiamo dato una prima occhiata a Spring, parlando della Dependency Injection e come viene applicata nel modulo Core.
Potete trovare il codice degli esempi sul mio github a questo link: Spring Framework Tutorial
Articoli su Spring: Spring
Libri consigliati su Spring:
- Cloud Native Spring in Action: https://amzn.to/3xZFg1S
- Pro Spring 5 (Spring da zero a hero): https://amzn.to/3KvfWWO
- Pivotal Certified Professional Core Spring 5 Developer Exam: A Study Guide Using Spring Framework 5 (per certificazione Spring): https://amzn.to/3KxbJSC
- Pro Spring Boot 2: An Authoritative Guide to Building Microservices, Web and Enterprise Applications, and Best Practices (Spring Boot del dettaglio): https://amzn.to/3TrIZic