diff --git a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java index 5ce56cb6f6..59a549fcee 100644 --- a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java +++ b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java @@ -24,6 +24,7 @@ import javax.script.ScriptEngineManager; import javax.script.ScriptException; import org.keycloak.models.ScriptModel; +import org.keycloak.services.ServicesLogger; /** * A {@link ScriptingProvider} that uses a {@link ScriptEngineManager} to evaluate scripts with a {@link ScriptEngine}. @@ -32,14 +33,10 @@ import org.keycloak.models.ScriptModel; */ public class DefaultScriptingProvider implements ScriptingProvider { - private final ScriptEngineManager scriptEngineManager; + private final DefaultScriptingProviderFactory factory; - DefaultScriptingProvider(ScriptEngineManager scriptEngineManager) { - if (scriptEngineManager == null) { - throw new IllegalStateException("scriptEngineManager must not be null!"); - } - - this.scriptEngineManager = scriptEngineManager; + DefaultScriptingProvider(DefaultScriptingProviderFactory factory) { + this.factory = factory; } /** @@ -69,7 +66,7 @@ public class DefaultScriptingProvider implements ScriptingProvider { throw new IllegalArgumentException("script must not be null or empty"); } - ScriptEngine engine = createPreparedScriptEngine(scriptModel); + ScriptEngine engine = getPreparedScriptEngine(scriptModel); if (engine instanceof Compilable) { return new CompiledEvaluatableScriptAdapter(scriptModel, tryCompile(scriptModel, (Compilable) engine)); @@ -99,13 +96,26 @@ public class DefaultScriptingProvider implements ScriptingProvider { /** * Looks-up a {@link ScriptEngine} with prepared {@link Bindings} for the given {@link ScriptModel Script}. */ - private ScriptEngine createPreparedScriptEngine(ScriptModel script) { + private ScriptEngine getPreparedScriptEngine(ScriptModel script) { + // Try to lookup shared engine in the cache first + if (factory.isEnableScriptEngineCache()) { + ScriptEngine scriptEngine = factory.getScriptEngineCache().get(script.getMimeType()); + if (scriptEngine != null) return scriptEngine; + } + ScriptEngine scriptEngine = lookupScriptEngineFor(script); if (scriptEngine == null) { throw new IllegalStateException("Could not find ScriptEngine for script: " + script); } + ServicesLogger.LOGGER.scriptEngineCreated(scriptEngine.getFactory().getEngineName(), scriptEngine.getFactory().getEngineVersion(), script.getMimeType()); + + // Nashorn scriptEngine is ok to cache and share across multiple threads + if (factory.isEnableScriptEngineCache()) { + factory.getScriptEngineCache().put(script.getMimeType(), scriptEngine); + } + return scriptEngine; } @@ -116,7 +126,7 @@ public class DefaultScriptingProvider implements ScriptingProvider { ClassLoader cl = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(DefaultScriptingProvider.class.getClassLoader()); - return scriptEngineManager.getEngineByMimeType(script.getMimeType()); + return factory.getScriptEngineManager().getEngineByMimeType(script.getMimeType()); } finally { Thread.currentThread().setContextClassLoader(cl); diff --git a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProviderFactory.java b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProviderFactory.java index 4ed1efd2da..509bc58fe0 100644 --- a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProviderFactory.java +++ b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProviderFactory.java @@ -16,10 +16,16 @@ */ package org.keycloak.scripting; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory; +import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; /** @@ -27,20 +33,40 @@ import javax.script.ScriptEngineManager; */ public class DefaultScriptingProviderFactory implements ScriptingProviderFactory { + private static final Logger logger = Logger.getLogger(DefaultScriptingProviderFactory.class); + static final String ID = "script-based-auth"; private ScriptEngineManager scriptEngineManager; + private boolean enableScriptEngineCache; + + // Key is mime-type. Value is engine for the particular mime-type. Cache can be used when the scriptEngine can be shared across multiple threads / requests (which is the case for nashorn) + private Map scriptEngineCache; + @Override public ScriptingProvider create(KeycloakSession session) { lazyInit(); - return new DefaultScriptingProvider(scriptEngineManager); + return new DefaultScriptingProvider(this); } @Override public void init(Config.Scope config) { - //NOOP + this.enableScriptEngineCache = config.getBoolean("enable-script-engine-cache", true); + logger.debugf("Enable script engine cache: %b", this.enableScriptEngineCache); + } + + ScriptEngineManager getScriptEngineManager() { + return scriptEngineManager; + } + + boolean isEnableScriptEngineCache() { + return enableScriptEngineCache; + } + + Map getScriptEngineCache() { + return scriptEngineCache; } @Override @@ -63,6 +89,9 @@ public class DefaultScriptingProviderFactory implements ScriptingProviderFactory synchronized (this) { if (scriptEngineManager == null) { scriptEngineManager = new ScriptEngineManager(); + if (enableScriptEngineCache) { + scriptEngineCache = new ConcurrentHashMap<>(); + } } } } diff --git a/services/src/main/java/org/keycloak/services/ServicesLogger.java b/services/src/main/java/org/keycloak/services/ServicesLogger.java index a67ba6efa4..cb4c15d0b8 100644 --- a/services/src/main/java/org/keycloak/services/ServicesLogger.java +++ b/services/src/main/java/org/keycloak/services/ServicesLogger.java @@ -462,4 +462,9 @@ public interface ServicesLogger extends BasicLogger { @LogMessage(level = ERROR) @Message(id=105, value="Response_mode 'query.jwt' is allowed only when the authorization response token is encrypted") void responseModeQueryJwtNotAllowed(); + + @LogMessage(level = INFO) + @Message(id=106, value="Created script engine '%s', version '%s' for the mime type '%s'") + @Once + void scriptEngineCreated(String engineName, String engineVersion, String mimeType); }