3 Replies Latest reply on Feb 1, 2011 8:08 AM by rhauch

    Classloading problems when deployed as OSGI bundles

    a.novarini

      Hello 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

        • 1. Classloading problems when deployed as OSGI bundles
          rhauch

          Thanks, Alessandro, for the fantastic and very detailed writeup. I expect that this will make solving this problem a lot easier.

           

          Frankly, it is a bit strange that not even the ModeShape classes are available on the classloader. This is something we never anticipated, and it looks like this assumption is preventing the ExecutionContext from properly initializing even with your custom class loader.

           

          Firstly, can you log a JIRA with a summary of the problem? You can reference this thread in the "JBoss Forum Reference" field, and that way you don't have to provide all the above context. I *think* we should be able to fix it pretty easily by better handling errors.

           

          Secondly, can you do some debugging to see if the Thread's context classloader has the ModeShape classes? That won't help you here, but it may help us tweak our classloading logic.

          • 2. Re: Classloading problems when deployed as OSGI bundles
            a.novarini

            Thanks Randall,

             

            I'm gathering the information you requested and then I'll log my issue to JIRA.

             

            In the meantime, I partially solved applying a "patch" to the ExecutionContext:

             

            private MimeTypeDetector createDefaultMimeTypeDetector() {
                MimeTypeDetectors detectors = new MimeTypeDetectors();
                detectors.setClassLoaderFactory(getClassLoaderFactory());
                detectors.addDetector(ExtensionBasedMimeTypeDetector.CONFIGURATION);
                return detectors;
            }
            
            

             

            So that if the method getClassLoaderFactory() returns null, the default behavior is set, otherwise it'll get my method implementation.

            As you can see, it's nothing serious, but it got me with a found class from a loaded bundle.

            Now I have another problem: after the code gets the class, it tries to do a class.newInstance(), and this breaks with the following exception:

             

            Could not initialize repository org.modeshape.common.SystemFailureException: java.lang.IllegalArgumentException: The stream argument may not be null

                      at org.modeshape.common.component.ComponentLibrary.newInstance(ComponentLibrary.java:318)

             

             

            I'm not an expert in tweaking the classloader and its friends, so it could be some other method I didn't implement.

             

            Thanks again, I'm going to move the conversation to JIRA.

             

            Ale

            • 3. Classloading problems when deployed as OSGI bundles
              rhauch

              Thanks Alessandro!

               

              For reference, the JIRA is MODE-1087.

              1 of 1 people found this helpful