Spring component-scan broken in osgi container

2019-04-09 03:50发布

问题:

I'm trying to integrate spring DI into an existing jaxws project. I've got this working locally on tomcat but when I deploy to the remote container it doesn't appear to do the classpath scanning. I can see in the logs that on tomcat it registers my @Components as beans but on the remote server they're not mentioned at all.

What I do see is the stacktrace below. It looks like it's down to the quirky web container I have to use. It's the "integrated application server for IBM i". http://www-03.ibm.com/systems/i/software/ias/ I believe this is built on the eclipse architecture and when you install a war file it converts each app into a bundle. That's great but it's breaking my classpath scanning. :(

Does anyone have a solution for this? Thanks

673 [Thread-6] WARN  org.springframework.core.io.support.PathMatchingResourcePatternResolver  - Cannot search for matching files underneath URL [bundleresource://32/com/company/application/] because it does not correspond to a directory in the file system
java.io.FileNotFoundException: URL [bundleresource://32/com/company/application/] cannot be resolved to absolute file path because it does not reside in the file system: bundleresource://32/com/company/application/
    at org.springframework.util.ResourceUtils.getFile(ResourceUtils.java:205)
    at org.springframework.core.io.AbstractFileResolvingResource.getFile(AbstractFileResolvingResource.java:52)
    at org.springframework.core.io.UrlResource.getFile(UrlResource.java:169)
    at org.springframework.core.io.support.PathMatchingResourcePatternResolver.doFindPathMatchingFileResources(PathMatchingResourcePatternResolver.java:526)
    at org.springframework.web.context.support.ServletContextResourcePatternResolver.doFindPathMatchingFileResources(ServletContextResourcePatternResolver.java:92)
    at org.springframework.core.io.support.PathMatchingResourcePatternResolver.findPathMatchingResources(PathMatchingResourcePatternResolver.java:347)
    at org.springframework.core.io.support.PathMatchingResourcePatternResolver.getResources(PathMatchingResourcePatternResolver.java:266)
    at org.springframework.context.support.AbstractApplicationContext.getResources(AbstractApplicationContext.java:1269)
    at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.findCandidateComponents(ClassPathScanningCandidateComponentProvider.java:248)
    at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:242)
    at org.springframework.context.annotation.ComponentScanBeanDefinitionParser.parse(ComponentScanBeanDefinitionParser.java:84)
    at org.springframework.beans.factory.xml.NamespaceHandlerSupport.parse(NamespaceHandlerSupport.java:73)
    at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1438)
    at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1428)
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:185)
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:139)
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:108)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.registerBeanDefinitions(XmlBeanDefinitionReader.java:493)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:390)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:334)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:302)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:174)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:209)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:180)
    at org.springframework.web.context.support.XmlWebApplicationContext.loadBeanDefinitions(XmlWebApplicationContext.java:125)
    at org.springframework.web.context.support.XmlWebApplicationContext.loadBeanDefinitions(XmlWebApplicationContext.java:94)
    at org.springframework.context.support.AbstractRefreshableApplicationContext.refreshBeanFactory(AbstractRefreshableApplicationContext.java:130)
    at org.springframework.context.support.AbstractApplicationContext.obtainFreshBeanFactory(AbstractApplicationContext.java:537)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:451)
    at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:389)
    at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:294)
    at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:112)
    at com.ibm.ws.webcontainer.webapp.WebApp.notifyServletContextCreated(WebApp.java:1678)
    at com.ibm.ws.webcontainer.webapp.WebApp.commonInitializationFinish(WebApp.java:371)
    at com.ibm.ws.webcontainer.webapp.WebApp.initialize(WebApp.java:347)
    at com.ibm.ws.webcontainer.webapp.WebGroup.addWebApplication(WebGroup.java:134)
    at com.ibm.ws.webcontainer.VirtualHost.addWebApplication(VirtualHost.java:145)
    at com.ibm.ws.webcontainer.WebContainer.addWebApp(WebContainer.java:542)
    at com.ibm.ws.webcontainer.WebContainer.addWebApplication(WebContainer.java:513)
    at com.ibm.pvc.internal.webcontainer.trackers.WebApplicationServiceTracker.addingService(WebApplicationServiceTracker.java:94)
    at org.osgi.util.tracker.ServiceTracker$Tracked.trackAdding(ServiceTracker.java:1064)
    at org.osgi.util.tracker.ServiceTracker$Tracked.trackInitialServices(ServiceTracker.java:926)
    at org.osgi.util.tracker.ServiceTracker.open(ServiceTracker.java:330)
    at org.osgi.util.tracker.ServiceTracker.open(ServiceTracker.java:274)
    at com.ibm.pvc.internal.webcontainer.trackers.XMLParserServiceTracker.initializeTrackers(XMLParserServiceTracker.java:520)
    at com.ibm.pvc.internal.webcontainer.trackers.XMLParserServiceTracker.startWebcontainer(XMLParserServiceTracker.java:235)
    at com.ibm.pvc.internal.webcontainer.trackers.XMLParserServiceTracker.addingService(XMLParserServiceTracker.java:140)
    at org.osgi.util.tracker.ServiceTracker$Tracked.trackAdding(ServiceTracker.java:1064)
    at org.osgi.util.tracker.ServiceTracker$Tracked.trackInitialServices(ServiceTracker.java:926)
    at org.osgi.util.tracker.ServiceTracker.open(ServiceTracker.java:330)
    at org.osgi.util.tracker.ServiceTracker.open(ServiceTracker.java:274)
    at com.ibm.pvc.internal.webcontainer.WebContainerActivator.start(WebContainerActivator.java:45)
    at org.eclipse.osgi.framework.internal.core.BundleContextImpl$2.run(BundleContextImpl.java:1009)
    at java.security.AccessController.doPrivileged(AccessController.java:251)
    at org.eclipse.osgi.framework.internal.core.BundleContextImpl.startActivator(BundleContextImpl.java:1003)
    at org.eclipse.osgi.framework.internal.core.BundleContextImpl.start(BundleContextImpl.java:984)
    at org.eclipse.osgi.framework.internal.core.BundleHost.startWorker(BundleHost.java:350)
    at org.eclipse.osgi.framework.internal.core.AbstractBundle.start(AbstractBundle.java:279)
    at com.ibm.rcp.internal.util.BundleManager.start(BundleManager.java:74)
    at com.ibm.rcp.internal.util.BundleManager.start(BundleManager.java:179)
    at com.ibm.rcp.lifecycle.internal.application.BundleControlImpl.start(BundleControlImpl.java:125)
    at com.ibm.rcp.lifecycle.internal.application.BundleControlImpl.start(BundleControlImpl.java:106)
    at com.ibm.rcp.lifecycle.application.BundleControl.start(BundleControl.java:89)
    at com.ibm.lwi.application.LWIApplication.run(LWIApplication.java:149)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:48)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:600)
    at org.eclipse.equinox.internal.app.EclipseAppContainer.callMethodWithException(EclipseAppContainer.java:574)
    at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:195)
    at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:110)
    at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:79)
    at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:386)
    at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:179)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:48)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:600)
    at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:561)
    at org.eclipse.equinox.launcher.Main.basicRun(Main.java:501)
    at org.eclipse.equinox.launcher.Main.run(Main.java:1239)
    at org.eclipse.equinox.launcher.Main.main(Main.java:1215)
    at org.eclipse.core.launcher.Main.main(Main.java:30)
    at com.ibm.lwi.LaunchLWI$1.run(LaunchLWI.java:731)  

Looking at the code that throws the exception, it's checking if the protocol is file://.

if (!URL_PROTOCOL_FILE.equals(resourceUrl.getProtocol())) {
            throw new FileNotFoundException(
                    description + " cannot be resolved to absolute file path " +
                    "because it does not reside in the file system: " + resourceUrl);

I can see that the app has been exploded onto the filesystem. I wonder if there is a way to override this loader in spring to not care about the protocol and obviously still work? I don't care for the osgi features of the container. I just want my app to deploy.

The fallback seems to be abandon classpath scanning in favour of defining beans in xml but that;d be a real step back imo. :(

回答1:

I hate the idea of answering my own question but it seemed the best way to report back what I did. I've upvoted the answers so far because they were helpful for me to make a decision.

Through reading the answers I've learned a bit about why classpath scanning is bad. Nonetheless it's a pretty common part of the spring framework these days. My solution was to go "old skool" and manually define my beans.

Can't do this:

<context:component-scan base-package="com.company.application.services" />

Do this instead:

<bean id="service1" class="com.company.application.services.impl.Service1" />
<bean id="service2" class="com.company.application.services.impl.Service2" />

You can at least still do this:

<context:annotation-config />

Annotation config means spring will still wire your beans together, it's just the process of discovering the beans that is being done manually. That's the compromise.

Ideally I would have preferred a means to deploy the project with component scanning but from what I can gather this requires changing the way the project is built specifically for an osgi container. My answer means it'll work both in an osgi and a normal container so it's less specialized.

If someone finds a way to deploy a war file with spring component scanning then I'll gladly re-consider the accepted answer.

Thanks



回答2:

For your web application to run with spring you need spring dm, and therefore you need to have something like the following:

    <context-param>
    <param-name>contextClass</param-name>
    <param-value>org.springframework.osgi.web.context.support.OsgiBundleXmlWebApplicationContext</param-value>
</context-param>

Or for a complete sample take a look at the spring-osgi sample at pax-web: Sample web.xml



回答3:

I tried commenting your initial post, however I can't do that due to my ranking in stackoverflow unfortunately. Don't get me wrong, I am not saying I have a solution here, but I would like share my workaround in newer version of Spring framework here.

Firstly, I have a big configuration class, which servers as the entry in this AnnotationConfigApplicationContext(PseudoSpringBootApplication.class) statement.

/**
 * This class serves as the entry point from OSGI framework to Spring framework
 *
 */
@Configuration
//@ComponentScan
//@SpringBootApplication
public class PseudoSpringBootApplication {

  @Autowired
  ApplicationContext context;

  @Bean
  DummySpringBean1 dummySpringBean1() {
    return new DummySpringBean("This is a dummy message");
  }

  @Bean
  DummySpringBean2 dummySpringBean2() {
    return new DummySpringBean("This is a dummy message");
  }

}

Basically, as @ComponentScan doesn't work with OSGI plugin, (as mentioned in you answer post, this is mainly due to the fact that Spring framework try to scan classes assuming the artifacts are in file system, however, in this case, OSGI (e.g., Apache Felix) has transformed the FILE:// prefixed url to its internal bundle:// prefixed. java.io.FileNotFoundException: URL [bundle://21.0:1/com/***] cannot be resolved to absolute file path because it does not reside in the file system: bundle://21.0:1/com/*** Nevertheless, @Autowired is still working. I've shown the example code below, where DummySpringBean2 has a dependency of DummySpringBean1 and is @Autowired annotated.

//@Component
class DummySpringBean1 {

  private final String dummyMessage;

  DummySpringBean1(String dummyMessage) {
    this.dummyMessage = dummyMessage;
  }
}

For DummySpringBean2, it has dependency on DummySpringBean1.

//@Component
class DummySpringBean2 {

  private final String dummyMessage;

  @Autowired
  private DummySpringBean1 bean1;

  DummySpringBean(String dummyMessage) {
    this.dummyMessage = dummyMessage;
  }
}

So I moved all bean declaration to the central configuration class above.



回答4:

Classpath scanning is a terrible idea and it will break in many runtimes, not just in OSGi. The OSGi service registry is a much more effective approach to the decoupling problem.

You can use the OSGi Service Registry outside of OSGi itself, take a look at PojoSR.

As for how to register and consume OSGi services... since you are using Spring it would be best to use Blueprint, which is an evolution of an older project called Spring Dynamic Modules.



回答5:

I did this little script for linux so I unpack TrackServer.jar and execute the main class without loosing simplicity from Spring (newb to spring here so not so pleased to maneuver and add extra code there)

# Remove any old file before new version
rm -rf tracker;
# Recreate executable folder
mkdir tracker;
# copy main jar to executable folder
cp TrackServer.jar tracker;
# move to executable folder
cd tracker;
# Extract jar on executable folder
jar xf TrackServer.jar;
# Execute main class (at the end of the line) 
# with all the jar dependencies that came 
# in original TrackerServer.jar
java -cp '.;'`ls -dm *.jar | sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/ /g' | sed -e 's/,[ \n\r\t]/:/g'` incodemode.mainApp.Main;
# sed bits are for getting .:dependency1.jar:dependency2.jar etc...
# incodemode.mainApp.Main is the full class path of the Main class