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.
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:
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 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:
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:
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:
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:
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();
}
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:
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:
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:
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).
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:
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.
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:
I need to directly debug on the application Tomcat server, not only on my local Tomcat instance.
Compile Java code
For debugging we need to keep the line numbers while compiling. To build the war file we use an ANT script and we have to add the debug and debuglevel attributes in the javac tag: