javaclassloaderminecraftebeanspigot

SpiRawSqlService not found setting up EBean in a Spigot plugin


Overview

I set up a github repo for this question to provide as much of the boiled down environment as possible.

My goal is to set up ebean ORM for database manangement from a Paper Minecraft plugin. I'm able to shade in the ebean dependencies, but creating a query using "io.ebean:ebean-querybean:" throws an error saying that it cannot find an implementation of SpiRawSqlService.

Environment

Paper Minecraft: paper-1.19.3-367.jar
Java 18
Ebean enhancement plugin for IntelliJ(I checked that I have it enabled for this project)
io.ebean gradle plugin version 13.10.0
shadowJar gradle plugin version 7.1.2

The Stacktrace

Everything is fine setting up the database, and saving to the Database. Queries without using a querybean work fine as well. The error is thrown when initializing any class containing a reference to a generated querybean.
The error outputted is printed the latest.log
Caused by: java.lang.IllegalStateException: No service implementation found for interface org.example.ebean.io.ebean.service.SpiRawSqlService

The stacktrace tells us that it couldn't find org.example.ebean.io.ebean.service.SpiRawSqlService.
Looking at the decompiled shadowJar after package relocation, the implementation for this class is found at org.example.ebean.io.ebeaninternal.server.rawsql.DRawSql;

Printing out the ClassLoader#getDefinedPackages on the instance supplied when creating the ebean Database connection results in this:

org.example.ebean
org.example.ebean.database
org.example.ebean.io.ebean
org.example.ebean.io.ebean.annotation
org.example.ebean.io.ebean.config
org.example.ebean.io.ebean.config.dbplatform
org.example.ebean.io.ebean.datasource
org.example.ebean.io.ebean.meta

As you can see, the org.example.ebean.io.ebeaninternal package and subpackages are not outputted in this list.

Underlying issue

How/where is the package "ebeaninternal" being loaded if at all? How can I get the enhanced querybean to find this package so it can load the implementation (DRawSql) of SpiRawSqlService?


Solution

  • Reasoning

    Bukkit's @EventHandler utilizes a different contextClassLoader than the ClassLoader that loads the ebean classes/services (contained in the ShadowJar).

    The error states No service implementation found because the thread that is initializing the querybean does not have access to that class.

    Explained Solution

    The solution here is to use Thread#setContextClassLoader() to use the same ClassLoader used when calling DatabaseFactory.createWithContextClassLoader(). Set the ClassLoader, initialize every Class that uses a QueryBean, revert the ClassLoader to what it originally was.

    EBean might be able to solve this problem. But for now, a fix is to just call an empty init method on every class that becomes an Enhanced-QueryBean from a thread that is using the proper ContextClassLoader

    Full Example

    I pushed the full example containing the fix to the original github repo

    Basic Example

    DatabaseSetup.java

    
        public static void load() {
            DataSourceConfig dataSourceConfig = configureDataSource();
            DatabaseConfig dbConfig = configureDatabase(dataSourceConfig);
    
            // We should use the classloader that loaded this plugin
            // because this plugin has our ebean dependencies
            ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
            ClassLoader pluginClassLoader = BukkitEBeanPlugin.class.getClassLoader();
    
            // create the DatabaseFactory with the classloader containing ebean dependencies
            DatabaseFactory.createWithContextClassLoader(dbConfig, pluginClassLoader);
    
            // Set the current thread's contextClassLoader to the classLoader with the ebean dependencies
            // This allows the class to initialize itself with access to the required class dependencies
            Thread.currentThread().setContextClassLoader(pluginClassLoader);
    
            // invoke the static initialization of every class that contains a querybean.
            // Note that any method in the class will initialize the class.
            FindByQueryBean.init();
    
            // Restore the contextClassLoader to what it was originally
            Thread.currentThread().setContextClassLoader(originalClassLoader);
    
            BukkitEBeanPlugin.get().getLogger().info("Successfully created database");
        }
        ...
    }
    

    FindByQueryBean.java

    public static void init() {
        // intentionally empty
    }