Skip to content

Relocate client proxies for public third-party normal-scope… - #3501

Open
jamezp wants to merge 1 commit into
weld:6.0from
jamezp:WELD-2838-6.0
Open

Relocate client proxies for public third-party normal-scope…#3501
jamezp wants to merge 1 commit into
weld:6.0from
jamezp:WELD-2838-6.0

Conversation

@jamezp

@jamezp jamezp commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

…d beans to avoid requiring --add-opens.

resolves #3502
Upstream #3500

@jamezp
jamezp requested a review from manovotn as a code owner September 3, 2026 17:04
@jamezp
jamezp marked this pull request as draft September 3, 2026 17:06
@jamezp
jamezp marked this pull request as ready for review September 3, 2026 17:37
@manovotn

manovotn commented Sep 3, 2026

Copy link
Copy Markdown
Member

Thanks for the PR James, I will take a closer look tomorrow.

Just two remarks from the top of my head.
Firstly, please create GH issue over a JIRA - project now mentions using GH issues everywhere (at least I hope it does :)).
Secondly, some time ago a did a more thorough yet unfinished attempt to convert Weld into a more JMPS-friendly manner. See #3460
It might be worth picking that up again - back then I had other duties and worries that dragged me away from it.

I am curious to see what case you had for this as I recall I had issues actually testing this for in EE environment you have to assume servers would handle the modular layers themselves (assuming they handle them at all).

@jamezp

jamezp commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

That's my bad. I can close the Jira and create an GitHub issue. I even looked the the contributing guide and still filed a Jira 🤦

I'll have a look at #3460. I just went through the process of adding Java modules to RESTEasy. In my experience the biggest issue I had, and still have, is testing. surefire/failsafe aren't great at it because they patch the module system. Anyway, I'm happy to have a look at it.

If we feel solving it as a whole is better, we can definitely close this too. I was just working on resteasy/resteasy-vertx#108 when I found this.

@jamezp jamezp changed the title WELD-2836 Relocate client proxies for public third-party normal-scope… Relocate client proxies for public third-party normal-scope… Sep 3, 2026
… avoid requiring --add-opens.

resolves weld#3502
Signed-off-by: James R. Perkins <jperkins@ibm.com>
@manovotn

manovotn commented Sep 3, 2026

Copy link
Copy Markdown
Member

I'll have a look at #3460. I just went through the process of adding Java modules to RESTEasy. In my experience the biggest issue I had, and still have, is testing. surefire/failsafe aren't great at it because they patch the module system. Anyway, I'm happy to have a look at it.

I have to look at it myself to see where I left it.
Though I would be interested in hearing your take on how it should be done.
From what I recall, the biggest issue/question is that Weld has to create those proxies and those should usually reside inside the package of the original class and that causes some nuissance :-/

@jamezp

jamezp commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Yes. I can see where it could be really tricky for Weld. I'll definitely have a look and do some thinking. I'm already finding bugs in what I did for RESTEasy :)

@manovotn manovotn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weld already avoids this for java.* and jakarta.* types by relocating their proxies into org.jboss.weld.generated.proxies.*.

The java.* are there since forever and the reason is that it was never possible to (even with CL usage) define a new class inside java packages.
I do not know the origin of the jakarta.* exception but I assume it was due to some EE servers where placing the proxy there might lead to leaks as the Jakarta lib would not be unloaded with the deployed app but I am just guessing there.

The general idea is to always leave the proxy class in the same package that the proxied type belongs to because that's the only way package private access keeps working.Note that by package private, I mean not only the class itself but also any methods on it. If you relocate a client proxy of a public class that has a pack private method, you still end up with broken proxy because overrides of pack private methods won't work across packages!

Plus, ProxyFactory is a base for other than client proxies such as interceptor proxies which are genuine subclasses so the requirements are stricter there. I'm not sure this change would affect them, need to check it as well.

Comment thread impl/src/main/java/org/jboss/weld/bean/proxy/ProxyFactory.java
proxyClassName = proxyClassName.replaceFirst(JAVA, WELD_PROXY_PREFIX);
} else if (proxyClassName.startsWith(JAKARTA)) {
proxyClassName = proxyClassName.replaceFirst(JAKARTA, WELD_PROXY_PREFIX);
} else if (bean != null && shouldRelocateProxy(bean.getBeanClass())) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bean.getBeanClass() likely isn't what you meant. For a producer bean, this will not return the class of the bean, but the declaring class of the producer.

I am not sure what would be the best approach to determine the right class. A simple approach could be:

            Class<?> target = (bean instanceof AbstractProducerBean)
                    ? ((AbstractProducerBean<?, ?, ?>) bean).getType()
                    : bean.getBeanClass();

But this is likely to still be incorrect for some edge cases such as custom Bean implementations.
There is quite complex derivation of the package name somewhere inside the createCompoundProxyName method.

A more robust approach would have to work with the proxyClassName which already contains derived name of the package and match against that.
Though I am wondering if it wouldn't be better to instead change the base logic that determines the package instead of attempting a relocation as an afterthought.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had originally tried the proxyClassName, but sometimes that was java.lang.Object and it was causing issues where it relocated beans it shouldn't because java.lang.Object is in the java.base module. However, now that I think about it, I don't know why that module wouldn't be open.

Anyway, I wouldn't doubt if the bean.getBeanClass() was wrong, it just fixed the failing tests I had :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had originally tried the proxyClassName, but sometimes that was java.lang.Object and it was causing issues where it relocated beans

Yea, this is one of the edge cases - I think it happens for custom Bean<T> implementations? Perhaps some other cases too, would need to debug.

However, now that I think about it, I don't know why that module wouldn't be open.

As far as I know, adding anything into java.base is a hard no-no. You couldn't do that even with direct CL class definitions so I doubt you'd be suddenly able to with JPMS.

Anyway, I wouldn't doubt if the bean.getBeanClass() was wrong, it just fixed the failing tests I had :)

Yes, it coincidentally fixes your scenario but would break others. From the top of my head, you'd just have to create a scenario where the declaring class of producer is open to Weld and the produced type isn't and it should fail.

}
// Only relocate proxy when we have a public modifier as we cannot override package-private methods.
// package-private access is intentionally handled by defineWithMethodLookup
if (!Modifier.isPublic(originalType.getModifiers())) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, this check is not enough as you only verify the class being public.
However, you can have a public class that has a package-private method in it. If you relocate a proxy for such class, you won't be able to delegate to said method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a good point. We should check the class is accessible too.

The general idea isn't necessarily to avoid requiring --add-opens, it's just to avoid it for most cases. IoW, if you have a non-publicly accessible method (via the method or class), you need to add the --add-opens. This only "fixes issues" for public beans in a named module.

@manovotn

manovotn commented Sep 4, 2026

Copy link
Copy Markdown
Member

Generally, even if we adapt your suggestion, it would have to have careful checks to only allow this for client proxies of public classes with no pack private methods which is a very small improvement.

Any single class in a JAR that doesn't meet these requirement will trigger the need for add-opens anyway.

That also leads to an awkward behavior where a seemingly working application can start throwing errors on adding a single method. Or perhaps on adding an interceptor to a bean that previously had none.
Not to mention you'd now sometimes be using MethodHandles and sometimes the custom CL to define the same class based on env. setup which does feel brittle and hard to understand/debug in the future.

@mkouba @Ladicek a penny for your thoughts on changes like these? Some other pitfalls you can think of when placing proxies into other than the original package?

Also, my limited knowledge of JPMS leads me to believe that using frameworks such as Weld in a modular app means you should be adding add-opens instead of looking for ways of bypassing it. Though I know little of it and will be happy to be proven wrong :)

@jamezp

jamezp commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Generally, even if we adapt your suggestion, it would have to have careful checks to only allow this for client proxies of public classes with no pack private methods which is a very small improvement.

Any single class in a JAR that doesn't meet these requirement will trigger the need for add-opens anyway.

Yes, and to be honest this may not be worth it. Even though users should not rely on a proxy class name, I've definitely seen cases where they do. In a non Java module environment, that is fine. It gets trickier for module environments if we relocate the package name.

TBH I do find it kind of hacky and we can definitely look for other/better solutions. We can definitely make this a draft if that's better.

That also leads to an awkward behavior where a seemingly working application can start throwing errors on adding a single method. Or perhaps on adding an interceptor to a bean that previously had none. Not to mention you'd now sometimes be using MethodHandles and sometimes the custom CL to define the same class based on env. setup which does feel brittle and hard to understand/debug in the future.

I definitely agree here.

Also, my limited knowledge of JPMS leads me to believe that using frameworks such as Weld in a modular app means you should be adding add-opens instead of looking for ways of bypassing it. Though I know little of it and will be happy to be proven wrong :)

I know we talked about this, but I'll add it here too for others. The add-opens isn't necessarily bad. Where it feels awkward to me is when you want to inject third-party beans. If you're project is a server type of environment you've got to let users know they need to add the add-opens directive. Honestly, maybe not a big deal.

I saw this when I was working on https://github.com/resteasy/resteasy-vertx and trying to have a smoke test for the Java module descriptors. I allow inject these types:

  • io.vertx.core.Vertx
  • io.vertx.ext.web.Router
  • io.vertx.ext.web.RoutingContext

Without the add-opens or this fix you end up with something like:

org.jboss.weld.exceptions.WeldException: WELD-001524: Unable to load proxy class for bean Producer Method [Router] with qualifiers [@Any @Default] declared as [[BackedAnnotatedMethod] @Produces @RequestScoped public dev.resteasy.vertx.cdi.VertxProducers.router()] with class class java.lang.Object
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.proxy.ProxyFactory.getProxyClass(ProxyFactory.java:424)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.proxy.ProxyFactory.instantiateProxy(ProxyFactory.java:375)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.proxy.ProxyFactory.create(ProxyFactory.java:368)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.proxy.ClientProxyFactory.create(ClientProxyFactory.java:84)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.proxy.ClientProxyProvider.createClientProxy(ClientProxyProvider.java:207)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.proxy.ClientProxyProvider.createClientProxy(ClientProxyProvider.java:197)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.proxy.ClientProxyProvider$CreateClientProxy.apply(ClientProxyProvider.java:52)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.proxy.ClientProxyProvider$CreateClientProxy.apply(ClientProxyProvider.java:48)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.util.cache.ReentrantMapBackedComputingCache.lambda$new$0(ReentrantMapBackedComputingCache.java:57)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.util.LazyValueHolder$1.computeValue(LazyValueHolder.java:32)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.util.LazyValueHolder.get(LazyValueHolder.java:46)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.util.cache.ReentrantMapBackedComputingCache.getValue(ReentrantMapBackedComputingCache.java:74)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.util.cache.ReentrantMapBackedComputingCache.getCastValue(ReentrantMapBackedComputingCache.java:80)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.proxy.ClientProxyProvider.getClientProxy(ClientProxyProvider.java:232)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.manager.BeanManagerImpl.getReference(BeanManagerImpl.java:720)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.manager.BeanManagerImpl.getInjectableReference(BeanManagerImpl.java:817)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.injection.FieldInjectionPoint.inject(FieldInjectionPoint.java:91)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.util.Beans.injectBoundFields(Beans.java:357)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.util.Beans.injectFieldsAndInitializers(Beans.java:368)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.injection.producer.ResourceInjector$1.proceed(ResourceInjector.java:70)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.injection.InjectionContextImpl.run(InjectionContextImpl.java:49)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.injection.producer.ResourceInjector.inject(ResourceInjector.java:72)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.injection.producer.BasicInjectionTarget.inject(BasicInjectionTarget.java:126)
	at org.jboss.resteasy.cdi@7.0.4.Final/org.jboss.resteasy.cdi.JaxrsInjectionTarget.inject(JaxrsInjectionTarget.java:94)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.ManagedBean.create(ManagedBean.java:165)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.contexts.AbstractContext.get(AbstractContext.java:96)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.ContextualInstanceStrategy$DefaultContextualInstanceStrategy.get(ContextualInstanceStrategy.java:106)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.ContextualInstanceStrategy$CachingContextualInstanceStrategy.get(ContextualInstanceStrategy.java:192)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.ContextualInstance.get(ContextualInstance.java:50)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.proxy.ContextBeanInstance.getInstance(ContextBeanInstance.java:101)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.proxy.ProxyMethodHandler.getInstance(ProxyMethodHandler.java:136)
	at dev.resteasy.vertx.it/dev.resteasy.vertx.it.CdiContextInjectionTest$ContextResource$Proxy$_$$_WeldClientProxy.securityContext(Unknown Source)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
	at java.base/java.lang.reflect.Method.invoke(Method.java:565)
	at org.jboss.resteasy.core@7.0.4.Final/org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:154)
	at org.jboss.resteasy.core@7.0.4.Final/org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:118)
	at org.jboss.resteasy.core@7.0.4.Final/org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:560)
	at org.jboss.resteasy.core@7.0.4.Final/org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:452)
	at org.jboss.resteasy.core@7.0.4.Final/org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$0(ResourceMethodInvoker.java:413)
	at org.jboss.resteasy.core@7.0.4.Final/org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:333)
	at org.jboss.resteasy.core@7.0.4.Final/org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:415)
	at org.jboss.resteasy.core@7.0.4.Final/org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:378)
	at org.jboss.resteasy.core@7.0.4.Final/org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:356)
	at org.jboss.resteasy.core@7.0.4.Final/org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:70)
	at org.jboss.resteasy.core@7.0.4.Final/org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:434)
	at org.jboss.resteasy.core@7.0.4.Final/org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$0(SynchronousDispatcher.java:245)
	at org.jboss.resteasy.core@7.0.4.Final/org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:159)
	at org.jboss.resteasy.core@7.0.4.Final/org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:333)
	at org.jboss.resteasy.core@7.0.4.Final/org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:162)
	at org.jboss.resteasy.core@7.0.4.Final/org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:234)
	at dev.resteasy.vertx.server@2.0.0.Beta3-SNAPSHOT/dev.resteasy.vertx.server.VertxRoutingContextHandler.service(VertxRoutingContextHandler.java:178)
	at dev.resteasy.vertx.server@2.0.0.Beta3-SNAPSHOT/dev.resteasy.vertx.server.VertxRoutingContextHandler.dispatch(VertxRoutingContextHandler.java:132)
	at dev.resteasy.vertx.server@2.0.0.Beta3-SNAPSHOT/dev.resteasy.vertx.server.VertxRoutingContextHandler.lambda$handle$0(VertxRoutingContextHandler.java:113)
	at io.vertx.core@5.1.7/io.vertx.core.Future.lambda$onComplete$1(Future.java:312)
	at io.vertx.core@5.1.7/io.vertx.core.impl.future.FutureImpl$ListenerArray.complete(FutureImpl.java:188)
	at io.vertx.core@5.1.7/io.vertx.core.impl.future.FutureBase$EmitResultTask.run(FutureBase.java:125)
	at io.vertx.core@5.1.7/io.vertx.core.impl.ContextBase.execute(ContextBase.java:96)
	at io.vertx.core@5.1.7/io.vertx.core.impl.future.FutureBase.emitResult(FutureBase.java:83)
	at io.vertx.core@5.1.7/io.vertx.core.impl.future.FutureImpl.completeInternal(FutureImpl.java:137)
	at io.vertx.core@5.1.7/io.vertx.core.impl.future.FutureImpl.tryComplete(FutureImpl.java:143)
	at io.vertx.core@5.1.7/io.vertx.core.http.impl.HttpEventHandler.handleEnd(HttpEventHandler.java:79)
	at io.vertx.core@5.1.7/io.vertx.core.http.impl.http1.Http1ServerRequest.onEnd(Http1ServerRequest.java:562)
	at io.vertx.core@5.1.7/io.vertx.core.impl.ContextBase.execute(ContextBase.java:112)
	at io.vertx.core@5.1.7/io.vertx.core.http.impl.http1.Http1ServerRequest.handleEnd(Http1ServerRequest.java:163)
	at io.vertx.core@5.1.7/io.vertx.core.http.impl.http1.Http1ServerConnection.onEnd(Http1ServerConnection.java:260)
	at io.vertx.core@5.1.7/io.vertx.core.http.impl.http1.Http1ServerConnection.handleMessage(Http1ServerConnection.java:208)
	at io.vertx.core@5.1.7/io.vertx.core.net.impl.VertxConnection.read(VertxConnection.java:309)
	at io.vertx.core@5.1.7/io.vertx.core.net.impl.VertxHandler.channelRead(VertxHandler.java:170)
	at io.netty.transport@4.2.17.Final/io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:355)
	at io.netty.transport@4.2.17.Final/io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:93)
	at io.netty.codec.http@4.2.17.Final/io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtensionHandler.channelRead(WebSocketServerExtensionHandler.java:87)
	at io.netty.transport@4.2.17.Final/io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:355)
	at io.netty.codec@4.2.17.Final/io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:361)
	at io.netty.codec@4.2.17.Final/io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:325)
	at io.netty.transport@4.2.17.Final/io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
	at io.vertx.core@5.1.7/io.vertx.core.http.impl.tcp.Http1xOrH2CHandler.end(Http1xOrH2CHandler.java:61)
	at io.vertx.core@5.1.7/io.vertx.core.http.impl.tcp.Http1xOrH2CHandler.channelRead(Http1xOrH2CHandler.java:38)
	at io.netty.transport@4.2.17.Final/io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
	at io.netty.transport@4.2.17.Final/io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1429)
	at io.netty.transport@4.2.17.Final/io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:918)
	at io.netty.transport@4.2.17.Final/io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:176)
	at io.netty.transport@4.2.17.Final/io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.handle(AbstractNioChannel.java:445)
	at io.netty.transport@4.2.17.Final/io.netty.channel.nio.NioIoHandler$DefaultNioRegistration.handle(NioIoHandler.java:388)
	at io.netty.transport@4.2.17.Final/io.netty.channel.nio.NioIoHandler.processSelectedKey(NioIoHandler.java:596)
	at io.netty.transport@4.2.17.Final/io.netty.channel.nio.NioIoHandler.processSelectedKeysPlain(NioIoHandler.java:541)
	at io.netty.transport@4.2.17.Final/io.netty.channel.nio.NioIoHandler.processSelectedKeys(NioIoHandler.java:514)
	at io.netty.transport@4.2.17.Final/io.netty.channel.nio.NioIoHandler.run(NioIoHandler.java:484)
	at io.netty.transport@4.2.17.Final/io.netty.channel.SingleThreadIoEventLoop.runIo(SingleThreadIoEventLoop.java:225)
	at io.netty.transport@4.2.17.Final/io.netty.channel.SingleThreadIoEventLoop.run(SingleThreadIoEventLoop.java:196)
	at io.netty.common@4.2.17.Final/io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:1204)
	at io.netty.common@4.2.17.Final/io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at io.netty.common@4.2.17.Final/io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base/java.lang.Thread.run(Thread.java:1474)
Caused by: java.lang.RuntimeException: java.lang.IllegalAccessException: module io.vertx.web does not open io.vertx.ext.web to module weld.core.impl
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.proxy.util.WeldDefaultProxyServices.defineWithMethodLookup(WeldDefaultProxyServices.java:155)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.proxy.util.WeldDefaultProxyServices.defineClass(WeldDefaultProxyServices.java:62)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.proxy.ProxyFactory.toClass(ProxyFactory.java:975)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.proxy.ProxyFactory.createProxyClass(ProxyFactory.java:509)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.proxy.ProxyFactory.getProxyClass(ProxyFactory.java:416)
	... 92 more
Caused by: java.lang.IllegalAccessException: module io.vertx.web does not open io.vertx.ext.web to module weld.core.impl
	at java.base/java.lang.invoke.MethodHandles.privateLookupIn(MethodHandles.java:268)
	at weld.core.impl@6.0.4.Final/org.jboss.weld.bean.proxy.util.WeldDefaultProxyServices.defineWithMethodLookup(WeldDefaultProxyServices.java:152)
	... 96 more

The workaround, as you suggested to me, is to simply wrap the types in your own wrapper with your package name. This does seem to work, but I do wrap io.vertx.Vertx and I don't have an issue injecting that.

It's also fair to be realistic to know if this is even an issue for others. It would be in a true Java module environment, but using add-opens works just fine and I don't even know how common it is to use Java modules with Weld or really other Jakarta libraries. None of the application servers I'm aware of use it.

@manovotn

manovotn commented Sep 6, 2026

Copy link
Copy Markdown
Member

You summed it up nicely, thanks for that.
I generally think we cannot avoid the add-opens formula for SE/Servlet but the real question is whether we want to enable some "happy path" for these cases. Whether it's worth the complexity it introduces.

It's also fair to be realistic to know if this is even an issue for others. It would be in a true Java module environment, but using add-opens works just fine and I don't even know how common it is to use Java modules with Weld or really other Jakarta libraries. None of the application servers I'm aware of use it.

This is something I would very much love to have the answer to myself.
If I were to judge just from issues, I'd have to say it's not being used. Then again, the reason for that might simply be that Weld doesn't have any module names so far and hence it disincentivizes people from even trying.

One other thought - since JPMS allows to pass on the privileges you gave to one module to another, I was always thinking whether there would be some way where users can give the needed privileges (opens in this case) to just CDI API and that would pass it on to known CDI impls.

Then again, that would never be able to solve your case as the original library has no idea its type is being used as a bean 🤷
For such case, we either need to support the happy path case or require explicit addition of the switch.

Either way, I need to think about this some more, figure out what to do with the pending draft I had and, last but not least, document this somewhere.

@Ladicek

Ladicek commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Hi! Not a JPMS expert here, but I believe all containers and components of Jakarta EE servers should support two modes:

  1. Standalone, where there application has to either open itself to the component (either directly or indirectly through the specification, as @manovotn noted) or pass privileges to itself to the component (again, directly or indirectly). This should be fairly straightforward, and it's necessary for SE usage at least.
  2. Integrated, where the application server arranges a module layer (or multiple layers) in which class loading is set up such that no code has to become open or pass privileges to components, because it's not necessary. I have never done this, but I know other people did (see Layrry or SmallRye Modules), so it should be possible; the hard thing here is probably assembling the experts that can actually specify this in Jakarta Platform.

I realize this is not very helpful on its own, but I think the second mode has to be solved on the WildFly level and probably/hopefully also at the Jakarta Platform level. Components cannot do this on their own.

@manovotn

manovotn commented Sep 6, 2026

Copy link
Copy Markdown
Member
  1. Standalone, where there application has to either open itself to the component (either directly or indirectly through the specification, as @manovotn noted) or pass privileges to itself to the component (again, directly or indirectly). This should be fairly straightforward, and it's necessary for SE usage at least.

This is the case we are discussing here.
Though note that the case James describes is a little more complicated and the way you suggest won't really work here. At least I don't see how.

It is a case of jar A not being a bean archive at all. Then jar B delivering a producer for type from jar A and finally your application consuming both.
In such case A cannot know it should open up to CDI or Weld and B has no way of opening it for them despite holding the producer. Issue being, the created proxy will try to land the package of the type from jar A.

  1. Integrated, where the application server arranges a module layer (or multiple layers) in which class loading is set up such that no code has to become open or pass privileges to components, because it's not necessary. I have never done this, but I know other people did (see Layrry or SmallRye Modules), so it should be possible; the hard thing here is probably assembling the experts that can actually specify this in Jakarta Platform.

I agree, we neednt really worry about this case. It's oit of our reach plus also theoretical as no EE serves that I know cares about modularity (in JPMS terms at least) as of today.

@Ladicek

Ladicek commented Sep 6, 2026

Copy link
Copy Markdown
Contributor
  1. Standalone, where there application has to either open itself to the component (either directly or indirectly through the specification, as @manovotn noted) or pass privileges to itself to the component (again, directly or indirectly). This should be fairly straightforward, and it's necessary for SE usage at least.

This is the case we are discussing here. Though note that the case James describes is a little more complicated and the way you suggest won't really work here. At least I don't see how.

It is a case of jar A not being a bean archive at all. Then jar B delivering a producer for type from jar A and finally your application consuming both. In such case A cannot know it should open up to CDI or Weld and B has no way of opening it for them despite holding the producer. Issue being, the created proxy will try to land the package of the type from jar A.

Right. It is my personal opinion that on JPMS, we cannot guarantee as many things as we can on the classpath. In this particular case, and I'm not sure Weld is well prepared to actually model it precisely enough (but that's another concern), it feels entirely right to only guarantee that public methods are proxied and the others fail. How do we do that exactly (e.g. generating the client proxy in another package) is an implementation matter.

  1. Integrated, where the application server arranges a module layer (or multiple layers) in which class loading is set up such that no code has to become open or pass privileges to components, because it's not necessary. I have never done this, but I know other people did (see Layrry or SmallRye Modules), so it should be possible; the hard thing here is probably assembling the experts that can actually specify this in Jakarta Platform.

I agree, we neednt really worry about this case. It's oit of our reach plus also theoretical as no EE serves that I know cares about modularity (in JPMS terms at least) as of today.

Correct, but it's coming relatively soon, at least on the CDI front, see what we're doing in Quarkus for example. It will probably take longer for the Jakarta Platform, but I'd still wager it's coming :-)

@manovotn

manovotn commented Sep 6, 2026

Copy link
Copy Markdown
Member

and I'm not sure Weld is well prepared to actually model it precisely enough (but that's another concern)

Can you be more specific? In what way do you think it cannot model it? That could be a good starting point too.

it feels entirely right to only guarantee that public methods are proxied and the others fail. How do we do that exactly (e.g. generating the client proxy in another package) is an implementation matter.

While placement of proxies is, in theory, an implementation detail. It will stop being one the moment you know specification expect you to support certain cases with package private access :-/
The approach drafted here kind of does what you hint at but at the cost of behaving differently based on whether we are in module aware env or not. Which does seem very fragile even without having any backing in the spec.

@Ladicek

Ladicek commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

and I'm not sure Weld is well prepared to actually model it precisely enough (but that's another concern)

Can you be more specific? In what way do you think it cannot model it? That could be a good starting point too.

I'm not saying it cannot model it, but here's a few questions that come to mind:

  • Do you know, for each archive (including bean archives, but also other archives) whether it's deployed on class path or module path?
  • For each archive deployed on module path, do you know whether it's an automatic module (doesn't have module-info) or explicit module?

Actually, thinking more deeply about this, I think the most important underlying question is: can we add a new class to the package of given class? It feels sensible to assume there are no spilt packages, at least for classes from archives deployed on module path.

I think you already have an implementation of the same question, but its probably something like ArC's classFQN.startsWith("java.lang."). In the module world, this should be something like "if the class comes from the boot module layer, then no, otherwise call into some SPI". That SPI should probably be able to answer the questions above. And the SPI should probably also provide a way to actually do that, which would mean you don't have to care about these details, but I'm not sure how feasible that is or isn't. And it would also abstract away things like how much you want to support mixed scenarios (some archives on class path, some on module path).

it feels entirely right to only guarantee that public methods are proxied and the others fail. How do we do that exactly (e.g. generating the client proxy in another package) is an implementation matter.

While placement of proxies is, in theory, an implementation detail. It will stop being one the moment you know specification expect you to support certain cases with package private access :-/

Yeah, but the specification doesn't know about JPMS. It seems entirely fair for CDI to not support certain cases in certain (not necessarily all) JPMS deployments. Like, I'd expect an application server to set up class loading such that package-private calls are properly proxied etc., but in an SE environment, we cannot guarantee that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants