From b334097c4bcc343bd1067a213446d887e7d6794d Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 20 Oct 2023 09:10:53 +0200 Subject: [PATCH 1/8] ArC: introduce optimized contexts - introduce the ContextInstances abstraction - if optimization is enabled then generate the ContextInstances implementation for application and request context --- .../io/quarkus/arc/deployment/ArcConfig.java | 9 + .../quarkus/arc/deployment/ArcProcessor.java | 9 +- .../io/quarkus/arc/runtime/ArcRecorder.java | 10 +- .../quarkus/arc/processor/BeanDeployment.java | 4 +- .../quarkus/arc/processor/BeanProcessor.java | 50 +++- .../ComponentsProviderGenerator.java | 42 ++- .../processor/ContextInstancesGenerator.java | 267 ++++++++++++++++++ .../arc/processor/MethodDescriptors.java | 4 + .../arc/processor/ReflectionRegistration.java | 7 + .../src/main/java/io/quarkus/arc/Arc.java | 8 +- .../java/io/quarkus/arc/ArcInitConfig.java | 13 + .../main/java/io/quarkus/arc/Components.java | 11 +- .../arc/impl/AbstractSharedContext.java | 14 +- .../quarkus/arc/impl/ApplicationContext.java | 8 + .../io/quarkus/arc/impl/ArcContainerImpl.java | 26 +- .../impl/ComputingCacheContextInstances.java | 41 +++ .../io/quarkus/arc/impl/ContextInstances.java | 20 ++ .../io/quarkus/arc/impl/RequestContext.java | 48 ++-- .../io/quarkus/arc/impl/SingletonContext.java | 2 +- .../io/quarkus/arc/test/ArcTestContainer.java | 20 +- .../ApplicationContextInstancesTest.java | 61 ++++ 21 files changed, 612 insertions(+), 62 deletions(-) create mode 100644 independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ContextInstancesGenerator.java create mode 100644 independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ComputingCacheContextInstances.java create mode 100644 independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ContextInstances.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/contexts/application/optimized/ApplicationContextInstancesTest.java diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java index 30f9356d1dbba..1c9b1f4c01629 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java @@ -223,6 +223,15 @@ public class ArcConfig { @ConfigItem public ArcContextPropagationConfig contextPropagation; + /** + * If set to {@code true}, the container should try to optimize the contexts for some of the scopes. + *

+ * Typically, some implementation parts of the context for {@link jakarta.enterprise.context.ApplicationScoped} could be + * pregenerated during build. + */ + @ConfigItem(defaultValue = "true", generateDocumentation = false) + public boolean optimizeContexts; + public final boolean isRemoveUnusedBeansFieldValid() { return ALLOWED_REMOVE_UNUSED_BEANS_VALUES.contains(removeUnusedBeans.toLowerCase()); } 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 1821e039d595b..dc44f9333bcc9 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 @@ -396,6 +396,7 @@ public Integer compute(AnnotationTarget target, Collection stere } builder.setBuildCompatibleExtensions(buildCompatibleExtensions.entrypoint); + builder.setOptimizeContexts(arcConfig.optimizeContexts); BeanProcessor beanProcessor = builder.build(); ContextRegistrar.RegistrationContext context = beanProcessor.registerCustomContexts(); @@ -516,6 +517,12 @@ public void generateResources(ArcConfig config, ExecutorService executor = parallelResourceGeneration ? buildExecutor : null; List resources; resources = beanProcessor.generateResources(new ReflectionRegistration() { + + @Override + public void registerMethod(String declaringClass, String name, String... params) { + reflectiveMethods.produce(new ReflectiveMethodBuildItem(declaringClass, name, params)); + } + @Override public void registerMethod(MethodInfo methodInfo) { reflectiveMethods.produce(new ReflectiveMethodBuildItem(methodInfo)); @@ -591,7 +598,7 @@ public ArcContainerBuildItem initializeContainer(ArcConfig config, ArcRecorder r throws Exception { ArcContainer container = recorder.initContainer(shutdown, currentContextFactory.isPresent() ? currentContextFactory.get().getFactory() : null, - config.strictCompatibility); + config.strictCompatibility, config.optimizeContexts); return new ArcContainerBuildItem(container); } 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 e4af7ea639bb5..23ffb5196720d 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 @@ -42,11 +42,13 @@ public class ArcRecorder { public static volatile Map, ?>> syntheticBeanProviders; public ArcContainer initContainer(ShutdownContext shutdown, RuntimeValue currentContextFactory, - boolean strictCompatibility) + boolean strictCompatibility, boolean optimizeContexts) throws Exception { - ArcContainer container = Arc.initialize(ArcInitConfig.builder() - .setCurrentContextFactory(currentContextFactory != null ? currentContextFactory.getValue() : null) - .setStrictCompatibility(strictCompatibility).build()); + ArcInitConfig.Builder builder = ArcInitConfig.builder(); + builder.setCurrentContextFactory(currentContextFactory != null ? currentContextFactory.getValue() : null); + builder.setStrictCompatibility(strictCompatibility); + builder.setOptimizeContexts(optimizeContexts); + ArcContainer container = Arc.initialize(builder.build()); shutdown.addShutdownTask(new Runnable() { @Override public void run() { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java index 72eda2708f2ba..f64a96a1c42f5 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java @@ -57,7 +57,7 @@ public class BeanDeployment { private static final Logger LOGGER = Logger.getLogger(BeanDeployment.class); - private final String name; + final String name; private final BuildContextImpl buildContext; private final IndexView beanArchiveComputingIndex; @@ -99,7 +99,7 @@ public class BeanDeployment { private final List injectionPoints; - private final boolean removeUnusedBeans; + final boolean removeUnusedBeans; private final List> unusedExclusions; diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java index fb8ae68c42553..18ed4f933f22b 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java @@ -78,6 +78,7 @@ public static Builder builder() { private final boolean generateSources; private final boolean allowMocking; private final boolean transformUnproxyableClasses; + private final boolean optimizeContexts; private final List>> suppressConditionGenerators; // This predicate is used to filter annotations for InjectionPoint metadata @@ -107,6 +108,7 @@ private BeanProcessor(Builder builder) { applicationClassPredicate); this.generateSources = builder.generateSources; this.allowMocking = builder.allowMocking; + this.optimizeContexts = builder.optimizeContexts; this.transformUnproxyableClasses = builder.transformUnproxyableClasses; this.suppressConditionGenerators = builder.suppressConditionGenerators; @@ -189,6 +191,7 @@ public List generateResources(ReflectionRegistration reflectionRegistr // These maps are precomputed and then used in the ComponentsProviderGenerator which is generated first Map beanToGeneratedName = new HashMap<>(); Map observerToGeneratedName = new HashMap<>(); + Map scopeToGeneratedName = new HashMap<>(); BeanGenerator beanGenerator = new BeanGenerator(annotationLiterals, applicationClassPredicate, privateMembers, generateSources, refReg, existingClasses, beanToGeneratedName, @@ -229,6 +232,13 @@ public List generateResources(ReflectionRegistration reflectionRegistr observerGenerator.precomputeGeneratedName(observer); } + ContextInstancesGenerator contextInstancesGenerator = new ContextInstancesGenerator(generateSources, + reflectionRegistration, beanDeployment, scopeToGeneratedName); + if (optimizeContexts) { + contextInstancesGenerator.precomputeGeneratedName(BuiltinScope.APPLICATION.getName()); + contextInstancesGenerator.precomputeGeneratedName(BuiltinScope.REQUEST.getName()); + } + CustomAlterableContextsGenerator alterableContextsGenerator = new CustomAlterableContextsGenerator(generateSources); List alterableContexts = customAlterableContexts.getRegistered(); @@ -251,7 +261,8 @@ public Collection call() throws Exception { name, beanDeployment, beanToGeneratedName, - observerToGeneratedName); + observerToGeneratedName, + scopeToGeneratedName); } })); @@ -350,6 +361,20 @@ public Collection call() throws Exception { })); } + if (optimizeContexts) { + // Generate _ContextInstances + primaryTasks.add(executor.submit(new Callable>() { + + @Override + public Collection call() throws Exception { + Collection resources = new ArrayList<>(); + resources.addAll(contextInstancesGenerator.generate(BuiltinScope.APPLICATION.getName())); + resources.addAll(contextInstancesGenerator.generate(BuiltinScope.REQUEST.getName())); + return resources; + } + })); + } + for (Future> future : primaryTasks) { resources.addAll(future.get()); } @@ -419,7 +444,14 @@ public Collection call() throws Exception { name, beanDeployment, beanToGeneratedName, - observerToGeneratedName)); + observerToGeneratedName, + scopeToGeneratedName)); + + if (optimizeContexts) { + // Generate _ContextInstances + resources.addAll(contextInstancesGenerator.generate(BuiltinScope.APPLICATION.getName())); + resources.addAll(contextInstancesGenerator.generate(BuiltinScope.REQUEST.getName())); + } } // Generate AnnotationLiterals - at this point all annotation literals must be processed @@ -469,7 +501,7 @@ public void accept(BytecodeTransformer transformer) { initialize(unsupportedBytecodeTransformer, Collections.emptyList()); ValidationContext validationContext = validate(unsupportedBytecodeTransformer); processValidationErrors(validationContext); - generateResources(null, new HashSet<>(), unsupportedBytecodeTransformer, true, null); + generateResources(null, new HashSet<>(), unsupportedBytecodeTransformer, beanDeployment.removeUnusedBeans, null); return beanDeployment; } @@ -510,6 +542,7 @@ public static class Builder { boolean failOnInterceptedPrivateMethod; boolean allowMocking; boolean strictCompatibility; + boolean optimizeContexts; AlternativePriorities alternativePriorities; final List> excludeTypes; @@ -545,6 +578,7 @@ public Builder() { failOnInterceptedPrivateMethod = false; allowMocking = false; strictCompatibility = false; + optimizeContexts = false; excludeTypes = new ArrayList<>(); @@ -780,6 +814,16 @@ public Builder setStrictCompatibility(boolean strictCompatibility) { return this; } + /** + * + * @param value + * @return self + */ + public Builder setOptimizeContexts(boolean value) { + this.optimizeContexts = value; + return this; + } + /** * Can be used to compute a priority of an alternative bean. A non-null computed value always * takes precedence over the priority defined by {@link Priority} or a stereotype. diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ComponentsProviderGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ComponentsProviderGenerator.java index 51e30cd865dc4..fd2973bfb7769 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ComponentsProviderGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ComponentsProviderGenerator.java @@ -70,10 +70,11 @@ public ComponentsProviderGenerator(AnnotationLiteralProcessor annotationLiterals * @param beanDeployment * @param beanToGeneratedName * @param observerToGeneratedName + * @param scopeToContextInstances * @return a collection of resources */ Collection generate(String name, BeanDeployment beanDeployment, Map beanToGeneratedName, - Map observerToGeneratedName) { + Map observerToGeneratedName, Map scopeToContextInstances) { ResourceClassOutput classOutput = new ResourceClassOutput(true, generateSources); @@ -129,19 +130,26 @@ Collection generate(String name, BeanDeployment beanDeployment, Map generate(String name, BeanDeployment beanDeployment, Map e : scopeToContextInstances.entrySet()) { + ResultHandle scope = getComponents.loadClass(e.getKey().toString()); + FunctionCreator supplier = getComponents.createFunction(Supplier.class); + BytecodeCreator bytecode = supplier.getBytecode(); + bytecode.returnValue(bytecode.newInstance(MethodDescriptor.ofConstructor(e.getValue()))); + getComponents.invokeInterfaceMethod(MethodDescriptors.MAP_PUT, contextInstances, scope, supplier.getInstance()); + } + } + ResultHandle componentsHandle = getComponents.newInstance( MethodDescriptor.ofConstructor(Components.class, Collection.class, Collection.class, Collection.class, - Set.class, Map.class, Supplier.class, Map.class, Set.class), + Set.class, Map.class, Supplier.class, Map.class, Set.class, Map.class), beansHandle, observersHandle, contextsHandle, interceptorBindings, transitiveBindingsHandle, - removedBeansSupplier.getInstance(), qualifiersNonbindingMembers, qualifiers); + removedBeansSupplier, qualifiersNonbindingMembers, qualifiers, contextInstances); getComponents.returnValue(componentsHandle); // Finally write the bytecode diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ContextInstancesGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ContextInstancesGenerator.java new file mode 100644 index 0000000000000..01a7ba04fc80a --- /dev/null +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ContextInstancesGenerator.java @@ -0,0 +1,267 @@ +package io.quarkus.arc.processor; + +import static org.objectweb.asm.Opcodes.ACC_FINAL; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ACC_VOLATILE; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; + +import org.jboss.jandex.DotName; + +import io.quarkus.arc.ContextInstanceHandle; +import io.quarkus.arc.impl.ContextInstances; +import io.quarkus.arc.processor.ResourceOutput.Resource; +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.CatchBlockCreator; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.FieldCreator; +import io.quarkus.gizmo.FieldDescriptor; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.gizmo.Switch.StringSwitch; +import io.quarkus.gizmo.TryBlock; + +public class ContextInstancesGenerator extends AbstractGenerator { + + static final String APP_CONTEXT_INSTANCES_SUFFIX = "_ContextInstances"; + + private final BeanDeployment beanDeployment; + private final Map scopeToGeneratedName; + + public ContextInstancesGenerator(boolean generateSources, ReflectionRegistration reflectionRegistration, + BeanDeployment beanDeployment, Map scopeToGeneratedName) { + super(generateSources, reflectionRegistration); + this.beanDeployment = beanDeployment; + this.scopeToGeneratedName = scopeToGeneratedName; + } + + void precomputeGeneratedName(DotName scope) { + String generatedName = DEFAULT_PACKAGE + "." + beanDeployment.name + UNDERSCORE + + scope.toString().replace(".", UNDERSCORE) + + APP_CONTEXT_INSTANCES_SUFFIX; + scopeToGeneratedName.put(scope, generatedName); + } + + Collection generate(DotName scope) { + List beans = new BeanStream(beanDeployment.getBeans()).withScope(scope).collect(); + ResourceClassOutput classOutput = new ResourceClassOutput(true, generateSources); + String generatedName = scopeToGeneratedName.get(scope); + reflectionRegistration.registerMethod(generatedName, MethodDescriptor.INIT); + + ClassCreator contextInstances = ClassCreator.builder().classOutput(classOutput).className(generatedName) + .interfaces(ContextInstances.class).build(); + + // Add fields for all beans + // The name of the field is a generated index + // For example: + // private volatile ContextInstanceHandle 1; + Map idToField = new HashMap<>(); + int fieldIndex = 0; + for (BeanInfo bean : beans) { + FieldCreator fc = contextInstances.getFieldCreator("" + fieldIndex++, ContextInstanceHandle.class) + .setModifiers(ACC_PRIVATE | ACC_VOLATILE); + idToField.put(bean.getIdentifier(), fc.getFieldDescriptor()); + } + + FieldCreator lockField = contextInstances.getFieldCreator("lock", Lock.class) + .setModifiers(ACC_PRIVATE | ACC_FINAL); + + MethodCreator constructor = contextInstances.getMethodCreator(MethodDescriptor.INIT, "V"); + constructor.invokeSpecialMethod(MethodDescriptors.OBJECT_CONSTRUCTOR, constructor.getThis()); + constructor.writeInstanceField(lockField.getFieldDescriptor(), constructor.getThis(), + constructor.newInstance(MethodDescriptor.ofConstructor(ReentrantLock.class))); + constructor.returnVoid(); + + implementComputeIfAbsent(contextInstances, beans, idToField, + lockField.getFieldDescriptor()); + implementGetIfPresent(contextInstances, beans, idToField); + implementRemove(contextInstances, beans, idToField, lockField.getFieldDescriptor()); + implementClear(contextInstances, idToField, lockField.getFieldDescriptor()); + implementGetAllPresent(contextInstances, idToField, lockField.getFieldDescriptor()); + + contextInstances.close(); + + return classOutput.getResources(); + } + + private void implementGetAllPresent(ClassCreator contextInstances, Map idToField, + FieldDescriptor lockField) { + MethodCreator getAllPresent = contextInstances.getMethodCreator("getAllPresent", Set.class) + .setModifiers(ACC_PUBLIC); + // lock.lock(); + // try { + // Set> ret = new HashSet<>(); + // ContextInstanceHandle copy = this.1; + // if (copy != null) { + // ret.add(copy); + // } + // return ret; + // } catch(Throwable t) { + // lock.unlock(); + // throw t; + // } + ResultHandle lock = getAllPresent.readInstanceField(lockField, getAllPresent.getThis()); + getAllPresent.invokeInterfaceMethod(MethodDescriptors.LOCK_LOCK, lock); + TryBlock tryBlock = getAllPresent.tryBlock(); + ResultHandle ret = tryBlock.newInstance(MethodDescriptor.ofConstructor(HashSet.class)); + for (FieldDescriptor field : idToField.values()) { + ResultHandle copy = tryBlock.readInstanceField(field, tryBlock.getThis()); + tryBlock.ifNotNull(copy).trueBranch().invokeInterfaceMethod(MethodDescriptors.SET_ADD, ret, copy); + } + tryBlock.invokeInterfaceMethod(MethodDescriptors.LOCK_UNLOCK, lock); + tryBlock.returnValue(ret); + CatchBlockCreator catchBlock = tryBlock.addCatch(Throwable.class); + catchBlock.invokeInterfaceMethod(MethodDescriptors.LOCK_UNLOCK, lock); + catchBlock.throwException(catchBlock.getCaughtException()); + } + + private void implementClear(ClassCreator applicationContextInstances, Map idToField, + FieldDescriptor lockField) { + MethodCreator clear = applicationContextInstances.getMethodCreator("clear", void.class).setModifiers(ACC_PUBLIC); + // lock.lock(); + // try { + // this.1 = null; + // lock.unlock(); + // } catch(Throwable t) { + // lock.unlock(); + // throw t; + // } + ResultHandle lock = clear.readInstanceField(lockField, clear.getThis()); + clear.invokeInterfaceMethod(MethodDescriptors.LOCK_LOCK, lock); + TryBlock tryBlock = clear.tryBlock(); + for (FieldDescriptor field : idToField.values()) { + tryBlock.writeInstanceField(field, tryBlock.getThis(), tryBlock.loadNull()); + } + tryBlock.invokeInterfaceMethod(MethodDescriptors.LOCK_UNLOCK, lock); + tryBlock.returnVoid(); + CatchBlockCreator catchBlock = tryBlock.addCatch(Throwable.class); + catchBlock.invokeInterfaceMethod(MethodDescriptors.LOCK_UNLOCK, lock); + catchBlock.throwException(catchBlock.getCaughtException()); + } + + private void implementRemove(ClassCreator contextInstances, List applicationScopedBeans, + Map idToField, FieldDescriptor lockField) { + MethodCreator remove = contextInstances + .getMethodCreator("remove", ContextInstanceHandle.class, String.class) + .setModifiers(ACC_PUBLIC); + + StringSwitch strSwitch = remove.stringSwitch(remove.getMethodParam(0)); + // https://github.com/quarkusio/gizmo/issues/164 + strSwitch.fallThrough(); + for (BeanInfo bean : applicationScopedBeans) { + FieldDescriptor instanceField = idToField.get(bean.getIdentifier()); + // There is a separate remove method for every bean instance field + MethodCreator removeBean = contextInstances.getMethodCreator("r" + instanceField.getName(), + ContextInstanceHandle.class).setModifiers(ACC_PRIVATE); + // lock.lock(); + // try { + // ContextInstanceHandle copy = this.1; + // if (copy != null) { + // this.1 = null; + // } + // lock.unlock(); + // return copy; + // } catch(Throwable t) { + // lock.unlock(); + // throw t; + // } + + ResultHandle lock = removeBean.readInstanceField(lockField, removeBean.getThis()); + removeBean.invokeInterfaceMethod(MethodDescriptors.LOCK_LOCK, lock); + TryBlock tryBlock = removeBean.tryBlock(); + ResultHandle copy = tryBlock.readInstanceField(instanceField, tryBlock.getThis()); + BytecodeCreator isNotNull = tryBlock.ifNotNull(copy).trueBranch(); + isNotNull.writeInstanceField(instanceField, isNotNull.getThis(), isNotNull.loadNull()); + tryBlock.invokeInterfaceMethod(MethodDescriptors.LOCK_UNLOCK, lock); + tryBlock.returnValue(copy); + CatchBlockCreator catchBlock = tryBlock.addCatch(Throwable.class); + catchBlock.invokeInterfaceMethod(MethodDescriptors.LOCK_UNLOCK, lock); + catchBlock.throwException(catchBlock.getCaughtException()); + + strSwitch.caseOf(bean.getIdentifier(), bc -> { + bc.returnValue(bc.invokeVirtualMethod(removeBean.getMethodDescriptor(), bc.getThis())); + }); + } + strSwitch.defaultCase(bc -> bc.throwException(IllegalArgumentException.class, "Unknown bean identifier")); + } + + private void implementGetIfPresent(ClassCreator contextInstances, List applicationScopedBeans, + Map idToField) { + MethodCreator getIfPresent = contextInstances + .getMethodCreator("getIfPresent", ContextInstanceHandle.class, String.class) + .setModifiers(ACC_PUBLIC); + + StringSwitch strSwitch = getIfPresent.stringSwitch(getIfPresent.getMethodParam(0)); + // https://github.com/quarkusio/gizmo/issues/164 + strSwitch.fallThrough(); + for (BeanInfo bean : applicationScopedBeans) { + strSwitch.caseOf(bean.getIdentifier(), bc -> { + bc.returnValue(bc.readInstanceField(idToField.get(bean.getIdentifier()), bc.getThis())); + }); + } + strSwitch.defaultCase(bc -> bc.throwException(IllegalArgumentException.class, "Unknown bean identifier")); + } + + private void implementComputeIfAbsent(ClassCreator contextInstances, List applicationScopedBeans, + Map idToField, FieldDescriptor lockField) { + MethodCreator computeIfAbsent = contextInstances + .getMethodCreator("computeIfAbsent", ContextInstanceHandle.class, String.class, Supplier.class) + .setModifiers(ACC_PUBLIC); + + StringSwitch strSwitch = computeIfAbsent.stringSwitch(computeIfAbsent.getMethodParam(0)); + // https://github.com/quarkusio/gizmo/issues/164 + strSwitch.fallThrough(); + for (BeanInfo bean : applicationScopedBeans) { + FieldDescriptor instanceField = idToField.get(bean.getIdentifier()); + // There is a separate compute method for every bean instance field + MethodCreator compute = contextInstances.getMethodCreator("c" + instanceField.getName(), + ContextInstanceHandle.class, Supplier.class).setModifiers(ACC_PRIVATE); + // ContextInstanceHandle copy = this.1; + // if (copy != null) { + // return copy; + // } + // lock.lock(); + // try { + // if (this.1 == null) { + // this.1 = supplier.get(); + // } + // lock.unlock(); + // return this.1; + // } catch(Throwable t) { + // lock.unlock(); + // throw t; + // } + ResultHandle copy = compute.readInstanceField(instanceField, compute.getThis()); + compute.ifNotNull(copy).trueBranch().returnValue(copy); + ResultHandle lock = compute.readInstanceField(lockField, compute.getThis()); + compute.invokeInterfaceMethod(MethodDescriptors.LOCK_LOCK, lock); + TryBlock tryBlock = compute.tryBlock(); + ResultHandle val = tryBlock.readInstanceField(instanceField, compute.getThis()); + BytecodeCreator isNull = tryBlock.ifNull(val).trueBranch(); + ResultHandle newVal = isNull.invokeInterfaceMethod(MethodDescriptors.SUPPLIER_GET, + compute.getMethodParam(0)); + isNull.writeInstanceField(instanceField, compute.getThis(), newVal); + tryBlock.invokeInterfaceMethod(MethodDescriptors.LOCK_UNLOCK, lock); + CatchBlockCreator catchBlock = tryBlock.addCatch(Throwable.class); + catchBlock.invokeInterfaceMethod(MethodDescriptors.LOCK_UNLOCK, lock); + catchBlock.throwException(catchBlock.getCaughtException()); + compute.returnValue(compute.readInstanceField(instanceField, compute.getThis())); + + strSwitch.caseOf(bean.getIdentifier(), bc -> { + bc.returnValue(bc.invokeVirtualMethod(compute.getMethodDescriptor(), bc.getThis(), bc.getMethodParam(1))); + }); + } + strSwitch.defaultCase(bc -> bc.throwException(IllegalArgumentException.class, "Unknown bean identifier")); + } + +} diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java index 57b9f53d10775..756833e572aa0 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.locks.Lock; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; @@ -307,6 +308,9 @@ public final class MethodDescriptors { public static final MethodDescriptor INTERCEPT_FUNCTION_INTERCEPT = MethodDescriptor.ofMethod(InterceptFunction.class, "intercept", Object.class, ArcInvocationContext.class); + public static final MethodDescriptor LOCK_LOCK = MethodDescriptor.ofMethod(Lock.class, "lock", void.class); + public static final MethodDescriptor LOCK_UNLOCK = MethodDescriptor.ofMethod(Lock.class, "unlock", void.class); + private MethodDescriptors() { } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ReflectionRegistration.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ReflectionRegistration.java index 828b4359039a4..ffeef0b1b5dd4 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ReflectionRegistration.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ReflectionRegistration.java @@ -6,6 +6,8 @@ public interface ReflectionRegistration { + void registerMethod(String declaringClass, String name, String... params); + void registerMethod(MethodInfo methodInfo); void registerField(FieldInfo fieldInfo); @@ -29,6 +31,11 @@ default void registerSubclass(DotName beanClassName, String subclassName) { } ReflectionRegistration NOOP = new ReflectionRegistration() { + + @Override + public void registerMethod(String declaringClass, String name, String... params) { + } + @Override public void registerMethod(MethodInfo methodInfo) { } 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 5a8d9d20afbb6..349f5793aad73 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 @@ -24,19 +24,19 @@ public static ArcContainer initialize() { /** * - * @param arcInitConfig + * @param config * @return the container instance * @see #initialize() */ - public static ArcContainer initialize(ArcInitConfig arcInitConfig) { + public static ArcContainer initialize(ArcInitConfig config) { ArcContainerImpl container = INSTANCE.get(); if (container == null) { synchronized (INSTANCE) { container = INSTANCE.get(); if (container == null) { // Set the container instance first because Arc.container() can be used within ArcContainerImpl.init() - container = new ArcContainerImpl(arcInitConfig.getCurrentContextFactory(), - arcInitConfig.isStrictCompatibility()); + container = new ArcContainerImpl(config.getCurrentContextFactory(), + config.isStrictCompatibility(), config.isOptimizeContexts()); INSTANCE.set(container); container.init(); } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcInitConfig.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcInitConfig.java index 596b0535a0230..976bf4b0d08d1 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcInitConfig.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcInitConfig.java @@ -24,10 +24,12 @@ public static Builder builder() { private ArcInitConfig(Builder builder) { this.currentContextFactory = builder.currentContextFactory; this.strictCompatibility = builder.strictCompatibility; + this.optimizeContexts = builder.optimizeContexts; } private final boolean strictCompatibility; private final CurrentContextFactory currentContextFactory; + private final boolean optimizeContexts; public boolean isStrictCompatibility() { return strictCompatibility; @@ -37,14 +39,20 @@ public CurrentContextFactory getCurrentContextFactory() { return currentContextFactory; } + public boolean isOptimizeContexts() { + return optimizeContexts; + } + public static class Builder { private boolean strictCompatibility; private CurrentContextFactory currentContextFactory; + private boolean optimizeContexts; private Builder() { // init all values with their defaults this.strictCompatibility = false; this.currentContextFactory = null; + this.optimizeContexts = false; } public Builder setStrictCompatibility(boolean strictCompatibility) { @@ -57,6 +65,11 @@ public Builder setCurrentContextFactory(CurrentContextFactory currentContextFact return this; } + public Builder setOptimizeContexts(boolean value) { + optimizeContexts = value; + return this; + } + public ArcInitConfig build() { return new ArcInitConfig(this); } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Components.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Components.java index 7bd74871c1138..8db408d26ca2d 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Components.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Components.java @@ -6,6 +6,8 @@ import java.util.Set; import java.util.function.Supplier; +import io.quarkus.arc.impl.ContextInstances; + public final class Components { private final Collection> beans; @@ -16,13 +18,15 @@ public final class Components { private final Map, Set> transitiveInterceptorBindings; private final Map> qualifierNonbindingMembers; private final Set qualifiers; + private final Map, Supplier> contextInstances; public Components(Collection> beans, Collection> observers, Collection contexts, Set interceptorBindings, Map, Set> transitiveInterceptorBindings, Supplier> removedBeans, Map> qualifierNonbindingMembers, - Set qualifiers) { + Set qualifiers, + Map, Supplier> contextInstances) { this.beans = beans; this.observers = observers; this.contexts = contexts; @@ -31,6 +35,7 @@ public Components(Collection> beans, Collection> getBeans() { @@ -75,4 +80,8 @@ public Set getQualifiers() { return qualifiers; } + public Map, Supplier> getContextInstances() { + return contextInstances; + } + } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/AbstractSharedContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/AbstractSharedContext.java index ab5f585af6523..748fae45d3fb7 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/AbstractSharedContext.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/AbstractSharedContext.java @@ -16,10 +16,14 @@ abstract class AbstractSharedContext implements InjectableContext, InjectableContext.ContextState { - protected final ComputingCache> instances; + protected final ContextInstances instances; public AbstractSharedContext() { - this.instances = new ComputingCache<>(); + this(new ComputingCacheContextInstances()); + } + + public AbstractSharedContext(ContextInstances instances) { + this.instances = Objects.requireNonNull(instances); } @SuppressWarnings("unchecked") @@ -47,7 +51,7 @@ public T get(Contextual contextual) { if (!Scopes.scopeMatches(this, bean)) { throw Scopes.scopeDoesNotMatchException(this, bean); } - ContextInstanceHandle handle = instances.getValueIfPresent(bean.getIdentifier()); + ContextInstanceHandle handle = instances.getIfPresent(bean.getIdentifier()); return handle != null ? (T) handle.get() : null; } @@ -63,7 +67,7 @@ public ContextState getStateIfActive() { @Override public Map, Object> getContextualInstances() { - return instances.getPresentValues().stream() + return instances.getAllPresent().stream() .collect(Collectors.toUnmodifiableMap(ContextInstanceHandle::getBean, ContextInstanceHandle::get)); } @@ -83,7 +87,7 @@ public void destroy(Contextual contextual) { @Override public synchronized void destroy() { - Set> values = instances.getPresentValues(); + Set> values = instances.getAllPresent(); // Destroy the producers first for (Iterator> iterator = values.iterator(); iterator.hasNext();) { ContextInstanceHandle instanceHandle = iterator.next(); diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ApplicationContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ApplicationContext.java index 2bc60e6abc04d..88ca9a41bbb15 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ApplicationContext.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ApplicationContext.java @@ -6,6 +6,14 @@ class ApplicationContext extends AbstractSharedContext { + ApplicationContext() { + super(); + } + + ApplicationContext(ContextInstances instances) { + super(instances); + } + @Override public Class getScope() { return ApplicationScoped.class; 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 a01607c33fc96..a64c0674a37cb 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 @@ -103,7 +103,7 @@ public class ArcContainerImpl implements ArcContainer { private final boolean strictMode; - public ArcContainerImpl(CurrentContextFactory currentContextFactory, boolean strictMode) { + public ArcContainerImpl(CurrentContextFactory currentContextFactory, boolean strictMode, boolean optimizeContexts) { this.strictMode = strictMode; id = String.valueOf(ID_GENERATOR.incrementAndGet()); running = new AtomicBoolean(true); @@ -117,6 +117,8 @@ public ArcContainerImpl(CurrentContextFactory currentContextFactory, boolean str Map, Set> transitiveInterceptorBindings = new HashMap<>(); Map> qualifierNonbindingMembers = new HashMap<>(); Set qualifiers = new HashSet<>(); + Supplier applicationContextInstances = null; + Supplier requestContextInstances = null; this.currentContextFactory = currentContextFactory == null ? new ThreadLocalCurrentContextFactory() : currentContextFactory; @@ -142,6 +144,12 @@ public ArcContainerImpl(CurrentContextFactory currentContextFactory, boolean str transitiveInterceptorBindings.putAll(c.getTransitiveInterceptorBindings()); qualifierNonbindingMembers.putAll(c.getQualifierNonbindingMembers()); qualifiers.addAll(c.getQualifiers()); + if (applicationContextInstances == null) { + applicationContextInstances = c.getContextInstances().get(ApplicationScoped.class); + } + if (requestContextInstances == null) { + requestContextInstances = c.getContextInstances().get(RequestScoped.class); + } } // register built-in beans @@ -190,12 +198,18 @@ public List get() { this.registeredQualifiers = new Qualifiers(qualifiers, qualifierNonbindingMembers); this.registeredInterceptorBindings = new InterceptorBindings(interceptorBindings, transitiveInterceptorBindings); + ApplicationContext applicationContext = applicationContextInstances != null + ? new ApplicationContext(applicationContextInstances.get()) + : new ApplicationContext(); + RequestContext requestContext = new RequestContext(this.currentContextFactory.create(RequestScoped.class), + notifierOrNull(Set.of(Initialized.Literal.REQUEST, Any.Literal.INSTANCE)), + notifierOrNull(Set.of(BeforeDestroyed.Literal.REQUEST, Any.Literal.INSTANCE)), + notifierOrNull(Set.of(Destroyed.Literal.REQUEST, Any.Literal.INSTANCE)), + requestContextInstances != null ? requestContextInstances : ComputingCacheContextInstances::new); + Contexts.Builder contextsBuilder = new Contexts.Builder( - new RequestContext(this.currentContextFactory.create(RequestScoped.class), - notifierOrNull(Set.of(Initialized.Literal.REQUEST, Any.Literal.INSTANCE)), - notifierOrNull(Set.of(BeforeDestroyed.Literal.REQUEST, Any.Literal.INSTANCE)), - notifierOrNull(Set.of(Destroyed.Literal.REQUEST, Any.Literal.INSTANCE))), - new ApplicationContext(), + requestContext, + applicationContext, new SingletonContext(), new DependentContext()); diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ComputingCacheContextInstances.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ComputingCacheContextInstances.java new file mode 100644 index 0000000000000..fd89543cbc65b --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ComputingCacheContextInstances.java @@ -0,0 +1,41 @@ +package io.quarkus.arc.impl; + +import java.util.Set; +import java.util.function.Supplier; + +import io.quarkus.arc.ContextInstanceHandle; + +class ComputingCacheContextInstances implements ContextInstances { + + protected final ComputingCache> instances; + + ComputingCacheContextInstances() { + instances = new ComputingCache<>(); + } + + @Override + public ContextInstanceHandle computeIfAbsent(String id, Supplier> supplier) { + return instances.computeIfAbsent(id, supplier); + } + + @Override + public ContextInstanceHandle getIfPresent(String id) { + return instances.getValueIfPresent(id); + } + + @Override + public ContextInstanceHandle remove(String id) { + return instances.remove(id); + } + + @Override + public Set> getAllPresent() { + return instances.getPresentValues(); + } + + @Override + public void clear() { + instances.clear(); + } + +} diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ContextInstances.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ContextInstances.java new file mode 100644 index 0000000000000..76ca4ad531e6f --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ContextInstances.java @@ -0,0 +1,20 @@ +package io.quarkus.arc.impl; + +import java.util.Set; +import java.util.function.Supplier; + +import io.quarkus.arc.ContextInstanceHandle; + +public interface ContextInstances { + + ContextInstanceHandle computeIfAbsent(String id, Supplier> supplier); + + ContextInstanceHandle getIfPresent(String id); + + ContextInstanceHandle remove(String id); + + Set> getAllPresent(); + + void clear(); + +} 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 e9296569b2643..08e2043ff9179 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 @@ -6,9 +6,8 @@ import java.util.Arrays; import java.util.Map; import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; import jakarta.enterprise.context.ContextNotActiveException; @@ -38,13 +37,16 @@ class RequestContext implements ManagedContext { private final Notifier initializedNotifier; private final Notifier beforeDestroyedNotifier; private final Notifier destroyedNotifier; + private final Supplier contextInstances; public RequestContext(CurrentContext currentContext, Notifier initializedNotifier, - Notifier beforeDestroyedNotifier, Notifier destroyedNotifier) { + Notifier beforeDestroyedNotifier, Notifier destroyedNotifier, + Supplier contextInstances) { this.currentContext = currentContext; this.initializedNotifier = initializedNotifier; this.beforeDestroyedNotifier = beforeDestroyedNotifier; this.destroyedNotifier = destroyedNotifier; + this.contextInstances = contextInstances; } @Override @@ -66,13 +68,17 @@ public T getIfActive(Contextual contextual, Function, Creat // Context is not active! return null; } - ContextInstanceHandle instance = (ContextInstanceHandle) ctxState.map.get(contextual); + ContextInstances contextInstances = ctxState.contextInstances; + ContextInstanceHandle instance = (ContextInstanceHandle) contextInstances.getIfPresent(bean.getIdentifier()); 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); - ctxState.map.put(contextual, instance); + return (T) contextInstances.computeIfAbsent(bean.getIdentifier(), new Supplier>() { + + @Override + public ContextInstanceHandle get() { + return new ContextInstanceHandleImpl<>(bean, contextual.create(creationalContext), creationalContext); + } + }).get(); } return instance.get(); } @@ -99,7 +105,8 @@ public T get(Contextual contextual) { if (state == null) { throw notActive(); } - ContextInstanceHandle instance = (ContextInstanceHandle) state.map.get(contextual); + ContextInstanceHandle instance = (ContextInstanceHandle) state.contextInstances + .getIfPresent(bean.getIdentifier()); return instance == null ? null : instance.get(); } @@ -115,7 +122,8 @@ public void destroy(Contextual contextual) { // Context is not active throw notActive(); } - ContextInstanceHandle instance = state.map.remove(contextual); + InjectableBean bean = (InjectableBean) contextual; + ContextInstanceHandle instance = state.contextInstances.remove(bean.getIdentifier()); if (instance != null) { instance.destroy(); } @@ -127,7 +135,7 @@ public ContextState activate(ContextState initialState) { traceActivate(initialState); } if (initialState == null) { - RequestContextState state = new RequestContextState(new ConcurrentHashMap<>()); + RequestContextState state = new RequestContextState(contextInstances.get()); currentContext.set(state); // Fire an event with qualifier @Initialized(RequestScoped.class) if there are any observers for it fireIfNotEmpty(initializedNotifier); @@ -202,12 +210,8 @@ public void destroy(ContextState state) { if (reqState.invalidate()) { // Fire an event with qualifier @BeforeDestroyed(RequestScoped.class) if there are any observers for it fireIfNotEmpty(beforeDestroyedNotifier); - Map, ContextInstanceHandle> map = ((RequestContextState) state).map; - if (!map.isEmpty()) { - //Performance: avoid an iterator on the map elements - map.forEach(this::destroyContextElement); - map.clear(); - } + reqState.contextInstances.getAllPresent().forEach(this::destroyContextElement); + reqState.contextInstances.clear(); // Fire an event with qualifier @Destroyed(RequestScoped.class) if there are any observers for it fireIfNotEmpty(destroyedNotifier); } @@ -225,7 +229,7 @@ private static void traceDestroy(ContextState state) { LOG.tracef("Destroy %s%s\n\t...", state != null ? Integer.toHexString(state.hashCode()) : "", stack); } - private void destroyContextElement(Contextual contextual, ContextInstanceHandle contextInstanceHandle) { + private void destroyContextElement(ContextInstanceHandle contextInstanceHandle) { try { contextInstanceHandle.destroy(); } catch (Exception e) { @@ -269,16 +273,16 @@ static class RequestContextState implements ContextState { } } - private final Map, ContextInstanceHandle> map; + private final ContextInstances contextInstances; private volatile int isValid; - RequestContextState(ConcurrentMap, ContextInstanceHandle> value) { - this.map = Objects.requireNonNull(value); + RequestContextState(ContextInstances contextInstances) { + this.contextInstances = Objects.requireNonNull(contextInstances); } @Override public Map, Object> getContextualInstances() { - return map.values().stream() + return contextInstances.getAllPresent().stream() .collect(Collectors.toUnmodifiableMap(ContextInstanceHandle::getBean, ContextInstanceHandle::get)); } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/SingletonContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/SingletonContext.java index 549ebed4de469..96487b8cf4c2a 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/SingletonContext.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/SingletonContext.java @@ -16,7 +16,7 @@ public Class getScope() { void destroyInstance(Object instance) { InstanceHandle handle = null; - for (ContextInstanceHandle contextInstance : instances.getPresentValues()) { + for (ContextInstanceHandle contextInstance : instances.getAllPresent()) { if (contextInstance.get() == instance) { handle = contextInstance; break; diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java index 05b799d33fde4..5a06041446b7a 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java @@ -92,6 +92,7 @@ public static class Builder { private AlternativePriorities alternativePriorities; private final List buildCompatibleExtensions; private boolean strictCompatibility = false; + private boolean optimizeContexts = false; public Builder() { resourceReferenceProviders = new ArrayList<>(); @@ -213,6 +214,11 @@ public Builder strictCompatibility(boolean strictCompatibility) { return this; } + public Builder optimizeContexts(boolean value) { + this.optimizeContexts = value; + return this; + } + public ArcTestContainer build() { return new ArcTestContainer(this); } @@ -248,6 +254,7 @@ public ArcTestContainer build() { private final List buildCompatibleExtensions; private final boolean strictCompatibility; + private final boolean optimizeContexts; public ArcTestContainer(Class... beanClasses) { this.resourceReferenceProviders = Collections.emptyList(); @@ -271,6 +278,7 @@ public ArcTestContainer(Class... beanClasses) { this.alternativePriorities = null; this.buildCompatibleExtensions = Collections.emptyList(); this.strictCompatibility = false; + this.optimizeContexts = false; } public ArcTestContainer(Builder builder) { @@ -295,6 +303,7 @@ public ArcTestContainer(Builder builder) { this.alternativePriorities = builder.alternativePriorities; this.buildCompatibleExtensions = builder.buildCompatibleExtensions; this.strictCompatibility = builder.strictCompatibility; + this.optimizeContexts = builder.optimizeContexts; } // this is where we start Arc, we operate on a per-method basis @@ -403,14 +412,16 @@ private ClassLoader init(ExtensionContext context) { } } + String deploymentName = testClass.getName().replace('.', '_'); BeanProcessor.Builder builder = BeanProcessor.builder() - .setName(testClass.getName().replace('.', '_')) + .setName(deploymentName) .setImmutableBeanArchiveIndex(immutableBeanArchiveIndex) .setComputingBeanArchiveIndex(BeanArchives.buildComputingBeanArchiveIndex(getClass().getClassLoader(), new ConcurrentHashMap<>(), immutableBeanArchiveIndex)) .setApplicationIndex(applicationIndex) .setBuildCompatibleExtensions(buildCompatibleExtensions) - .setStrictCompatibility(strictCompatibility); + .setStrictCompatibility(strictCompatibility) + .setOptimizeContexts(optimizeContexts); if (!resourceAnnotations.isEmpty()) { builder.addResourceAnnotations(resourceAnnotations.stream() .map(c -> DotName.createSimple(c.getName())) @@ -469,7 +480,10 @@ public void writeResource(Resource resource) throws IOException { .setContextClassLoader(testClassLoader); // Now we are ready to initialize Arc - Arc.initialize(ArcInitConfig.builder().setStrictCompatibility(strictCompatibility).build()); + ArcInitConfig.Builder initConfigBuilder = ArcInitConfig.builder(); + initConfigBuilder.setStrictCompatibility(strictCompatibility); + initConfigBuilder.setOptimizeContexts(optimizeContexts); + Arc.initialize(initConfigBuilder.build()); } catch (Throwable e) { if (shouldFail) { diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/contexts/application/optimized/ApplicationContextInstancesTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/contexts/application/optimized/ApplicationContextInstancesTest.java new file mode 100644 index 0000000000000..247173814784e --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/contexts/application/optimized/ApplicationContextInstancesTest.java @@ -0,0 +1,61 @@ +package io.quarkus.arc.test.contexts.application.optimized; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.util.UUID; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.InjectableContext; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.test.ArcTestContainer; + +public class ApplicationContextInstancesTest { + + @RegisterExtension + ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(Boom.class) + .optimizeContexts(true) + .build(); + + @Test + public void testContext() { + ArcContainer container = Arc.container(); + InstanceHandle handle = container.instance(Boom.class); + Boom boom = handle.get(); + String id1 = boom.ping(); + assertEquals(id1, boom.ping()); + + handle.destroy(); + String id2 = boom.ping(); + assertNotEquals(id1, id2); + assertEquals(id2, boom.ping()); + + InjectableContext appContext = container.getActiveContext(ApplicationScoped.class); + appContext.destroy(); + assertNotEquals(id2, boom.ping()); + } + + @ApplicationScoped + public static class Boom { + + private String id; + + String ping() { + return id; + } + + @PostConstruct + void init() { + id = UUID.randomUUID().toString(); + } + + } +} From 0ac910af5df91d403f787dd970ad2ad6830f555c Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Mon, 23 Oct 2023 10:38:42 +0200 Subject: [PATCH 2/8] ArC: optimize client proxy delegate access - for normal scopes for which a single context is registered - similar to how we optimize the client proxy delegate access for application context --- .../quarkus/arc/processor/BeanProcessor.java | 41 ++++++++++++++++--- .../arc/processor/ClientProxyGenerator.java | 26 +++++++++--- .../processor/CustomAlterableContexts.java | 10 +++-- .../arc/processor/MethodDescriptors.java | 7 ++++ .../bcextensions/ExtensionsEntryPoint.java | 3 +- .../io/quarkus/arc/impl/ClientProxies.java | 27 ++++++++---- 6 files changed, 92 insertions(+), 22 deletions(-) diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java index 18ed4f933f22b..83fabb23a25cb 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java @@ -201,8 +201,14 @@ public List generateResources(ReflectionRegistration reflectionRegistr beanGenerator.precomputeGeneratedName(bean); } + CustomAlterableContextsGenerator alterableContextsGenerator = new CustomAlterableContextsGenerator(generateSources); + List alterableContexts = customAlterableContexts.getRegistered(); + + // Set of normal scopes for which the client proxy delegate can be optimized + Set singleContextNormalScopes = findSingleContextNormalScopes(); + ClientProxyGenerator clientProxyGenerator = new ClientProxyGenerator(applicationClassPredicate, generateSources, - allowMocking, refReg, existingClasses); + allowMocking, refReg, existingClasses, singleContextNormalScopes); InterceptorGenerator interceptorGenerator = new InterceptorGenerator(annotationLiterals, applicationClassPredicate, privateMembers, generateSources, refReg, existingClasses, beanToGeneratedName, @@ -233,15 +239,12 @@ public List generateResources(ReflectionRegistration reflectionRegistr } ContextInstancesGenerator contextInstancesGenerator = new ContextInstancesGenerator(generateSources, - reflectionRegistration, beanDeployment, scopeToGeneratedName); + refReg, beanDeployment, scopeToGeneratedName); if (optimizeContexts) { contextInstancesGenerator.precomputeGeneratedName(BuiltinScope.APPLICATION.getName()); contextInstancesGenerator.precomputeGeneratedName(BuiltinScope.REQUEST.getName()); } - CustomAlterableContextsGenerator alterableContextsGenerator = new CustomAlterableContextsGenerator(generateSources); - List alterableContexts = customAlterableContexts.getRegistered(); - List resources = new ArrayList<>(); if (executor != null) { @@ -509,6 +512,34 @@ public Predicate getInjectionPointAnnotationsPredicate() { return injectionPointAnnotationsPredicate; } + private Set findSingleContextNormalScopes() { + Set ret = new HashSet<>(); + Set builtinScopes = Set.of(BuiltinScope.REQUEST.getName()); + Set customScopes = beanDeployment.getCustomContexts().keySet().stream().filter(ScopeInfo::isNormal) + .map(ScopeInfo::getDotName) + .collect(Collectors.toSet()); + Set alterableScopes = customAlterableContexts.getRegistered().stream() + .filter(cac -> Boolean.TRUE.equals(cac.isNormal)).map(cac -> cac.scopeAnnotation) + .map(DotName::createSimple) + .collect(Collectors.toSet()); + for (DotName s : builtinScopes) { + if (!customScopes.contains(s) && !alterableScopes.contains(s)) { + ret.add(s); + } + } + for (DotName s : customScopes) { + if (!builtinScopes.contains(s) && !alterableScopes.contains(s)) { + ret.add(s); + } + } + for (DotName s : alterableScopes) { + if (!builtinScopes.contains(s) && !customScopes.contains(s)) { + ret.add(s); + } + } + return ret; + } + public static class Builder { String name; diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java index 9edfed2862acb..3cf0934171c44 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java @@ -59,13 +59,17 @@ public class ClientProxyGenerator extends AbstractGenerator { private final Predicate applicationClassPredicate; private final boolean mockable; private final Set existingClasses; + // We optimize the access to the delegate if a single context is registered for a given scope + private final Set singleContextNormalScopes; public ClientProxyGenerator(Predicate applicationClassPredicate, boolean generateSources, boolean mockable, - ReflectionRegistration reflectionRegistration, Set existingClasses) { + ReflectionRegistration reflectionRegistration, Set existingClasses, + Set singleContextNormalScopes) { super(generateSources, reflectionRegistration); this.applicationClassPredicate = applicationClassPredicate; this.mockable = mockable; this.existingClasses = existingClasses; + this.singleContextNormalScopes = singleContextNormalScopes; } /** @@ -136,8 +140,9 @@ Collection generate(BeanInfo bean, String beanClassName, clientProxy.getFieldCreator(MOCK_FIELD, providerType.descriptorName()).setModifiers(ACC_PRIVATE | ACC_VOLATILE); } FieldCreator contextField = null; - if (BuiltinScope.APPLICATION.is(bean.getScope())) { - // It is safe to store the application context instance on the proxy + if (BuiltinScope.APPLICATION.is(bean.getScope()) + || singleContextNormalScopes.contains(bean.getScope().getDotName())) { + // It is safe to store the context instance on the proxy contextField = clientProxy.getFieldCreator(CONTEXT_FIELD, InjectableContext.class) .setModifiers(ACC_PRIVATE | ACC_FINAL); } @@ -275,11 +280,14 @@ void createConstructor(ClassCreator clientProxy, String superClasName, FieldDesc beanIdentifierHandle); creator.writeInstanceField(beanField, creator.getThis(), beanHandle); if (contextField != null) { - creator.writeInstanceField(contextField, creator.getThis(), creator.invokeInterfaceMethod( - MethodDescriptors.ARC_CONTAINER_GET_ACTIVE_CONTEXT, + // At this point we can be sure there's only one context implementation available + ResultHandle contextList = creator.invokeInterfaceMethod( + MethodDescriptors.ARC_CONTAINER_GET_CONTEXTS, containerHandle, creator .invokeInterfaceMethod(MethodDescriptor.ofMethod(InjectableBean.class, "getScope", Class.class), - beanHandle))); + beanHandle)); + creator.writeInstanceField(contextField, creator.getThis(), + creator.invokeInterfaceMethod(MethodDescriptors.LIST_GET, contextList, creator.load(0))); } creator.returnValue(null); } @@ -305,6 +313,12 @@ void implementDelegate(ClassCreator clientProxy, ProviderType providerType, Fiel FieldDescriptor.of(clientProxy.getClassName(), CONTEXT_FIELD, InjectableContext.class), creator.getThis()), beanHandle)); + } else if (singleContextNormalScopes.contains(bean.getScope().getDotName())) { + creator.returnValue(creator.invokeStaticMethod(MethodDescriptors.CLIENT_PROXIES_GET_SINGLE_CONTEXT_DELEGATE, + creator.readInstanceField( + FieldDescriptor.of(clientProxy.getClassName(), CONTEXT_FIELD, InjectableContext.class), + creator.getThis()), + beanHandle)); } else { creator.returnValue(creator.invokeStaticMethod(MethodDescriptors.CLIENT_PROXIES_GET_DELEGATE, beanHandle)); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/CustomAlterableContexts.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/CustomAlterableContexts.java index aa4dcf4189f03..167e4a01dba7f 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/CustomAlterableContexts.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/CustomAlterableContexts.java @@ -1,5 +1,6 @@ package io.quarkus.arc.processor; +import java.lang.annotation.Annotation; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; @@ -19,11 +20,12 @@ public class CustomAlterableContexts { this.applicationClassPredicate = applicationClassPredicate; } - public CustomAlterableContextInfo add(Class contextClass, Boolean isNormal) { + public CustomAlterableContextInfo add(Class contextClass, Boolean isNormal, + Class scopeAnnotation) { String generatedName = contextClass.getName() + "_InjectableContext"; boolean isApplicationClass = applicationClassPredicate.test(DotName.createSimple(contextClass)); CustomAlterableContextInfo result = new CustomAlterableContextInfo(contextClass, isNormal, generatedName, - isApplicationClass); + isApplicationClass, scopeAnnotation); registered.add(result); return result; } @@ -52,13 +54,15 @@ public static class CustomAlterableContextInfo { public final Boolean isNormal; public final String generatedName; public final boolean isApplicationClass; + public final Class scopeAnnotation; CustomAlterableContextInfo(Class contextClass, Boolean isNormal, - String generatedName, boolean isApplicationClass) { + String generatedName, boolean isApplicationClass, Class scopeAnnotation) { this.contextClass = contextClass; this.isNormal = isNormal; this.generatedName = generatedName; this.isApplicationClass = isApplicationClass; + this.scopeAnnotation = scopeAnnotation; } } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java index 756833e572aa0..8cc52eb815fb9 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java @@ -240,6 +240,9 @@ public final class MethodDescriptors { public static final MethodDescriptor ARC_CONTAINER_GET_ACTIVE_CONTEXT = MethodDescriptor.ofMethod(ArcContainer.class, "getActiveContext", InjectableContext.class, Class.class); + public static final MethodDescriptor ARC_CONTAINER_GET_CONTEXTS = MethodDescriptor.ofMethod(ArcContainer.class, + "getContexts", List.class, Class.class); + public static final MethodDescriptor CONTEXT_GET = MethodDescriptor.ofMethod(Context.class, "get", Object.class, Contextual.class, CreationalContext.class); @@ -273,6 +276,10 @@ public final class MethodDescriptors { public static final MethodDescriptor CLIENT_PROXIES_GET_APP_SCOPED_DELEGATE = MethodDescriptor.ofMethod(ClientProxies.class, "getApplicationScopedDelegate", Object.class, InjectableContext.class, InjectableBean.class); + public static final MethodDescriptor CLIENT_PROXIES_GET_SINGLE_CONTEXT_DELEGATE = MethodDescriptor.ofMethod( + ClientProxies.class, + "getSingleContextDelegate", Object.class, InjectableContext.class, InjectableBean.class); + public static final MethodDescriptor CLIENT_PROXIES_GET_DELEGATE = MethodDescriptor.ofMethod(ClientProxies.class, "getDelegate", Object.class, InjectableBean.class); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java index 2fec16043a592..cf490e5604ff8 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java @@ -221,7 +221,8 @@ public void register(RegistrationContext registrationContext) { if (InjectableContext.class.isAssignableFrom(contextClass)) { config.contextClass((Class) contextClass); } else { - CustomAlterableContextInfo info = customAlterableContexts.add(contextClass, context.isNormal); + CustomAlterableContextInfo info = customAlterableContexts.add(contextClass, context.isNormal, + scopeAnnotation); config.creator(bytecode -> { return bytecode.newInstance(MethodDescriptor.ofConstructor(info.generatedName)); }); diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ClientProxies.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ClientProxies.java index 8a2417950e416..ab13f8d44467e 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ClientProxies.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ClientProxies.java @@ -23,6 +23,15 @@ public static T getApplicationScopedDelegate(InjectableContext applicationCo return result; } + // This method is only used if a single context is registered for the given scope + public static T getSingleContextDelegate(InjectableContext context, InjectableBean bean) { + T result = context.getIfActive(bean, ClientProxies::newCreationalContext); + if (result == null) { + throw notActive(bean); + } + return result; + } + public static T getDelegate(InjectableBean bean) { List contexts = Arc.container().getContexts(bean.getScope()); T result = null; @@ -46,17 +55,21 @@ public static T getDelegate(InjectableBean bean) { } } if (result == null) { - String msg = String.format( - "%s context was not active when trying to obtain a bean instance for a client proxy of %s", - bean.getScope().getSimpleName(), bean); - if (bean.getScope().equals(RequestScoped.class)) { - msg += "\n\t- you can activate the request context for a specific method using the @ActivateRequestContext interceptor binding"; - } - throw new ContextNotActiveException(msg); + throw notActive(bean); } return result; } + private static ContextNotActiveException notActive(InjectableBean bean) { + String msg = String.format( + "%s context was not active when trying to obtain a bean instance for a client proxy of %s", + bean.getScope().getSimpleName(), bean); + if (bean.getScope().equals(RequestScoped.class)) { + msg += "\n\t- you can activate the request context for a specific method using the @ActivateRequestContext interceptor binding"; + } + return new ContextNotActiveException(msg); + } + private static CreationalContextImpl newCreationalContext(Contextual contextual) { return new CreationalContextImpl<>(contextual); } From d6c9f735e5d3257057ab38454be93424781e22b3 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Fri, 27 Oct 2023 11:58:35 +0200 Subject: [PATCH 3/8] ArC: improve how single-context normal scopes are found --- .../quarkus/arc/processor/BeanProcessor.java | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java index 83fabb23a25cb..beb80d5153a80 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java @@ -513,31 +513,22 @@ public Predicate getInjectionPointAnnotationsPredicate() { } private Set findSingleContextNormalScopes() { - Set ret = new HashSet<>(); - Set builtinScopes = Set.of(BuiltinScope.REQUEST.getName()); - Set customScopes = beanDeployment.getCustomContexts().keySet().stream().filter(ScopeInfo::isNormal) + Map contextsForScope = new HashMap<>(); + // built-in contexts + contextsForScope.put(BuiltinScope.REQUEST.getName(), 1); + // custom contexts + beanDeployment.getCustomContexts() + .keySet() + .stream() + .filter(ScopeInfo::isNormal) .map(ScopeInfo::getDotName) + .forEach(scope -> contextsForScope.merge(scope, 1, Integer::sum)); + + return contextsForScope.entrySet() + .stream() + .filter(entry -> entry.getValue() == 1) + .map(Map.Entry::getKey) .collect(Collectors.toSet()); - Set alterableScopes = customAlterableContexts.getRegistered().stream() - .filter(cac -> Boolean.TRUE.equals(cac.isNormal)).map(cac -> cac.scopeAnnotation) - .map(DotName::createSimple) - .collect(Collectors.toSet()); - for (DotName s : builtinScopes) { - if (!customScopes.contains(s) && !alterableScopes.contains(s)) { - ret.add(s); - } - } - for (DotName s : customScopes) { - if (!builtinScopes.contains(s) && !alterableScopes.contains(s)) { - ret.add(s); - } - } - for (DotName s : alterableScopes) { - if (!builtinScopes.contains(s) && !customScopes.contains(s)) { - ret.add(s); - } - } - return ret; } public static class Builder { From bf706b04cf1399d6457b0b9b27d2c345e4b724eb Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 27 Oct 2023 13:26:48 +0200 Subject: [PATCH 4/8] Qute: dev mode - add config to skip restart for some templates - resolves #36692 --- docs/src/main/asciidoc/qute-reference.adoc | 8 +++- .../qute/deployment/QuteProcessor.java | 7 +++- .../deployment/devmode/NoRestartRoute.java | 31 ++++++++++++++ .../NoRestartTemplatesDevModeTest.java | 42 +++++++++++++++++++ .../io/quarkus/qute/runtime/QuteConfig.java | 6 +++ .../qute/runtime/QuteDevModeConfig.java | 24 +++++++++++ .../qute/runtime/TemplateProducer.java | 40 ++++++++++++++++-- .../qute/runtime/devmode/QuteSetup.java | 29 +++++++++++++ .../io.quarkus.dev.spi.HotReplacementSetup | 3 +- 9 files changed, 182 insertions(+), 8 deletions(-) create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/NoRestartRoute.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/NoRestartTemplatesDevModeTest.java create mode 100644 extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteDevModeConfig.java create mode 100644 extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/devmode/QuteSetup.java diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 9950e247f45c8..d25cfa49e0de5 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -2632,7 +2632,13 @@ WARNING: Unlike with `@Inject` the templates obtained via `RestTemplate` are not === Development Mode -In the development mode, all files located in `src/main/resources/templates` are watched for changes and modifications are immediately visible. +In the development mode, all files located in `src/main/resources/templates` are watched for changes. +By default, a template modification results in an application restart that also triggers build-time validations. + +However, it's possible to use the `quarkus.qute.dev-mode.no-restart-templates` configuration property to specify the templates for which the application is not restarted. +The configration value is a regular expression that matches the template path relative from the `templates` directory and `/` is used as a path separator. +For example, `quarkus.qute.dev-mode.no-restart-templates=templates/foo.html` matches the template `src/main/resources/templates/foo.html`. +The matching templates are reloaded and only runtime validations are performed. [[type-safe-message-bundles]] === Type-safe Message Bundles diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index c551f46da2c6b..0fc9df79094f6 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -3330,8 +3330,11 @@ private static void produceTemplateBuildItems(BuildProducer root + .addClass(NoRestartRoute.class) + .addAsResource(new StringAsset( + "Hello {foo}!"), + "templates/norestart.html") + .addAsResource(new StringAsset( + "quarkus.qute.dev-mode.no-restart-templates=templates/norestart.html"), + "application.properties")); + + @Test + public void testNoRestartTemplates() { + Response resp = given().get("norestart"); + resp.then() + .statusCode(200); + String val = resp.getBody().asString(); + assertTrue(val.startsWith("Hello ")); + + config.modifyResourceFile("templates/norestart.html", t -> t.concat("!!")); + + resp = given().get("norestart"); + resp.then().statusCode(200); + assertEquals(val + "!!", resp.getBody().asString()); + } + +} diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java index d7d77efa332da..5849bb84a5fdd 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java @@ -88,4 +88,10 @@ public class QuteConfig { @ConfigItem(defaultValue = "UTF-8") public Charset defaultCharset; + /** + * Dev mode configuration. + */ + @ConfigItem + public QuteDevModeConfig devMode; + } diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteDevModeConfig.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteDevModeConfig.java new file mode 100644 index 0000000000000..be0787fc88113 --- /dev/null +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteDevModeConfig.java @@ -0,0 +1,24 @@ +package io.quarkus.qute.runtime; + +import java.util.Optional; +import java.util.regex.Pattern; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class QuteDevModeConfig { + + /** + * By default, a template modification results in an application restart that triggers build-time validations. + *

+ * This regular expression can be used to specify the templates for which the application is not restarted. + * I.e. the templates are reloaded and only runtime validations are performed. + *

+ * The matched input is the template path relative from the {@code templates} directory and the + * {@code /} is used as a path separator. For example, {@code templates/foo.html}. + */ + @ConfigItem + public Optional noRestartTemplates; + +} \ No newline at end of file diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java index b35a7e5efc999..6f15bfa4bca36 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java @@ -1,9 +1,12 @@ package io.quarkus.qute.runtime; import java.lang.annotation.Annotation; +import java.lang.ref.WeakReference; import java.lang.reflect.Field; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -32,6 +35,7 @@ import io.quarkus.qute.TemplateInstanceBase; import io.quarkus.qute.Variant; import io.quarkus.qute.runtime.QuteRecorder.QuteContext; +import io.quarkus.runtime.LaunchMode; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; @@ -44,7 +48,10 @@ public class TemplateProducer { private final Map templateVariants; - TemplateProducer(Engine engine, QuteContext context, ContentTypes contentTypes) { + // In the dev mode, we need to keep track of injected templates so that we can clear the cached values + private final List> injectedTemplates; + + TemplateProducer(Engine engine, QuteContext context, ContentTypes contentTypes, LaunchMode launchMode) { this.engine = engine; Map templateVariants = new HashMap<>(); for (Entry> entry : context.getVariants().entrySet()) { @@ -53,6 +60,7 @@ public class TemplateProducer { templateVariants.put(entry.getKey(), var); } this.templateVariants = Collections.unmodifiableMap(templateVariants); + this.injectedTemplates = launchMode == LaunchMode.DEVELOPMENT ? Collections.synchronizedList(new ArrayList<>()) : null; LOGGER.debugf("Initializing Qute variant templates: %s", templateVariants); } @@ -71,7 +79,7 @@ Template getDefaultTemplate(InjectionPoint injectionPoint) { LOGGER.warnf("Parameter name not present - using the method name as the template name instead %s", name); } } - return new InjectableTemplate(name, templateVariants, engine); + return newInjectableTemplate(name); } @Produces @@ -87,14 +95,38 @@ Template getTemplate(InjectionPoint injectionPoint) { if (path == null || path.isEmpty()) { throw new IllegalStateException("No template location specified"); } - return new InjectableTemplate(path, templateVariants, engine); + return newInjectableTemplate(path); } /** * Used by NativeCheckedTemplateEnhancer to inject calls to this method in the native type-safe methods. */ public Template getInjectableTemplate(String path) { - return new InjectableTemplate(path, templateVariants, engine); + return newInjectableTemplate(path); + } + + public void clearInjectedTemplates() { + if (injectedTemplates != null) { + synchronized (injectedTemplates) { + for (Iterator> it = injectedTemplates.iterator(); it.hasNext();) { + WeakReference ref = it.next(); + InjectableTemplate template = ref.get(); + if (template == null) { + it.remove(); + } else if (template.unambiguousTemplate != null) { + template.unambiguousTemplate.clear(); + } + } + } + } + } + + private Template newInjectableTemplate(String path) { + InjectableTemplate template = new InjectableTemplate(path, templateVariants, engine); + if (injectedTemplates != null) { + injectedTemplates.add(new WeakReference<>(template)); + } + return template; } /** diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/devmode/QuteSetup.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/devmode/QuteSetup.java new file mode 100644 index 0000000000000..acc8650c2ceed --- /dev/null +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/devmode/QuteSetup.java @@ -0,0 +1,29 @@ +package io.quarkus.qute.runtime.devmode; + +import java.util.Set; +import java.util.function.Consumer; + +import io.quarkus.arc.Arc; +import io.quarkus.dev.spi.HotReplacementContext; +import io.quarkus.dev.spi.HotReplacementSetup; +import io.quarkus.qute.Engine; +import io.quarkus.qute.runtime.TemplateProducer; + +public class QuteSetup implements HotReplacementSetup { + + @Override + public void setupHotDeployment(HotReplacementContext context) { + context.consumeNoRestartChanges(new Consumer>() { + + @Override + public void accept(Set files) { + // Make sure all templates are reloaded + Engine engine = Arc.container().instance(Engine.class).get(); + engine.clearTemplates(); + TemplateProducer templateProducer = Arc.container().instance(TemplateProducer.class).get(); + templateProducer.clearInjectedTemplates(); + } + }); + } + +} diff --git a/extensions/qute/runtime/src/main/resources/META-INF/services/io.quarkus.dev.spi.HotReplacementSetup b/extensions/qute/runtime/src/main/resources/META-INF/services/io.quarkus.dev.spi.HotReplacementSetup index 2d9eb0704bb28..0d57164bb9c06 100644 --- a/extensions/qute/runtime/src/main/resources/META-INF/services/io.quarkus.dev.spi.HotReplacementSetup +++ b/extensions/qute/runtime/src/main/resources/META-INF/services/io.quarkus.dev.spi.HotReplacementSetup @@ -1 +1,2 @@ -io.quarkus.qute.runtime.devmode.QuteErrorPageSetup \ No newline at end of file +io.quarkus.qute.runtime.devmode.QuteErrorPageSetup +io.quarkus.qute.runtime.devmode.QuteSetup \ No newline at end of file From d5f4696131ef3272ae76d2a2a0562849ca6c7e1a Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Thu, 26 Oct 2023 16:26:15 +0200 Subject: [PATCH 5/8] Recommend GraalVM CE --- docs/src/main/asciidoc/building-native-image.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/building-native-image.adoc b/docs/src/main/asciidoc/building-native-image.adoc index fb26e300b8028..4353b5fbff90e 100644 --- a/docs/src/main/asciidoc/building-native-image.adoc +++ b/docs/src/main/asciidoc/building-native-image.adoc @@ -103,7 +103,8 @@ GraalVM {graalvm-version} is required. 1. Install GraalVM if you haven't already. You have a few options for this: ** Download the appropriate archive from or , and unpack it like you would any other JDK. -** Use platform-specific install tools like https://sdkman.io/jdks#Oracle[sdkman], https://github.com/graalvm/homebrew-tap[homebrew], or https://github.com/ScoopInstaller/Java[scoop]. +** Use platform-specific installer tools like https://sdkman.io/jdks#graalce[sdkman], https://github.com/graalvm/homebrew-tap[homebrew], or https://github.com/ScoopInstaller/Java[scoop]. +We recommend the _community edition_ of GraalVM. For example, install it with `sdk install java 21-graalce`. 2. Configure the runtime environment. Set `GRAALVM_HOME` environment variable to the GraalVM installation directory, for example: + [source,bash] From 3c525d8e2e8b8977c6b1321c1d5de87085140e88 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Mon, 30 Oct 2023 07:56:55 +0100 Subject: [PATCH 6/8] Fix missing section in the gRPC getting started guide --- docs/src/main/asciidoc/grpc-getting-started.adoc | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/src/main/asciidoc/grpc-getting-started.adoc b/docs/src/main/asciidoc/grpc-getting-started.adoc index 7d4b834f150a5..32d8d0a91fb0d 100644 --- a/docs/src/main/asciidoc/grpc-getting-started.adoc +++ b/docs/src/main/asciidoc/grpc-getting-started.adoc @@ -99,10 +99,6 @@ If this retrieved version does not work in your context, you can either force to You can also download the suitable binary and specify the location via `-Dquarkus.grpc.protoc-path=/path/to/protoc`. - -Alternatively to using the `generate-code` goal of the `quarkus-maven-plugin`, you can use `protobuf-maven-plugin` to generate these files. -See the <> section for more information. - Let's start with a simple _Hello_ service. Create the `src/main/proto/helloworld.proto` file with the following content: From 2ff3e63394e2e3263034a80e6b17870c88faabb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Mon, 30 Oct 2023 12:49:02 +0100 Subject: [PATCH 7/8] Security JPA: support Hibernate multitenancy --- docs/src/main/asciidoc/security-jpa.adoc | 7 +++ .../QuarkusSecurityJpaProcessor.java | 57 ++++++++++++++++--- .../jpa/CustomHibernateTenantResolver.java | 35 ++++++++++++ ...gerAuthMultiTenantPersistenceUnitTest.java | 37 ++++++++++++ ...azyAuthMultiTenantPersistenceUnitTest.java | 44 ++++++++++++++ .../security/jpa/RolesEndpointClassLevel.java | 13 +++++ .../application.properties | 19 +++++++ .../jpa/runtime/JpaIdentityProvider.java | 40 +++++++++---- .../runtime/JpaTrustedIdentityProvider.java | 40 +++++++++---- 9 files changed, 259 insertions(+), 33 deletions(-) create mode 100644 extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/CustomHibernateTenantResolver.java create mode 100644 extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/EagerAuthMultiTenantPersistenceUnitTest.java create mode 100644 extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/LazyAuthMultiTenantPersistenceUnitTest.java create mode 100644 extensions/security-jpa/deployment/src/test/resources/multitenant-persistence-unit/application.properties diff --git a/docs/src/main/asciidoc/security-jpa.adoc b/docs/src/main/asciidoc/security-jpa.adoc index e93fd321100fb..4735b1ea11a09 100644 --- a/docs/src/main/asciidoc/security-jpa.adoc +++ b/docs/src/main/asciidoc/security-jpa.adoc @@ -185,6 +185,13 @@ For applications running in a production environment, do not store passwords as However, it is possible to store passwords as plain text with the `@Password(PasswordType.CLEAR)` annotation when operating in a test environment. ==== +[TIP] +==== +The xref:hibernate-orm.adoc#multitenancy[Hibernate Multitenancy] is supported and you can store the user entity in a persistence unit with enabled multitenancy. +However, if your `io.quarkus.hibernate.orm.runtime.tenant.TenantResolver` must access the `io.vertx.ext.web.RoutingContext` to resolve request details, you must disable proactive authentication. +For more information about proactive authentication, please see the Quarkus xref:security-proactive-authentication.adoc[Proactive authentication] guide. +==== + include::{generated-dir}/config/quarkus-security-jpa.adoc[opts=optional, leveloffset=+2] == References diff --git a/extensions/security-jpa/deployment/src/main/java/io/quarkus/security/jpa/deployment/QuarkusSecurityJpaProcessor.java b/extensions/security-jpa/deployment/src/main/java/io/quarkus/security/jpa/deployment/QuarkusSecurityJpaProcessor.java index 5c61111d08b36..a3a892bfe8399 100644 --- a/extensions/security-jpa/deployment/src/main/java/io/quarkus/security/jpa/deployment/QuarkusSecurityJpaProcessor.java +++ b/extensions/security-jpa/deployment/src/main/java/io/quarkus/security/jpa/deployment/QuarkusSecurityJpaProcessor.java @@ -13,10 +13,10 @@ import jakarta.inject.Singleton; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.Query; import org.hibernate.Session; +import org.hibernate.SessionFactory; import org.hibernate.SimpleNaturalIdLoadAccess; import org.hibernate.annotations.NaturalId; import org.jboss.jandex.AnnotationInstance; @@ -42,7 +42,10 @@ import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import io.quarkus.hibernate.orm.PersistenceUnit; +import io.quarkus.hibernate.orm.deployment.PersistenceUnitDescriptorBuildItem; +import io.quarkus.hibernate.orm.runtime.migration.MultiTenancyStrategy; import io.quarkus.panache.common.deployment.PanacheEntityClassesBuildItem; +import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.TrustedAuthenticationRequest; import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; @@ -56,7 +59,7 @@ class QuarkusSecurityJpaProcessor { private static final DotName DOTNAME_NATURAL_ID = DotName.createSimple(NaturalId.class.getName()); - private static final DotName ENTITY_MANAGER_FACTORY_FACTORY = DotName.createSimple(EntityManagerFactory.class.getName()); + private static final DotName SESSION_FACTORY_FACTORY = DotName.createSimple(SessionFactory.class.getName()); private static final DotName JPA_IDENTITY_PROVIDER_NAME = DotName.createSimple(JpaIdentityProvider.class.getName()); private static final DotName JPA_TRUSTED_IDENTITY_PROVIDER_NAME = DotName .createSimple(JpaTrustedIdentityProvider.class.getName()); @@ -68,19 +71,21 @@ FeatureBuildItem feature() { } @BuildStep - void configureJpaAuthConfig(ApplicationIndexBuildItem index, - BuildProducer beanProducer, + void configureJpaAuthConfig(ApplicationIndexBuildItem index, List puDescriptors, + BuildProducer beanProducer, SecurityJpaBuildTimeConfig secJpaConfig, Optional jpaSecurityDefinitionBuildItem, PanacheEntityPredicateBuildItem panacheEntityPredicate) { if (jpaSecurityDefinitionBuildItem.isPresent()) { + final boolean requireActiveCDIRequestContext = shouldActivateCDIReqCtx(puDescriptors, secJpaConfig); JpaSecurityDefinition jpaSecurityDefinition = jpaSecurityDefinitionBuildItem.get().get(); generateIdentityProvider(index.getIndex(), jpaSecurityDefinition, jpaSecurityDefinition.passwordType(), - jpaSecurityDefinition.customPasswordProvider(), beanProducer, panacheEntityPredicate); + jpaSecurityDefinition.customPasswordProvider(), beanProducer, panacheEntityPredicate, + requireActiveCDIRequestContext); generateTrustedIdentityProvider(index.getIndex(), jpaSecurityDefinition, - beanProducer, panacheEntityPredicate); + beanProducer, panacheEntityPredicate, requireActiveCDIRequestContext); } } @@ -90,7 +95,7 @@ InjectionPointTransformerBuildItem transformer(SecurityJpaBuildTimeConfig config @Override public boolean appliesTo(Type requiredType) { - return requiredType.name().equals(ENTITY_MANAGER_FACTORY_FACTORY); + return requiredType.name().equals(SESSION_FACTORY_FACTORY); } public void transform(TransformationContext context) { @@ -123,7 +128,8 @@ private Set collectPanacheEntities(List p private void generateIdentityProvider(Index index, JpaSecurityDefinition jpaSecurityDefinition, AnnotationValue passwordTypeValue, AnnotationValue passwordProviderValue, - BuildProducer beanProducer, PanacheEntityPredicateBuildItem panacheEntityPredicate) { + BuildProducer beanProducer, PanacheEntityPredicateBuildItem panacheEntityPredicate, + boolean requireActiveCDIRequestContext) { GeneratedBeanGizmoAdaptor gizmoAdaptor = new GeneratedBeanGizmoAdaptor(beanProducer); String name = jpaSecurityDefinition.annotatedClass.name() + "__JpaIdentityProviderImpl"; @@ -137,6 +143,10 @@ private void generateIdentityProvider(Index index, JpaSecurityDefinition jpaSecu .setModifiers(Modifier.PRIVATE) .getFieldDescriptor(); + if (requireActiveCDIRequestContext) { + activateCDIRequestContext(classCreator); + } + try (MethodCreator methodCreator = classCreator.getMethodCreator("authenticate", SecurityIdentity.class, EntityManager.class, UsernamePasswordAuthenticationRequest.class)) { methodCreator.setModifiers(Modifier.PUBLIC); @@ -161,7 +171,8 @@ private void generateIdentityProvider(Index index, JpaSecurityDefinition jpaSecu } private void generateTrustedIdentityProvider(Index index, JpaSecurityDefinition jpaSecurityDefinition, - BuildProducer beanProducer, PanacheEntityPredicateBuildItem panacheEntityPredicate) { + BuildProducer beanProducer, PanacheEntityPredicateBuildItem panacheEntityPredicate, + boolean requireActiveCDIRequestContext) { GeneratedBeanGizmoAdaptor gizmoAdaptor = new GeneratedBeanGizmoAdaptor(beanProducer); String name = jpaSecurityDefinition.annotatedClass.name() + "__JpaTrustedIdentityProviderImpl"; @@ -175,6 +186,10 @@ private void generateTrustedIdentityProvider(Index index, JpaSecurityDefinition EntityManager.class, TrustedAuthenticationRequest.class)) { methodCreator.setModifiers(Modifier.PUBLIC); + if (requireActiveCDIRequestContext) { + activateCDIRequestContext(classCreator); + } + ResultHandle username = methodCreator.invokeVirtualMethod( MethodDescriptor.ofMethod(TrustedAuthenticationRequest.class, "getPrincipal", String.class), methodCreator.getMethodParam(1)); @@ -234,6 +249,30 @@ private ResultHandle lookupUserById(JpaSecurityDefinition jpaSecurityDefinition, return user; } + private static void activateCDIRequestContext(ClassCreator classCreator) { + try (MethodCreator methodCreator = classCreator.getMethodCreator("requireActiveCDIRequestContext", + DotName.createSimple(boolean.class.getName()).toString())) { + methodCreator.setModifiers(Modifier.PROTECTED); + methodCreator.returnBoolean(true); + } + } + + private static boolean shouldActivateCDIReqCtx(List puDescriptors, + SecurityJpaBuildTimeConfig secJpaConfig) { + var descriptor = puDescriptors.stream() + .filter(desc -> secJpaConfig.persistenceUnitName().equals(desc.getPersistenceUnitName())).findFirst(); + if (descriptor.isEmpty()) { + throw new ConfigurationException("Persistence unit '" + secJpaConfig.persistenceUnitName() + + "' specified with the 'quarkus.security-jpa.persistence-unit-name' configuration property" + + " does not exist. Please set valid persistence unit name."); + } + // 'io.quarkus.hibernate.orm.runtime.tenant.TenantResolver' is only resolved when CDI request context is active + // we need to active request context even when TenantResolver is @ApplicationScoped for tenant to be set + // see io.quarkus.hibernate.orm.runtime.tenant.HibernateCurrentTenantIdentifierResolver.resolveCurrentTenantIdentifier + // for more information + return descriptor.get().getConfig().getMultiTenancyStrategy() != MultiTenancyStrategy.NONE; + } + static final class EnabledIfNonDefaultPersistenceUnit implements BooleanSupplier { private final boolean useNonDefaultPersistenceUnit; diff --git a/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/CustomHibernateTenantResolver.java b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/CustomHibernateTenantResolver.java new file mode 100644 index 0000000000000..a5b87c2e2e9d0 --- /dev/null +++ b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/CustomHibernateTenantResolver.java @@ -0,0 +1,35 @@ +package io.quarkus.security.jpa; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; + +import io.quarkus.hibernate.orm.PersistenceUnitExtension; +import io.quarkus.hibernate.orm.runtime.tenant.TenantResolver; +import io.vertx.ext.web.RoutingContext; + +@PersistenceUnitExtension +@RequestScoped +public class CustomHibernateTenantResolver implements TenantResolver { + + static volatile boolean useRoutingContext = false; + + @Inject + RoutingContext routingContext; + + @Override + public String getDefaultTenantId() { + return "one"; + } + + @Override + public String resolveTenantId() { + if (useRoutingContext) { + var tenant = routingContext.queryParam("tenant"); + if (!tenant.isEmpty()) { + return tenant.get(0); + } + } + return "two"; + } + +} diff --git a/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/EagerAuthMultiTenantPersistenceUnitTest.java b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/EagerAuthMultiTenantPersistenceUnitTest.java new file mode 100644 index 0000000000000..2aa7aeedda8d3 --- /dev/null +++ b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/EagerAuthMultiTenantPersistenceUnitTest.java @@ -0,0 +1,37 @@ +package io.quarkus.security.jpa; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class EagerAuthMultiTenantPersistenceUnitTest extends JpaSecurityRealmTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(testClasses) + .addClass(MinimalUserEntity.class) + .addClass(CustomHibernateTenantResolver.class) + .addAsResource("minimal-config/import.sql", "import.sql") + .addAsResource("multitenant-persistence-unit/application.properties", "application.properties")); + + @Test + public void testRoutingCtxAccessInsideTenantResolver() { + // RoutingContext is not used inside TenantResolver to resolve tenant + RestAssured.given().auth().preemptive().basic("user", "user").when().get("/jaxrs-secured/roles-class/routing-context") + .then().statusCode(200); + + // RoutingContext is used and proactive auth is enabled => expect error + CustomHibernateTenantResolver.useRoutingContext = true; + try { + RestAssured.given().auth().preemptive().basic("user", "user").queryParam("tenant", "two").when() + .get("/jaxrs-secured/roles-class") + .then().statusCode(500); + } finally { + CustomHibernateTenantResolver.useRoutingContext = false; + } + } + +} diff --git a/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/LazyAuthMultiTenantPersistenceUnitTest.java b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/LazyAuthMultiTenantPersistenceUnitTest.java new file mode 100644 index 0000000000000..00916a836ece3 --- /dev/null +++ b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/LazyAuthMultiTenantPersistenceUnitTest.java @@ -0,0 +1,44 @@ +package io.quarkus.security.jpa; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class LazyAuthMultiTenantPersistenceUnitTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MinimalUserEntity.class, CustomHibernateTenantResolver.class, RolesEndpointClassLevel.class) + .addAsResource("minimal-config/import.sql", "import.sql") + .addAsResource("multitenant-persistence-unit/application.properties", "application.properties") + .addAsResource(new StringAsset("quarkus.http.auth.proactive=false\n"), + "META-INF/microprofile-config.properties")); + + @Test + public void testRoutingCtxAccessInsideTenantResolver() { + // RoutingContext is used and proactive auth is disabled => no issues + CustomHibernateTenantResolver.useRoutingContext = true; + try { + // tenant 'one' + RestAssured.given().auth().preemptive().basic("user", "user") + .queryParam("tenant", "one").when().get("/roles-class/routing-context").then() + .statusCode(200).body(Matchers.is("true")); + // tenant 'two' + RestAssured.given().auth().preemptive().basic("user", "user") + .queryParam("tenant", "two").when().get("/roles-class/routing-context").then() + .statusCode(200).body(Matchers.is("true")); + // tenant 'unknown' + RestAssured.given().auth().preemptive().basic("user", "user") + .queryParam("tenant", "unknown").when().get("/roles-class/routing-context").then() + .statusCode(500); + } finally { + CustomHibernateTenantResolver.useRoutingContext = false; + } + } + +} diff --git a/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/RolesEndpointClassLevel.java b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/RolesEndpointClassLevel.java index d1585c21ee309..fbead74f68468 100644 --- a/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/RolesEndpointClassLevel.java +++ b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/RolesEndpointClassLevel.java @@ -1,20 +1,33 @@ package io.quarkus.security.jpa; import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.SecurityContext; +import io.vertx.ext.web.RoutingContext; + /** * Test JAXRS endpoint with RolesAllowed specified at the class level */ @Path("/roles-class") @RolesAllowed("user") public class RolesEndpointClassLevel { + + @Inject + RoutingContext routingContext; + @GET public String echo(@Context SecurityContext sec) { return "Hello " + sec.getUserPrincipal().getName(); } + @Path("routing-context") + @GET + public boolean hasRoutingContext() { + return routingContext != null; + } + } diff --git a/extensions/security-jpa/deployment/src/test/resources/multitenant-persistence-unit/application.properties b/extensions/security-jpa/deployment/src/test/resources/multitenant-persistence-unit/application.properties new file mode 100644 index 0000000000000..42b5b2cfb7489 --- /dev/null +++ b/extensions/security-jpa/deployment/src/test/resources/multitenant-persistence-unit/application.properties @@ -0,0 +1,19 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:mem:default + +quarkus.datasource.one.db-kind=h2 +quarkus.datasource.one.username=sa +quarkus.datasource.one.password=sa +quarkus.datasource.one.jdbc.url=jdbc:h2:mem:shared + +quarkus.datasource.two.db-kind=h2 +quarkus.datasource.two.username=sa +quarkus.datasource.two.password=sa +quarkus.datasource.two.jdbc.url=jdbc:h2:mem:shared + +quarkus.hibernate-orm.multitenant=DATABASE +quarkus.hibernate-orm.sql-load-script=import.sql +quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.hibernate-orm.packages=io.quarkus.security.jpa diff --git a/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/JpaIdentityProvider.java b/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/JpaIdentityProvider.java index 0d94b5752823d..15583f19b7bdb 100644 --- a/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/JpaIdentityProvider.java +++ b/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/JpaIdentityProvider.java @@ -5,12 +5,14 @@ import jakarta.inject.Inject; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.Query; import org.hibernate.FlushMode; +import org.hibernate.Session; +import org.hibernate.SessionFactory; import org.jboss.logging.Logger; +import io.quarkus.arc.Arc; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.IdentityProvider; @@ -24,7 +26,7 @@ public abstract class JpaIdentityProvider implements IdentityProvider getRequestType() { @@ -37,27 +39,41 @@ public Uni authenticate(UsernamePasswordAuthenticationRequest return context.runBlocking(new Supplier() { @Override public SecurityIdentity get() { - EntityManager em = entityManagerFactory.createEntityManager(); - ((org.hibernate.Session) em).setHibernateFlushMode(FlushMode.MANUAL); - ((org.hibernate.Session) em).setDefaultReadOnly(true); - try { - return authenticate(em, request); - } catch (SecurityException e) { - log.debug("Authentication failed", e); - throw new AuthenticationFailedException(); - } finally { - em.close(); + if (requireActiveCDIRequestContext() && !Arc.container().requestContext().isActive()) { + var requestContext = Arc.container().requestContext(); + requestContext.activate(); + try { + return authenticate(request); + } finally { + requestContext.terminate(); + } } + return authenticate(request); } }); } + private SecurityIdentity authenticate(UsernamePasswordAuthenticationRequest request) { + try (Session session = sessionFactory.openSession()) { + session.setHibernateFlushMode(FlushMode.MANUAL); + session.setDefaultReadOnly(true); + return authenticate(session, request); + } catch (SecurityException e) { + log.debug("Authentication failed", e); + throw new AuthenticationFailedException(); + } + } + protected T getSingleUser(Query query) { @SuppressWarnings("unchecked") List results = (List) query.getResultList(); return JpaIdentityProviderUtil.getSingleUser(results); } + protected boolean requireActiveCDIRequestContext() { + return false; + } + public abstract SecurityIdentity authenticate(EntityManager em, UsernamePasswordAuthenticationRequest request); diff --git a/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/JpaTrustedIdentityProvider.java b/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/JpaTrustedIdentityProvider.java index c2bce02e629cc..00de38dda2989 100644 --- a/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/JpaTrustedIdentityProvider.java +++ b/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/JpaTrustedIdentityProvider.java @@ -5,12 +5,14 @@ import jakarta.inject.Inject; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.Query; import org.hibernate.FlushMode; +import org.hibernate.Session; +import org.hibernate.SessionFactory; import org.jboss.logging.Logger; +import io.quarkus.arc.Arc; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.IdentityProvider; @@ -24,7 +26,7 @@ public abstract class JpaTrustedIdentityProvider implements IdentityProvider getRequestType() { @@ -37,21 +39,35 @@ public Uni authenticate(TrustedAuthenticationRequest request, return context.runBlocking(new Supplier() { @Override public SecurityIdentity get() { - EntityManager em = entityManagerFactory.createEntityManager(); - ((org.hibernate.Session) em).setHibernateFlushMode(FlushMode.MANUAL); - ((org.hibernate.Session) em).setDefaultReadOnly(true); - try { - return authenticate(em, request); - } catch (SecurityException e) { - log.debug("Authentication failed", e); - throw new AuthenticationFailedException(); - } finally { - em.close(); + if (requireActiveCDIRequestContext() && !Arc.container().requestContext().isActive()) { + var requestContext = Arc.container().requestContext(); + requestContext.activate(); + try { + return authenticate(request); + } finally { + requestContext.terminate(); + } } + return authenticate(request); } }); } + private SecurityIdentity authenticate(TrustedAuthenticationRequest request) { + try (Session session = sessionFactory.openSession()) { + session.setHibernateFlushMode(FlushMode.MANUAL); + session.setDefaultReadOnly(true); + return authenticate(session, request); + } catch (SecurityException e) { + log.debug("Authentication failed", e); + throw new AuthenticationFailedException(); + } + } + + protected boolean requireActiveCDIRequestContext() { + return false; + } + protected T getSingleUser(Query query) { @SuppressWarnings("unchecked") List results = (List) query.getResultList(); From 2dcb5107da64d9b9b0e1483b13716e36459bcc54 Mon Sep 17 00:00:00 2001 From: xstefank Date: Mon, 30 Oct 2023 13:13:06 +0100 Subject: [PATCH 8/8] Exclude resteasy-client from lra-proxy-api in narayana-lra extension --- extensions/narayana-lra/runtime/pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/extensions/narayana-lra/runtime/pom.xml b/extensions/narayana-lra/runtime/pom.xml index fc9af12c2cd27..d508f17cf3c8e 100644 --- a/extensions/narayana-lra/runtime/pom.xml +++ b/extensions/narayana-lra/runtime/pom.xml @@ -36,6 +36,12 @@ org.jboss.narayana.rts lra-proxy-api + + + org.jboss.resteasy + resteasy-client + + org.jboss.narayana.rts