Spring Context and Dependency Injection

Überblick Spring Context and Dependency Injection

Mit Spring Context and Dependency Injection wurde 2003 ein Framework eingeführt, der JEE damals in vielen Bereichen überlegen war. Die grundsätzlichen Ideen wurden zwar größtenteils in den aktuellen Versionen Spezifikation nachgezogen, trotzdem bleibt Spring bis heute umfangreicher. Dieser Vorteil wird zwar durch eine Bindung an ein proprietäres Framework bezahlt, aber viele Architekten sind bereit, diese Konsequenz zu akzeptieren.

Spring Context and Dependency Injection unterstützt drei unterschiedliche Technologien:

  • XML
  • Annotations
  • Java Config

Diese können jederzeit untereinander beliebig gemischt werden.

XML

Die Definition der CDI-Objekte mit XML war der ursprüngliche Ansatz, der von der Spring Community gewählt wurde. Hierzu werden die Spring Beans in einer XML-Datei abgelegt:

<?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:p="http://www.springframework.org/schema/p"
	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-4.3.xsd">

	<bean class="org.javacream.books.warehouse.impl.MapBooksService"
		id="mapBooksService" scope="singleton">
		<property name="isbnGenerator" ref="randomIsbnGenerator" />
		<property name="storeService" ref="simpleStoreService" />
	</bean>
	<bean class="org.javacream.books.isbngenerator.impl.RandomIsbnGenerator"
		id="randomIsbnGenerator" scope="singleton" init-method="initGenerator"
		destroy-method="destroyGenerator">
		<property name="prefix" value="${isbngenerator.prefix}" />
		<property name="countryCode" value="${isbngenerator.countryCode}" />
	</bean>
	<bean class="org.javacream.store.impl.SimpleStoreService" scope="singleton"
		id="simpleStoreService" p:stock="${storeService.stock}" />
	<context:property-placeholder location="bookswarehouse.properties" />
</beans>

Mit XML können Anwendungen sehr elegant und kompakt definiert werden. Allerdings wird zur Unterstützung eine komplexe IDE wie IntelliJ oder die Spring Tools Suite benötigt. Sonst werden elementare Operationen der Software-Entwicklung wie Refactoring oder Code-Assistenten nicht unterstützt.

Annotations

Mit der Einführung der Annotationen mit Java 5 wurde die Definition der Spring Beans auch direkt aus dem Java Code heraus möglich. Dazu wurde ein Satz von Spring-spezifischen Annotations-Typen eingeführt:

  • @Component
  • @Service
  • @Repository

Diese Typen sind im stereotypes-Paket definiert und ermöglichen deshalb eine sinnvolle Gruppierung und Dokumentation der Anwendungs-Klassen. Prinzipiell würde die@Component-Annotation genügen.

Die Annotations-basierte Definition ist bei den Anwendungs-Programmieren sehr beliebt und wird in vielen Projekten bevorzugt benutzt.

Java Config

Dies ist der bisher neueste Ansatz, Spring Beans zu definieren. Hierzu wird eine mit @Configurationannotierte Klasse eingeführt, welche mit@Beanannotierte Factory-Methoden enthält. Der Spring-Kontext instanziert die diese Klasse und ruft diese Method je nach Bedarf auf. Wichtig ist hierbei, dass die @Configuration-Klasse sowie die Factory-Methoden selber ebenfalls wieder Dependencies aufweisen dürfen. Ein einfaches Beispiel ist hier noch gegeben:

package org.javacream;

import org.javacream.books.isbngenerator.api.IsbnGenerator;
import org.javacream.books.isbngenerator.impl.RandomIsbnGenerator;
import org.javacream.books.warehouse.api.BooksService;
import org.javacream.books.warehouse.impl.MapBooksService;
import org.javacream.store.api.StoreService;
import org.javacream.store.impl.SimpleStoreService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ApplicationConfiguration {

	@Bean public BooksService booksService() {
		MapBooksService booksService = new MapBooksService();
		booksService.setIsbnGenerator(isbnGenerator());
		booksService.setStoreService(storeService());
		return booksService;
	}
	@Bean public IsbnGenerator isbnGenerator() {
		RandomIsbnGenerator isbnGenerator = new RandomIsbnGenerator();
		isbnGenerator.setPrefix("JAVA-ISBN");
		isbnGenerator.setCountryCode("-is");
		return isbnGenerator;
	}
	@Bean public StoreService storeService() {
		SimpleStoreService storeService = new SimpleStoreService();
		storeService.setStock(42);
		return storeService;
}
}

Java Config scheint zwar ein Rückschritt zu sein, da nun ja wieder der Entwickler selber den Lebenszyklus der Fachobjekte übernimmt. Allerdings stehen auch hier die weiteren Spring-Features zur Verfügung, so dass dieser Ansatz eine durchaus sinnvolle Ergänzung ist.

Zusammenspiel mit Spring Boot

Spring Context and Dependeny Injection wird selbstverständlich komplett von Spring Boot unterstützt. Ohne weitere Konfiguration werden automatisch alle Pakete unterhalb und inklusive des Application-Hauptpakets nach Klassen mit Spring-Annotationen durchforstet und analysiert. Für das Einbinden einer XML-Konfiguration kann @ImportResource benutzt werden.

 


Seminar zum Thema

Weiterlesen

Konfiguration von Spring-Anwendungen

Konfiguration von Spring-Anwendungen

Die Konfiguration von Spring-Anwendungen ist ein elementarer Bestandteil des Frameworks. Damit können auch komplexeste Anforderungen in produktiven Anwendungen umgesetzt werden.

Berücksichtigt werden dabei die folgenden Features:

  • Die eigentliche Konfiguration der Anwendung
  • Die Konfiguration der Laufzeitumgebung sowie deren Überwachung
  • Die Unterstützung verschiedener Profile der Anwendung.

Konfiguration der Anwendung

Bei Spring Boot-Anwendungen wird hierzu automatisch eine Konfigurationsdatei geladen. Dabei werden simple Properties-Dateien (die application.properties)   oder YAML-Dateien (application.yml) unterstützt.

isbngenerator.prefix=ISBN:
isbngenerator.countryCode=-dk

Innerhalb der Anwendung werden Properties mit der @Value-Annotation definiert. Diese lesen über eine Spring Expression einen Wert aus der Konfigurationsdatei:

public class RandomIsbnGenerator implements IsbnGenerator {

	@Value("${isbngenerator.prefix}")
	private String prefix;
	@Value("${isbngenerator.countryCode}")
        private String countryCode;

Profiles

Mit Profiles werden unterschiedliche Stages der Anwendung konfiguriert. Dabei kann jegliche Konfiguration (XML, Annotations oder Java Config) zusätzlich mit der @Profile-Annotation einem benannten Profil zugeordnet werden.

Profile werden innerhalb der Konfiguration oder durch einen Aufruf-Parameter der Anwendung aktiviert.

Die oben bereits angesprochenen Konfigurations-Dateien werden automatisch mit Profiles ergänzt. Existiert beispielsweise ein Profile namens “test”, so wirde neben der application.properties-Datei auch eineapplication-test.propertiesgelesen.

Konfiguration der Laufzeitumgebung

Auch die Laufzeitumgebung der Anwendung kann konsequent in der application-Konfiguration erfolgen. Beispiele hierfür sind

  • Logging
  • Datenbank-URLs
  • Server-Ports
  • Aktive Profile

Im Folgenden Beispiel ist eine Konfiguration für Logging gegeben. Weiterhin wird das aktive Profile gesetzt:

logging.level.root=INFO
logging.pattern.console= "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
spring.profiles.active=test

 

Setup für Test-Datenbanken

Wird eine Spring-Boot-Anwendung mit einer Datenbank-Anbindung konfiguriert, so können automatisch Skripte zur Schema-Erstellung sowie zum initialen Befüllen der Tabellen ausgeführt werden. Diese Dateien sind die schema.sql bzw. die data.sql.

Beispiel: Eine schema.sql

create table AUDIT (auditmessage varchar(2048))
create table ISBNCOUNTER (counter integer)

Beispiel: Eine data.sql

insert into ISBNCOUNTER values(1)

Auch hier ist wiederum eine Unterscheidung verschiedener Stages möglich. Dies wird jedoch nicht über ein Profil geregelt sonder durch den Eintrag einer Platform in der Anwendungs-Konfiguration. Die folgende Konfiguration zeigt eine komplette Datenbank-Konfiguration für eine Test-Datenbank:

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=sa
spring.datasource.platform=test
spring.jpa.hibernate.ddl-auto=create

 

Gelesen werden dann die beiden Dateienschema-test.sqlund data-test.sql.

Der Spring Konfigurationsserver

In einer Cloud-basierten Lösung können die Konfigurationsdateien von einem zentralen Server, dem Spring Cloud Config Server, geliefert werden. Die Standard-Implementierung des Spring-Cloud-Projekts liest die Konfigurationswerte einfach aus einem Git-Repository. Werden Änderungen in die Versionsverwaltung gepushed so wird damit die Anwendung neu konfiguriert. Eine wirklich interessante Lösung! Selbstverständlich können die Konfigurationen aber auch klassisch in einer Datenbank oder dem Dateisystem abgelegt werden.


Seminar zum Thema

Weiterlesen

Spring Data

Spring Data

Spring Data ist ein zentrales Projekt des Spring Frameworks und besticht durch seine Einfachheit und Konsistenz. So können selbst komplexe Datenabfragen mit wenigen Zeilen an Quellcode erstellt werden. Insbesondere im Zusammenspiel mit Spring Boot ist es so möglich, mit wenigen Programmzeilen eine komplette Anwendung wie beispielsweise einen Restful WebService zu realisieren.

Eine Übersicht

Der Begriff “Datenabfragen” ist im Übrigen nicht zufällig so allgemein gewählt: Spring Data selbst definiert nur eine relativ schlanke Abstraktionsschicht auf einen Datenbestand. So besitzen alle Datenobjekte eine eindeutige ID und werden über ein Repository mit  CRUD-Operationen verwaltet.

Alle weiteren Details sind abhängig von der konkret verwendeten Datenbank-Technologie. So unterscheidet sich selbstverständlich ein konkretes JPA-Repository mit einer transaktionsfähigen relationalen Datenbank im Hintergrund von einem Mongo-Repository, bei dem eine Dokumenten-orientierte Datenbank aus dem NoSQL-Umfeld angesprochen wird.

Spring Data Projekte

Die Spring-Entwickler haben damit nicht den Fehler gemacht zu versuchen, jegliche Datenzugriffs-Technologien allgemein zu abstrahieren. Dieser Versuch ist mit der allgemeinen JEE-Connector-Spezifikation bereits misslungen. Statt dessen gruppiert Spring Data verschiedene Implementierungen mit durchaus unterschiedlichen APIs unter einem gemeinsamen Namen.

Die Anzahl der Projekte ist selbst in der offiziellen Spring-Distribution bereits sehr umfassend und wird durch die Community permanent weiter ergänzt.

Gemeinsam ist all diesen Teilprojekten jedoch die grundsätzliche Arbeitsweise: Entitäten werden über ein dynamisch generiertes Repository im Storage abgelegt.

Ein einfaches Beispiel mit Spring Data JPA

Diese Arbeitsweise wird im folgenden Beispiel näher erläutert:

Wir beginnen hierzu mit einer einfachen Daten-Entität:

@Entity
@Table(name="PEOPLE")
public class Person {
	
	private String lastname;
	
	private String firstname;
	private int height;
	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
        private Long id;

...
}

Zu beachten ist hierbei bereits, dass diese Entität die normalen Annotationen des Java Persistence APIs benutzt. Insbesondere kann @javax.persistence.Id benutzt werden, die Verwendung von @org.springframework.data.annotation.Id ist optional.

Das Repository selber ist für eine einfache Anwendung komplett trivial:

public interface PeopleRepository extends CrudRepository<Person, Long>{

}

Die Methoden des CrudRepository werden von Spring zur Laufzeit automatisch generiert, so dass das Repository ohne jegliche eigene Programmierung injiziert und sofort benutzt werden kann. Es ist nicht notwendig, das umfangreiche API zu implementieren! Zur Laufzeit generiert Spring hierfür einen generischen Proxy, der die Aufrufe in geeigneter Form an den gekapselten JPA-EntityManager delegiert.

Dies ist übrigens keine Prototyp- oder Dummy-Implementierung! Es ist weder notwendig noch gebräuchlich noch sinnvoll, für den  produktiven Einsatz diese Methoden durch eigene Sequenzen zu ersetzen und damit zu “verbessern”.

Genügen die im Basis-Interface definierten Methoden nicht, können diese durch eigene ergänzt werden. Hierbei werden folgende Möglichkeiten bereitgestellt:

  • Namenskonventionen: Eine Methode mit der Signatur findByLastnameOrderedByHeight(String lastname) kann einfach im Interface deklariert werden. Eine Implementierung ist nicht notwendig, da das im Endeffekt zu generierende JQL-Statement wohl offensichtlich ist.
  • Sollen diese Namenskonvention nicht verwendet werden, kann statt dessen eine Methode beliebiger Signatur mit der @Query-Annotation versehen werden.
    @Query("SELECT p FROM Person p WHERE p.lastname = :lastname order by p.height")
    public Collection<Person> getByLastname(@Param("lastname") String lastname);

    Statt der Annotationen können auch Named Queries der orm.xml benutzt werden.

  • Und schließlich kann das Interface in einer abstrakten Klasse selbst programiert werden. Dabei werden aber selbstverständlich nur die besonders zu behandelnden Fälle implementiert.

Seminar zum Thema

Weiterlesen

DockerDuke

Docker und Java – Teil 4: Ein RESTful Web Service mit Spring Boot

Im letzten Teil dieser Serie programmieren wir einen RESTful Web Service mit Spring Boot und stellen diesen über ein Docker-Image zur Verfügung. Dabei stützen wir uns auf den im dritten Teil beschriebenen Build-Prozess.

Spring

Was ist Spring Boot?

Mit Hilfe von Spring Boot können auch komplexe Server-Anwendung als einfaches Java-Archiv ausgebracht und gestartet werden. Dazu werden durch einen ausgefeilten Build-Prozess die Anwendungsklassen zusammen mit allen notwendigen Server-Bibliotheken in ein einziges ausführbares Java-Archiv gepackt.

Der Build-Prozess selbst wird wie üblich mit Hilfe eines Maven-Parents definiert. Dieser wird von der Spring-Community zur Verfügung gestellt.

<project
<groupId>org.javacream</groupId>
<artifactId>org.javacream.training.spring-boot-docker</artifactId>
<version>0.1.0</version>
<packaging>pom</packaging>
<name>Spring Boot Docker</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.1.RELEASE</version>
<relativePath />
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Benutzen wie dieses POM als Parent für das im 3. Teil der Artikelreihe benutzten Parents, so haben wir den Docker- und den Spring-Boot-Build vereint. Mehr ist tatsächlich nicht zu tun! Im  folgenden ist die vollständige Parent-POM dieser Anwendung gegeben, ergänzt um die (hier noch nicht benutzten) Abhängigkeiten zu Spring Data JPA und einer MySQL-Datenbank.

<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.javacream.training.docker</groupId>
	<artifactId>org.javacream.training.docker.parent</artifactId>
	<version>1.0</version>
	<packaging>pom</packaging>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.1.RELEASE</version>
		<relativePath />
	</parent>
	<properties>
		<docker.namespace.prefix>javacream</docker.namespace.prefix>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			<!-- tag::plugin[] -->
			<plugin>
				<groupId>com.spotify</groupId>
				<artifactId>docker-maven-plugin</artifactId>
				<version>0.4.11</version>
				<configuration>
					<imageName>${docker.image.prefix}/${project.artifactId}</imageName>
					<dockerDirectory>src/main/docker</dockerDirectory>
					<resources>
						<resource>
							<targetPath>/</targetPath>
							<directory>${project.build.directory}</directory>
							<include>${project.build.finalName}.jar</include>
						</resource>
					</resources>
				</configuration>
			</plugin>
			<!-- end::plugin[] -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-dependency-plugin</artifactId>
				<executions>
					<execution>
						<id>copy-dependencies</id>
						<phase>package</phase>
						<goals>
							<goal>copy-dependencies</goal>
						</goals>
						<configuration>
							<outputDirectory>${project.build.directory}/libs</outputDirectory>
							<overWriteReleases>false</overWriteReleases>
							<overWriteSnapshots>false</overWriteSnapshots>
							<overWriteIfNewer>true</overWriteIfNewer>
						</configuration>
					</execution>
				</executions>
			</plugin>

			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>

			<plugin>
				<groupId>com.spotify</groupId>
				<artifactId>docker-maven-plugin</artifactId>
				<version>0.4.11</version>
				<configuration>
					<imageName>${docker.namespace.prefix}/${project.artifactId}</imageName>
					<dockerDirectory>src/main/docker</dockerDirectory>
					<resources>
						<resource>
							<targetPath>/</targetPath>
							<directory>${project.build.directory}</directory>
							<include>${project.build.finalName}.jar</include>
						</resource>
						<resource>
							<targetPath>/</targetPath>
							<directory>${project.build.directory}</directory>
							<include>libs/*.jar</include>
						</resource>
					</resources>
					<imageTags>
						<imageTag>${project.version}</imageTag>
						<imageTag>latest</imageTag>
					</imageTags>
				</configuration>
			</plugin>
		</plugins>
	</build>
	<distributionManagement>
		<repository>
			<uniqueVersion>false</uniqueVersion>
			<id>nexus</id>
			<name>Corporate Repository</name>
			<url>http://localhost:8081/repository/maven-releases/</url>
			<layout>default</layout>
		</repository>
		<snapshotRepository>
			<uniqueVersion>true</uniqueVersion>
			<id>nexus</id>
			<name>Corporate Snapshots</name>
			<url>http://localhost:8081/repository/maven-snapshots/</url>
			<layout>legacy</layout>
		</snapshotRepository>
	</distributionManagement>
</project>

 

Ein RESTful Web Service

RESTful Web Services werden in Java meistens mit Annotationen realisiert. Dabei wird eine URL auf eine Java-Methodensignatur abgebildet. Dies erfolgt meistens durch Annotationen, entweder mit JAX-RS oder mit den RequestMappings aus Spring-MVC. Im folgenden Beispiel benutzen wir den zweiten Ansatz:

package org.javacream.training.rest.spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class Application {

	
	@RequestMapping(path = "/echo/{message}", method = { RequestMethod.GET })
	public String echo(@PathVariable("message") String message) {
		return "Echoing from Server: " + message;
	}
}

 

Diese Java-Klasse kann aus Eclipse heraus sofort gestartet werden und anschließend beispielsweise mit Hilfe des curl-Kommandos getestet werden:

curl -X GET http://localhost:8080/echo/Hello

Ein Maven-Build des Projekts erzeugt das Java-Archiv bzw. lädt es in das Artefakt-Repository

mvn package
mvn deploy

Ein Aufruf ist dann unter der Angabe des Java-Archivs möglich:
java -jar org.javacream.training.rest.spring.basic-0.1.0.jar

Natürlich kann das Archiv auch unbenannt werden, dann ist der Aufruf noch einfacher:
java -jar app.jar

Wichtig ist hier, dass das gebildete Artefakt alle notwendigen Bibliotheken mitbringt, um den Web Server für die http-Requests zu starten. Das Archiv ist vollständig.

Der Docker-Build

Zum Erstellen des Docker-Images brauchen wir nun nur noch ein Dockerfile! Dessen Inhalt ist aus den vorherigen Ausführungen heraus allerdings schon fast trivial:

  1. Als Basis nehmen wir eine Java-Grundinstallation.
  2. Dann kopieren wir noch das generierte Artefakt und
  3. definieren als EntryPoint den Java-Aufruf.
  4. Eine Port-Mapping oder ein Mounten des Log-Directories des Containers ist selbstverständlich noch möglich.
FROM openjdk:latest
ADD org.javacream.training.rest.spring.basic-0.0.1.jar app.jar
ENTRYPOINT java -jar app.jar

Der RESTful Web Service wird nun ganz normal gestartet:

docker run --rm javacream:org.javacream.training.rest.spring.basic:0.0.1

und könnte wieder über curl getestet werden.

Soll das Image noch auf das Artefakt-Repository geschoben werden genügt ein

mvn docker:push

Das funktioniert aber natürlich nur, wenn ein eigenes Repository betrieben wird. Aber auch dieses Problem ist bereits gelöst: Nexus und Artefactory unterstützen Docker ganz analog zu Java-Artefakten.

Damit steht das Image anderen Entwicklern, der Test&QS-Abteilung oder den System-Administratoren der Produktionsumgebung zur Verfügung.


Dieser Artikel ist der Abschluss meiner Reihe über “Docker und Java”. Wer mehr über den Einsatz von Java in einer Microservice-Systemarchitektur lesen möchte: Im Frühjahr 2017 erscheint eine Reihe von Artikeln zum Thema “Microservice-Architekturen mit Docker”.


Seminare zum Thema

 

Weiterlesen