Classloading and versioning
alesj May 5, 2008 2:26 AMWhile hacking the demo, I had 'problems' creating this 'expected' behavior:
I have foo.jar which contains FooBar service.
This FooBar service uses two other services - AlesService and ScottService.
<bean name="FooBarService" class="org.jboss.foo.FooBarService"> <property name="alesService"><inject/></property> <property name="scottService"><inject/></property> </bean>
Then I have two similar jars - acme1 and acme2.
Containing AlesService(Impl) and ScottService(Impl).
acme1 has all classes in version 1.
Where acme2 has ales package in version 2 and scott package in version 3.
Then I have the following classloading metadata in foo.jar:
<classloading xmlns="urn:jboss:classloading:1.0" export-all="NON_EMPTY"> <capabilities> <package name="org.jboss.acme.foo" version="1"/> </capabilities> <requirements> <package name="org.jboss.acme.ales" from="1" to="2" from-inclusive="true"/> <package name="org.jboss.acme.scott" from="3" to="4" from-inclusive="true"/> </requirements> </classloading>
Expecting that FooBar would use AlesService(Impl) from acme1 and ScottService(Impl) from acme2.
But that's not what happened when using the bootstrap configuration similar to what we use in AS5_trunk. :-(
Either I don't know how to properly configure this, or it's broken. ;-)
The problems that I've found:
When ClassLoaderPolicy returns non-null getExported delegate loader, this gets cached in BaseClassLoaderDomain::classLoadersByPackageName
if (packageNames != null && info.getExported() != null) { for (String packageName : packageNames) { List<ClassLoaderInformation> list = classLoadersByPackageName.get(packageName); if (list == null) { list = new CopyOnWriteArrayList<ClassLoaderInformation>(); classLoadersByPackageName.put(packageName, list); } list.add(info);
and this is then used when looking up the class in other bundles as well, but w/o knowing the right version:
BaseClassLoaderDomain::findLoaderInExports
List<ClassLoaderInformation> list = classLoadersByPackageName.get(packageName); if (trace) log.trace(this + " trying to load " + name + " from all exports of package " + packageName + " " + list); if (list != null && list.isEmpty() == false) { for (ClassLoaderInformation info : list) { BaseDelegateLoader exported = info.getExported(); // See whether the policies allow caching/blacklisting BaseClassLoaderPolicy loaderPolicy = exported.getPolicy(); if (loaderPolicy == null || loaderPolicy.isCacheable() == false) canCache = false; if (loaderPolicy == null || loaderPolicy.isBlackListable() == false) canBlackList = false; if (exported.getResource(name) != null) { if (canCache) globalClassCache.put(name, exported); return exported; } } }
So, when attempting to load class with the same name from foo.jar's classlaoder, but expecting it in diff version, I still get the previous one.
OK, then I disabled this exported usage - either by returning null on getExported or null getPackageNames.
Still didn't work as expected. :-(
Now the version-unaware delegates were kicking in:
BaseClassLoaderDomain::findLoaderInImports
for (DelegateLoader delegate : delegates) { if (trace) log.trace(this + " trying to load " + name + " from import " + delegate + " for " + info.getClassLoader()); if (delegate.getResource(name) != null) { info.cacheLoader(name, delegate); return delegate; } }
where by default in CLPolicy we return exported, which is just filtered by package names - no version awareness.
So again the previous classloader was picked.
What I had to do then - and I don't see why this isn't default behavior - is the following piece of code:
public DelegateLoader getDelegateLoader(Module module, Requirement requirement) { List<Capability> capabilities = getCapabilities(); if (capabilities != null && capabilities.isEmpty() == false) { for(Capability capability : capabilities) { if (capability.resolves(this, requirement)) return new FilteredDelegateLoader(getPolicy(), new CapabilityFilter(capability)); } // none of the capabilities match - don't put it as delegate return null; } return super.getDelegateLoader(module, requirement); } public class CapabilityFilter implements ClassFilter { private Capability capability; public CapabilityFilter(Capability capability) { if (capability == null) throw new IllegalArgumentException("Null capability."); this.capability = capability; } public boolean matchesClassName(String className) { return matchesPackageName(ClassLoaderUtils.getClassPackageName(className)); } public boolean matchesResourcePath(String resourcePath) { return matchesPackageName(ClassLoaderUtils.getResourcePackageName(resourcePath)); } public boolean matchesPackageName(String packageName) { if (capability instanceof ExportPackages) { ExportPackages ep = (ExportPackages)capability; Set<String> packages = ep.getPackageNames(null); if (packages != null && packages.contains(packageName)) return true; } return false; } }
This finally got me to my expected behavior. :-)
All this code can be found here:
- http://anonsvn.jboss.org/repos/jbossas/projects/demos/osgi/