FREE

ΕΝΟΤΗΤΑ 2 SPRING - CONSTRUCTOR-BASED DEPENDENCY INJECTION

Στο σημερινό δωρεάν μάθημα Spring Framework θα εξηγήσουμε τι είναι το Dependency Injection και πως ορίζουμε Constructor Dependency Injection σε ένα Spring Project. Μην ξεχνάτε ότι ακόμα θα δείχνουμε τα παραδείγματα μας χρησιμοποιώντας XML configuration μέχρι να κατανοήσουμε κάποιες βασικές λειτουργίες του Spring Framework και μετά θα κάνουμε αποκλειστικά χρήση μόνο των annotations. Ας ξεκινήσουμε λοιπόν την θεωρία μας.

Στην προηγούμενη ενότητα είχαμε δει αναλυτικά τι είναι ένα Spring Container και ποια είναι η δουλειά του Inversion of Control. Επίσης είχαμε αναφέρει ότι οι κύριες υπηρεσίες του Spring Framework είναι δύο:

1. Να δημιουργεί και να διαχειρίζεται αντικείμενα (Inversion of Control)

2. Να εισάγει αντικείμενα όπου χρειάζονται στην εφαρμογή (Dependency Injection).

Στο σημερινό δωρεάν μάθημα Spring Framework θα δούμε πως ακριβώς λειτουργεί το Dependency Injection μέσα από ένα ολοκληρωμένο παράδειγμα. Όπως καταλαβαίνετε δεν μπορούμε να έχουμε dependency injection χωρίς πρώτα να έχουμε ορίσει το Spring container και κατά συνέπεια το Inversion of Control. Νομίζω αυτό είναι απόλυτα κατανοητό.

Ας δούμε ένα παράδειγμα από την αρχή αλλά αυτή τη φορά θα προσθέσουμε και το Dependency Injection και ποιο συγκεκριμένα Constructor-based Dependency Injection.

Έχουμε λοιπόν μια εφαρμογή κράτησης αεροπορικών εισιτηρίων. Φυσικά η εφαρμογή μας δεν θα είναι πολύπλοκη όσο αφορά τον κώδικα για να επικεντρωθούμε περισσότερο στην θεωρία. Αυτό που ζητάμε από την εφαρμογή μας είναι να μπορούμε να κάνουμε κράτηση θέσης για μια πτήση. Αυτή η λογική μπορεί εύκολα να περιγραφεί από ένα interface και μια class που θα υλοποιεί τις μεθόδους του interface. Πως συνδέεται όμως αυτό με το Spring Framework? Εμείς ζητάμε ένα αντικείμενο είδος Flight και το Object Factory του Spring θα μας το δώσει. Μέχρι εδώ είναι η λογική του Inversion of Control όπως την είδαμε στην προηγούμενη ενότητα.

Η λογική του προγράμματος όμως απαιτεί να υπάρχουν και άλλα αντικείμενα μέσα στην εφαρμογή, όπως π.χ discount (έκπτωση) για κάποιες συγκεκριμένες πτήσεις. Αυτά τα αντικείμενα επειδή είναι βοηθητικά στην κύρια λογική της εφαρμογής ονομάζονται helper objects. Και εδώ είναι που χρειαζόμαστε το Dependency Injection. Εμείς εξακολουθούμε να ζητάμε ένα αντικείμενο είδος πτήσης για να κάνουμε την κράτηση μας, όμως πριν το Spring Framework μας προμηθεύσει με το αντικείμενο έχει δημιουργήσει όλα τα άλλα αντικείμενα που συνδέονται μεταξύ τους (με injection) και μας δίνει το τελικό αντικείμενο που χρειαζόμαστε. Αν θέλαμε να το αντιπροσωπεύσουμε με γραφικό τρόπο την λογική μας θα είχαμε το εξής διάγραμμα:

Δωρεάν Μαθήματα και Σεμινάρια Spring Framework

Βλέπετε λοιπόν στο παράδειγμα μας, ότι ο client θα ζητήσει ένα αντικείμενο είδος Flight από το Spring Object Factory. Όμως υπάρχουν και άλλα dependencies στα οποία στηρίζονται οι κλάσεις μας πριν δημιουργήσουν το τελικό αντικείμενο. Ποια είναι αυτά τα helper objects?

Στο δικό μας παράδειγμα, κάθε αντικείμενο είδος Flight περιέχει και ένα αντικείμενο είδος DailyOffer. Αυτό προγραμματιστικά ονομάζεται dependency γιατί για να δημιουργηθεί σωστά το αντικείμενο Flight θα πρέπει πρώτα να δημιουργηθεί το αντικείμενο DailyOffer και να το εισάγουμε στο Flight. Αν το DailyOffer δεν μπορεί να δημιουργηθεί τότε δεν μπορούμε να έχουμε αντικείμενο Flight.

Δωρεάν Μαθήματα και Σεμινάρια Spring Framework

Για να λύσουμε αυτό το πρόβλημα πρέπει να ακολουθήσουμε μια από τις προτεινόμενες λύσεις που μας προσφέρει το Spring Framework η οποία είναι να εφαρμόσουμε Constructor Injection. Φυσικά υπάρχουν και άλλα είδη injection όπως Setter Injection και auto-wiring αλλά αυτά τα δούμε σε μελλοντικές ενότητες.

Για να μπορέσουμε να καταλάβουμε καλύτερα το Constructor Dependency Injection, ας οργανώσουμε την προσέγγιση μας και ας ακολουθήσουμε τρία βήματα:

1. Πρέπει να ορίσουμε το Interface και την Class την οποία θέλουμε να κάνουμε injection

2. Να δημιουργήσουμε έναν constructor σε μια καινούργια κλάση που να δέχεται injections

3. Να ορίσουμε το dependency injection μέσα στο Spring configuration αρχείο.

Ας ξεκινήσουμε λοιπόν με την δημιουργία της απλής εφαρμογής μας.

Μπορείτε είτε να αντιγράψετε το project από την ΕΝΟΤΗΤΑ 1 και να διαγράψετε όλες τις κλάσεις αφού θα δημιουργήσουμε καινούργιες, ή να δημιουργήσετε ένα καινούργιο project από την αρχή. Και στις δύο περιπτώσεις το POM.xml αρχείο παραμένει το ίδιο.

ΒΗΜΑ 1 – Define the dependency interface and class

Μέσα σε ένα πακέτο με το όνομα com.mycompany.flights δημιουργούμε ένα απλό Interface με το όνομα DailyOfferService. Το interface περιέχει μόνο μια μέθοδο που ονομάζεται getDailyOffer( ).

DailyOfferService.java


package com.mycompany.flights;

public interface DailyOfferService {
	public String getDailyOffer();
}

	

Τώρα πρέπει να γράψουμε την κλάση που κληρονομεί από το interface και υλοποιεί την μέθοδο του. Η υλοποίηση της απλά περιλαμβάνει ένα μήνυμα που ανακοινώνει μια έκπτωση 20% στο επόμενο ταξίδι στην Αθήνα.

DailyOfferServiceImpl.java


package com.mycompany.flights;

public class DailyOfferServiceImpl implements DailyOfferService{

	@Override
	public String getDailyOffer() {
		return "You have received 20% discount on your next trip to Athens";
	}
}

	

Με αυτή την υλοποίηση ολοκληρώσαμε το βήμα 1. Τώρα πρέπει να δημιουργήσουμε το κύριο μέρος της εφαρμογής όπου δημιουργούμε αντικείμενα είδος Flight που εξαρτιούνται από τα DailyOffer αντικείμενα.

ΒΗΜΑ 2 – Create a constructor in your class for injections

Σε αυτό το δεύτερο βήμα, θα δημιουργήσουμε πρώτα το interface που θα ορίζει τις μεθόδους και μετά την κλάση που θα τις υλοποιεί. Μέσα στην κλάση όμως θα δημιουργήσουμε ένα dependency στο DailyOfferService. Ας τα δούμε ένα-ένα και ας εξηγήσουμε τι ακριβώς συμβαίνει στον κώδικα.

Για αρχή λοιπόν θέλουμε να ορίσουμε ένα interface που θα ορίζει τις μεθόδους που απαιτεί η εφαρμογή. Για να κρατήσουμε των κώδικα σε απλό επίπεδο θα δημιουργήσουμε δύο μόνο μεθόδους. Η πρώτη θα ονομάζεται getDestination( ) και η δεύτερη getDailyOffer( ). Από τα ονόματα καταλαβαίνετε το σκοπό και τον ρόλο της κάθε μεθόδου. Η πρώτη θα μας επιστρέφει έναν τελικό προορισμό ενώ η δεύτερη θα μας επιστρέφει κάποιου είδους έκπτωση που μπορούμε να χρησιμοποιήσουμε στην επόμενη πτήση μας. Ο ολοκληρωμένος κώδικας του interface είναι ο εξής:

Flight.java


package com.mycompany.flights;

public interface Flight {
	public String getDestination();
	public String getDailyOffer();
}

	

Τώρα δημιουργούμε την κλάση που κάνει implements αυτό το interface και υλοποιεί τις μεθόδους που κληρονόμησε. Ο κώδικας της κλάσης είναι ο εξής:

AthensFlight.java


package com.mycompany.flights;

public class AthensFlight implements Flight {

	private DailyOfferService dailyOfferService;

	public AthensFlight(DailyOfferService dailyOfferService) {
		this.dailyOfferService = dailyOfferService;
	}

	@Override
	public String getDestination() {
		return "Your final destination is Athens Greece";
	}

	@Override
	public String getDailyOffer() {
		return dailyOfferService.getDailyOffer();
	}

}

	

Στην κλάση AthensFlight δημιουργούμε έναν constructor. Όπως ήδη γνωρίζουμε από την θεωρία της Java, ο constructor έχει πάντα το ίδιο όνομα με την κλάση. Όμως, για να δημιουργήσει ο constructor ένα αντικείμενο AthensFlight θα πρέπει πρώτα να του περάσουμε ένα αντικείμενο DailyOfferService σαν input παράμετρο στον constructor. Αν και εφόσον γίνει αυτό, τότε ο constructor αναθέτει στην μεταβλητή dailyOfferService, που έχουμε ορίσει σαν private, το αντικείμενο DailyOfferService και μετά δημιουργεί ένα αντικείμενο AthensFlight. Εδώ βλέπουμε καθαρά να υπάρχει ένα depedency ανάμεσα στο DailyOfferService και στο AthensFlight. Δεν μπορεί να δημιουργηθεί το δεύτερο αν πρώτα δεν έχουμε δημιουργήσει το πρώτο. Σε αυτό λοιπόν το σημείο θα μας φανεί χρήσιμο το Spring Framework γιατί μπορεί να δημιουργήσει τα αντικείμενα αυτόματα για εμάς, να κάνει inject το DailyOfferService στο AthensFlight και τέλος να μας επιστρέψει μέσω του Spring Object Factory το τελικό αντικείμενο. Για να μπορέσει όμως να το κάνει αυτό θα πρέπει να ενημερώσουμε το Spring Framework και κατά συνέπεια το Spring Container ποιες κλάσεις είναι αυτές που έχουν σχέση μεταξύ τους. Αυτό το ορίζουμε μέσα από το Spring Configuration αρχείο.

ΒΗΜΑ 3 – Configure the dependency injection in Spring Configuration file

Πριν προχωρήσουμε στην ανάλυση μας, ας δούμε πρώτα πως θα πρέπει να ορίσουμε το Dependency Injection μέσα στο Spring Configuration αρχείο.

flight.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="myDailyOfferService" class="com.mycompany.flights.DailyOfferServiceImpl">

	</bean>
	<bean id="myFlight" class="com.mycompany.flights.AthensFlight">
		<constructor-arg ref="myDailyOfferService" />
	</bean>


</beans>

	

Ορίζουμε λοιπόν εξ αρχής τα δύο αντικείμενα. Τα αντικείμενα στο Spring Framework ονομάζονται beans. Για αυτό και άλλωστε τα tags αρχίζουν και κλείνουν με την λέξη beans. Τώρα, όπως ήδη γνωρίζουμε από την ΕΝΟΤΗΤΑ 1 , θα χρειαστεί να δώσουμε ένα δικό μας όνομα σε κάθε αντικείμενο (αυτό είναι το id) και το όνομα του πακέτου μαζί με το όνομα της κλάσης (class). Μέχρι εδώ είναι όλα γνωστά.

Στο κώδικα μας όμως, έχουμε ορίσει στον constructor του AthensFlight ότι θα δημιουργηθεί ένα αντικείμενο AthensFlight χρειαζόμαστε και ένα αντικείμενο DailyOfferService. Αν απλά ορίσουμε τα beans τότε το Spring θα γνωρίζει αυτά τα δύο αντικείμενα αλλά δεν θα μπορεί να κάνει inject το ένα μέσα στο άλλο γιατί δεν γνωρίζει τον τρόπο.

Αν θυμάστε από την προηγούμενη ενότητα, είχαμε πει ότι ο ορισμός του bean περιλαμβάνει κάποια properties. Από αυτά τα properties θα χρειαστούμε τώρα να χρησιμοποιήσουμε το constructor arguments.

(https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-definition ).

Δωρεάν Μαθήματα και Σεμινάρια Spring Framework

Επίσης στο documentation μπορείτε να δείτε ένα γενικό κώδικα που περιγράφει το Constructor-based Dependency Injection όπως και ένα παράδειγμα του Spring Configuration αρχείου.

(https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-dependencies)

Δωρεάν Μαθήματα και Σεμινάρια Spring Framework

Δωρεάν Μαθήματα και Σεμινάρια Spring Framework

Οπότε, ακολουθώντας και εμείς το documentation του Spring Framework, ορίσαμε με το constructor-arg property ότι όποτε το Spring Container θέλει να δημιουργήσει ένα αντικείμενο AthensFlight, και λόγο του dependency που υπάρχει στην κλάση, θα δημιουργήσει πρώτα ένα αντικείμενο από το myDailyOfferService (αυτό είναι το όνομα που δώσαμε εμείς στο id του DailyOfferServiceImpl bean) και αφού το κάνει inject τότε θα προχωρήσει με την δημιουργία του AthensFlight αντικειμένου. Με αυτό το configuration στην ουσία λέμε στο Spring Framework δημιούργησε ένα DailyOfferService bean, κάντο inject στο AthensFlight και δώσε μου το αντικείμενο από το AthensFlight.

Αν και μπορεί η εξήγηση με λόγια είναι κάπως μεγάλη για να κατανοήσετε την θεωρία, στην πραγματικότητα όμως γλυτώνετε πολύ κώδικα σε σύγκριση αν γράφατε την ίδια λογική χωρίς Spring.

Τώρα αν πραγματικά έχετε απορία τι ακριβώς κάνει το Spring για εσάς ή τι θα έπρεπε να γράφατε εσείς μόνο με την χρήση της Java τότε νομίζω ότι η παρακάτω εικόνα θα σας δώσει την απάντηση:

Δωρεάν Μαθήματα και Σεμινάρια Spring Framework

Ας γράψουμε λοιπόν έναν απλό client που ζητάει ένα αντικείμενο AthensFlight από το Spring Container και καλεί τις μεθόδους του.

HelloFlightApp.java


package com.mycompany.flights;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class HelloFlightApp {
	public static void main(String[] args) {
	ClassPathXmlApplicationContext context =
		new ClassPathXmlApplicationContext("flight.xml");
	Flight myFlight = context.getBean("myFlight", Flight.class);
	System.out.println(myFlight.getDestination());
	System.out.println(myFlight.getDailyOffer());
	context.close();

	}
}

	

Το POM.xml αρχείο είναι ως εξής:

POM.xml


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>org.example</groupId>
	<artifactId>SimpleApplicationContext</artifactId>
	<version>1.0-SNAPSHOT</version>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework</groupId>
				<artifactId>spring-framework-bom</artifactId>
				<version>5.2.7.RELEASE</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>



	<dependencies>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-core</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-beans</artifactId>
		</dependency>
	</dependencies>

	<properties>
		<maven.compiler.target>11</maven.compiler.target>
		<maven.compiler.source>11</maven.compiler.source>
	</properties>

</project>

	
Δωρεάν Μαθήματα και Σεμινάρια Spring Framework

Αν τρέξετε την εφαρμογή, θα πρέπει να δείτε κάτι αντίστοιχο με το παρακάτω output:

Output


Your final destination is Athens Greece
You have received 20% discount on your next trip to Athens