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.