Classloading problems when deployed as OSGI bundles
a.novarini Jan 29, 2011 2:03 PMHello all,
First, sorry for the long post, I'm trying to write it as detailed as possible so that you could understand how I got here.
I have a problem with ModeShape deployed as a set of OSGI bundles.
My analysis is the following about what's happening under the hood, I would like to have some feedback and a possible solution to this problem.
This is the code I'm starting from, in its "basic" form, without try/catch, without problems checking and so on:
public Repository findSuitableRepository() { String configFilePath = (String)getComponentContext().getProperties().get("config"); URL configURL = new URL(configFilePath); JcrConfiguration configuration = new JcrConfiguration(); configuration.loadFrom(configURL); engine = configuration.build(); engine.start(); return engine.getRepository("MyRepository"); }
Running this I got the following exception:
java.lang.ClassNotFoundException: org.modeshape.graph.mimetype.ExtensionBasedMimeTypeDetector
Debugging the ModeShape code, I stopped at this code, into the class org.modeshape.common.component.ComponentLibrary<ComponentType, ConfigType>:
protected ComponentType newInstance( ConfigType config ) { ... Class<?> componentClass = Class.forName(config.getComponentClassname(), true, classLoader); ... return newInstance; }
What I got is that into the classLoader I have no class from the bundle, but just the the Sling classes, even though the classLoader content seemed "unpredictable", sometimes I had about 17 elements, sometimes I had 230 elements and so on.
Following Randall's suggestion, I tried to define a custom class loader, which would try to find the class into every installed bundle, and then fallback to the default class loader.
The following is a "snippet" of what I wrote:
JcrConfiguration configuration = new JcrConfiguration(); configuration.withClassLoaderFactory(new ClassLoaderFactory() { @Override public ClassLoader getClassLoader( String... classpath ) { return new ClassLoader() { @Override protected Class<?> findClass( String name ) throws ClassNotFoundException { Bundle[] bundles = getComponentContext().getBundleContext().getBundles(); for (Bundle bundle : bundles) { try { return bundle.loadClass(name); } catch (ClassNotFoundException e) { if (log.isInfoEnabled()) { log.info("Bundle {} does not contain class {}", bundle.getSymbolicName(), name); } } } throw new ClassNotFoundException( String.format("Class with name %s not found in loaded bundles", name)); } }; } });
This didn't solve my problem because the exception raised during the creation of the JcrConfiguration, so I couldn't set any custom class loader.
I tried a new attempt with the following code:
ExecutionContext executionContext = new ExecutionContext() { @Override public ClassLoader getClassLoader( String... classpath ) { return new ClassLoader() { @Override protected Class<?> findClass( String name ) throws ClassNotFoundException { Bundle[] bundles = getComponentContext().getBundleContext().getBundles(); for (Bundle bundle : bundles) { try { return bundle.loadClass(name); } catch (ClassNotFoundException e) { if (log.isInfoEnabled()) { log.info("Bundle {} does not contain class {}", bundle.getSymbolicName(), name); } } } throw new ClassNotFoundException(String.format("Class with name %s not found in loaded bundles", name)); } }; } }; JcrConfiguration jcrConfiguration = new JcrConfiguration(executionContext);
I thought I went close to the point, but I still had the same problem.
This happened because of the ExecutionContext constructor:
@SuppressWarnings( "synthetic-access" ) public ExecutionContext() { this(new NullSecurityContext(), null, null, null, null, null, null, null); initializeDefaultNamespaces(this.getNamespaceRegistry()); assert securityContext != null; }
This calls the protected constructor
protected ExecutionContext( SecurityContext securityContext, NamespaceRegistry namespaceRegistry, ValueFactories valueFactories, PropertyFactory propertyFactory, MimeTypeDetector mimeTypeDetector, ClassLoaderFactory classLoaderFactory, Map<String, String> data, String processId ) { assert securityContext != null; this.securityContext = securityContext; this.namespaceRegistry = namespaceRegistry != null ? namespaceRegistry : new ThreadSafeNamespaceRegistry( new SimpleNamespaceRegistry()); this.valueFactories = valueFactories == null ? new StandardValueFactories(this.namespaceRegistry) : valueFactories; this.propertyFactory = propertyFactory == null ? new BasicPropertyFactory(this.valueFactories) : propertyFactory; this.classLoaderFactory = classLoaderFactory == null ? new StandardClassLoaderFactory() : classLoaderFactory; this.mimeTypeDetector = mimeTypeDetector != null ? mimeTypeDetector : createDefaultMimeTypeDetector(); this.data = data != null ? data : Collections.<String, String>emptyMap(); this.processId = processId != null ? processId : UUID.randomUUID().toString(); }
Now we're getting closer to the point: here the constructor checks whether the mimeTypeDetector is null or not, and since it is, the method createDefaultMimeTypeDetector() is executed.
private MimeTypeDetector createDefaultMimeTypeDetector() { MimeTypeDetectors detectors = new MimeTypeDetectors(); detectors.addDetector(ExtensionBasedMimeTypeDetector.CONFIGURATION); return detectors; }
As we can see here, a configuration is added to the detectors. The MimeTypeDetector is defined is this way:
public MimeTypeDetectors() { library = new ComponentLibrary<MimeTypeDetector, MimeTypeDetectorConfig>(true); library.setClassLoaderFactory(DEFAULT_CLASSLOADER_FACTORY); }
Where DEFAULT_CLASSLOADER_FACTORY is
protected static final ClassLoaderFactory DEFAULT_CLASSLOADER_FACTORY = new StandardClassLoaderFactory( MimeTypeDetectors.class.getClassLoader());
The method addDetector does the following things:
public boolean addDetector( MimeTypeDetectorConfig config ) { return library.add(config); }
And the method add(config) calls in different parts of the code the method newInstance(config) (see above for the piece of code).
Now the final considerations and concerns.
It seems to me that in different part of this "flow" there is a definition of the class loader that at some point isn't propagated to the direct collaborators.
Taking at the moment "for granted" that the code I wrote for my custom classloader works fine, how could I send it through this set of classes to the method newInstance(config) ???
Again, sorry for the long post, I hope you didn't fall asleep along the way.
Thanks
Ale