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
{