Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid resource collisions in library jars?

I'm worried about the situation when libraries Foo and Bar each expose a resource on the classpath with the same name, say properties.txt in this example.

Assuming a Maven set up and the jars are deployed with Maven, if I have this set up:

Library Foo:

$ cat Foo/src/main/resources/properties.txt
$ Foo

and Library Bar:

$ cat Bar/src/main/resources/properties.txt
$ Bar

And an App that depends on them, whose pom looks something like this - in a nutshell this just says "build a jar-with-dependencies, and depend on Foo and Bar:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-assembly-plugin</artifactId>
  <executions>
    <execution>
      <id>bundle-project-sources</id>
      <phase>package</phase>
      <goals>
        <goal>single</goal>
      </goals>
      <configuration>
        <archive>
          <manifest>
            <mainClass>me.unroll.deptest.App</mainClass>
            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
          </manifest>
          <manifestEntries>
            <Implementation-Build>${buildNumber}</Implementation-Build>
          </manifestEntries>
        </archive>
        <descriptorRefs>
          <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
      </configuration>
    </execution>
  </executions>
</plugin>

Problem is it really seems like the properties.txt file was clobbered. Let's try a jar tf:

unrollme-dev-dan:target Dan$ jar tf App-1.0-SNAPSHOT-jar-with-dependencies.jar 
META-INF/
META-INF/MANIFEST.MF
properties.txt
META-INF/maven/
META-INF/maven/me.unroll.deptest/
META-INF/maven/me.unroll.deptest/Bar/
META-INF/maven/me.unroll.deptest/Bar/pom.xml
META-INF/maven/me.unroll.deptest/Bar/pom.properties
META-INF/maven/me.unroll.deptest/Foo/
META-INF/maven/me.unroll.deptest/Foo/pom.xml
META-INF/maven/me.unroll.deptest/Foo/pom.properties
me/
me/unroll/
me/unroll/deptest/
me/unroll/deptest/App.class

So I ran a main class in App that does:

try (InputStream is = App.class.getClassLoader().getResourceAsStream("properties.txt")) {

    java.util.Scanner s = new java.util.Scanner(is);
        System.out.println("Scanner: " + s.next());

}

And the output is:

unrollme-dev-dan:target Dan$ java -jar App-1.0-SNAPSHOT-jar-with-dependencies.jar 
Scanner: Bar

Whoops, Bar won. No warnings or errors when mvn package-ing App. No warnings or errors when running that the wrong file may have been chosen, in fact it failed silently.

So I want to ask proper practice to avoid this. One, something like this should fail loudly, not softly. Two, the only solution I can think is that all resource files should be properly packaged like everything else in Java development, i.e., a library should never expose properties.txt in the "global" namespace; it should appear in a folder like me/unroll/deptest/foo just like everything else. I'm skeptical because I haven't seen any Maven example that actually does this. So what is best practice here?

like image 678
djechlin Avatar asked Feb 18 '13 17:02

djechlin


2 Answers

And what do you do in Java to avoid collisions between libraries? Packages! This is established and well understood approach. Packages work with resources as well:

com/example/foo/Foo.class
com/example/foo/properties.txt

and second library:

com/example/bar/Bar.class
com/example/bar/properties.txt

Note that properties.txt lies in different packages and thus directories in final JAR. Actually this approach is preferred because the API for retrieving such resources becomes easier:

App.class.getResourceAsStream("properties.txt"))

vs.

Bar.class.getResourceAsStream("properties.txt"))

It just works because Class.getResourceAsStream() is by default local to package of underlying class. Of course when you are inside an instance method of either Foo or Bar you simply say getClass().getResourceAsStream("properties.txt"). Moreover you can still reference both files easily, just like you reference classes:

getClass().getResourceAsStream("/com/example/foo/properties.txt");
getClass().getResourceAsStream("/com/example/bar/properties.txt");

I'm skeptical because I haven't seen any Maven example that actually does this.

Real world example: you have a Spring integration test named com.example.foo.FooTest. By default Spring expects the context file to reside under: /src/test/resources/com/example/foo/FooTest-context.xml.

like image 161
Tomasz Nurkiewicz Avatar answered Oct 13 '22 20:10

Tomasz Nurkiewicz


To complete the @Tomasz's answer with the maven code to do that :

<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <targetPath>com/example/bar/</targetPath>
        </resource>
    </resources>
</build>
like image 30
6pi Avatar answered Oct 13 '22 19:10

6pi