Rabu, 13 Agustus 2014

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> beans = manager.getBeans(ConverterProvider.class);
Bean bean = (Bean) manager.resolve(beans);
CreationalContext 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:






org.omnifaces
omnifaces
1.6.3




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 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).



Source:http://balusc.blogspot.com/2013/10/cdi-behaved-unexpectedly-in-ear-so.html

Tidak ada komentar:

Posting Komentar