Swift und Java

Java und Apple Swift

Apple Swift – Eine moderne Programmiersprache

Mit der Programmiersprache Swift hat Apple ein modernes Werkzeug für die Anwendungsentwicklung für Mac und iOS bereit gestellt. Diese Sprache löst das von vielen Entwicklern als etwas gewöhnungsbedürftig und sperrig betrachtete Objective C ab.  Mit der aktuellen Version 3 steht eine erste vollständige und unabhängige Implementierung zur Verfügung. Die anstehende Version 4 wird weitere Features einführen, jedoch keine Änderungen an der Core-Sprache mehr vornehmen.

Swift wurde von Apple als Open Source-Initiative freigegeben und steht damit auch für andere Plattformen zur Verfügung, z.B. für Ubuntu. Eine durchaus beachtenswerte Abkehr von der bisherigen Abschottung der Apple-Produkte. Allerdings gilt dies natürlich nur für den Kern der Sprache, die App-Programmierung muss aus Lizenz-Gründen weiterhin unter Apple Hardware und Betriebssystem erfolgen. Diese Einschränkung ist für diesen Artikel allerdings nicht relevant, da ich mich hier bewusst auf den Vergleich der Programmiersprachen konzentrieren möchte; für einen (durchaus interessanten!) Vergleich von Google Android  und Apple Swift gibt es andere Artikel.

Swift vereinigt etablierte Konzepte aus Sprachen wie Java, C#, JavaScript und natürlich auch Objective C und ergänzt diese durch eigene Ideen. Damit finden Programmierer einesteils recht viel Bekanntes, aber eben auch unerwartete und damit überraschende Konstrukte.  Hier möchte ich auf die Unterschiede zu Java eingehen, die aus meiner Erfahrung heraus relevant sind.

Swift und Java – Welche Konzepte sind interessant?

Für Java-Entwickler ist Swift auch deshalb interessant, weil einige Konzepte durchaus auch in einer zukünftigen Java-Version Einzug halten könnten. Weiterhin gibt es doch eine Reihe von Besonderheiten, die den Einstieg in Swift erschweren könnten.

Konsequente Type Inference

Swift ist wie Java streng und statisch typisiert. Damit prüft der Swift-Compiler, dass einer einmal deklarierten Variable kein Wert eines anderen Datentyps zugewiesen werden kann.

Allerdings ist eine Typ-Angabe bei der Deklaration optional; der Typ kann meistens aus dem Typen der Zuweisung bestimmt werden:

 

var message = "Hello" //message is a String
message = "Hello!"    //OK
//message = 42        //Compiler Error

var message2:String   //No type inference

Optionals

Sehr gut gelöst ist meiner Meinung nach der Umgang mit Null-Werten, in Swift nilgenannt. Einer Variablen muss ein Nicht-Null-Wert zugewiesen werden, es sei denn, sie wird explizit als Optional-Typ deklariert:

var message = "Hello"          //message non optional String
//message = nil                //Compiler error

var message2:String? = "Hello" //message2 an optional String
message2 = nil                 //OK

Optionals sind in Swift eigene Typen, so dass der Compiler bei Zuweisungen die normale Typ-Prüfung durchführt und Fehler erkennt:

//message = message2  //Compiler error: String? not of type String

Optionale Typen müssen vor ihrer weiteren Verwendung “ausgepackt”=unwrapped werden. Dafür gibt es in Swift zwei Möglichkeiten:

  • Explizites Unwrap mit dem !-Operator
  • Sicheres Unwrap über die if -let -Konstruktion:
message = message2! 

if let x = message2 {
   print (x)
}

Obwohl diese Sequenzen für einen Java-Entwickler erst einmal merkwürdig aussehen liegt der Vorteil auf der Hand: Bereits der Compiler erkennt  Zuweisungen von Null-Werten, so dass der Entwickler auf null-Prüfungen oder das Fangen einer NullPointerException verzichten kann.

Und dann haben wir noch das “Optional Chaining” beim Zugriff auf Eigenschaften eines Objekts:

if (message2!.hasPrefix("H"){ //Unsafe
   //...
}

if (message2?.hasPrefix("H"){ //Safe
   //...
}

Falls message2 nil sein sollte wird die hasPrefix-Methode nicht ausgeführt.

Klassen und Strukturen in Swift

Swift kennt zwei unterschiedliche Definitionen eines benutzerdefinierten Datentyps: Klassen und Strukturen.

struct Address{
    var city: String, street: String
}
class Person{
    private var name:String
    init(name: String){
        self.name = name
    }
    var Name: String{
        get {
            return name
        }
    set {
            print ("setting lastname...")
            name = newValue
        }
    }
    func info() -> String {
        var greeting =  "Hello, my name is \(name)"
        return greeting
    }
}

Während Instanzen von Klassen wie in Java stets über Referenzen angesprochen werden, werden Strukturen immer als Werte angesehen:

let address1 = Address(city: "München", street: "Marienplatz")
let address2 = address1

let person1 = Person(name: "Hugo")
let person2 = person1
Swift - Der Unterschied zwischen Strukturen und Klassen
Der Unterschied zwischen Strukturen und Klassen

Ganz besonders verwirrend für Java-Entwickler ist, dass alle Collections-Implementierungen in Swift als Strukturen realisiert wurden!

Weitere Besonderheiten sind eher unter “syntactic sugar” zu sehen:

  • Einfache Definition von Properties durch get– und set-Blöcke
  • Bei Strukturen werden die Konstruktoren automatisch angelegt

Extensions

Alle Klassen und Strukturen sind in Swift “Open for Extension”. Das bedeutet, dass auch ohne Vererbung vorhandene Klassen jederzeit erweitert werden können. Nur zur Klarstellung: Dies gilt selbstverständlich auch für die Klassen der Standard-Bibliothek!

Zur Definition wird das Schlüsselwort extension benutzt. Im Folgenden eine (zugegebenermaßen triviale) Erweiterung der Person-Klasse sowie ein neuer Konstruktor für String:

extension Person{
    func sayHello{
        return "Hello",
    }
}

extension String{
    init?(_ address: Address){
        self.init("Die Adresse \(address.city)")
    }
}

 

Extensions können eine Klasse nicht mit neuen Attributen ausstatten. get– und set-Blöcke können jedoch verwendet werden.

Verschiedenes

  • Neben Klassen und Strukturen unterstützt Swift auch Enums und Tuples
  • Als Zeichen für Namen von Variablen oder Funktionen können beliebige Unicode-Zeichen benutzt werden.
  • Funktionsparameter haben eine  internen und gegebenenfalls einen öffentlichen Namen, der beim Aufruf mitgegeben werden muss:
    func func1(message: String){
       print(message)
    }
    
    func1(message: "Hello")
    //func1("Hello) //compiler error
    
    func func2(message m: String){
       print(m)
       //print (message)
    }
    
    func2(message: "Hello")
    //func2("Hello) //compiler error
    //func2(m: "Hello) //compiler error
    

    Sollen die Parameter nicht benannt sein wird der Unterstrich als öffentlicher Name benutzt:

    func func3(_ message: String){
       print(message)
    }
    
    func3("Hello")
    //func3(message: "Hello") //compiler error
  • Die Swift-Installation enthält eine REPL, so dass Sequenzen und einfache Anwendungen auch ohne Installation einer Entwicklungsumgebung ausgeführt werden können.
  • Schnittstellen heißen in Swift protocol, nicht interface.
  • Mit der String Interpolation werden Variablen bzw. Ausdrücke direkt in Zeichenketten ausgewertet:
let multiplier = 3.0
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message is "3 times 2.5 is 7.5"
  • Operatoren können jederzeit geändert oder für eigene Datentypen definiert werden
func + (left: Int, right: Int) -> Int{ //just for fun
    return left - right
}

func == (left: Person, right: Person) -> Bool{ //all people are equal
    return true
}
func + (left: Person, right: Person){ //adding to people = marriage, suggestions for '-'?
    left.marry(right)
}
  • Auch neue Operatoren sind einfach zu deklarieren. So wird im folgenden Beispiel das Unicode-Zeichen für das Herz-Symbol als Operator zwischen zwei Personen definiert:
infix operator ❤ {associativity left precedence 200}

func ❤ (left: Person, right: Person) -> Person{
    left.marry(right)
    return left
}
//In action:

rainer ❤ carola

 

Die Beispiele

Die im Artikel beschriebenen Code-Fragmente stehen über meinen GitHub-Account zur freien Verfügung und können gerne für eigene Experimente benutzt werden. Die Sourcen gibt es hier.

Die Beispiele benötigen eine Swift-Installation, ich habe hierzu die Ubuntu-Installation benutzt.


Seminar zum Thema

Weiterlesen

Project Lombok - Java scharf gewürzt

Project Lombok – Java Klassen scharf gewürzt

 Die Motivation hinter Project Lombok

Was steckt eigentlich hinter dem “Project Lombok”? Jeder Java-Entwickler kennt das Problem bei der Erstellung einer einfachen Datenstruktur. Die eigentliche Modellierung und Definition der Attribute einer Klasse ist erst einmal einfach:

public class Person {

	private String name;
	private int height;
}

Anschließend müssen dann aber erst einmal aufwändig weitere notwendige Elemente geschrieben werden:

  • Getter- und Setter-Methoden
  • Konstruktoren
  • hashCode und equals
  • toString
public class Person {

	private String name;
	private int height;
	@Override
	public String toString() {
		return "Person [name=" + name + ", height=" + height + "]";
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + height;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Person other = (Person) obj;
		if (height != other.height)
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}
	public int getHeight() {
		return height;
	}
	public void setHeight(int height) {
		this.height = height;
	}
	public String getName() {
		return name;
	}
	public Person(String name, int height) {
		super();
		this.name = name;
		this.height = height;
	}
}

 

Ohne die Hilfe einer Entwicklungsumgebung wie Eclipse, IntelliJ oder ähnliches wäre dieser Aufwand absurd hoch!

Ausschnitt des Kontext-Menüs zur Code-Generierung in Eclipse
Ausschnitt des Kontext-Menüs zur Code-Generierung in Eclipse

Die Intention des Lombok-Projekts  ist es, diesen Aufwand zu vermeiden. Insbesondere bei Änderungen der Datenstruktur müssten ja sonst alle diese Aktionen nochmals neu ausgeführt werden, damit Inkonsistenzen vermieden werden. Mit Lombok muss der Entwickler nur noch Annotationen definieren, die bei jedem Compiler-Lauf neu ausgewertet werden und folglich den aktuellen Stand repräsentieren.

Ein Beispiel mit Project Lombok

Betrachten wir doch einmal das Outline der oben dargestellten Klasse Person und vergleichen diese mit dem der Klasse LombokPerson

Ein Vergleich der Struktur zweier Klassen zeigt keine Unterschiede
Ein Vergleich der Struktur zweier Klassen

Selbst bei genauer Betrachtung lässt sich hier, außer dem Namen der Klasse, kein Unterschied erkennen. Nun aber der Quellcode der Klasse LombokPerson:

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class LombokPerson {

	private final String name;
	private int height;
}

Das ist tatsächlich alles! Keine Konstruktoren, keine Setter etc. Das alles wurde automatisch an Hand der Annotationen erzeugt.

Details zur Arbeitsweise

  • Lombok stellt einen Annotation Processor zur Verfügung, der die notwendigen Code-Elemente automatisch erstellt. Dafür ist es nur notwendig, die lombok.jar dem Projekt hinzuzufügen und über die IDE das Annotation Processing zu aktivieren. Dann wird der Code generiert und anschließend compiliert.
  • Falls der Entwickler tatsächlich den generierten Java-Code benutzen möchte, kann hierfür delombok benutzt werden.
  • Die Code-Generierung kann durch die Angabe weitere Annotationen entsprechend fein angepasst werden. Damit können beispielsweise die in hashCode und equals auszuwertenden Attribute definiert oder der parameterlose Konstruktor generiert werden.
  • Zur Integration in Entwicklungsumgebungen steht in der lombok.jar eine ausführbare Installationsroutine zur Verfügung.

 

Der Installer für Project Lombok
Der Lombok-Installer, hier für eine Eclipse-Installation
  • Selbstverständlich kann Lombok auch in automatisierte Buildprozesse integriert werden. Für einen Maven-getriebenen Prozess genügt es beispielsweise, die Abhängigkeit auf Lombok in der POM einzutragen:
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>0.9.2</version>
        </dependency>
    </dependencies>

     

Bewertung

Mit Lombok steht meiner Meinung nach ein wirklich sinnvolles Werkzeug zur Verfügung, das den Aufwand bei der Erstellung von Java-Datenklassen deutlich verringert. Programmierern, die sich auch im C#-, Swift oder Groovy-Umfeld bewegen, wird diese Art der Entwicklung sehr bekannt vorkommen. Java ist in diesem Bereich selbst in der Version 8 immer noch ziemlich altbacken, überdies ist eine echte Verbesserung leider auch in der Java 9 -Roadmap nicht erkennbar. Lombok wird deshalb auch noch mittelfristig sinnvoll eingesetzt werden.

Mit den “Experimental Features” werden weitere hochinteressante Elemente eingeführt, die allerdings teilweise einen Überlapp zur Aspekt-orientierten Programmierung aufweisen. Ob diese Annotationen in eigenen Projekten sinnvoll benutzt werden können muss individuell entschieden werden. Nicht vergessen: Project Lombok ist Open Source und folglich Community-getrieben! Welche Richtung dieses Projekt in Zukunft nehmen wird kann deshalb jeder aktiv mit beeinflussen.

Bildnachweis: pixabay ch1310 CC0 Public Domain


Seminar zum Thema

Weiterlesen

DockerDuke

Docker und Java – Teil 3: Der Build-Prozess

Im zweiten Teil der Reihe wurden die notwendigen Werkzeuge vorgestellt und installiert. Nun werden wir den Build-Prozess für Java-Projekte und die Erstellung der Docker-Images zusammenführen.

Teil 3: Der Build-Prozess

Build von Java-Projekten mit Apache Maven

Apache Maven ist neben Apache Ant mit Ivy und Gradle ein Standard-Werkzeug, das zum automatisierten Erzeugen von Java-Archiven benutzt werden kann. Allen Werkzeugen gemeinsam ist das Dependency Management: Projekt-Abhängigkeiten werden in einer Konfigurationsdatei abgelegt und vom Build-Werkzeug automatisch aus einem Artefakt-Repository geladen. Repositories werden von verschiedenen Anbietern frei zugänglich im Internet betrieben. Maven Central ist das wahrscheinlich bekannteste Beispiel.

Um die selbst erzeugten Artefakte selbst verwalten zu können, werden im Unternehmen meistens eigene Server betrieben. Dazu existieren sofort einsetzbare Produkte wie Sonatype NexusJFrog Artefactory oder Apache Archiva.

Entscheiden wir uns für einen Maven-basierten Build-Prozess erfolgt die allgemeine Konfiguration des Build-Prozesses mit zwei Dateien:

  • Die settings.xml enthält die URL des Artefakt-Repositories und gegebenenfalls Authentifizierungs-Informationen
<settings>
	<mirrors>
		<mirror>
			<id>nexus</id>
			<mirrorOf>*</mirrorOf>
			<url>http://xxxxx/repository/maven-public/</url>
		</mirror>
	</mirrors>
	<repositories>
		<repository>
			<id>central</id>
			<url>http://central</url>
			<releases><enabled>true</enabled></releases>
			<snapshots><enabled>true</enabled></snapshots>
		</repository>
	</repositories>
	<pluginRepositories>
		<pluginRepository>
			<id>central</id>
			<url>http://central</url>
			<releases><enabled>true</enabled></releases>
			<snapshots><enabled>true</enabled></snapshots>
		</pluginRepository>
	</pluginRepositories>
	<servers>
		<server>
			<id>nexus</id>
			<username>xxx</username>
			<password>xxx</password>
	</server>
	</servers>
</settings>

 

Ein Parent-POM enthält neben allgemeinen Informationen für alle Build-Prozesse auch die Informationen zum Ausbringen der Artefakte in das Repository

<distributionManagement>
<repository>
<uniqueVersion>false</uniqueVersion>
<id>nexus</id>
<name>Corporate Repository</name>
<url>http://…</url>
</repository>
<snapshotRepository>
<uniqueVersion>true</uniqueVersion>
<id>nexus</id>
<name>Corporate Snapshots</name>
<url>http://…</url>
</snapshotRepository>
</distributionManagement>

 

Nach all diesen Vorbereitungen ist der Build-Prozess eines eigenen Projekts sehr einfach: Es wird ein Projekt-POM definiert, das im einfachsten Fall nur den Parent angeben muss.

<project>
<modelVersion>4.0.0</modelVersion>
<groupId>org.javacream.training</groupId>
<artifactId>org.javacream.training.business</artifactId>
<version>1.0</version>
<parent>
<groupId>org.javacream.training.docker</groupId>
<artifactId>org.javacream.training.docker.parent</artifactId>
<version>1.0</version>
</parent>
</project>

 

Trotz dieser wirklich sehr einfachen Konfiguration kann das Java-Projekt sofort bis hin zu verschiedenen Phasen gebaut werden:

  • compile: Das Projekt wird kompiliert
  • package: Ein Java-Archiv wird im target-Verzeichnis des Projekts erzeugt
  • deploy: Das Archiv wird in das Unternehmens-Repository ausgebracht

So einfach funktioniert Maven!

Ein Maven System
Ein Maven System

Maven und Docker

Auch Docker definiert einen Build-Prozess: Images werden aus dem Dockerfile erzeugt. Was liegt also näher, als diesen Prozess mit Maven zu integrieren? Diese Idee wird durch Maven-PlugIns für Docker umgesetzt. Hierzu stehen sogar verschieden Produkte zur Auswahl:

 

  • Durch das Parent-POM ist die Integration dieser PlugIns für den Entwickler vollkommen transparent! Wird beispielsweise das Spotify-PlugIn in den Parent aufgenommen

 

<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>

 

so stehen nun neue Maven-Befehle zur Verfügung, insbesondere

  • docker:build: Erzeugen des Images aus dem Dockerfile des Projekts
  • docker:push: Pushen des Images in ein Docker-Repository

Damit ist die gewünschte Integration der Build-Prozesse durchgeführt. Und nachdem Nexus oder Artefactory neben Java-Artefakten auch Docker-Images verwalten können, ist unser System praktisch fertig! Dass die Quellcodes, das POM und das Dockerfile in einem Versionsverwaltungssystem abgelegt werden und auf ein Build-Server wie Jenkins die Builds automatisiert ablaufen lassen wird, ist selbstverständlich.

Die vollständige Parent-POM kann hier angezeigt werden:

<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>
	<properties>
		<docker.namespace.prefix>javacream</docker.namespace.prefix>
	</properties>
	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-core</artifactId>
			<version>2.8</version>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-dependency-plugin</artifactId>
				<version>3.0.0</version>
				<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>
				<version>3.6.1</version>
				<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>
						<imageTag>localhost:5000/${project.build.finalName}</imageTag>
					</imageTags>
				</configuration>
			</plugin>
		</plugins>
	</build>
	<distributionManagement>
		<repository>
			<uniqueVersion>false</uniqueVersion>
			<id>nexus</id>
			<name>Corporate Repository</name>
			<url>http://10.44.1.101:8081/repository/maven-releases/</url>
			<layout>default</layout>
		</repository>
		<snapshotRepository>
			<uniqueVersion>true</uniqueVersion>
			<id>nexus</id>
			<name>Corporate Snapshots</name>
			<url>http://10.44.1.101:8081/repository/maven-snapshots/</url>
			<layout>legacy</layout>
		</snapshotRepository>
	</distributionManagement>
</project>

 


Im vierten und letzten Teil wird dann programmiert: Wir erstellen einen Microservice auf Basis von Spring Boot!


Seminare zum Thema

 

Weiterlesen

DockerDuke

Docker und Java Teil 2 – Installation und erstes Arbeiten

Nachdem wir uns im ersten Teil der Reihe um Docker gekümmert haben geht es im zweiten Teil um die Installation der Werkzeuge, die für ein effizientes Arbeiten mit Docker und Java notwendig sind.

Teil 2: Installation und erstes Arbeiten

SUSE Linux Enterprise

Schritte

Die Installation von Docker auf verschiedenen Plattformen ist in der Dokumentation sehr gut beschrieben. Dabei ist auf Linux-Systemen wie SUSE Enterprise diese Routine sehr einfach durchzuführen:

  1. Hinzufügen des Docker-Repositories mit sudo zypper addrepo https://yum.dockerproject.org/repo/main/opensuse/13.2/ docker-main
  2. Refresh des SUSE Package Managers mit sudo zypper refresh
  3. Die eigentliche Installation erfolgt mit sudo zypper install docker-engine

Während der Installation werden eventuell einige Warnungen bezüglich Signaturen etc. angezeigt, die aber einfach akzeptiert werden können.

Docker ist hiermit installiert, wie einfach überprüft werden kann:

docker -v
mit der Ausgabe
Docker version 1.13.1, build 092cba3

Andererseits scheitern alle anderen docker-Befehle an fehlenden Berechtigungen. Dies liegt aber nur daran, dass der Docker-Dämon als root-Benutzer ausgeführt wird. Zur Lösung gibt es zwei Möglichkeiten:

  • Arbeiten mit sudo. Dies ist ziemlich “quick & dirty” und auf Dauer nicht genügend.
  • Hinzufügen von Usern zur Gruppe docker. Dies ist die saubere Lösung, die jetzt beschrieben wird:
    1. Erstellen der Gruppe docker mit sudo groupadd docker. Allerdings ist diese  Gruppe wahrscheinlich bereits bei der Installation angelegt worden.
    2. Anschließend wird der User  mit sudo gpasswd -a ${USERNAME} docker hinzugefügt.

Nach einer Neuanmeldung am System ist die Installation komplett. Der Start des Dämons erfolgt mit: sudo service docker start.

Was ist während der Installation alles passiert?

  • Es wurde der Docker-Dämon installiert und gestartet. Dieser verwaltet alle Images und Container. Außerdem ist er in der Lage, über https mit dem Docker-Hub-Repository zu kommunizieren.
  • Es wurde ein lokales Repository für Images und Container angelegt. Dieses befindet sich in /var/lib/docker
  • Der Docker-Client ist ein Konsolen-Programm, mit dessen Hilfe an den Docker-Dämon Befehle gesendet werden können. Dieser Client ist für einen Benutzer das primäre Werkzeug.
Die Bestandteile von Docker im Zusammenspiel

Kurzübersicht wichtiger Docker-Kommandos

Der Befehlssatz des Docker-Clients ist umfangreich, eine detaillierte Beschreibung ist in der Referenz-Dokumentation aufgeführt. Wichtige Befehle sind:

  • pull: Lädt ein Image vom entfernten Repository
  • create: Erzeugt einen Container
  • start|stop: Start/Stop eines Containers
  • run: Erzeugt einen Container und startet diesen sofort
  • build: Erstellt ein Image

Eclipse und das Docker-PlugIn

Eclipse wird für SUSE Linux  als Archiv zur Verfügung gestellt. Nach dem Download wird dieses anschließend extrahiert. Dann wird das Docker-PlugIn installiert. Damit stehen schließlich innerhalb der Java-Entwicklungsumgebung Views zur Benutzung von Docker-Befehlen zur Verfügung. Oder anders formuliert: Ein Java-Entwickler benutzt das PlugIn mit Dialogen und Kontextmenüs statt des Konsolen-basierten Docker-Clients. Weiterhin existiert ein eigener Editor, der das Format eines Dockerfiles unterstützt. Und schließlich werden in einer Übersicht alle vorhandenen Images und Container angezeigt.

Nach dem Öffnen der Docker Tooling Perspektive findet das PlugIn automatisch eine Verbindung zum Docker-Dämon auf dem lokalen Rechner. Sollte dies nicht der Fall sein, so ist wahrscheinlich der aktuelle Benutzer nicht Bestandteil der docker-Gruppe und hat folglich keine Zugriffs-Berechtigung. In diesem Fall prüfen Sie bitte Ihre Installation und melden sich anschließend nochmals neu an.

Erstes Arbeiten

Jetzt ist die Bühne bereitet, um ein erstes eigenes Java-Image zu erstellen und auszuführen!

Dazu erzeugen wir im Dockerfile-Editor die folgende Textdatei namens Dockerfile:

FROM openjdk:latest
ENTRYPOINT java -version

Ein Rechtsklick auf das Dockerfile öffnet das Kontextmenü:

Run As - Docker Image Build

Die Ausgaben zeigen zwei Schritte:

  1. Erstens wird das  openjdk-Layer geladen. Dies kann etwas dauern, da es einmalig von Docker-Hub geladen werden muss.
  2. Zweitens wird der EntryPoint hinzugefügt. Dies erzeugt faktisch ein weiteres Layer.

Entry Point

Genauso hätte das Erzeugen des Images auch über den Befehl
docker build -t javacream
erfolgen können.

Was haben wir nun alles im lokalen Repository? Das zeigt die Explorer-View:

 Explorer-View lokalen Repository

Neben unserem eben erzeugten Image wurde somit automatisch das in der FROM-Klausel angegebenen Basis-Image geladen.

Zum Abschluss erzeugen wir noch einen temporären Container, der das Image als Vorlage hat und anschließend gestartet wird. Dies erreichen wir, indem im Kontextmenü des Images Run... aufgerufen wird:

Temporärer Container mit Image als Vorlage

Mit der Ausgabe:

Log for Container

Auch hier hätte ein Kommando den gleichen Effekt gehabt, beispielsweise

docker run --rm javacream


Im nächsten Teil des Artikels geht es um das Build-Management. Wie kann das Erzeugen der Docker-Images automatisiert werden? Und wie werden Images über ein Repository zur Verfügung gestellt?


Seminare zum Thema

Konferenzen zum Thema

  • DevOpsCon
  • Die Konferenz für Continuous Delivery, Microservices, Docker, Clouds & Lean Business

Weiterlesen

DockerDuke

Docker und Java Teil 1 – Einführung in Docker

In diesem mehrteiligen Artikel möchte ich in einem pragmatischen Ansatz Docker und dessen Einsatz in Java-Projekten vorstellen.

Teil 1: Was ist eigentlich Docker?

Architektur

Docker ist eine Container-Lösung, die für eigene Anwendungen eine gekapselte und konfigurierbare Laufzeitumgebung zur Verfügung stellt.

Applications versus Container
Applikationen versus Container

Im Gegensatz zu einer Virtualisierungssoftware wie VMware Player oder Virtual Box sind diese Container jedoch leichtgewichtig, da sie kein komplettes Betriebssystem emulieren. Sie werden statt dessen “nur” in abgeschotteten Bereichen auf einem Host-System ausgeführt.

Layers und Images

Docker-Images werden durch so genannte “Layer” komponiert, wobei jedes Layer dem Image eine bestimmte Funktionalität oder Ressource (z.B. das Dateisystem oder Netzwerk) hinzufügt. Nachdem auf diese Art und Weise eine Basis geschaffen wurde, werden auf dem Image weitere Programme installiert und konfiguriert. Auf diese Art und Weise wird das Basis-Image modifiziert und dadurch effektiv um weitere Layers erweitert.

Hier als Beispiel die einzelnen Layers eines Images für einen MySQL-Datenbankserver:

MySQL-Image
MySQL-Image, visualisiert von MicroBadger

Beachten Sie hier insbesondere die Größe des Images und wie viel die einzelnen Layer dazu beitragen.

Images werden intern durch einen Hash-Wert eindeutig identifiziert. Dies ermöglicht folglich eine sehr effiziente Verwaltung: Haben zwei Images den gleichen Hash-Wert, so müssen diese zwangsläufig identisch sein. Deshalb können Images zentral in einem Repository verwaltet werden! Somit kann eine Docker-Installation auf dem Zielsystem einen lokalen Cache einrichten. Und zentrale Repositories sind im Internet verfügbar, als Paradebeispiel Docker Hub. Das Angebot ist übrigens sehr umfassend: Linux-Betriebssysteme, Datenbanken, Server-Lösungen…, ein Erkunden ist sehr zu empfehlen!

Die Installation eines Images auf einem eigenen Rechner ist brilliant einfach: Sie benötigen z.B. eine MySQL-Datenbank?

docker pull mysql

Das war es bereits!

Container

Docker-Container werden aus einem Image heraus erzeugt. Auch der Container selbst wird über einen Hash-Wert identifiziert. Folglich kann er in einem Cache auf dem Host-Dateisystem abgelegt werden. Der Container selber wird anschließend auf dem Host ausgeführt.

Beim Erzeugen des Containers aus einem Image können verschiedene Konfigurationen durchgeführt werden:

  • Port-Mappings: Enthält das Image beispielsweise einen Datenbank-Server, der auf dem Port 3306 lauscht, so kann der Host diesen Port auf 4306 bereitstellen.
  • Mounten von Dateisystemen: Ein beliebiges Verzeichnis des Hosts kann in den Container gemounted werden. Auch hier ist eine Datenbank ein gutes Beispiel: So kann beispielsweise das Log-Verzeichnis im Host-Dateisystem liegen. Nicht vergessen: Das Dateisystem des Containers ist komplett abgeschottet und deshalb sonst nicht zugreifbar!
  • Setzen von Environment-Variablen: Das Image definiert einen beliebigen Satz von Keys, die beim Erzeugen des Containers mit Values vorbelegt werden können. Welche Keys benutzt werden hängt natürlich vom Image ab. So könnte ein Datenbank-Container das Root-Password setzen lassen.

Auch hier wieder ein konkretes Beispiel: Wir erzeugen den MySQL-Container und starten diesen anschließend:

docker run -p 4306:3306 -v /my_log:/var/log/mysql -e MYSQL_ROOT_PASSWORD=pw mysql

Und schon läuft eine MySQL-Datenbank auf dem Port 4306 und verwendet intern das angegebene Root-Passwort. Als Log-Verzeichnis benutzt der Container das Verzeichnis /my_log des Hosts.


Im nächsten Teil des Artikels werden wir uns dann näher mit der Installation der Werkzeuge beschäftigen. Außerdem werden wir ein erstes eigenes Docker-Image erzeugen.


Seminare zum Thema

 

Weiterlesen