Keycloak 20.0.1自定义UserStorageProvider创建EntityManagerFactory遇空指针
问题
我们正在将Keycloak 13.0.1迁移至Keycloak 20.0.1,应用将用户存储在外部PostgreSQL数据库中,通过自定义UserStorageProvider暴露给Keycloak。在Keycloak 20中,无法获取可用的EntityManagerFactory或EntityManager来支撑自定义UserStorageProvider。
相关代码
CustomUserStorageProviderFactory.java
public class CustomUserStorageProviderFactory implements UserStorageProviderFactory<CustomUserStorageProvider> { private static final String PROVIDER_ID = "CUSTOM_ID"; @Override public CustomUserStorageProvider create(KeycloakSession session, ComponentModel model) { try { log.info("Intializing user federation provider."); EntityManagerFactory factory = EntityManagerFactoryHelper.getFactory(); CustomUserStorageProvider provider = new CustomUserStorageProvider(session, model, factory.createEntityManager()); return provider; } catch (Exception e) { throw new RuntimeException(e); } } @Override public String getId() { return PROVIDER_ID; } }
EntityManagerFactoryHelper.java
public class EntityManagerFactoryHelper { private static String PERSISTENCE_UNIT_NAME = "customunit"; private static EntityManagerFactory factory; static { try { factory = Persistence.createEntityManagerFactory(PERSISTENCE_UNIT_NAME); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } } public static EntityManagerFactory getFactory() { return factory; } }
persistence.xml
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0"> <persistence-unit name="customunit"> <class>my.custom.Class</class> <exclude-unlisted-classes>true</exclude-unlisted-classes> <properties> <property name="javax.persistence.jdbc.url" value="jdbc:postgresql://database_url:5432/database_name" /> <property name="javax.persistence.jdbc.user" value="user" /> <property name="javax.persistence.jdbc.password" value="password" /> <property name="javax.persistence.provider" value="org.hibernate.jpa.HibernatePersistenceProvider" /> <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" /> <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" /> <property name="hibernate.hbm2ddl.auto" value="none" /> <property name="hibernate.show_sql" value="true" /> <property name="hibernate.format_sql" value="true" /> </properties> </persistence-unit> </persistence>
异常信息
自定义提供者代码打包为JAR放置在providers目录下,初始化CustomUserStorageProviderFactory时抛出如下异常:
java.lang.NullPointerException at io.quarkus.hibernate.orm.runtime.FastBootHibernatePersistenceProvider.getEntityManagerFactoryBuilderOrNull(FastBootHibernatePersistenceProvider.java:172) at io.quarkus.hibernate.orm.runtime.FastBootHibernatePersistenceProvider.createEntityManagerFactory(FastBootHibernatePersistenceProvider.java:66) at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:80) at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:55) at my.package.EntityManagerFactoryHelper.<clinit>(EntityManagerFactoryHelper.java:14) at my.package.CustomUserStorageProviderFactory.create(CustomUserStorageProviderFactory.java:22) at my.package.CustomUserStorageProviderFactory.create(CustomUserStorageProviderFactory.java:10)
空指针源于FastBootHibernatePersistenceProvider中以下代码,此处RecordedState为null:
RecordedState recordedState = PersistenceUnitsHolder.popRecordedState(persistenceUnitName); if (recordedState.isReactive()) { throw new IllegalStateException( "Attempting to boot a blocking Hibernate ORM instance on a reactive RecordedState"); }
开启TRACE日志后,Keycloak启动时JPA启动线程执行此代码无异常,能正常连接数据库、检查并注册实体。仅在初始化自定义提供者获取EntityManagerFactory时出现错误。
解决方案
Keycloak 20基于Quarkus重构,原有的JPA初始化逻辑不再适配,需调整为Quarkus兼容的方式:
移除静态块初始化逻辑
静态块在Quarkus类加载阶段执行,此时JPA上下文尚未完全初始化,导致无法获取RecordedState。直接通过Keycloak上下文获取EntityManager。通过KeycloakSession获取EntityManager
Keycloak 20的KeycloakSession已集成Quarkus上下文,可直接获取EntityManager:@Override public CustomUserStorageProvider create(KeycloakSession session, ComponentModel model) { try { log.info("Intializing user federation provider."); // 获取绑定到指定persistence unit的EntityManager EntityManager em = session.getEntityManagerFactory("customunit").createEntityManager(); CustomUserStorageProvider provider = new CustomUserStorageProvider(session, model, em); return provider; } catch (Exception e) { throw new RuntimeException(e); } }配置Quarkus数据源与Persistence Unit绑定
在conf/quarkus.properties中添加自定义数据源配置:# 自定义数据源配置 quarkus.datasource.customdb.db-kind=postgresql quarkus.datasource.customdb.jdbc.url=jdbc:postgresql://database_url:5432/database_name quarkus.datasource.customdb.username=user quarkus.datasource.customdb.password=password # 绑定persistence unit到自定义数据源 quarkus.hibernate-orm.customunit.datasource=customdb quarkus.hibernate-orm.customunit.packages=my.custom同时修改
persistence.xml,移除JDBC相关配置,适配Quarkus规范:<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_2.xsd" version="2.2"> <persistence-unit name="customunit"> <class>my.custom.Class</class> <exclude-unlisted-classes>true</exclude-unlisted-classes> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" /> <property name="hibernate.hbm2ddl.auto" value="none" /> <property name="hibernate.show_sql" value="true" /> <property name="hibernate.format_sql" value="true" /> </properties> </persistence-unit> </persistence>精简依赖包
自定义提供者JAR无需打包Hibernate或PostgreSQL驱动,Keycloak 20已内置这些依赖,避免版本冲突。
内容的提问来源于stack exchange,提问作者ziggy

