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/