简体   繁体   中英

Can't find ruby gem in Spring Boot application

I have created a jar file that contains a Ruby gem (activemerchant) that is wrapped by a Spring service, which uses the JRuby ScriptContainer to invoke a small Ruby script that utilises the ruby gem. Unit tests for this jar all work fine with the Ruby gem - the gem gets picked up and runs as expected.

The jar contains the following folders:

com
    ... <not relevant> ...
gateways
    ... <not relevant> ...
gems
    activemerchant-1.75.0
    activesupport-5.2.0.beta2
    builder-3.2.3
    concurrent-ruby-1.0.5-java
    i18n-0.9.1
    minitest-5.11.1
    nokogiri-1.8.1-java
    thread_safe-0.3.6-java
    tzinfo-1.2.4
META-INF
    MANIFEST.MF
scripts
    main.rb
specifications
    activemerchant-1.75.0.gemspec
    activesupport-5.2.0.beta2.gemspec
    builder-3.2.3.gemspec
    concurrent-ruby-1.0.5-java.gemspec
    i18n-0.9.1.gemspec
    minitest-5.11.1.gemspec
    nokogiri-1.8.1-java.gemspec
    thread_safe-0.3.6-java.gemspec
    tzinfo-1.2.4.gemspec

The jar is embedded into a Spring Boot application that uses an embedded Tomcat container. When spinning up the Spring Boot application, I get the error:

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'activeMerchantGatewayProvider': Invocation of init method failed; nested exception is org.jruby.embed.EvalFailedException: (LoadError) no such file to load -- activemerchant
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:136)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:408)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1570)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:545)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:220)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:353)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:334)
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1088)
    at com.mydomain.financial.gateway.GatewayService.lambda$init$0(GatewayService.java:39)
    at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
    at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
    at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
    at com.mydomain.financial.gateway.GatewayService.init(GatewayService.java:40)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:365)
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:310)
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:133)
    ... 81 common frames omitted
Caused by: org.jruby.embed.EvalFailedException: (LoadError) no such file to load -- activemerchant
    at org.jruby.embed.internal.EmbedEvalUnitImpl.run(EmbedEvalUnitImpl.java:131)
    at org.jruby.embed.ScriptingContainer.runUnit(ScriptingContainer.java:1307)
    at org.jruby.embed.ScriptingContainer.runScriptlet(ScriptingContainer.java:1352)
    at com.mydomain.financial.gateway.activemerchant.ActiveMerchantGatewayProvider.init(ActiveMerchantGatewayProvider.java:36)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:365)
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:310)
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:133)
    ... 108 common frames omitted
Caused by: org.jruby.exceptions.RaiseException: (LoadError) no such file to load -- activemerchant
    at org.jruby.RubyKernel.require(org/jruby/RubyKernel.java:955)
    at uri_3a_classloader_3a_.META_minus_INF.jruby_dot_home.lib.ruby.stdlib.rubygems.core_ext.kernel_require.require(uri:classloader:/META-INF/jruby.home/lib/ruby/stdlib/rubygems/core_ext/kernel_require.rb:55)
    at RUBY.<main>(classpath:/scripts/main.rb:2)

How do I get the Spring Boot embedded Tomcat/JRuby to recognise the gems in the embedded jar?

What a PITA. I found a work around, but it took a bit of effort.

The cause for the failure is that that JRuby couldn't scan the inner jar file for its gems. And it seems like this a problem with Java more than a problem with JRuby. I was also unable to find a way to scan the directories of a jar within a jar, although I could get a file from a jar in a jar. My solution was to create a list of the gems in the inner jar during it's build and retrieve that list file during the start up of the Spring Boot app.

Specifically:

In the inner jar, I changed the ScriptContainer logic as follows (GEMS_PATH = /gems):

@PostConstruct
private void init() {
    ScriptingContainer scriptingContainer = new ScriptingContainer(LocalContextScope.CONCURRENT);

    // Locate load paths and add them to the container. This doesn't work in JRuby as expected
    // once this jar is embedded into a Spring Boot uber project, so I need to do it explicitly
    // here.
    List<String> loadPaths = scriptingContainer.getLoadPaths();
    URL resource = getClass().getResource(GEMS_PATH + "/gems.list");
    if (resource == null) {
        throw new RuntimeException("Unable to find " + GEMS_PATH + "/gems.list");
    }

    try {
        log.debug("ActiveMerchant gems root: {}", resource);
        String content = IOUtils.toString(resource, "UTF-8");
        if (StringUtils.isEmpty(content)) {
            throw new RuntimeException(GEMS_PATH + "/gems.list is empty");
        }

        Stream.of(content.split(";")).forEach(gem -> {
            String gemDir = "uri:classloader:" + GEMS_PATH + "/" + gem + "/lib";
            if (!loadPaths.contains(gemDir)) {
                loadPaths.add(gemDir);
                log.debug("ActiveMerchant added gem entry to load paths: {}", gemDir);
            }
        });
    } catch (IOException e) {
        throw new RuntimeException(e);
    }

As you can see this is part of a Spring-managed component in the inner jar.

And in it's pom.xml, I added this plugin:

<plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <version>1.8</version>
    <executions>
        <execution>
            <phase>prepare-package</phase>
            <configuration>
                <target>
                    <dirset id="gems-dir" dir="${project.build.outputDirectory}/gems" includes="*"/>
                    <property name="gem-dirs" refid="gems-dir"/>
                    <echo file="${project.build.outputDirectory}/gems/gems.list">${gem-dirs}</echo>
                </target>
            </configuration>
            <goals>
                <goal>run</goal>
            </goals>
        </execution>
    </executions>
</plugin>

This creates the file /gems/gems.list in the inner jar containing all the gems that are packed into the jar. When Spring Boot is initialising the component, it finds the gems.list and uses this to build the loadpath list.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM