This is an automated email from the ASF dual-hosted git repository.

tandraschko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/openwebbeans.git


The following commit(s) were added to refs/heads/main by this push:
     new bee4295e0 OWB-1464 - Improve WebBeansELResolver performance
bee4295e0 is described below

commit bee4295e07169c71189d77df8e282e4b1f409ae6
Author: tandraschko <[email protected]>
AuthorDate: Tue Jul 7 10:49:38 2026 +0200

    OWB-1464 - Improve WebBeansELResolver performance
---
 .../apache/webbeans/el22/WebBeansELResolver.java   | 118 +++++++++++++++++++--
 .../apache/webbeans/el/test/DotNamedBeansTest.java |  99 +++++++++++++++++
 2 files changed, 207 insertions(+), 10 deletions(-)

diff --git 
a/webbeans-el22/src/main/java/org/apache/webbeans/el22/WebBeansELResolver.java 
b/webbeans-el22/src/main/java/org/apache/webbeans/el22/WebBeansELResolver.java
index f97cfcd0f..d54befcd9 100644
--- 
a/webbeans-el22/src/main/java/org/apache/webbeans/el22/WebBeansELResolver.java
+++ 
b/webbeans-el22/src/main/java/org/apache/webbeans/el22/WebBeansELResolver.java
@@ -30,9 +30,13 @@ import org.apache.webbeans.container.BeanManagerImpl;
 import org.apache.webbeans.el.ELContextStore;
 
 import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.NavigableMap;
 import java.util.Set;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.stream.Collectors;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
 
 
 /**
@@ -55,12 +59,28 @@ import java.util.stream.Collectors;
 public class WebBeansELResolver extends ELResolver
 {
     private final WebBeansContext webBeansContext;
-    private Set<String> dotNamedBeansNegativeCache;
+
+    /**
+     * beanName (or dotted-prefix) -> {@code true}, for names which are known 
to never resolve
+     * to a dot-named {@link Bean}. Backed by a {@link ConcurrentHashMap} 
instead of a
+     * {@link java.util.concurrent.CopyOnWriteArraySet} because misses can be 
caused by
+     * arbitrary/high-cardinality EL identifiers (e.g. per-row expressions), 
which would
+     * otherwise degrade to an O(n) array copy on every single new entry.
+     */
+    private final Map<String, Boolean> dotNamedBeansNegativeCache = new 
ConcurrentHashMap<>();
+
+    /**
+     * Lazily built, immutable-after-publish index of all {@link Bean}s whose 
name contains a dot,
+     * keyed by their full name. Only such beans can ever be candidates for
+     * {@link #findDottedName(ELContext, Object, BeanManagerImpl, 
ELContextStore, String)}, so
+     * indexing just this (usually tiny or empty) subset avoids a linear scan 
over
+     * <b>all</b> managed beans on every EL root-property access.
+     */
+    private volatile NavigableMap<String, Set<Bean<?>>> dottedBeanNameIndex;
 
     public WebBeansELResolver()
     {
         webBeansContext = WebBeansContext.getInstance();
-        dotNamedBeansNegativeCache = new CopyOnWriteArraySet<>();
     }
     
     /**
@@ -105,6 +125,18 @@ public class WebBeansELResolver extends ELResolver
         //Name of the bean
         final String beanName = (String) property;
 
+        // Fast path for root-level identifiers already known to never resolve 
to any CDI
+        // bean (exact or dot-prefixed), e.g. JSF implicit objects 
(#{request}, #{cc}, ...)
+        // or <ui:repeat>/<h:dataTable> row variables, which get re-evaluated 
for every row
+        // of every request. Skips the ELContextStore lookup and bean-name 
resolution below
+        // entirely. Safe because this cache is only ever populated from 
findDottedName(...)
+        // with a null base after beanManager.getBeans(beanName) was already 
found empty, so
+        // a hit here guarantees neither an exact nor a dotted match exists.
+        if (base == null && dotNamedBeansNegativeCache.containsKey(beanName))
+        {
+            return null;
+        }
+
         // Local store, create if not exist
         final ELContextStore elContextStore = ELContextStore.getInstance(true);
 
@@ -160,20 +192,17 @@ public class WebBeansELResolver extends ELResolver
                                   final ELContextStore elContextStore, final 
String beanName)
     {
         final String fqBeanName = base == null ? beanName : base + "." + 
beanName;
-        if (dotNamedBeansNegativeCache.contains(fqBeanName))
+        if (dotNamedBeansNegativeCache.containsKey(fqBeanName))
         {
             return null;
         }
 
-        final Set<Bean<?>> anyBeanName = beanManager.getBeans().stream()
-                .filter(b -> b.getName() != null)
-                .filter(b -> b.getName().equals(fqBeanName) || 
b.getName().startsWith(fqBeanName + "."))
-                .collect(Collectors.toSet());
+        final Set<Bean<?>> anyBeanName = getDottedBeanCandidates(beanManager, 
fqBeanName);
 
         // no exact and no startsWith match
         if (anyBeanName.isEmpty())
         {
-            dotNamedBeansNegativeCache.add(fqBeanName);
+            dotNamedBeansNegativeCache.put(fqBeanName, Boolean.TRUE);
             return null;
         }
 
@@ -188,6 +217,75 @@ public class WebBeansELResolver extends ELResolver
         return new WrappedValueExpressionNode(fqBeanName);
     }
 
+    /**
+     * Returns all beans whose name is either exactly {@code fqBeanName} or 
starts with
+     * {@code fqBeanName + "."}, using a sorted index over dot-named beans 
only, instead of
+     * scanning every managed bean.
+     */
+    private Set<Bean<?>> getDottedBeanCandidates(final BeanManagerImpl 
beanManager, final String fqBeanName)
+    {
+        NavigableMap<String, Set<Bean<?>>> index = 
getDottedBeanNameIndex(beanManager);
+        if (index.isEmpty())
+        {
+            return Collections.emptySet();
+        }
+
+        String prefix = fqBeanName + '.';
+        Set<Bean<?>> result = null;
+
+        Set<Bean<?>> exact = index.get(fqBeanName);
+        if (exact != null)
+        {
+            result = new HashSet<>(exact);
+        }
+
+        for (Map.Entry<String, Set<Bean<?>>> entry : index.tailMap(prefix, 
true).entrySet())
+        {
+            if (!entry.getKey().startsWith(prefix))
+            {
+                break;
+            }
+            if (result == null)
+            {
+                result = new HashSet<>();
+            }
+            result.addAll(entry.getValue());
+        }
+
+        return result == null ? Collections.emptySet() : result;
+    }
+
+    /**
+     * Lazily builds an index of all beans with a dot in their name, sorted by 
name so that
+     * prefix-matches can be found via a small {@link 
NavigableMap#tailMap(Object, boolean)} range
+     * instead of iterating over every bean in the application. This mirrors 
the permanent-cache
+     * assumption already used by {@link #dotNamedBeansNegativeCache}: once 
the container is up and
+     * serving EL expressions, the deployed bean set is stable.
+     */
+    private NavigableMap<String, Set<Bean<?>>> getDottedBeanNameIndex(final 
BeanManagerImpl beanManager)
+    {
+        if (dottedBeanNameIndex == null)
+        {
+            synchronized (this)
+            {
+                if (dottedBeanNameIndex == null)
+                {
+                    NavigableMap<String, Set<Bean<?>>> index = new TreeMap<>();
+                    for (Bean<?> bean : beanManager.getBeans())
+                    {
+                        final String name = bean.getName();
+                        if (name != null && name.indexOf('.') >= 0)
+                        {
+                            index.computeIfAbsent(name, k -> new 
HashSet<>()).add(bean);
+                        }
+                    }
+                    dottedBeanNameIndex = index;
+                }
+            }
+        }
+        return dottedBeanNameIndex;
+    }
+
     protected Object getNormalScopedContextualInstance(BeanManagerImpl 
manager, ELContextStore store, ELContext context,
                                                        Bean<?> bean, String 
beanName)
     {
diff --git 
a/webbeans-el22/src/test/java/org/apache/webbeans/el/test/DotNamedBeansTest.java
 
b/webbeans-el22/src/test/java/org/apache/webbeans/el/test/DotNamedBeansTest.java
index 75e7bc150..f624d9e85 100644
--- 
a/webbeans-el22/src/test/java/org/apache/webbeans/el/test/DotNamedBeansTest.java
+++ 
b/webbeans-el22/src/test/java/org/apache/webbeans/el/test/DotNamedBeansTest.java
@@ -37,14 +37,20 @@ import jakarta.inject.Named;
 import org.apache.el.ExpressionFactoryImpl;
 import org.apache.el.lang.FunctionMapperImpl;
 import org.apache.el.lang.VariableMapperImpl;
+import org.apache.webbeans.config.WebBeansContext;
+import org.apache.webbeans.el22.WebBeansELResolver;
 import org.apache.webbeans.el22.WrappedExpressionFactory;
 import org.apache.webbeans.el22.WrappedValueExpressionNode;
 import org.apache.webbeans.exception.WebBeansConfigurationException;
+import org.apache.webbeans.spi.ContextsService;
 import org.junit.Assert;
 import org.junit.Test;
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
 
 public class DotNamedBeansTest extends AbstractUnitTest
 {
@@ -63,6 +69,99 @@ public class DotNamedBeansTest extends AbstractUnitTest
         shutDownContainer();
     }
 
+    /**
+     * Regression test for the root-level negative-cache fast path in
+     * {@link WebBeansELResolver#getValue(ELContext, Object, Object)}: a name 
which is
+     * confirmed to never resolve to any bean must keep returning {@code null} 
on every
+     * call to the same resolver, both before and after the negative cache is 
populated.
+     */
+    @Test
+    public void testRepeatedRootMissStaysNull() throws Exception
+    {
+        Collection<Class<?>> classes = new ArrayList<>();
+
+        classes.add(GoldenFish.class);
+
+        startContainer(classes);
+
+        final ELResolver resolver = getBeanManager().getELResolver();
+
+        // first call: "bla" is unknown, populates the negative cache
+        Assert.assertNull(resolver.getValue(new MockELContext(), null, "bla"));
+        // second call: must hit the new fast path and still resolve to null
+        Assert.assertNull(resolver.getValue(new MockELContext(), null, "bla"));
+
+        shutDownContainer();
+    }
+
+    /**
+     * The lazily built {@code dottedBeanNameIndex} uses double-checked 
locking. Hammer a
+     * single shared resolver instance from many threads concurrently, all 
racing to build
+     * the index for the first time, and verify every thread still resolves 
the correct bean.
+     */
+    @Test
+    public void testConcurrentDottedIndexBuilding() throws Exception
+    {
+        Collection<Class<?>> classes = new ArrayList<>();
+
+        classes.add(GoldenFish.class);
+        classes.add(BlueGoldenFish.class);
+
+        startContainer(classes);
+
+        final WebBeansELResolver resolver = new WebBeansELResolver();
+        final int threadCount = 20;
+        final List<Throwable> failures = new CopyOnWriteArrayList<>();
+        final CountDownLatch ready = new CountDownLatch(threadCount);
+        final CountDownLatch go = new CountDownLatch(1);
+        final List<Thread> threads = new ArrayList<>();
+
+        for (int i = 0; i < threadCount; i++)
+        {
+            Thread t = new Thread(() ->
+            {
+                final ContextsService contextsService = 
WebBeansContext.currentInstance().getContextsService();
+                contextsService.startContext(RequestScoped.class, null);
+                try
+                {
+                    ready.countDown();
+                    go.await();
+
+                    final Object intermediate = resolver.getValue(new 
MockELContext(), null, "magic");
+                    Assert.assertTrue(intermediate instanceof 
WrappedValueExpressionNode);
+
+                    final Object goldenNode = resolver.getValue(new 
MockELContext(), intermediate, "golden");
+                    Assert.assertTrue(goldenNode instanceof 
WrappedValueExpressionNode);
+
+                    final Object fish = resolver.getValue(new MockELContext(), 
goldenNode, "fish");
+                    Assert.assertTrue(fish instanceof GoldenFish);
+                }
+                catch (Throwable e)
+                {
+                    failures.add(e);
+                }
+                finally
+                {
+                    contextsService.endContext(RequestScoped.class, null);
+                }
+            });
+            threads.add(t);
+            t.start();
+        }
+
+        ready.await();
+        go.countDown();
+
+        for (Thread t : threads)
+        {
+            t.join();
+        }
+
+        Assert.assertTrue("Concurrent dotted-name resolution failed: " + 
failures, failures.isEmpty());
+
+        shutDownContainer();
+    }
+
     @Test
     public void testIntermediateNode() throws Exception
     {

Reply via email to