Saturday, October 5, 2013

CDI behaved unexpectedly in EAR, so OmniFaces 1.6.3 released!

After the OmniFaces 1.6 release, some OmniFaces users reported an undeployable EAR when it contained multiple WARs with OmniFaces bundled. It threw the following exception:

java.lang.IllegalArgumentException: 
    Registering converter 'class org.omnifaces.converter.ListIndexConverter' failed,
    duplicates converter ID 'omnifaces.ListIndexConverter' of other converter 'class org.omnifaces.converter.ListIndexConverter'.
        at org.omnifaces.cdi.converter.ConverterExtension.processConverters(ConverterExtension.java:82)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        ...

It completely blocked the deployment. Well, that was pretty awkward and astonishing. I could reproduce it myself as well by creating a very simple EAR with 2 WARs with each an own OmniFaces JAR file and beans.xml and no further additional code. In first instance, I had no idea how/why it was caused and didn't immediately bother to debug it ("will check it later on"), so I decided to just perform a quick fix by bypassing the exception if it concerns exactly the same converter (and validator) class. I.e. duplicate entries were allowed as long as it's exactly the same class.

WELD-001414 Bean name is ambiguous

However, when it's fixed, then it throws:

org.jboss.weld.exceptions.DeploymentException: 
    WELD-001414 Bean name is ambiguous. Name omnifaces_ViewScopeProvider resolves to beans 
    Managed Bean [class org.omnifaces.cdi.viewscope.ViewScopeManager] with qualifiers [@Any @Default @Named], 
    Managed Bean [class org.omnifaces.cdi.viewscope.ViewScopeManager] with qualifiers [@Any @Default @Named]
        at org.jboss.weld.bootstrap.Validator.validateBeanNames(Validator.java:476)
        at org.jboss.weld.bootstrap.Validator.validateDeployment(Validator.java:373)
        at org.jboss.weld.bootstrap.WeldBootstrap.validateBeans(WeldBootstrap.java:379)
        at org.jboss.as.weld.WeldStartService.start(WeldStartService.java:64)
        ...

It still completely blocked the deployment. My astonishment increased. When I remove the ViewScopeManager class for testing purposes, exactly the same exception is thrown for ConverterManager and ValidatorManager. All those beans have one common thing: a hardcoded @Named name:

@Named(ViewScopeProvider.NAME)
public class ViewScopeManager extends ViewScopeProvider {

}

CDI bean management context is by default EAR-wide

It turns out that the CDI is sharing the very same bean management context across all WARs in the same EAR. It is thus apparently not possible to have a @Named("somename") instance with the same name in multiple WARs in the same EAR, also not an implicit one (i.e. @Named without value). This problem was by Arjan T nailed down to the following SSCCE using an explicit name:

Deployment structure:

ear
    war1
        WEB-INF
            classes/test/Foo.class
            beans.xml
    war2
        WEB-INF
            classes/test/Bar.class
            beans.xml

Source codes:

package test;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;

@Named("test")
@ApplicationScoped
public class Foo {
    //
}
package test;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;

@Named("test")
@ApplicationScoped
public class Bar {
    //
}

The same problem manifests when you have e.g. a @Named public class Foo {} in both WARs without an explicit name (and thus the implicit name "foo" is being used). Giving the @Named a different explicit name in each WAR or removing the @Named annotation altogether (with CDI, each Javabean/POJO found in the classpath is still implicitly automatically registered as a CDI managed bean, but without the @Named it would get an autogenerated name) fixes the "WELD-001414 Bean name is ambiguous" deployment problem. This is a very strange and completely unexpected problem. JSF managed beans doesn't behave like that. Spring managed beans also not — as far as I know, I never actually used Spring, but I couldn't find any evidence on the Internet confirming that.

It's beyond me why CDI behaves by default like that and that CDI is apparently not configurable in such way that only one CDI context per WAR of an EAR is created. It'd perhaps make sense if the EAR-wide CDI context was configurable via some configuration setting in a /META-INF/beans.xml in the EAR (not in WAR!), or even implicitly enabled as EAR-wide when a /META-INF/beans.xml is present in the EAR, but it is not. There have been numerous discussions about this, among others in CDI spec issue 129.

Well, to fix this I had to remove the @Named annotation from all OmniFaces-packaged CDI beans ConverterManager, ValidatorManager and ViewScopeManager. They had previously a hardcoded @Named annotation, because it allowed an easy way to grab an instance of them by just programmatically EL-evaluating their name as follows without the need for a CDI dependency (so that OmniFaces also deploys successfully in e.g. Tomcat where CDI isn't supported out the box!):


ConverterProvider instance = Faces.evaluateExpressionGet("#{" + ConverterProvider.NAME + "}");

Note: ConverterProvider is an interface. The ConverterManager is the actual implementation.

Right now, after removing the @Named, they have to be resolved through CDI's own BeanManager as follows *cough*:


@Inject
private BeanManager manager;

// ...

Set<Bean<?>> beans = manager.getBeans(ConverterProvider.class);
Bean<ConverterProvider> bean = (Bean<ConverterProvider>) manager.resolve(beans);
CreationalContext<ConverterProvider> context = manager.createCreationalContext(bean);
ConverterProvider instance = manager.getReference(bean, ConverterProvider.class, context);

In normal CDI managed beans, the BeanManager is available by just @Injecting it. However, this made it undeployable to Tomcat, so I had to resort to grabbing it from JNDI by java:comp/BeanManager (actually, Tomcat required an extra env/ path) and manually performing nasty reflection trickery to grab the instance. You can see all the code in the new org.omnifaces.config.BeanManager enum singleton. Ultimately, with help of it I ended up with the following Tomcat-compatible approach without any direct CDI dependencies:


ConverterProvider instance = BeanManager.INSTANCE.getReference(ConverterProvider.class);

WELD-001408 Unsatisfied dependencies for type

Still, it didn't deploy successfully, causing yet more astonishment:

Caused by: org.jboss.weld.exceptions.DeploymentException: 
    WELD-001408 Unsatisfied dependencies for type [ValidatorExtension] with qualifiers [@Default] at injection point 
    [[BackedAnnotatedParameter] Parameter 1 of [BackedAnnotatedConstructor] @Inject public org.omnifaces.cdi.validator.ValidatorManager(ValidatorExtension)]
        at org.jboss.weld.bootstrap.Validator.validateInjectionPointForDeploymentProblems(Validator.java:405)
        at org.jboss.weld.bootstrap.Validator.validateInjectionPoint(Validator.java:327)
        at org.jboss.weld.bootstrap.Validator.validateGeneralBean(Validator.java:178)
        at org.jboss.weld.bootstrap.Validator.validateRIBean(Validator.java:209)
        at org.jboss.weld.bootstrap.Validator.validateBean(Validator.java:521)
        ...

This is regardless of whether I injected it as a property:


@Inject
private ValidatorExtension extension;

or in the constructor (as per Weld documentation):


private ValidatorExtension extension;

@Inject
public ValidatorManager(ValidatorExtension extension) {
    this.extension = extension;
}

When I remove the ValidatorExtension class for testing purposes, exactly the same exception is thrown for ConverterExtension.

CDI extensions from one WAR not injectable in another WAR of same EAR

After all, it turns out that there's only one CDI extension instance being created EAR-wide, even though the extension is loaded from a JAR in WAR and not from a JAR in EAR. This finally totally explains the initial IllegalArgumentException problem of converters and validators being added twice. The duplicate converter/validator class actually came from the other WAR.

However, as the CDI extension is loaded from a JAR in WAR, which has thus a different classloader than the other WARs, it's uninjectable in another WARs. In effect, the extension is successfully initialized and injected in one WAR, but not in another. The CDI extension loaded from one WAR couldn't be found in the other WARs of the same EAR (because of being loaded by a different .class file), causing exactly the aforementioned exception.

OmniFaces 1.6 has three extensions: ConverterExtension to proactively collect all @FacesConverter instances, ValidatorExtension to proactively collect all @FacesValidator instances and ViewScopeExtension to create the ViewScopeContext for @ViewScoped annotation. The extension wherein all converter/validator instances eligible for CDI injection are being collected, needs to be @Injected in an application scoped bean which implements an interface so that the application could create and resolve them via the dependency-free (Tomcat!) interface.

However, this peculiar CDI problem thus caused the whole CDI extension based approach to be completely unusable for proactively collecting CDI converter/validator instances. A workaround was found by lazily collecting them inside the application scoped bean itself, so that we could get rid of those CDI extensions ConverterExtension and ValidatorExtension.

With all those changes, the CDI converters/validators are finally fully usable throughout all WARs in the same EAR (only not in GlassFish 3.1.2.2 due to a bug in classloading, this is not related to OmniFaces, it works fine in GlassFish 4).

WELD-001303 No active contexts for scope type

The ViewScopeExtension fortunately doesn't need to be @Injected in the ViewScopeManager, so that part worked fine and the whole EAR finally deployed successfully. However, the ViewScopeContext, which is supposed to be registered by the extension, was available only in the very same WAR as where the CDI extension class is loaded from. The other WARs would throw the following exception:

org.jboss.weld.context.ContextNotActiveException:
    WELD-001303 No active contexts for scope type org.omnifaces.cdi.ViewScoped
        at org.jboss.weld.manager.BeanManagerImpl.getContext(BeanManagerImpl.java:578)
        at org.jboss.weld.bean.proxy.ContextBeanInstance.getInstance(ContextBeanInstance.java:71)
        at org.jboss.weld.bean.proxy.ProxyMethodHandler.invoke(ProxyMethodHandler.java:79)
        at com.example.CdiViewScopedBean$Proxy$_$$_WeldClientProxy.submit(CdiViewScopedBean$Proxy$_$$_WeldClientProxy.java)
        ...

Noted should be that it didn't block the deployment. It's thrown during runtime when an OmniFaces CDI @ViewScoped bean is referenced for the first time and thus needs to be constructed.

Custom scope context created by CDI extension from one WAR not available in another WAR of same EAR

This is very unfortunate as there doesn't seem to be any way to register a custom context programmatically other than via a CDI extension. There's thus no feasible solution to this problem. We would be all ears to implement something like a ServletContextListener to programmatically register the custom CDI scope context WAR-wide, but there seems to be no API-provided way for even something simple like that. All initialization has to take place in a CDI extension per se.

So far, the new OmniFaces CDI @ViewScoped works unfortunately in only one WAR of such an EAR. This all is also summarized along with a table of the tested application servers in Known issues of OmniFaces CDI features in combination with specific application servers. I didn't test it with JSF 2.2's @ViewScoped, but by looking at the codebase of Mojarra and MyFaces, it would expose exactly the same problem when JSF 2.2 JARs are being bundled in those WARs instead of provided by the appserver (as by default on a decent Java EE application server).

OmniFaces 1.6.1 1.6.2 1.6.3 released

Our important goal was to make OmniFaces at least deployable, i.e. it shouldn't be blocking the deployment of WAR or EAR in any way, even though some CDI features may work only half due to the way how CDI works in EARs. We succeed in this with 1.6.3. It's available in Maven on the following coordinates:


<dependency>
    <groupId>org.omnifaces</groupId>
    <artifactId>omnifaces</artifactId>
    <version>1.6.3</version>
</dependency>

WAR-only users are also recommended to upgrade, because during some intensive stress testing of zeef.com by our server admin with hundreds of sessions, the new version 1.4 of the ConcurrentLinkedHashMap, which is repackaged by OmniFaces specifically for usage in <o:cache> and @ViewScoped, was without any clear reason allocating hundreds of megabytes of memory in flavor of a lot of PaddedAtomicLong references. Downgrading the repackaged ConcurrentLinkedHashMap back to its previous version 1.2 fixed the problem of excessive memory allocation while the performance has not significantly changed. Note that older OmniFaces versions before 1.6 already used version 1.2 of ConcurrentLinkedHashMap, so OmniFaces version 1.6 is the only version having this problematic version 1.4 of ConcurrentLinkedHashMap.

Also, a new CDI extension was added which should bypass ("veto") the registration as CDI managed bean of all classes in org.omnifaces package other than org.omnifaces.cdi.* and org.omnifaces.showcase.*. This should eliminate exceptions about ambiguous injection points (some classes in OmniFaces implement Map and that may conflict with a @Producer returning Map) and warnings in server logs about unmanagable beans (like as produced by OpenWebBeans in TomEE 1.6 and reported as issue 894).

By the way, to the users who encountered those problems in their EARs after upgrading to OmniFaces 1.6: herewith we want to wholeheartedly say "Sorry for that!" to them and hopefully OmniFaces 1.6.3 will do the needful job for them. We should really have tested the EAR part as well although those problems were after all completely unexpected. Also, thank you very much for reporting back those problems.

UPDATE: well, it appears that 1.6.1 blocked deployment on non-CDI containers like Tomcat and friends. This was my own mistake. My "Tomcat-Without-CDI" project in Eclipse was misconfigured to have a physical 1.6 JAR in /WEB-INF/lib which got precedence over 1.6.1 in "Deployment Assembly". In effect, I actually never tested 1.6.1 in Tomcat, but always 1.6. Now, 1.6.2 has really been tested on Tomcat and it works fine over there (and of course all others which we tested before).

UPDATE 2: an user of a server which we never used, Google App Engine, reported that 1.6.1/1.6.2 blocked deployment because GAE doesn't support JNDI. This is in turn fixed in 1.6.3.

Conclusion: CDI spec is broken as to behavior in EAR

The CDI guys really need to work out this. What started as clean code in OmniFaces converter/validator injection ended up as ugly code with some reflection trickery and even then it still doesn't work 100% as intented in an EAR with OmniFaces in multiple WARs.

In my opinion, the right solution would be to make the EAR-wide CDI context configurable via some configuration setting in a /META-INF/beans.xml in the EAR (not in WAR!), or even implicitly enabled as EAR-wide when a /META-INF/beans.xml is present in the EAR and otherwise default to WAR-wide (as intuitively expected by developers).

Admittedly, the CDI spec is in Java EE 6 "only" the crisp first version 1.0, so some astonishing teething problems like this are understandable (Servlet 1.0 and JSF 1.0 were also tear jerking), but those issues are even not covered/fixed in 1.1, so all of above still fails the same way in Java EE 7 containers (GlassFish 4.0 and WildFly 8 alpha 4 were tested).

16 comments:

XXIII said...

Thanks for sharing your experiences!
But why do you use Ear-deployment? I try to avoid this and it is mostly not necessary.

Luca Masini said...

Thank for this article.

I felt so "lonely" trying to use CDI in a multi-WAR EAR and I found nothing important in the spec but this:

"In an application deployed as an ear, the container searches every bean archive bundled with or referenced by the ear, including bean archives bundled with or referenced by wars and EJB jars contained in the ear. The bean archives might be library jars, EJB jars, rars or war WEB-INF/classes directories."

but even this is not respect trying to Inject beans in different modules.

I tryed with GF4 and 3.x, WLS12c and JBoss AS 7.x with no luck. I should test a container with OpenWebBeans but they are not my target.

daorte said...

Same situation happens in JBoss EAP 6.0.1 (AS 7.1.3) apparently there is a bug with WELD implementation resolved on version 2 of WELD (WildFly8) but i didn´t test it yet, i did post on PrimeFaces forums hoping someone could hellp me with that with no luck...

Unknown said...

I have the exact opposite issue. I have two WAR applications that have been using various versions of OmniFaces and running under various flavors of the vFabric tc Server for the last year without incident.

Obviously no CDI so no issues. These are very straight forward JSF 2.1, myFaces 2.1.12, PrimeFaces 4.0, Spring JPA 1.4.1, QueryDSL 3.2.3 applications running under vFabric tc Server 2.9.2.

This morning I switched from OmniFaces 1.6 to 1.6.1 and now get this error on startup:

08-Oct-2013 14:43:55.949 SEVERE [localhost-startStop-1] org.apache.myfaces.webapp.AbstractFacesInitializer.initFaces An error occured while initializing MyFaces: BeanManager is not initialized yet. Please use #init(BeanManager) method to manually initialize it.

java.lang.IllegalStateException: BeanManager is not initialized yet. Please use #init(BeanManager) method to manually initialize it.

at org.omnifaces.config.BeanManager.checkInitialized(BeanManager.java:142)

It could very well be that I have always been doing something wrong and finally got caught, but I will admit that I do not know what it is that I am doing wrong.

Any thoughts would be appreciated.

BalusC said...

@Arvid: I reproduced it at Tomcat. Silly mistake on my side, sorry! I will prepare an 1.6.2.

BalusC said...

@Arvid: I created issue 259 on it.

BalusC said...

@Arvid: can you please give 1.6.2 snapshot a try? Download link is available in above issue link.

Arjan Tijms said...

>But why do you use Ear-deployment? I try to avoid this and it is mostly not necessary.

It's indeed for many use cases better to avoid the EAR. A WAR is a much simpler structure and in contrast to older Java EE versions there's hardly anything that requires an EAR.

Still, an EAR has some uses. The virtue of a separate EJB module (which incidentally doesn't even have to contain EJB beans, but typically does) and a WAR is that you have some level of isolation. The business code in the EJB module can't accidentally use web artifacts, since they are simply not available in the EJB module.

It's thus a very basic way to do modularization or layering. I expect that when modularization becomes available in Java SE things may change, but that's still far away in the future.

For OmniFaces things are of course a bit different. Some users simply use EARs, and since EARs are part of the Java EE spec we feel that we should support them.

Unknown said...

Issue appears to have been resolved in 1.6.2-Snapshot. Both applications start and run without incident.

Thanks for the great support!!

Shivreet Sekhon said...

Hi BalusC,
I am really hoping you could help me with something completely unrelated to this POST. Its a question I have on stackoverflow
http://stackoverflow.com/q/19274557/1299508

I am really out of my depths here.

BalusC said...

For the interested: 1.6.2 is released!

BalusC said...

For users of Google App Engine: 1.6.1/1.6.2 blocked deployment on GAE because it doesn't support JNDI. It's fixed in 1.6.3 which has just been released! There are no changes for users of other servers.

John said...

Hi Bauke, there is a problem with omnifaces. I have 1.6.2 version and when I use an omnifaces validator like validateOneOreMore, validateAll, etc. over readonly component it doesn' work. I don't know if I need to register it as a bug

BalusC said...

@John: if you encounter unexpected behavior, by all means report an issue :)

John said...

Thanks Bauke

Unknown said...

Hi Bauke,
Sorry for JSP issue comment on this post but i need your help.

Can you please help me out for combining two image in jsp/servlet from DB. I need to show both up and down together. But my images are broken in different-2 browser.