From 98efb9ab6697895354b95fca65019009a872b2c2 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 17 Dec 2021 13:45:02 +0100 Subject: [PATCH] ArC - introduce the CurrentContextFactory abstraction - this API allows quarkus to supply an optimized backend for a context of a normal scope - currently it's only used by the request context - an extension can provide a custom factory via CurrentContextFactoryBuildItem --- .../quarkus/arc/deployment/ArcProcessor.java | 6 +- .../CurrentContextFactoryBuildItem.java | 22 ++++++ .../io/quarkus/arc/runtime/ArcRecorder.java | 6 +- .../src/main/java/io/quarkus/arc/Arc.java | 9 ++- .../java/io/quarkus/arc/ArcContainer.java | 7 ++ .../java/io/quarkus/arc/CurrentContext.java | 31 +++++++++ .../io/quarkus/arc/CurrentContextFactory.java | 15 +++++ .../io/quarkus/arc/InjectableContext.java | 3 - .../io/quarkus/arc/impl/ArcContainerImpl.java | 15 ++++- .../io/quarkus/arc/impl/RequestContext.java | 67 ++++++++----------- .../arc/impl/ThreadLocalCurrentContext.java | 30 +++++++++ .../ThreadLocalCurrentContextFactory.java | 20 ++++++ 12 files changed, 181 insertions(+), 50 deletions(-) create mode 100644 extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/CurrentContextFactoryBuildItem.java create mode 100644 independent-projects/arc/runtime/src/main/java/io/quarkus/arc/CurrentContext.java create mode 100644 independent-projects/arc/runtime/src/main/java/io/quarkus/arc/CurrentContextFactory.java create mode 100644 independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ThreadLocalCurrentContext.java create mode 100644 independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ThreadLocalCurrentContextFactory.java diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java index f8f0e332c04552..5a9a4255e52ac9 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java @@ -512,7 +512,8 @@ public BeanContainerBuildItem generateResources(ArcConfig config, ArcRecorder re LiveReloadBuildItem liveReloadBuildItem, BuildProducer generatedResource, BuildProducer bytecodeTransformer, - List reflectiveBeanClasses) throws Exception { + List reflectiveBeanClasses, + Optional currentContextFactory) throws Exception { for (ValidationErrorBuildItem validationError : validationErrors) { for (Throwable error : validationError.getValues()) { @@ -593,7 +594,8 @@ public void registerSubclass(DotName beanClassName, String subclassName) { reflectiveClasses.produce(new ReflectiveClassBuildItem(true, false, binding.name().toString())); } - ArcContainer container = recorder.getContainer(shutdown); + ArcContainer container = recorder.initContainer(shutdown, + currentContextFactory.isPresent() ? currentContextFactory.get().getFactory() : null); BeanContainer beanContainer = recorder.initBeanContainer(container, beanContainerListenerBuildItems.stream().map(BeanContainerListenerBuildItem::getBeanContainerListener) .collect(Collectors.toList())); diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/CurrentContextFactoryBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/CurrentContextFactoryBuildItem.java new file mode 100644 index 00000000000000..52be0075faea26 --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/CurrentContextFactoryBuildItem.java @@ -0,0 +1,22 @@ +package io.quarkus.arc.deployment; + +import io.quarkus.arc.CurrentContextFactory; +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.runtime.RuntimeValue; + +/** + * An extension can provide a custom {@link CurrentContextFactory}. + */ +public final class CurrentContextFactoryBuildItem extends SimpleBuildItem { + + private final RuntimeValue factory; + + public CurrentContextFactoryBuildItem(RuntimeValue factory) { + this.factory = factory; + } + + public RuntimeValue getFactory() { + return factory; + } + +} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java index 82de993b4c2589..2f48f6ec8c5266 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java @@ -14,6 +14,7 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.CurrentContextFactory; import io.quarkus.arc.InjectableBean; import io.quarkus.arc.InjectableBean.Kind; import io.quarkus.arc.impl.ArcContainerImpl; @@ -36,8 +37,9 @@ public class ArcRecorder { */ public static volatile Map> supplierMap; - public ArcContainer getContainer(ShutdownContext shutdown) throws Exception { - ArcContainer container = Arc.initialize(); + public ArcContainer initContainer(ShutdownContext shutdown, RuntimeValue currentContextFactory) + throws Exception { + ArcContainer container = Arc.initialize(currentContextFactory != null ? currentContextFactory.getValue() : null); shutdown.addShutdownTask(new Runnable() { @Override public void run() { diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Arc.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Arc.java index f8237ae0924916..1041df2099e7df 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Arc.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Arc.java @@ -11,18 +11,23 @@ public final class Arc { private static final AtomicReference INSTANCE = new AtomicReference<>(); + public static ArcContainer initialize() { + return initialize(null); + } + /** * + * @param currentContextFactory * @return the initialized container */ - public static ArcContainer initialize() { + public static ArcContainer initialize(CurrentContextFactory currentContextFactory) { ArcContainerImpl container = INSTANCE.get(); if (container == null) { synchronized (INSTANCE) { container = INSTANCE.get(); if (container == null) { - container = new ArcContainerImpl(); // Set the container instance first because Arc.container() can be used within ArcContainerImpl.init() + container = new ArcContainerImpl(currentContextFactory); INSTANCE.set(container); container.init(); } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainer.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainer.java index 5979a5bae573c8..1a8192c6082e2a 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainer.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainer.java @@ -179,4 +179,11 @@ public interface ArcContainer { * @return the default executor service */ ExecutorService getExecutorService(); + + /** + * + * @return the factory + * @see CurrentContext + */ + CurrentContextFactory getCurrentContextFactory(); } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/CurrentContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/CurrentContext.java new file mode 100644 index 00000000000000..d85cdf754ba80d --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/CurrentContext.java @@ -0,0 +1,31 @@ +package io.quarkus.arc; + +import io.quarkus.arc.InjectableContext.ContextState; + +/** + * Represents the current context of a normal scope. + * + * @param + * @see CurrentContextFactory + */ +public interface CurrentContext { + + /** + * + * @return the current state + */ + T get(); + + /** + * Sets the current state. + * + * @param state + */ + void set(T state); + + /** + * Removes the current state. + */ + void remove(); + +} diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/CurrentContextFactory.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/CurrentContextFactory.java new file mode 100644 index 00000000000000..d85a4452be3a04 --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/CurrentContextFactory.java @@ -0,0 +1,15 @@ +package io.quarkus.arc; + +import io.quarkus.arc.InjectableContext.ContextState; +import java.lang.annotation.Annotation; + +/** + * This factory can be used to create a new {@link CurrentContext} for a normal scope, e.g. for + * {@link javax.enterprise.context.RequestScoped}. It's usually not necessary for shared contexts, such as + * {@link javax.enterprise.context.ApplicationScoped}. + */ +public interface CurrentContextFactory { + + CurrentContext create(Class scope); + +} diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableContext.java index a7b8346cfb1846..4a48af59363efc 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableContext.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableContext.java @@ -85,9 +85,6 @@ default boolean isNormal() { return getScope().isAnnotationPresent(NormalScope.class); } - /** - * - */ interface ContextState { /** diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java index 3f80c908bc6d64..37d0faca572a53 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java @@ -6,6 +6,7 @@ import io.quarkus.arc.ArcContainer; import io.quarkus.arc.Components; import io.quarkus.arc.ComponentsProvider; +import io.quarkus.arc.CurrentContextFactory; import io.quarkus.arc.InjectableBean; import io.quarkus.arc.InjectableContext; import io.quarkus.arc.InjectableDecorator; @@ -46,6 +47,7 @@ import javax.enterprise.context.Destroyed; import javax.enterprise.context.Initialized; import javax.enterprise.context.NormalScope; +import javax.enterprise.context.RequestScoped; import javax.enterprise.event.Event; import javax.enterprise.inject.AmbiguousResolutionException; import javax.enterprise.inject.Any; @@ -95,7 +97,9 @@ public class ArcContainerImpl implements ArcContainer { private volatile ExecutorService executorService; - public ArcContainerImpl() { + private final CurrentContextFactory currentContextFactory; + + public ArcContainerImpl(CurrentContextFactory currentContextFactory) { id = String.valueOf(ID_GENERATOR.incrementAndGet()); running = new AtomicBoolean(true); List> beans = new ArrayList<>(); @@ -105,10 +109,12 @@ public ArcContainerImpl() { List> observers = new ArrayList<>(); Map, Set> transitiveInterceptorBindings = new HashMap<>(); Map> qualifierNonbindingMembers = new HashMap<>(); + this.currentContextFactory = currentContextFactory == null ? new ThreadLocalCurrentContextFactory() + : currentContextFactory; applicationContext = new ApplicationContext(); singletonContext = new SingletonContext(); - requestContext = new RequestContext(); + requestContext = new RequestContext(currentContextFactory.create(RequestScoped.class)); contexts = new HashMap<>(); putContext(requestContext); putContext(applicationContext); @@ -342,6 +348,11 @@ public void setExecutor(ExecutorService executor) { this.executorService = executor; } + @Override + public CurrentContextFactory getCurrentContextFactory() { + return currentContextFactory; + } + @Override public String toString() { return "ArcContainerImpl [id=" + id + ", running=" + running + ", beans=" + beans.size() + ", observers=" diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/RequestContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/RequestContext.java index 43a1f83333c800..c805d84593d613 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/RequestContext.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/RequestContext.java @@ -1,6 +1,7 @@ package io.quarkus.arc.impl; import io.quarkus.arc.ContextInstanceHandle; +import io.quarkus.arc.CurrentContext; import io.quarkus.arc.InjectableBean; import io.quarkus.arc.ManagedContext; import io.quarkus.arc.impl.EventImpl.Notifier; @@ -33,14 +34,14 @@ class RequestContext implements ManagedContext { private static final Logger LOGGER = Logger.getLogger(RequestContext.class.getPackage().getName()); - // It's a normal scope so there may be no more than one mapped instance per contextual type per thread - private final ThreadLocal currentContext = new ThreadLocal<>(); + private final CurrentContext currentContext; private final LazyValue> initializedNotifier; private final LazyValue> beforeDestroyedNotifier; private final LazyValue> destroyedNotifier; - public RequestContext() { + public RequestContext(CurrentContext currentContext) { + this.currentContext = currentContext; this.initializedNotifier = new LazyValue<>(RequestContext::createInitializedNotifier); this.beforeDestroyedNotifier = new LazyValue<>(RequestContext::createBeforeDestroyedNotifier); this.destroyedNotifier = new LazyValue<>(RequestContext::createDestroyedNotifier); @@ -62,17 +63,16 @@ public T getIfActive(Contextual contextual, Function, Creat } RequestContextState ctxState = currentContext.get(); if (ctxState == null) { - // Thread local not set - context is not active! + // Context is not active! return null; } - Map, ContextInstanceHandle> ctxMap = currentContext.get().value; - ContextInstanceHandle instance = (ContextInstanceHandle) ctxMap.get(contextual); + ContextInstanceHandle instance = (ContextInstanceHandle) ctxState.map.get(contextual); if (instance == null) { CreationalContext creationalContext = creationalContextFun.apply(contextual); // Bean instance does not exist - create one if we have CreationalContext instance = new ContextInstanceHandleImpl((InjectableBean) contextual, contextual.create(creationalContext), creationalContext); - ctxMap.put(contextual, instance); + ctxState.map.put(contextual, instance); } return instance.get(); } @@ -82,7 +82,6 @@ public T get(Contextual contextual, CreationalContext creationalContex T result = getIfActive(contextual, CreationalContextImpl.unwrap(Objects.requireNonNull(creationalContext, "CreationalContext must not be null"))); if (result == null) { - // Thread local not set - context is not active! throw new ContextNotActiveException(); } return result; @@ -96,12 +95,11 @@ public T get(Contextual contextual) { if (!Scopes.scopeMatches(this, bean)) { throw Scopes.scopeDoesNotMatchException(this, bean); } - Map, ContextInstanceHandle> ctx = currentContext.get().value; - if (ctx == null) { - // Thread local not set - context is not active! + RequestContextState state = currentContext.get(); + if (state == null) { throw new ContextNotActiveException(); } - ContextInstanceHandle instance = (ContextInstanceHandle) ctx.get(contextual); + ContextInstanceHandle instance = (ContextInstanceHandle) state.map.get(contextual); return instance == null ? null : instance.get(); } @@ -112,12 +110,12 @@ public boolean isActive() { @Override public void destroy(Contextual contextual) { - Map, ContextInstanceHandle> ctx = currentContext.get().value; - if (ctx == null) { - // Thread local not set - context is not active! + RequestContextState state = currentContext.get(); + if (state == null) { + // Context is not active throw new ContextNotActiveException(); } - ContextInstanceHandle instance = ctx.remove(contextual); + ContextInstanceHandle instance = state.map.remove(contextual); if (instance != null) { instance.destroy(); } @@ -131,7 +129,7 @@ public void activate(ContextState initialState) { fireIfNotEmpty(initializedNotifier); } else { if (initialState instanceof RequestContextState) { - currentContext.set(((RequestContextState) initialState)); + currentContext.set((RequestContextState) initialState); } else { throw new IllegalArgumentException("Invalid initial state: " + initialState.getClass().getName()); } @@ -140,20 +138,16 @@ public void activate(ContextState initialState) { @Override public ContextState getState() { - RequestContextState ctx = currentContext.get(); - if (ctx == null) { + RequestContextState state = currentContext.get(); + if (state == null) { // Thread local not set - context is not active! throw new ContextNotActiveException(); } - return ctx; + return state; } public ContextState getStateIfActive() { - RequestContextState ctx = currentContext.get(); - if (ctx == null) { - return null; - } - return ctx; + return currentContext.get(); } @Override @@ -175,15 +169,8 @@ public void destroy(ContextState state) { if (state instanceof RequestContextState) { RequestContextState reqState = ((RequestContextState) state); reqState.isValid.set(false); - destroy(reqState.value); - } else { - throw new IllegalArgumentException("Invalid state: " + state.getClass().getName()); - } - } - - private void destroy(Map, ContextInstanceHandle> currentContext) { - if (currentContext != null) { - synchronized (currentContext) { + synchronized (state) { + Map, ContextInstanceHandle> map = ((RequestContextState) state).map; // Fire an event with qualifier @BeforeDestroyed(RequestScoped.class) if there are any observers for it try { fireIfNotEmpty(beforeDestroyedNotifier); @@ -191,15 +178,17 @@ private void destroy(Map, ContextInstanceHandle> currentContext LOGGER.warn("An error occurred during delivery of the @BeforeDestroyed(RequestScoped.class) event", e); } //Performance: avoid an iterator on the map elements - currentContext.forEach(this::destroyContextElement); + map.forEach(this::destroyContextElement); // Fire an event with qualifier @Destroyed(RequestScoped.class) if there are any observers for it try { fireIfNotEmpty(destroyedNotifier); } catch (Exception e) { LOGGER.warn("An error occurred during delivery of the @Destroyed(RequestScoped.class) event", e); } - currentContext.clear(); + map.clear(); } + } else { + throw new IllegalArgumentException("Invalid state implementation: " + state.getClass().getName()); } } @@ -238,17 +227,17 @@ private static Notifier createDestroyedNotifier() { static class RequestContextState implements ContextState { - private final ConcurrentMap, ContextInstanceHandle> value; + private final Map, ContextInstanceHandle> map; private final AtomicBoolean isValid; RequestContextState(ConcurrentMap, ContextInstanceHandle> value) { - this.value = value; + this.map = Objects.requireNonNull(value); this.isValid = new AtomicBoolean(true); } @Override public Map, Object> getContextualInstances() { - return value.values().stream() + return map.values().stream() .collect(Collectors.toUnmodifiableMap(ContextInstanceHandle::getBean, ContextInstanceHandle::get)); } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ThreadLocalCurrentContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ThreadLocalCurrentContext.java new file mode 100644 index 00000000000000..5ffd6437ca1939 --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ThreadLocalCurrentContext.java @@ -0,0 +1,30 @@ +package io.quarkus.arc.impl; + +import io.quarkus.arc.CurrentContext; +import io.quarkus.arc.InjectableContext.ContextState; + +/** + * {@link ThreadLocal} implementation of {@link CurrentContext}. + * + * @param + */ +final class ThreadLocalCurrentContext implements CurrentContext { + + private final ThreadLocal currentContext = new ThreadLocal<>(); + + @Override + public T get() { + return currentContext.get(); + } + + @Override + public void set(T state) { + currentContext.set(state); + } + + @Override + public void remove() { + currentContext.remove(); + } + +} diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ThreadLocalCurrentContextFactory.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ThreadLocalCurrentContextFactory.java new file mode 100644 index 00000000000000..636ac5f02fcbf8 --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ThreadLocalCurrentContextFactory.java @@ -0,0 +1,20 @@ +package io.quarkus.arc.impl; + +import io.quarkus.arc.CurrentContext; +import io.quarkus.arc.CurrentContextFactory; +import io.quarkus.arc.InjectableContext.ContextState; +import java.lang.annotation.Annotation; + +/** + * The default implementation makes use of {@link ThreadLocal} variables. + * + * @see ThreadLocalCurrentContext + */ +final class ThreadLocalCurrentContextFactory implements CurrentContextFactory { + + @Override + public CurrentContext create(Class scope) { + return new ThreadLocalCurrentContext<>(); + } + +}