Categories
Development Java

Java Configuration

Die Konfiguration in Java war für mich jahrelang kein Problem, denn ich durfte mit einem Framework arbeiten, dass die Konfiguration sehr flexibel und komfortabel gelöst hat.

Beispielsweise konnte die URL für ein angebundenes System für die verschiedenen Stages ganz einfach in einer ini-Datei hinterlegt werden:

dev..othersystem.url=http://othersystem-dev.intranet.cnb
qa...othersystem.url=http://othersystem-qa.intranet.cnb
prod.othersystem.url=http://othersystem-prod.intranet.cnb

Diese ini-Datei ist Teil des Java Projekts und liegt im Classpath.

In der ini-Datei sind alle Informationen zu allen Stages zum OtherSystem gespeichert, was ich immer sehr übersichtlich und leicht zu pflegen fand.

Für die laufende Anwendung muss dann lediglich die Stage festgelegt werden, in welcher sie läuft und dann wird die Konfiguration passend zur Stage gezogen. Die Stage kann definiert werden über eine Konfiguration mit der Zuweisung über den HostName, eine System Property (zB im Tomcat definiert) oder über eine lokale Konfigurationsdatei.

Praktisch ist auch die Möglichkeit, über die lokale Konfigurationsdatei einzelne Konfigurationen überschreiben zu können. So ist es beispielsweise möglich, auf dem Entwicklerrechner in der DEV-Stage zu laufen, aber die Verbindung zur PROD DB zu konfigurieren um einen Bug zu reproduzieren.

localhost.othersystem.url=http://othersystem-prod.intranet.cnb

Das Thema Sicherheit lasse ich bewusst außen vor, denn hier soll es einzig um die Konfiguration gehen.

Als ich dann ein Projekt in einem anderen Kundenkreis startete, und das propritäre Framework nicht mehr verwenden konnte, war ich schon sehr erstaunt, dass es anscheinend keine schlanke, flexible Möglichkeit der Konfiguration im Java SE Umfeld gibt.

Sehr schnell bin ich an die Grenzen der Konfigurierbarbeit gekommen, wie zB bei der Konfiguration der Datenbankanbindung.

Also muss ich selbst etwas basteln, etwas kleines, leichtgewichtiges und trotzdem flexibles.

Anforderung

Von dem Luxus, sämtliche Konfigurationen per Präfix in verschiedenen Dateien im Projekt zu hinterlegen, muss ich mich verabschieden. Statt dessen wird es eine Konfigurationsdatei im Projekt geben, deren Konfiguration dann von außen überschrieben werden muss. Beispielsweise mit den Datenbankverbindungsparametern auf dem PROD Server.
Aufgrund der geringeren Komplexität des Projektes ist das aber durchaus ausreichend.

Die im Projekt hinterlegte Standard-Konfiguration soll über eine lokale Konfigurationsdatei überschrieben werden können.
Dazu muss eine Umgebungsvariable (System Environment, bzw. System Property) “localconf” gesetzt werden, die auf diese Datei zeigt.

Außerdem sollen einzelne Konfigurationen über Umgebungsvariablen (System Environment, bzw. System Property) gesetzt werden können.

In den Umgebungsvariablen stehen sehr viele Konfigurationen, wie zB JAVA_HOME,TMP, user.name etc., welche nicht direkt mit der Anwendung zu tun haben. Ob diese Werte auch in unserer Anwendungskonfiguration aufgenommen werden sollen, wird über eine Property “config.includeSystemEnvironmentAndProperties” gesteuert.

Umsetzung

Zum Nachlesen dokumentiere ich hier ein paar Schritte aus dem Code, das Ganze soll später auch in einem GitHub Projekt landen.

Zuerst die Properties aus System Environment und System Properties sammeln:

// System Environment
Properties systemEnvironmentProperties = new Properties();		
systemEnvironmentProperties.putAll(System.getenv());

// System Properties
Properties systemPropertiesProperties = new Properties();		
systemPropertiesProperties.putAll(System.getProperties());

Die BaseProperties / Standard Properties aus dem ClassPath der Anwendung laden, sie müssen unter: /src/main/resources/application.properties gespeichert sein:

String basePropertiesFilename = "application.properties";

Properties baseProperties = new Properties();
try {
  InputStream is = Config.class.getClassLoader().getResourceAsStream(basePropertiesFilename);
  baseProperties.load(is);
} catch (Exception e) {
  logger.error("Could not read {} from ClassLoader", basePropertiesFilename, e);
}

Falls LocalProperties geladen werden sollen, muss der Pfad zu der Datei in der Umgebungsvariablen “localconf” übergeben werden:

String localPropertiesProperty = "localconf";


Properties localProperties = new Properties();
logger.debug("----------------------------------------------------------------------------------");
logger.debug("LocalProperties Path from System Environment: {}", systemEnvironmentProperties.getProperty(localPropertiesProperty));
logger.debug("LocalProperties Path from System Properties: {}", systemPropertiesProperties.getProperty(localPropertiesProperty));
String localPropertiesPath = systemPropertiesProperties.getProperty(localPropertiesProperty) != null ? systemPropertiesProperties.getProperty(localPropertiesProperty) : systemEnvironmentProperties.getProperty(localPropertiesProperty);
if (localPropertiesPath == null) {
  logger.debug("LocalProperties Path is not set, skip loading Local Properties");
} else {
  logger.debug("Load LocalProperties from {}", localPropertiesPath);
  try {
    localProperties.load(new FileInputStream(localPropertiesPath));
  } catch (Exception e) {
    logger.error("Could not read {} from File", localPropertiesPath, e);
  }
}

Sollen die Umgebungsvariablen auch übernommen werden:

String includeSystemEnvironmentAndPropertiesProperty = "config.includeSystemEnvironmentAndProperties";

String includeS = Stream.of(
  systemPropertiesProperties.getProperty(includeSystemEnvironmentAndPropertiesProperty),
  systemEnvironmentProperties.getProperty(includeSystemEnvironmentAndPropertiesProperty),
  localProperties.getProperty(includeSystemEnvironmentAndPropertiesProperty),
  baseProperties.getProperty(includeSystemEnvironmentAndPropertiesProperty))
  .filter(Objects::nonNull)
  .findFirst()
  .orElse(null);		
Boolean include = Boolean.parseBoolean(includeS);

Abschließend alle Properties mergen:

Properties mergedProperties = new Properties();
mergedProperties.putAll(baseProperties);
mergedProperties.putAll(localProperties);
if (include) {
  mergedProperties.putAll(systemEnvironmentProperties);
  mergedProperties.putAll(systemPropertiesProperties);
} else {
  mergedProperties.forEach((key, value) -> {
    value = systemEnvironmentProperties.getProperty((String)key, (String)value);
    value =  systemPropertiesProperties.getProperty((String)key, (String)value);
    mergedProperties.setProperty((String)key, (String)value);
  });
}

Beispiel

In dem vorherigen Post hatte ich die Konfigurierbarkeit von JPA EntityManagerFactory im Code so gelöst:

import static org.hibernate.cfg.AvailableSettings.SHOW_SQL;

Properties properties = new Properties();
Optional.ofNullable(System.getenv(SHOW_SQL)).ifPresent( value -> properties.put(SHOW_SQL, value));
Optional.ofNullable(System.getenv(JPA_JDBC_URL)).ifPresent( value -> properties.put(JPA_JDBC_URL, value));
Optional.ofNullable(System.getenv(JPA_JDBC_USER)).ifPresent( value -> properties.put(JPA_JDBC_USER, value));
Optional.ofNullable(System.getenv(JPA_JDBC_PASSWORD)).ifPresent( value -> properties.put(JPA_JDBC_PASSWORD, value));

EntityManagerFactory emf = Persistence.createEntityManagerFactory("myapp-persistence-unit", properties);

Das lässt sich jetzt einfacher über die Config lösen:

import static org.hibernate.cfg.AvailableSettings.SHOW_SQL;

Properties properties = new Properties();
properties = Config.getAllProperties();

EntityManagerFactory emf = Persistence.createEntityManagerFactory("myapp-persistence-unit", properties);

Sources

Zum Schluss habe ich das Ganze auch noch in ein GitHub Projekt gepackt: configuration

Categories
Development Java

Logging

Zuletzt kochte die Log4J Lücke hoch, so dass man sich mit dem Thema Logging auseinander setzen musste.

Mich betraf der Bug nicht besonders, nach eingehender Analyse stellte sich heraus, dass keines meiner im Betrieb befindlichen Projekte Log4J verwendet. Ein Paar Projekte, die ich jahrelang betreuen durfte, waren betroffen, aber für die bin ich nicht mehr verantwortlich und war nur beratend tätig und habe meine Einschätzung und Handlungsempfehlung abgegeben.

Allerdings trägt das grade in der Entwicklung, aber noch nicht in Betrieb gegangene, Projekt Log4J in sich, so dass das Thema vor dem GoLive angegangen werden muss.

Java Logging

Einen sehr schönen, pragmatischen Einstieg in Java Util Logging habe ich auf Java Code Geeks gefunden.

Das einfachste Beispiel, um einen Ausgabe auf der Console zu erhalten:

package deringo.jpa;

import java.util.logging.Logger;

public class TestMain {

	public static void main(String[] args) throws Exception {
		Logger logger = Logger.getLogger(TestMain.class.getName());
		logger.warning("Dies ist nur ein Test!");
		
	}

}

Ein paar Code Beispiele:

package deringo.jpa;

import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

public class TestMain {

	public static void main(String[] args) throws Exception {
		Logger logger = Logger.getLogger(TestMain.class.getName());
		logger.warning("Dies ist nur ein Test!");

		// Warnung wird ausgegeben, Fine nicht
		logger.fine("Eine fine Nachricht. 1");
		logger.warning("Eine warnende Nachricht. 1");
		
		// Das Level des Loggers auf ALL setzen 
		logger.setLevel(Level.ALL);

		// Trotzdem: Warnung wird ausgegeben, Fine nicht
		logger.fine("Eine fine Nachricht. 2");
		logger.warning("Eine warnende Nachricht. 2");

		// Einen Handler für den Logger definieren, der Handler Level wird auf ALL gesetzt
		Handler consoleHandler = new ConsoleHandler();
		consoleHandler.setLevel(Level.ALL);
		logger.addHandler(consoleHandler);

		// Warnung wird ausgegeben, Fine wird ausgegeben
		// ABER: Warnung wird doppelt ausgegeben
		logger.fine("Eine fine Nachricht. 3");
		logger.warning("Eine warnende Nachricht. 3");
	}
}

Überraschend ist erstmal, dass die dritte Ausgabe, zumindest für die Warnung, doppelt erscheint.

Die Erklärung ist, dass es noch einen Root Logger gibt, welcher der Parent des TestMain Loggers ist.
Standardmäßig gibt ein Logger seine Einträge an den Parent Logger weiter. Bzw. an die Handler des Parent Loggers.
Der Root Logger hat die ersten Logs ausgegeben, als der TestMain Logger noch gar keinen Handler hatte, der die Log Einträge verarbeiten konnte.

Wird die Weitergabe an den Parent Handler deaktiviert, wird nicht mehr doppelt geloggt:

package deringo.jpa;

import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

public class TestMain {

	public static void main(String[] args) throws Exception {
		Logger logger = Logger.getLogger(TestMain.class.getName());

		// Warnung wird ausgegeben, Fine nicht
		logger.fine("Eine fine Nachricht. 1");
		logger.warning("Eine warnende Nachricht. 1");
		
		// Nicht an Parent Handler weiter reichen
		logger.setUseParentHandlers(false);

		// Warnung wird NICHT mehr ausgegeben, Fine ebenfalls nicht
		logger.fine("Eine fine Nachricht. 2");
		logger.warning("Eine warnende Nachricht. 2");

		// Eigenen Handler definieren
		Handler consoleHandler = new ConsoleHandler();
		consoleHandler.setLevel(Level.ALL);
		logger.addHandler(consoleHandler);

		// Warnung wird ausgegeben, Fine wird NICHT ausgegeben
		// Warnung wird NICHT doppelt ausgegeben
		logger.fine("Eine fine Nachricht. 3");
		logger.warning("Eine warnende Nachricht. 3");
		
		// Das Level des Loggers auf ALL setzen 
		logger.setLevel(Level.ALL);

		// Warnung wird ausgegeben, Fine wird ausgegeben
		// Warnung wird NICHT doppelt ausgegeben
		logger.fine("Eine fine Nachricht. 4");
		logger.warning("Eine warnende Nachricht. 4");
	}
}

Java Logging – Konfiguration per Datei

Möchte man die Konfiguration des Java Util Loggers nicht per Code, wie oben, vornehmen, sondern per Datei findet sich ein guter Einstieg auf Wikibooks.

Davon abgeleitet meine Konfigurationsdatei logging.properties, die ich in src/main/resources abgelegt habe:

# Der ConsoleHandler gibt die Nachrichten auf std.err aus
#handlers= java.util.logging.ConsoleHandler

# Alternativ können weitere Handler hinzugenommen werden. Hier z.B. der Filehandler
handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler

# Festlegen des Standard Loglevels
.level= INFO
 
############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################

# Die Nachrichten in eine Datei im Benutzerverzeichnis schreiben
java.util.logging.FileHandler.pattern = d:/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter

java.util.logging.FileHandler.level = ALL

# Zusätzlich zu den normalen Logleveln kann für jeden Handler noch ein eigener Filter 
# vergeben werden. Das ist nützlich wenn beispielsweise alle Nachrichten auf der Konsole ausgeben werden sollen
# aber nur ab INFO in das Logfile geschrieben werden soll.
java.util.logging.ConsoleHandler.level = ALL
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
 
############################################################
# Extraeinstellungen für einzelne Logger
############################################################

# Für einzelne Logger kann ein eigenes Loglevel festgelegt werden.
deringo.jpa.TestMain.level = FINEST

Leider funktionierte es nicht.
Es wird nach wie vor die originale logging.properties von Java genommen, die im Java Installationsverzeichnis $JAVA_HOME/jre unterhalb des lib Verzeichnises liegt, bzw. ab Java 9 in $JAVA_HOME/conf. Vgl. Mkyong

Falls nicht die Original-logging.properties-Datei benutzt werden soll, kann über die System-Property java.util.logging.config.file die stattdessen zu verwendende Datei angegeben werden.

Wie das praktisch geht, kann bei Mkyong nachgesehen werden.

Ich habe folgenden Code verwendet:

package deringo.jpa;

import java.util.logging.Logger;

public class TestMain {

	public static void main(String[] args) throws Exception {
		String path = TestMain.class.getClassLoader().getResource("logging.properties").getFile();
		System.setProperty("java.util.logging.config.file", path);

		Logger logger = Logger.getLogger(TestMain.class.getName());
		
		// Warnung wird ausgegeben, Fine wird ausgegeben
        // Beides auf der Console und in der Datei D:/java0.log
		logger.fine("Eine fine Nachricht. 1");
		logger.warning("Eine warnende Nachricht. 1");
	}
}

Es wird in der Console und der definierten Datei geloggt.

Warum die logging.properties des Projektes nicht standartmäßig anstelle der Java logging.properties gezogen wird, kann ich mir nicht erklären.

SLF4J

SLF4J ist kein Logging Framework, sondern eine Fassade vor der eigentlichen Implementierung. Man kann also im Code mit SLF4J loggen und SLF4J leitet das dann an das gewählte Framework, zB Java Util Logging oder Log4J weiter. So kann man das Logging Framework austauschen ohne den Code anfassen zu müssen.

Ob das jemals jemand vor dem Log4J Bug gemacht hat lasse ich mal dahingestellt, mir gefällt aber das eingebaute Templating, bzw. Parameterisierung, von SLF4J:

Object entry = new SomeObject();
logger.debug("The entry is {}.", entry);

Das SLF4J Manual und das SLF4J Configuration File Example waren mir gute Informationsquellen.

Maven Dependency:

<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.32</version>
</dependency>

Erster Beispielcode:

package deringo.jpa.repository;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SLF4JTest {

	public static void main(String[] args) {
		Logger logger = LoggerFactory.getLogger(SLF4JTest.class);
		logger.info("Hallo Welt!");
	}

}

Folgende Grafik aus dem SLF4J Manual zeigt, dass nach /dev/null geloggt wurde:

Es wird also eine Logging Framework Implementierung benötigt.

Ich entscheide mich für das Java Util Logging Framework, denn dieses ist in Java bereits enthalten und ich muss keine weitere Bibliothek, wie zB Log4J, in mein Projekt einbinden.

Es kommt also eine weitere Maven Abhängigkeit hinzu:

<properties>
  <org.slf4j.version>1.7.32</org.slf4j.version>
</properties>


<dependencies>
  <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>${org.slf4j.version}</version>
  </dependency>
  <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-jdk14 -->
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <version>${org.slf4j.version}</version>
  </dependency>
</dependencies>

Der selbe Beispielcode von oben:

package deringo.jpa.repository;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SLF4JTest {

	public static void main(String[] args) {
		Logger logger = LoggerFactory.getLogger(SLF4JTest.class);
		logger.info("Hallo Welt!");
	}

}

Führt jetzt zu folgender Ausgabe:

Äquivalent zu dem Code Beispiel zu Java Util Logging – Konfiguration per Datei weiter oben, führt folgender Code zusätzlich zu einem Logging in einer Datei:

package deringo.jpa.repository;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SLF4JTest {

	public static void main(String[] args) {
		String path = SLF4JTest.class.getClassLoader().getResource("logging.properties").getFile();
		System.setProperty("java.util.logging.config.file", path);
		Logger logger = LoggerFactory.getLogger(SLF4JTest.class);
		logger.info("Hallo Welt!");
	}

}

SLF4J & Log4J

Maven:

<properties>
  <org.slf4j.version>1.7.32</org.slf4j.version>
  <log4j.version>2.17.1</log4j.version>
</properties>


<dependencies>
  <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>${org.slf4j.version}</version>
  </dependency>
  <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>${org.slf4j.version}</version>
  </dependency>
  <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
  <dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>${log4j.version}</version>
  </dependency>
</dependencies>

Selber Beispielcode wie oben:

package deringo.jpa.repository;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SLF4JTest {

	public static void main(String[] args) {
		Logger logger = LoggerFactory.getLogger(SLF4JTest.class);
		logger.info("Hallo Welt!");
	}

}

Ausgabe:

Zur Initialisierung bzw. Konfiguration von Log4J wird in src/main/resources eine Datei log4j.properties angelegt:

log4j.debug=false
log4j.rootLogger=INFO, CONSOLE

log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.Encoding=UTF-8
log4j.appender.CONSOLE.layout = org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern = %d [%t] %-5p %c- %m%n

# Configure which loggers log to which appenders
log4j.logger.deringo.jpa=DEBUG

Der Java Code bleibt unverändert, die Ausgabe sieht jetzt aber so aus:

Categories
Development Java

JPA – (nicht) ohne persistence.xml

Mein Ziel war es, JPA konfigurativ ohne persistence.xml zu verwenden. Das habe ich nicht ganz geschafft, aber schon mal den Weg erarbeitet, wie es prinzipiell funktionieren könnte.

Hintergrund ist einfach der, dass ich in der persistence.xml die Konfiguration meiner Datenbank hinterlegen kann, aber wenn ich das war-File baue und auf den produktiven Server schiebe, dann möchte ich, dass diese Konfiguration durch die der produktiven Datenbank überschrieben werden kann.

persistence.xml

Die im vorherigen Post zu JPA sieht folgendermaßen aus:

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
  http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
  version="2.2">
    <persistence-unit name="myapp-persistence-unit">
        <properties>
            <!-- Configure a database connection in Java SE -->
            <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:postgresql://127.0.0.1:5432/myapp" />
            <property name="javax.persistence.jdbc.user" value="postgres" />
            <property name="javax.persistence.jdbc.password" value="PASSWORD" />
 
            <!-- Configure timeouts -->     
            <property name="javax.persistence.lock.timeout" value="100"/>
            <property name="javax.persistence.query.timeout" value="100"/>
        </properties>
    </persistence-unit>
</persistence>

persistence.xml löschen

Der Versuch, die persistence.xml zu löschen führt zu einem Fehler:

persistence.xml minimal

Der einfachste Weg, diesen Fehler zum umgehen, ist eine minimale persistence.xml anzulegen:

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
  http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
  version="2.2">
    <persistence-unit name="myapp-persistence-unit">
        <properties>
        </properties>
    </persistence-unit>
</persistence>

Wird der EntityManager wie bisher erzeugt, gibt es einen Fehler:

EntityManagerFactory emf = Persistence.createEntityManagerFactory("myapp-persistence-unit");

Konfiguration im Java Code

Die ursprüngliche Konfiguration aus der persistence.xml wird jetzt im Java Code vorgenommen:

Properties properties = new Properties();
properties.put("javax.persistence.jdbc.driver", "org.postgresql.Driver");
properties.put("javax.persistence.jdbc.url", "jdbc:postgresql://127.0.0.1:5432/myapp");
properties.put("javax.persistence.jdbc.user", "postgres");
properties.put("javax.persistence.jdbc.password", "PASSWORD");
properties.put("javax.persistence.lock.timeout", "100");
properties.put("javax.persistence.query.timeout", "100");

Der EntityManager wird dann ganz einfach fehlerfrei wie folgt erzeugt:

EntityManagerFactory emf = Persistence.createEntityManagerFactory("myapp-persistence-unit", properties);

DB Zugriff testen

Um den fehlerhaften Zugriff mit minimaler persistence.xml zu testen:

// Test 'broken' persistence.xml
Error error = assertThrows(NoClassDefFoundError.class, () -> {
  AdresseRepository.getLastObjectID();
});
assertTrue(error != null);

Um den fehlerlosen Zugriff mit minimaler persistence.xml und Java Konfiguration zu testen:

Properties properties = new Properties();
properties.put("javax.persistence.jdbc.driver", "org.postgresql.Driver");
properties.put("javax.persistence.jdbc.url", "jdbc:postgresql://127.0.0.1:5432/myapp");
properties.put("javax.persistence.jdbc.user", "postgres");
properties.put("javax.persistence.jdbc.password", "PASSWORD");
properties.put("javax.persistence.lock.timeout", "100");
properties.put("javax.persistence.query.timeout", "100");

EntityManagerFactory emf = Persistence.createEntityManagerFactory("myapp-persistence-unit", properties);

String sequenceName = "public.object_id_seq";
String sql = "SELECT s.last_value FROM " + sequenceName + " s";
EntityManager em = emf.createEntityManager();
BigInteger value = (BigInteger)em.createNativeQuery(sql).getSingleResult();

assertNotNull(value);

Anderer Error Bugfix

Nach meiner Mittagspause hat sich der Test auf einmal anders verhalten und es wurde kein NoClassDefFoundError geschmissen, sondern ein ExceptionInInizlialisationError. Warum dem so ist 🤷‍♂️.

Beide Errors erweitern allerdings den LinkageError, also ist mein Test fix gefixt:

// Test 'broken' persistence.xml
Error error = assertThrows(LinkageError.class, () -> {
  AdresseRepository.getLastObjectID();
});
assertTrue(error != null);

Available Settings

Die Konstanten der verfügbaren Einstellungen sind in der Klasse org.hibernate.cfg.AvailableSettings zu finden.

Dadurch lassen sich die Properties etwas eleganter setzen:

properties.put(org.hibernate.cfg.AvailableSettings.SHOW_SQL, Boolean.TRUE);
// bzw.
properties.put(AvailableSettings.SHOW_SQL, Boolean.TRUE);
// oder
import static org.hibernate.cfg.AvailableSettings.*;
properties.put(SHOW_SQL, Boolean.TRUE);

Umsetzungsvorschlag

Am einfachsten lasse ich meine persistence.xml wie bisher, mit den Einstellungen der Entwicklungsdatenbank.

Einstellungen wie zB der JDBC Driver bleiben auf allen Systemen gleich. Lediglich die URL, Username und das Passwort werden sich ändern.

Diese Werte können als Umgebungsvariable gesetzt werden, zB in einem Docker-File oder im Tomcat, und dann die Werte der persistence.xml überschreiben.

Also wieder die komplette persistence.xml nutzen:

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
  http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
  version="2.2">
    <persistence-unit name="myapp-persistence-unit">
        <properties>
            <!-- Configure a database connection in Java SE -->
            <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:postgresql://127.0.0.1:5432/myapp" />
            <property name="javax.persistence.jdbc.user" value="postgres" />
            <property name="javax.persistence.jdbc.password" value="PASSWORD" />
 
            <!-- Configure timeouts -->     
            <property name="javax.persistence.lock.timeout" value="100"/>
            <property name="javax.persistence.query.timeout" value="100"/>
        </properties>
    </persistence-unit>
</persistence>

Im Java Code könnte man dann die Werte überschreiben, beispielsweise für das Anzeigen der SQLs:

Properties properties = new Properties();
Optional.ofNullable(System.getenv(SHOW_SQL)).ifPresent( value -> properties.put(SHOW_SQL, value));

EntityManagerFactory emf = Persistence.createEntityManagerFactory("myapp-persistence-unit", properties);

Alternativ könnte man auch den Pfad zu einer Konfigurationsdatei auf dem Server setzen und dann von dort die Werte auslesen.

Categories
Database Development Java

JPA

Bisher habe ich für den Datenbankzugriff mit einem proprietärem Framework gearbeitet, das ich jedoch für das aktuelle Projekt nicht verwenden kann. Bei der Wahl einer frei zugänglichen Alternative entschied ich mich für JPA, die Java/Jakarta Persistence API.

Die Datenbank

Als Datenbank benutze ich einfach das Setup aus meinem letzten Post.

Projekt Setup

Es wird ein neues Maven Projekt angelegt. Java Version 1.8.

Es wird die Javax Persistence API benötigt und eine Implementierung, hier: Hibernate. Als DB wird PostgreSQL verwendet, dazu wird der entsprechende Treiber benötigt.

Die pom.xml des Projekts:

<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>deringo</groupId>
  <artifactId>jpa</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>JPATest</name>
  <description>JPA Test Project</description>

  <properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>javax.persistence</groupId>
      <artifactId>javax.persistence-api</artifactId>
      <version>2.2</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>5.6.1.Final</version>
    </dependency>
   <!-- https://mvnrepository.com/artifact/org.postgresql/postgresql -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.2.18</version>
    </dependency>
  </dependencies>

</project>

Verbindungsbeschreibung

Die benötigten Informationen für den Verbindungsaufbau mit der DB werden in der persistence.xml hinterlegt:

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
  http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
  version="2.2">
    <persistence-unit name="myapp-persistence-unit">
        <properties>
            <!-- Configure a database connection in Java SE -->
            <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:postgresql://127.0.0.1:5432/myapp" />
            <property name="javax.persistence.jdbc.user" value="postgres" />
            <property name="javax.persistence.jdbc.password" value="PASSWORD" />
 
            <!-- Configure timeouts -->     
            <property name="javax.persistence.lock.timeout" value="100"/>
            <property name="javax.persistence.query.timeout" value="100"/>
        </properties>
    </persistence-unit>
</persistence>

Java Klassen

Die beiden Tabellen Adresse und Person werden jeweils in eine Java Klasse überführt. Dabei handelt es sich um POJOs mit Default Constructor, (generierter) toString, hashCode und equals Methoden. Annotation als Entity und für die ID, die uA objectID heißen soll und nicht wie in der DB object_id.

package deringo.jpa.entity;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Adresse implements Serializable {
	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "object_id")
	private int objectID;
	
	private String strasse;
	private String ort;
	
	public Adresse() {
		// default constructor
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + objectID;
		result = prime * result + ((ort == null) ? 0 : ort.hashCode());
		result = prime * result + ((strasse == null) ? 0 : strasse.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;
		Adresse other = (Adresse) obj;
		if (objectID != other.objectID)
			return false;
		if (ort == null) {
			if (other.ort != null)
				return false;
		} else if (!ort.equals(other.ort))
			return false;
		if (strasse == null) {
			if (other.strasse != null)
				return false;
		} else if (!strasse.equals(other.strasse))
			return false;
		return true;
	}

	@Override
	public String toString() {
		return String.format("Adresse [objectID=%s, strasse=%s, ort=%s]", objectID, strasse, ort);
	}

	public int getObjectID() {
		return objectID;
	}

	public void setObjectID(int objectID) {
		this.objectID = objectID;
	}

	public String getStrasse() {
		return strasse;
	}

	public void setStrasse(String strasse) {
		this.strasse = strasse;
	}

	public String getOrt() {
		return ort;
	}

	public void setOrt(String ort) {
		this.ort = ort;
	}
}

Für den Zugriff auf die Tabellen werden die jeweiligen Repository Klassen angelegt.

package deringo.jpa.repository;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

import deringo.jpa.entity.Adresse;

public class AdresseRepository {
	private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("myapp-persistence-unit");

	public static Adresse getAdresseById(int id) {
		EntityManager em = emf.createEntityManager();
		return em.find(Adresse.class, id);
	}

}

“Geschäftslogik” um zu testen, ob es funktioniert:

package deringo.jpa;

import deringo.jpa.entity.Adresse;
import deringo.jpa.repository.AdresseRepository;

public class TestMain {

	public static void main(String[] args) {
		int adresseID = 4;
		Adresse adresse = AdresseRepository.getAdresseById(adresseID);
		System.out.println(adresse);
	}

}

Test Driven

Den Zugriff über die Repositories (und später auch Service Klassen) habe ich Test Driven entwickelt mit JUnit. Zur Entwicklung mit JUnit hatte ich schon mal einen Post verfasst.

Folgende Dependencies wurden der pom.xml hinzugefügt:

    <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
    <dependency>
	  <groupId>org.junit.jupiter</groupId>
	  <artifactId>junit-jupiter-api</artifactId>
	  <version>5.8.1</version>
	  <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.hamcrest/hamcrest -->
    <dependency>
      <groupId>org.hamcrest</groupId>
      <artifactId>hamcrest</artifactId>
      <version>2.2</version>
      <scope>test</scope>
    </dependency>
package deringo.jpa.repository;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

import org.junit.jupiter.api.Test;

import deringo.jpa.entity.Adresse;

public class AdresseRepositoryTest {

	@Test
	public void getAnmeldungById() {
		int adresseID = 1;
		Adresse adresse = AdresseRepository.getAdresseById(adresseID);
		assertNull(adresse);
		
		adresseID = 4;
		adresse = AdresseRepository.getAdresseById(adresseID);
		assertNotNull(adresse);
		assertThat(adresse.getObjectID(), is(adresseID));
		assertThat(adresse.getStrasse(), is("Beispielstrasse"));
		assertThat(adresse.getOrt(), is("Beispielstadt"));
	}
}

Projektstruktur

Query

Alle Adressen eines Ortes suchen:

	public static List<Adresse> getAdresseByOrt(String ort) {
		EntityManager em = emf.createEntityManager();
		TypedQuery<Adresse> query = em.createQuery("SELECT a FROM Adresse a WHERE a.ort = :ort", Adresse.class);
		query.setParameter("ort", ort);
		return query.getResultList();
	}

Native Query

Um zB herauszufinden, wie die zuletzt vergebene ObjectID lautet, kann ein native Query verwendet werden:

	public static int getLastObjectID() {
		String sequenceName = "public.object_id_seq";
		String sql = "SELECT s.last_value FROM " + sequenceName + " s";
		EntityManager em = emf.createEntityManager();
		BigInteger value = (BigInteger)em.createNativeQuery(sql).getSingleResult();
		return value.intValue();
	}

Kreuztabelle

Nehmen wir mal an, eine Person kann mehrere Adressen haben und an eine Adresse können mehrere Personen gemeldet sein.

Um das abzubilden benötigen wir zunächst eine Kreuztabelle, die wir in der DB anlegen:

DROP TABLE IF EXISTS public.adresse_person;
CREATE TABLE public.adresse_person (
    adresse_object_id integer NOT NULL,
    person_object_id integer NOT NULL
);

Solch eine Relation programmatisch anlegen:

	public static void createAdressePersonRelation(int adresseId, int personId) {
		String sql = "INSERT INTO adresse_person (adresse_object_id, person_object_id) VALUES (?, ?)";//, adresseId, personId);
		EntityManager em = emf.createEntityManager();
		em.getTransaction().begin();
		em.createNativeQuery(sql)
		  .setParameter(1, adresseId)
		  .setParameter(2, personId)
		  .executeUpdate();
		em.getTransaction().commit();
	}

Die Adresse zu einer Person(enID) lässt sich ermitteln:

	public static Adresse getAdresseByPersonID(int personId) {
		String sql = "SELECT adresse_object_id FROM adresse_person WHERE person_object_id = " + personId;
		EntityManager em = emf.createEntityManager();
		Integer adresseId;
		try {
			adresseId = (Integer)em.createNativeQuery(sql).getSingleResult();
		} catch (NoResultException nre) {
			return null;
		}
		return getAdresseById(adresseId.intValue());
	}

Das funktioniert nur, solange die Person nur eine Adresse hat.

Das kann man so machen, schöner ist es aber über entsprechend ausmodellierte ManyToMany Beziehungen in den Entities.
Das Beispiel vervollständige ich hier erstmal nicht, da ich bisher es in meinem Projekt nur so wie oben beschrieben benötigte.

OneToMany

Wandeln wir obiges Beispiel mal ab: An einer Adresse können mehrere Personen gemeldet sein, aber eine Person immer nur an einer Adresse.

Wir fügen also der Person eine zusätzliche Spalte für die Adresse hinzu:

ALTER TABLE person ADD COLUMN adresse_object_id integer;
--
UPDATE person SET adresse_object_id = 4
public class Person implements Serializable {
  [...]
    @ManyToOne
    @JoinColumn(name="adresse_object_id")
	private Adresse adresse;
  [...]
}
public class Adresse implements Serializable {
  [..]
 	@OneToMany
	@JoinColumn(name="adresse_object_id")
	private List<Person> personen = new ArrayList<>();
  [...]
}

Anschließend noch die Getter&Setter, toString, hashCode&equals neu generieren und einen Test ausführen:

	@Test
	public void getAnmeldungById() {
		int adresseID = 4;
		adresse = AdresseRepository.getAdresseById(adresseID);
		assertNotNull(adresse);
		assertThat(adresse.getObjectID(), is(adresseID));
		assertThat(adresse.getStrasse(), is("Beispielstrasse"));
		assertThat(adresse.getOrt(), is("Beispielstadt"));		
		assertThat(adresse.getPersonen().size(), is(3));
	}

Der Test funktioniert.

ABER: Folgende Zeile am Ende bewirkt einen StackOverflow Error:

	public void getAnmeldungById() {
        [...]
		System.out.println(adresse);
    }

Das Problem ist die generierte toString-Methode in Person:

	@Override
	public String toString() {
		return String.format("Person [objectID=%s, vorname=%s, nachname=%s, adresse=%s]", objectID, vorname, nachname, 
                             adresse);
	}

Es soll das Objekt adresse ausgegeben werden, in welchem in der toString-Methode das Objekt person ausgegeben werden soll, in welchem das Objekt adresse ausgegeben werden, in welchem in der toString-Methode das Objekt person ausgegeben werden soll, in welchem das Objekt adresse … usw.

Als Lösung muss die toString-Methode von Person händisch angepasst werden, so dass nicht mehr das Objekt adresse, sondern lediglich dessen ID ausgegeben wird:

	@Override
	public String toString() {
		return String.format("Person [objectID=%s, vorname=%s, nachname=%s, adresse=%s]", objectID, vorname, nachname, 
                             adresse == null ? null : adresse.getObjectID());
	}

siehe auch: https://stackoverflow.com/questions/23973347/jpa-java-lang-stackoverflowerror-on-adding-tostring-method-in-entity-classes

Neuen Eintrag speichern

Adresse speichern:

	public static void saveAdresse(Adresse adresse) {
		EntityManager em = emf.createEntityManager();
		em.getTransaction().begin();
		if (adresse.getObjectID() == 0) {
			em.persist(adresse);
		} else {
			em.merge(adresse);
		}
		em.getTransaction().commit();
	}

Testen:

	@Test
	public void saveNewAdresse() {
		int objectID = AdresseRepository.getLastObjectID();

		Adresse adresse = new Adresse();
		adresse.setStrasse("neue Stasse");
		adresse.setOrt("neuer Ort");
		assertThat(adresse.getObjectID(), is(0));
		AdresseRepository.saveAdresse(adresse);
		assertThat(adresse.getObjectID(), is(objectID + 1));
		assertThat(adresse.getOrt(), is("neuer Ort"));
		
		adresse.setOrt("neuerer Ort");
		AdresseRepository.saveAdresse(adresse);
		assertThat(adresse.getObjectID(), is(objectID + 1));
		assertThat(adresse.getOrt(), is("neuerer Ort"));
	}

Eintrag löschen

Man möchte meinen, dass der Code zum löschen einer Adresse wie folgt lautet:

	public static void deleteAdresse(Adresse adresse) {
		EntityManager em = emf.createEntityManager();
		em.getTransaction().begin();
		em.remove(adresse);
		em.getTransaction().commit();
	}

Testen:

	@Test
	public void deleteAdresse() {
		int adresseID = 8;
		Adresse adresse = AdresseRepository.getAdresseById(adresseID);
		assertNotNull(adresse);
		
		AdresseRepository.deleteAdresse(adresse);
		assertNull(adresse);
	}

Der Test schlägt fehl mit der Nachricht: “Removing a detached instance”.

Das Problem besteht darin, dass die Adresse zuerst über einen EntityManager gezogen wird, aber das Löschen in einem anderen EntityManager, bzw. dessen neuer Transaktion, erfolgen soll. Dadurch ist die Entität detached und muss erst wieder hinzugefügt werden, um sie schließlich löschen zu können:

	public static void deleteAdresse(Adresse adresse) {
		EntityManager em = emf.createEntityManager();
		em.getTransaction().begin();
		em.remove(em.contains(adresse) ? adresse : em.merge(adresse));
		em.getTransaction().commit();
	}
Categories
Development Java

Tomcat Start beschleunigen

Das Starten des Tomcat-Servers hat für ein Projekt sehr lange gedauert. Im Eclipse kann man die Zeit bis zum Timeout hoch setzen, den Nerven des Entwicklers hilft das aber nur bedingt.

Eine Ursache für die lange Startzeit liegt darin, dass der Tomcat-Server beim Start alle jar-Files nach Taglibs durchsucht. Das Projekt hat sehr viele Libraries.

Eine Abhilfe schafft hier die Konfiguration, dass Tomcat keine jar-Files scannen soll, außer denen, in denen eine Taglib enthalten ist.

Wie man Jars mit Taglibs findet

Tomcat kann anzeigen lassen, welche Jars, die beim Start gescannt werden, Taglibs enthalten. Dazu muss das entsprechende Log-Level gesetzt werden.

In meinem Fall musste ich lediglich die logging.properties aus dem Original-Tomcat Verzeichnis in das Verzeichnis des Eclipse Tomcats kopieren:

Am Ende der logging.properties das Log-Level für den TLDScanner setzen:

[...]

org.apache.jasper.compiler.TldLocationsCache.level = FINE
org.apache.jasper.servlet.TldScanner.level = FINE

In den VM Arguments des Tomcats muss der Pfad zur logging.properties angegeben werden:

Beim Start wird jetzt angezeigt, in welchen JARs TLDs zu gefunden wurden.

Wie nur noch ausgewählte JARs gescannt werden

In aktuellen Projekt wurden folgende JARs mit TLDs gefunden:

  • standard-1.1.2.jar
  • jstl-1.2.jar
  • jsf-impl-2.2.20.jar
  • tomahawk20-1.1.14.jar

Die Konfiguration des JARScanFilters für den Tomcat Server erfolgt in der catalina.properties Datei.

Bei den jarsToSkip lasse ich alle (*.jar) skippen.

Bei den jarsToScan füge ich obige JARs hinzu:

Alleine durch diese Konfigurationsänderung konnte die Startzeit von 25 Sekunden auf 10 Sekunden reduziert werden.

JarScanner Konfiguration im Projekt

Der obige Weg beschreibt die Konfigurationsänderung im Server. Das hat den Nachteil, dass jede Serverinstanz diese Konfiguration gesetzt bekommen muss. Eine Konfiguration im Projekt selbst hat den Vorteil, dass zB alle Mitentwickler direkt mit profitieren können und nicht erst die Konfiguration selbst setzen müssen.

Die Konfiguration im Projekt erfolgt über context.xml:

<?xml version="1.0" encoding="UTF-8"?>
<Context>
  <JarScanner>
    <JarScanFilter tldScan="standard-1.1.2.jar,jstl-1.2.jar,jsf-impl-2.2.20.jar,tomahawk20-1.1.14.jar" 
                   defaultTldScan="false" 
                   defaultPluggabilityScan="false"/>
  </JarScanner>
</Context>
Categories
Java

Lokales Maven Repository

Aktuell migrieren wir eine Legacy Anwendung in eine neue Systemlandschaft und dabei wollen wir das Dependency Managment überarbeiten, das bisher aus einem Verzeichnis mit *.jar Library Dateien besteht, ohne Sourcecode. Wir wollen im Projekt Maven einführen. Das funktioniert überwiegend recht gut und einfach, indem wir die einzelnen *.jar Dateien durchgehen, die Versionsnummer bestmöglich raten und dann die entsprechende Abhängigkeit in die Maven Datei pom.xml hinzufügen. Die einzelnen Bibliotheken lassen sich gut über diese Seite finden: https://mvnrepository.com/

Allerdings gibt es auch firmeninterne oder propritäre Java Bibliotheken, diese lassen sich natürlich nicht über das zentrale Maven Repository finden.

Bei einem anderen Kunden haben wir zur Lösung dieses Problems einen eigenes, internes Repository aufgebaut: Sonatype Nexus. Dieses dient als Proxy für das öffentliche Maven Repository und zusätlich können hier die internen Bibliotheken hochgeladen und so verfügbar gemacht werden. Dieses Vorgehen kam bei dem aktuellen Projekt allerdings nicht in Betracht, so dass wir uns nach einer Alternative umgesehen haben. Das Dependency Managment nicht zu ändern wäre auch eine Alternative gewesen.

Die Alternative, die wir dann fanden, war das Einrichten eines im Projekt eingebetteten, lokalen Repositories. Die Idee dazu fanden wir in diesem Artikel: https://devcenter.heroku.com/articles/local-maven-dependencies

Das Vorgehen ist von der Idee her recht simpel: Im Projekt Ordner wird ein Ordner, zB maven/repository, angelegt und in diesem ein lokales Repository aufgebaut, welches dann in der pom.xml referenziert werden kann, um so von dort die internen Libraries verfügbar zu machen.

Repository anlegen

Im Projekt wird ein Verzeichnis für das lokale Repository angelegt, zB maven/repository.

Mittels des Maven deploy Befehls können dann die internen Libraries aus dem bisherigen Repository (aka WEB-INF/lib Folder) in das Maven Repository importiert werden.

Beispielsweise wird die firmeninterne companyLib.jar incl. Sourcecode in der Version ‘latest’, denn es gibt immer nur die letzte Version, installiert:

mvn deploy:deploy-file -Durl=file://C:/workspace/myLegacyProject/maven/repository/ -Dfile=companyLib-latest.jar -Dsources=companyLib-latest-sources.jar -DgroupId=de.deringo -DartifactId=companyLib -Dpackaging=jar -Dversion=latest

Das macht man dann für alle Libraries und kann anschließend das legacy Repository löschen.

Repository referenzieren

Das lokale Repository im Projekt wird wie folgt referenziert:

  <repositories>
    <repository>
      <id>project.local</id>
      <name>project</name>
      <url>file:${project.basedir}/maven/repository</url>
    </repository>
  </repositories>

Lokale Libraries einbinden

Die lokalen Libraries lassen sich wie gewohnt einbinden:

  <dependencies>
    <dependency>
      <groupId>de.deringo</groupId>
      <artifactId>companyLib</artifactId>
      <version>latest</version>
    </dependency>
    [...]
</dependencies>
Categories
Java

ThreadLocal

Bei der Migration einer größeren Anwendung (> 120.000 LOC) von einem SAP NetWeaver 7.3 mit Java 1.6 auf einen Tomcat 8.5 mit Java 1.8 hatten wir ein “interessantes” Problem:

Nach der erfolgreichen Umstellung einer Schnittstelle (SAP PI/PO) auf unseren neuen Tomcat Server wurden wir am nächsten Tag damit konfrontiert, dass die PI keinen Zugang mehr zu unserem Server hätte. Die Verbindung würde zwar aufgebaut, aber die Annahme der Daten dann mit HTTP Code 401 abgelehnt.

Das Kuriose dabei: Im Prinzip findet gar keine Authentifizierung in der Anwendung statt, denn diese Schnittstelle ist lediglich für die PI freigegeben, über einen IP Filter.

Ein Neutstart unseres Tomcat-Servers konnte das Problem kurzfristig beheben, bis es dann wieder auftrat.

Der Code des empfangenden Servlets sieht stark vereinfacht so aus:

public class IdocImportServlet extends HttpServlet implements Servlet {
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        if (UserInSession.get() == null) {
            UserInSession.put("DiesIstDiePI");
        } else {
            response.setStatus(401);
            response.getWriter().println("Illegal attempt to submit data");
            return;
        }
   		doStuff(request, response);
    }
}

Da diese Kommunikation an den üblichen Authentifizierungsmechanismen vorbei läuft, muss der UserInSession auch immer NULL sein. Aber schauen wir uns diese Klasse mal genauer an, denn der Name “InSession” ist irreführend:

public final class UserInSession {
	private static ThreadLocal sessionUser = new ThreadLocal() {
        protected synchronized Object initialValue() {
            return null;
        }
    };
	public static void put(String userId) {
        sessionUser.set(user);
    }
	public static String get() {
        return ((String) sessionUser.get());
    }
}

Unabhängig von der Session wird der User zu Begin der Verarbeitung im Servlet gesetzt. Es ist also eher ein “UserInRequest”, der da gesetzt wird.
Der User wird in einer ThreadLocal gespeichert und kann so später an anderer Stelle wieder aus dieser ThreadLocale ausgelesen werden.
Das alles sollte auch kein Problem machen und so funktionieren. Und beim Debugging hat es dann auch erstmal funktioniert.

Um mir das Debugging-Leben etwas einfacher zu gestalten, habe ich die Payload einer Übertragung aus den Logfiles herausgesucht und in eine Datei (data.xml) gespeichert und dann per Terminal Befehl an die Schnittstelle zu schicken:

curl -X POST -d @data.xml http://localhost:8080/myApp/PISchnittstelle

Beim ersten Mal hat alles funktioniert und auch dann noch ein paar Mal, bis es dann nicht mehr ging.
Das Problem ließ sich beim Debuggen erkennen: der UserInSession war auf einmal schon mit dem User “DiesIstDiePI” gesetzt und daher wurde der Zugang verweigert.

Wie sich bei der Analyse herausstelle, läuft ein Request in einem Thread. So weit so gut.
Der Tomcat stellt eine bestimmte Anzahl an Threads bereit. Und nach <Anzahl der Threads +1> Requests wird der erste Thread wiederverwertet. Dummerweise ist dabei aber noch die ThreadLocale aus dem ersten Request gefüllt.

In der alten SAP NetWeaver Umgebung scheint immer ein “frischer” Thread zu kommen, so dass der Code in der alten Umgebung funktioniert hat. Wodurch genau dieses unterschiedliche Verhalten ausgelöst wird, konnten wir leider nicht in vertretbarer Zeit herausfinden.

Lösung

Die Lösung (oder vielleicht doch eher ein Workaround) besteht darin, bei jedem Request die ThreadLocal zu löschen. Gerne hätten wir nach einer Lösung gesucht, die ThreadLocal möglicherweise ganz los zu werden, aber der Aufwand wäre in Summe zu groß gewesen (was ich hier auf dieser Seite darstelle ist eine sehr stark vereinfachte und verkürzte Version des Codes und des Problems).

Um den UserInSession vor jedem Aufruf des Servlets löschen zu können, haben wir einen zusätzlichen Filter eingebaut:

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;


public class ThreadLocalFilter implements Filter {

    public void destroy() {
        // nothing to do
    }

    public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2)
            throws IOException, ServletException {
        // Wipe all ThreadLocal
        UserInSession.put(null);
        arg2.doFilter(arg0, arg1);
    }

    public void init(FilterConfig arg0) throws ServletException {
        // nothing to do
    }
}

Und diesen Filter entsprechend in der web.xml eingebaut:

    <filter>
        <filter-name>ThreadLocalFilter</filter-name>
        <filter-class>deringo.filter.ThreadLocalFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>ThreadLocalFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

Categories
Java

Generate Test Data

I need some test data to be displayed in my PrimeFaces Template App.

A good library to generate fake data is: Java Faker.

Add Java Faker to dependencies in Maven:

		<dependency>
			<groupId>com.github.javafaker</groupId>
			<artifactId>javafaker</artifactId>
			<version>1.0.2</version>
		</dependency>

For Example I want to have a Person with following fields:

	private String firstName;
	private String lastName;
	private Date dayOfBirth;
	private String phoneNumber;
	private String streetName;
	private String streetAddressNumber;
	private String zipCode;
	private String city;
	private String state;

A Person can be generated with fake data from Java Faker like this:

String firstName = faker.name().firstName();
String lastName = faker.name().lastName();
Date dayOfBirth = faker.date().birthday();
String phoneNumber = faker.phoneNumber().phoneNumber();
String streetName = faker.address().streetName();
String streetAddressNumber = faker.address().streetAddressNumber();
String zipCode = faker.address().zipCode();
String city = faker.address().city();
String state = faker.address().state();

Person person = new Person(firstName, lastName, dayOfBirth, phoneNumber, streetName, streetAddressNumber, zipCode, city, state);

Categories
Java

Test driven Json Analysis

To analyse an unknown Json API I setup a small project with Smallrye Rest Client to access the Json structure. I added JUnit for a test driven approach and Hamcrest for Matchers (like assertThat or is).

<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>deringo</groupId>
	<artifactId>testproject</artifactId>
	<version>1.0-SNAPSHOT</version>
	<name>testproject</name>
	<description></description>

	<properties>
		<maven.compiler.source>8</maven.compiler.source>
		<maven.compiler.target>8</maven.compiler.target>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
	</properties>

	<dependencies>
		<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
		<dependency>
			<groupId>org.junit.jupiter</groupId>
			<artifactId>junit-jupiter-api</artifactId>
			<version>5.7.0</version>
			<scope>test</scope>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.hamcrest/hamcrest -->
		<dependency>
			<groupId>org.hamcrest</groupId>
			<artifactId>hamcrest</artifactId>
			<version>2.2</version>
			<scope>test</scope>
		</dependency>
		<!-- https://mvnrepository.com/artifact/io.smallrye/smallrye-rest-client -->
		<dependency>
			<groupId>io.smallrye</groupId>
			<artifactId>smallrye-rest-client</artifactId>
			<version>1.2.2</version>
		</dependency>
	</dependencies>

</project>
package deringo.testproject;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.junit.jupiter.api.Test;

public class TestJson {

	@Test
	public void testClientBuilderWorking() {
		Client client = ClientBuilder.newClient();
		WebTarget target = client.target("https://www.intensivregister.de/api/public/intensivregister");
		Response response = target.request(MediaType.APPLICATION_JSON).get();
		assertThat(response.getStatus(), is(200));
		JsonObject jo = response.readEntity(JsonObject.class);
		int rowCount = jo.getJsonNumber("rowCount").intValue();
		JsonArray ja = jo.getJsonArray("data");
		assertThat(ja.size(), is(rowCount));
		assertNotNull(ja.get(0).asJsonObject().getJsonObject("krankenhausStandort").getString("id"));

		ja.forEach(value -> 
				assertThat(
						value.asJsonObject().getString("id"),
						is(value.asJsonObject().getJsonObject("krankenhausStandort").getString("id"))
				));
	}

}
Categories
Java

PrimeFaces Template App

I need an easy way to show some database data of an existing application. The architecture and technic of the application is quite old and unconfortable, so I decided to setup a new project with a modern framework.

I need overview of data in a table, maybe with CSV or PDF file export. A chart to show the number of incoming data per time etc.
I want to use a framwork that provides components for this requirements, so I do not have to code much for things like paging, file export etc.
I one of my prior projects we used Java Server Faces, and so I came up to give PrimeFaces a try. They have a good ShowCase to show their components.

Unfortunatly it was a little bit more complex to setup the project than I thought at the beginning. No rocket science, but I took me some time for the initial setup, therefore I decided to extract this to a PrimeFaces Template Application to easily reuse it next time and uploaded it to GitHub.

Setup project

Created a new Maven project in Eclipse.
Added Eclipse Gitignore defaults from GitHub and target folder (created by Maven) to .gitignore file.
Added beans.xml, web.xml and index.xhtml files to project:


CDI

For CDI we need the beans.xml file. Nothing special, it just has to be there:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
	bean-discovery-mode="all">
</beans>

PrimeFaces Configuration

Minimum setup in web.xml, except of the explicit use of the Omega theme:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="4.0"
	xmlns="http://xmlns.jcp.org/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd">

	<servlet>
		<servlet-name>Faces Servlet</servlet-name>
		<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<context-param>
		<param-name>primefaces.THEME</param-name>
		<param-value>omega</param-value>
	</context-param>
	
	<servlet-mapping>
		<servlet-name>Faces Servlet</servlet-name>
		<url-pattern>*.xhtml</url-pattern>
	</servlet-mapping>

	<welcome-file-list>
		<welcome-file>index.xhtml</welcome-file>
	</welcome-file-list>
</web-app>

Dependencies

I want to use Tomcat and not a EE application server like Payara. Therefore I have to add JSF. And I want to use the current version, which is 2.3, so I have to add CDI (JBoss Weld) also.

Since this JSF version, the JSF managed bean facility @ManagedBean is DEPRECATED in in favour of CDI and CDI has become a REQUIRED dependency for JSF 2.3.

Of course PrimeFaces has to be added as dependency, currently in version 8.0 and PrimeFaces Themes.

<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>deringo</groupId>
	<artifactId>primefacestemplate</artifactId>
	<version>1.0-SNAPSHOT</version>
	<packaging>war</packaging>
	<name>PrimeFacesTemplate</name>
	<description></description>

	<properties>
		<maven.compiler.source>15</maven.compiler.source>
		<maven.compiler.target>15</maven.compiler.target>
		<failOnMissingWebXml>false</failOnMissingWebXml>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
	</properties>

	<repositories>
		<repository>
			<id>prime-repo</id>
			<name>PrimeFaces Maven Repository</name>
			<url>http://repository.primefaces.org</url>
			<layout>default</layout>
		</repository>
	</repositories>

	<dependencies>
		<dependency>
			<groupId>org.apache.tomcat</groupId>
			<artifactId>tomcat</artifactId>
			<version>9.0.41</version>
			<type>pom</type>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax</groupId>
			<artifactId>javaee-api</artifactId>
			<version>8.0</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.enterprise</groupId>
			<artifactId>cdi-api</artifactId>
			<version>2.0</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.jboss.weld.servlet</groupId>
			<artifactId>weld-servlet</artifactId>
			<version>2.4.4.Final</version>
		</dependency>
		<dependency>
			<groupId>javax.faces</groupId>
			<artifactId>javax.faces-api</artifactId>
			<version>2.3</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.glassfish</groupId>
			<artifactId>javax.faces</artifactId>
			<version>2.3.0</version>
		</dependency>
		<dependency>
			<groupId>org.omnifaces</groupId>
			<artifactId>omnifaces</artifactId>
			<version>3.2</version>
		</dependency>
		<dependency>
			<groupId>org.primefaces</groupId>
			<artifactId>primefaces</artifactId>
			<version>8.0</version>
		</dependency>
		<dependency>
			<groupId>org.primefaces.themes</groupId>
			<artifactId>all-themes</artifactId>
			<version>1.0.10</version>
		</dependency>
		<dependency>
			<groupId>org.webjars.npm</groupId>
			<artifactId>primeflex</artifactId>
			<version>2.0.0</version>
		</dependency>
	</dependencies>

	<build>
		<finalName>PrimefacesTemplate</finalName>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-war-plugin</artifactId>
				<version>3.2.2</version>
				<configuration>
					<failOnMissingWebXml>false</failOnMissingWebXml>
					<warName>PrimefacesTemplate</warName>
					<wtpContextName>PrimefacesTemplate</wtpContextName>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

FlexGrid / PrimeFlex

Flex Grid CSS is a lightweight flex based responsive layout utility optimized for mobile phones, tablets and desktops. Flex Grid CSS is not included in PrimeFaces as it is provided by PrimeFlex, a shared grid library between PrimeFaces, PrimeNG and PrimeReact projects.

Add dependency for Webjar, so we do not need to download and copy the files in our project:

		<dependency>
			<groupId>org.webjars.npm</groupId>
			<artifactId>primeflex</artifactId>
			<version>2.0.0</version>
		</dependency>

Import PrimeFlex in index.xhtml:

<h:head>
  <h:outputStylesheet name="webjars/primeflex/2.0.0/primeflex.css" />
</h:head>

For the usage of PrimeFlex please have a look into the documentation.

PrimeIcons

The usage of PrimeIcons is well documented.
Just import the stylesheet and use them, example:

<h:head>
  <h:outputStylesheet name="primeicons/primeicons.css" library="primefaces" />
</h:head>

<h:body>
  <i class="pi pi-check"></i>
  <i class="pi pi-times"></i>
</h:body>