JExHibernate — Hibernate für Plugins, Spring Boot und alles dazwischen
Eigentlich war es als Wrapper für Minecraft-Plugins gedacht. Heute räumt JExHibernate auch in Spring-Boot-Projekten und Standalone-Java-Anwendungen ungefähr 65 % des Datenbank-Boilerplates weg.
Ich habe vor zwei Jahren angefangen, JExHibernate zu schreiben, weil mich der Zustand von Persistenz-Code in Minecraft-Plugins zu sehr nervte. Was als kleiner Wrapper begann, ist heute eine echte Bibliothek, die ich auch in produktiven Spring-Boot-Diensten einsetze. Repository: github.com/JExcellence/JEHibernate (das Artefakt heißt aus historischen Gründen JEHibernate; mündlich nenne ich es JExHibernate, weil es zur JExcellence-Familie gehört).
Dieser Post erklärt, was die Bibliothek macht, warum sie existiert, und wie sie sich in den drei Welten verhält, in denen sie regelmäßig läuft: Bukkit/Paper/Folia, Spring Boot, und Standalone-Java.
Was JExHibernate ist
Ein dünner, opinionierter Wrapper um Hibernate ORM 7.x mit einer fluenten API, die laut Eigenmessung etwa 65 % des Boilerplate-Codes wegnimmt, den man in einem typischen JPA-Projekt schreibt — Repository-Klassen, Pagination, Query-Builder, Caching, Transaktionsbehandlung, async-Varianten von allem. Java 17+, mit automatischer Nutzung von Virtual Threads, sobald die Laufzeit Java 21 oder neuer ist.
Acht Datenbanken werden out-of-the-box unterstützt: H2, MySQL, MariaDB, PostgreSQL, Oracle, SQL Server, SQLite und HSQLDB. Connection-Pooling über Agroal, Second-Level-Cache über JCache — beide optional, beide mit sinnvollen Defaults.
Drei Welten, ein Wrapper
1. Spigot, Paper, Folia
Hier ist JExHibernate geboren. Das Plugin-Umfeld hat drei spezifische Eigenarten, die jede ungeschützte Hibernate-Integration zerlegen:
- Class loaders. Bukkit, Paper und Folia laden jedes Plugin mit einem eigenen ClassLoader. Hibernate erwartet alle Entity-Klassen unter einem kanonischen Loader. JExHibernate übernimmt das Thread-Context-Wechseln intern, damit das nicht jeder Plugin-Entwickler einzeln debuggen muss.
- Main-Thread-Hygiene. Jede Repository-Methode hat eine Async-Variante, die ein
CompletableFuturezurückgibt. Das ist nicht bloß Convenience — auf einem Server, der zwanzig Mal pro Sekunde ticken muss, ist das die Grenze zwischen „läuft" und „zuckt unter Last". - Folia-Bewusstsein. Folia (PaperMC's regionalisierter Threading-Branch) verändert die Main-Thread-Annahmen. Die Repository-Schicht ist regions-agnostisch; die Async-Calls funktionieren auf allen drei Servern identisch.
Das Setup im Plugin-onEnable:
public class MyPlugin extends JavaPlugin {
private JEHibernate jeHibernate;
@Override
public void onEnable() {
saveResource("database/hibernate.properties", false);
jeHibernate = JEHibernate.builder()
.configuration(config -> config.fromProperties(
PropertyLoader.load(
getDataFolder(), "database", "hibernate.properties"
)))
.scanPackages("com.example.myplugin")
.build();
var playerRepo = jeHibernate.repositories().get(PlayerRepository.class);
playerRepo.preloadAsync();
}
@Override
public void onDisable() {
jeHibernate.close();
}
}Server-Admins wechseln die Datenbank über die mitgelieferte hibernate.properties ohne Recompilation — database.type=H2 für eine eingebettete Entwicklungs-DB, database.type=MYSQL für die produktive Box. Sechs weitere Anbieter funktionieren genauso.
2. Spring Boot
Spring Data JPA ist hervorragend — wenn man im Spring-Universum bleibt. Sobald man aber Code zwischen Plugins und Spring-Diensten teilen möchte (was bei mir oft der Fall ist, weil ein Web-Dashboard und das zugehörige Plugin auf dieselben Entities greifen), fängt es an zu reiben.
JExHibernate funktioniert in Spring Boot als reguläres @Bean und integriert sich mit @PreDestroy sauber in den Spring-Lifecycle:
@Configuration
public class JEHibernateConfig {
@Bean
public JEHibernate jeHibernate() {
return JEHibernate.builder()
.configuration(config -> config
.database(DatabaseType.POSTGRESQL)
.url("jdbc:postgresql://localhost:5432/mydb")
.credentials("user", "pass")
.ddlAuto("validate")
.connectionPool(5, 20))
.scanPackages("com.example")
.build();
}
@Bean
public PlayerRepository playerRepository(JEHibernate jeh) {
return jeh.repositories().get(PlayerRepository.class);
}
@PreDestroy
public void shutdown(JEHibernate jeh) { jeh.close(); }
}Was Spring Boot sich erspart: keine spring-boot-starter-data-jpa-Abhängigkeit, kein @EnableJpaRepositories-Geraffel, keine Property-Magie über drei verschiedene application.yml-Profile. Stattdessen ein einziger Bean, der überall identisch konfiguriert ist — Plugin oder Service.
3. Standalone-Java / CLI-Tools
Für CLI-Tools, Migrations-Skripte und ad-hoc-Datenfixes funktioniert es genauso. JEHibernate.fromProperties(...) in der main-Methode, fertig. Kein Container, kein Servlet-Stack, keine Annotations-Magie.
Die Features, die in der Praxis zählen
Repositories ohne Boilerplate
Eine vollständige Repository-Klasse ist drei Zeilen:
public class PlayerRepository extends AbstractCrudRepository<PlayerData, UUID> {
public PlayerRepository(ExecutorService ex, EntityManagerFactory emf, Class<PlayerData> cls) {
super(ex, emf, cls);
}
}Damit hast du findById, findAll, save, create, update, delete, refresh, exists, count, plus alle Async-Varianten, plus Batch-Operationen (saveAll, deleteAll), plus Pagination mit PageResult-Metadaten, Specifications und einen typsicheren Query Builder.
Query Builder — kein SQL, kein JPQL
var richActive = repo.query()
.and("active", true)
.greaterThan("balance", 10_000)
.like("username", "%alice%")
.in("rank", List.of("VIP", "ADMIN"))
.orderByDesc("balance")
.fetch("inventory") // INNER JOIN FETCH
.fetchLeft("guild") // LEFT JOIN FETCH (nullable)
.getPage(0, 20);
richActive.totalElements(); // 14_823
richActive.totalPages(); // 742
richActive.hasNext(); // trueDas ist der Punkt, an dem JExHibernate sich klar von einem rohen Hibernate-Setup absetzt: kein N+1 mehr aus Versehen, kein LazyInitializationException-Drama, kein manuelles Page-Counting in einer zweiten Query.
Cached Repositories
Für Lookups, die häufig vorkommen aber selten ändern (Profile, Rangliste, Berechtigungen), gibt es AbstractCachedRepository — eine Doppelschicht aus Caffeine-Caches mit ID- und Custom-Key-Lookup, Stale-while-Revalidate, TTL-Jitter gegen Stampede-Probleme, automatischer Invalidierung bei Mutationen.
public class PlayerRepository
extends AbstractCachedRepository<PlayerData, UUID, String> {
public PlayerRepository(ExecutorService ex, EntityManagerFactory emf, Class<PlayerData> cls) {
super(ex, emf, cls,
PlayerData::getUsername, // Cache-Key
CacheConfig.builder()
.expiration(Duration.ofMinutes(30))
.refreshAfterWrite(Duration.ofMinutes(25))
.maxSize(5000)
.jitterPercent(10)
.build());
}
}Auf einem Server mit zehntausend Spielern macht das den Unterschied zwischen einem Lookup in 5 ms und einem in 200 ns.
Optimistic Lock Retry
Konkurrierende Updates auf derselben Entity sind Realität, sobald mehr als ein Thread mit den Daten arbeitet. Statt jeden Aufruf einzeln in Try-Catch zu wickeln:
OptimisticLockRetry.execute(() -> {
var p = repo.findByIdOrThrow(uuid);
p.setBalance(p.getBalance() + amount);
return repo.save(p);
});Standardmäßig drei Versuche mit Exponential Backoff. Optional auch für Datenbank-Deadlocks aktivierbar.
Slow Query Detection
Jede Query, die länger als 500 ms läuft, wird automatisch auf WARN-Level geloggt. Die Schwelle ist konfigurierbar. Eine der wenigen Funktionen, die ich im Tagesgeschäft jeden Tag nutze — auf einem produktiven Server ist das oft das Erste, was vor einer kommenden Performance-Wand warnt.
Was JExHibernate bewusst nicht macht
- Kein eigenes DI-Framework. Es gibt eine kleine
@Inject-Implementierung für ad-hoc-Wiring, aber wer Spring oder Guice einsetzt, behält das. - Keine REST-Schicht. Wer das braucht, kombiniert JExHibernate mit Javalin, Spark oder Spring — was ich für Web-Dashboards regelmäßig tue.
- Keine Multi-Server-Synchronisation. Eventbus oder Redis-Pub/Sub liegen außerhalb des Scope.
Wo es hingeht
Aktuelle Version (Mai 2026): 3.0.1 mit Hibernate 7.x und Jakarta Persistence 3.1+. 78 Tests, alle grün.
Auf der Roadmap:
- Bessere Test-Werkzeuge — eine
@RepositoryTest-Annotation, die ein H2-In-Memory-DB plus Schema-Migrationen automatisch aufsetzt. - Tiefere Folia-Integration. Die Repository-Schicht ist heute regions-agnostisch; das Lifecycle-Management wird sauberer Folia-spezifisch ausgewiesen.
- Mehr Datenbank-Dialekte falls Bedarf aufkommt — CockroachDB und YugabyteDB sind die nächsten Kandidaten.
Wer Plugins, Spring-Services oder Standalone-Tools mit echter Persistenz baut und nicht jedes Mal denselben Boilerplate schreiben will, schaut sich JExHibernate an. Pull Requests willkommen — der Code ist absichtlich übersichtlich gehalten und vollständig dokumentiert.
Repository: github.com/JExcellence/JEHibernate · Apache License 2.0 · 78 Tests · Java 17+, Hibernate 7.x.