Start
JExcellenceJExcellenceStart
Zurück zur Übersicht
Backend

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.

26. April 20269 Min Lesezeitvon Justin Eiletz
  • Hibernate
  • JPA
  • Java
  • Spring Boot
  • Minecraft
  • Open Source

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 CompletableFuture zurü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();          // true

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