Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JEP 238 - Multi-Release JAR files break bytecode scanning #1797

Closed
phax opened this issue Sep 7, 2017 · 39 comments
Closed

JEP 238 - Multi-Release JAR files break bytecode scanning #1797

phax opened this issue Sep 7, 2017 · 39 comments
Assignees

Comments

@phax
Copy link

phax commented Sep 7, 2017

Hi!

Runing Jetty 9.4.6 gives a startup error with the "multi-version" JARs. An example is e.g. LOG4J2 2.9.0.

This seems to be similar to #1692

Here are the stack traces:

[2017-09-07T13:14:46,461] [ERB] [WARN ] [main] Failed startup of context o.e.j.w.WebAppContext@13cf7d52{/,file:///removed/target/webapp-classes/,UNAVAILABLE} -- org.eclipse.jetty.webapp.WebAppContext.doStart(WebAppContext.java:554)
org.eclipse.jetty.util.MultiException: Multiple exceptions
	at org.eclipse.jetty.annotations.AnnotationConfiguration.scanForAnnotations(AnnotationConfiguration.java:450) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.annotations.AnnotationConfiguration.configure(AnnotationConfiguration.java:363) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.webapp.WebAppContext.configure(WebAppContext.java:517) ~[jetty-webapp-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1458) ~[jetty-webapp-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.server.handler.ContextHandler.doStart(ContextHandler.java:785) ~[jetty-server-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.servlet.ServletContextHandler.doStart(ServletContextHandler.java:261) ~[jetty-servlet-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.webapp.WebAppContext.doStart(WebAppContext.java:545) [jetty-webapp-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:68) [jetty-util-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.util.component.ContainerLifeCycle.start(ContainerLifeCycle.java:131) [jetty-util-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.server.Server.start(Server.java:452) [jetty-server-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.util.component.ContainerLifeCycle.doStart(ContainerLifeCycle.java:105) [jetty-util-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.server.handler.AbstractHandler.doStart(AbstractHandler.java:113) [jetty-server-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.server.Server.doStart(Server.java:419) [jetty-server-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:68) [jetty-util-9.4.6.v20170531.jar:9.4.6.v20170531]
	at com.helger.photon.jetty.JettyStarter.run(JettyStarter.java:491) [classes/:?]
Caused by: org.eclipse.jetty.util.MultiException: Multiple exceptions
	at org.eclipse.jetty.annotations.AnnotationParser.parseJar(AnnotationParser.java:878) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.annotations.AnnotationParser.parse(AnnotationParser.java:837) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.annotations.AnnotationConfiguration$ParserTask.call(AnnotationConfiguration.java:159) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.annotations.AnnotationConfiguration$1.run(AnnotationConfiguration.java:462) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:673) ~[jetty-util-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:591) ~[jetty-util-9.4.6.v20170531.jar:9.4.6.v20170531]
	at java.lang.Thread.run(Thread.java:748) ~[?:1.8.0_144]
	Suppressed: java.lang.RuntimeException: Error scanning entry META-INF/versions/9/org/apache/logging/log4j/util/StackLocator.class from jar file:///D:/Entwicklung/maven-repo/org/apache/logging/log4j/log4j-api/2.9.0/log4j-api-2.9.0.jar
		at org.eclipse.jetty.annotations.AnnotationParser.parseJar(AnnotationParser.java:891) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
		at org.eclipse.jetty.annotations.AnnotationParser.parse(AnnotationParser.java:837) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
		at org.eclipse.jetty.annotations.AnnotationConfiguration$ParserTask.call(AnnotationConfiguration.java:159) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
		at org.eclipse.jetty.annotations.AnnotationConfiguration$1.run(AnnotationConfiguration.java:462) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
		at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:673) ~[jetty-util-9.4.6.v20170531.jar:9.4.6.v20170531]
		at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:591) ~[jetty-util-9.4.6.v20170531.jar:9.4.6.v20170531]
		at java.lang.Thread.run(Thread.java:748) ~[?:1.8.0_144]
	Caused by: java.lang.IllegalArgumentException
		at org.objectweb.asm.ClassReader.<init>(Unknown Source) ~[asm-5.1.jar:5.1]
		at org.objectweb.asm.ClassReader.<init>(Unknown Source) ~[asm-5.1.jar:5.1]
		at org.objectweb.asm.ClassReader.<init>(Unknown Source) ~[asm-5.1.jar:5.1]
		at org.eclipse.jetty.annotations.AnnotationParser.scanClass(AnnotationParser.java:959) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
		at org.eclipse.jetty.annotations.AnnotationParser.parseJarEntry(AnnotationParser.java:940) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
		at org.eclipse.jetty.annotations.AnnotationParser.parseJar(AnnotationParser.java:887) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
		... 6 more
Caused by: java.lang.RuntimeException: Error scanning entry META-INF/versions/9/org/apache/logging/log4j/util/ProcessIdUtil.class from jar file:///D:/Entwicklung/maven-repo/org/apache/logging/log4j/log4j-api/2.9.0/log4j-api-2.9.0.jar
	at org.eclipse.jetty.annotations.AnnotationParser.parseJar(AnnotationParser.java:891) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.annotations.AnnotationParser.parse(AnnotationParser.java:837) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.annotations.AnnotationConfiguration$ParserTask.call(AnnotationConfiguration.java:159) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.annotations.AnnotationConfiguration$1.run(AnnotationConfiguration.java:462) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:673) ~[jetty-util-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:591) ~[jetty-util-9.4.6.v20170531.jar:9.4.6.v20170531]
	at java.lang.Thread.run(Thread.java:748) ~[?:1.8.0_144]
Caused by: java.lang.IllegalArgumentException
	at org.objectweb.asm.ClassReader.<init>(Unknown Source) ~[asm-5.1.jar:5.1]
	at org.objectweb.asm.ClassReader.<init>(Unknown Source) ~[asm-5.1.jar:5.1]
	at org.objectweb.asm.ClassReader.<init>(Unknown Source) ~[asm-5.1.jar:5.1]
	at org.eclipse.jetty.annotations.AnnotationParser.scanClass(AnnotationParser.java:959) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.annotations.AnnotationParser.parseJarEntry(AnnotationParser.java:940) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.annotations.AnnotationParser.parseJar(AnnotationParser.java:887) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.annotations.AnnotationParser.parse(AnnotationParser.java:837) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.annotations.AnnotationConfiguration$ParserTask.call(AnnotationConfiguration.java:159) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.annotations.AnnotationConfiguration$1.run(AnnotationConfiguration.java:462) ~[jetty-annotations-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:673) ~[jetty-util-9.4.6.v20170531.jar:9.4.6.v20170531]
	at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:591) ~[jetty-util-9.4.6.v20170531.jar:9.4.6.v20170531]
	at java.lang.Thread.run(Thread.java:748) ~[?:1.8.0_144]
@joakime
Copy link
Contributor

joakime commented Sep 7, 2017

Multi-Release JAR Files are documented at http://openjdk.java.net/jeps/238

@joakime joakime changed the title JDK 9 - multi version JAR - log4j2 2.9.x JEP 238 - Multi-Release JAR files break bytecode scanning Sep 7, 2017
joakime added a commit that referenced this issue Sep 7, 2017
joakime added a commit that referenced this issue Sep 7, 2017
…bytecode scanning

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
@joakime
Copy link
Contributor

joakime commented Sep 7, 2017

@janbartel please see PR #1801 for a proposed fix on this.

@joakime
Copy link
Contributor

joakime commented Sep 12, 2017

Bringing this conversation back over to the issue (from PR #1801) to preserve the history of the issue better.

Relying on our ASM support version isn't sufficient for the skip of META-INF/versions, its only sufficient for knowing what level bytecode the jetty runtime can support scanning.

Its the JVM version that dictates the use of META-INF/versions/<java.vm.specification.version>/...

With Java 1.8, there is no support for META-INF/versions/8/..., so we have this hybrid approach for Jetty 9 were we skip META-INF/versions when the JVM is 1.8 (something we don't have to do in Jetty 10), and support it on JVM versions 9+.

However, to follow the JEP 238 guidelines, and the proposed increased velocity on JDK releases, we should also keep in mind that META-INF/versions/10/... is something we will start to see next year. The changes in this PR support the Jetty 9.x cases with Java 1.8 and Java 9 along with jars utilizing the JEP 238 (Multi-Release JARs) features.

Java-VM  | Jetty-Ver | META-INF/versions/9 | META-INF/versions/10
---------+-----------+---------------------+----------------------
1.8      | 9.x       | Skip                | Skip
  9      | 9.x       | Support             | Skip
 10      | 9.x       | Support             | Support
1.8      | 10.x      | (unsupported)       | (unsupported)
  9      | 10.x      | Support             | Skip
 10      | 10.x      | Support             | Support

Incidentally, we don't need ASM 6 to support Java 9 bytecode scanning.
(Both jacoco and eclipse have figured out that ASM 5 can do Java 9 bytecode scanning already).

@joakime
Copy link
Contributor

joakime commented Sep 12, 2017

The current PR #1801 does the appropriate thing for Java 1.8 on Jetty 9.4+

However, @janbartel has pointed out that the replacement / override behavior of class files in Multi-Release JARs in our bytecode scanning isn't correct on the Java 9+ use cases.

What this means ...

Lets take log4j-api-2.9.0.jar as an example.
This is a Java 1.8 majority bytecode jar, with 2 java 9 classes.

$ jar -tvf log4j-api-2.9.0.jar | grep versions
     0 Sat Aug 26 13:37:26 GMT 2017 META-INF/versions/
     0 Sat Aug 26 13:37:26 GMT 2017 META-INF/versions/9/
     0 Sat Aug 26 13:37:26 GMT 2017 META-INF/versions/9/org/
     0 Sat Aug 26 13:37:26 GMT 2017 META-INF/versions/9/org/apache/
     0 Sat Aug 26 13:37:26 GMT 2017 META-INF/versions/9/org/apache/logging/
     0 Sat Aug 26 13:37:26 GMT 2017 META-INF/versions/9/org/apache/logging/log4j/
     0 Sat Aug 26 13:37:26 GMT 2017 META-INF/versions/9/org/apache/logging/log4j/util/
   778 Sat Aug 26 13:37:10 GMT 2017 META-INF/versions/9/org/apache/logging/log4j/util/ProcessIdUtil.class
  7237 Sat Aug 26 13:37:10 GMT 2017 META-INF/versions/9/org/apache/logging/log4j/util/StackLocator.class

Of these two classes org/apache/logging/log4j/util/ProcessIdUtil.class and org/apache/logging/log4j/util/StackLocator.class there exists a Java 1.8 version in the root of the jar, and a java 9 version in the META-INF/versions/9/ base.

$ jar -tvf log4j-api-2.9.0.jar  | grep -E "(StackLocator|ProcessIdUtil)"
  1077 Sat Aug 26 13:37:16 GMT 2017 org/apache/logging/log4j/util/ProcessIdUtil.class
  1849 Sat Aug 26 13:37:16 GMT 2017 org/apache/logging/log4j/util/StackLocator$PrivateSecurityManager.class
  2039 Sat Aug 26 13:37:16 GMT 2017 org/apache/logging/log4j/util/StackLocatorUtil.class
  6336 Sat Aug 26 13:37:16 GMT 2017 org/apache/logging/log4j/util/StackLocator.class
  7237 Sat Aug 26 13:37:10 GMT 2017 META-INF/versions/9/org/apache/logging/log4j/util/StackLocator.class
   778 Sat Aug 26 13:37:10 GMT 2017 META-INF/versions/9/org/apache/logging/log4j/util/ProcessIdUtil.class

If using log4j-api-2.9.0.jar on a Java 1.8 JVM and requesting the resource org/apache/logging/log4j/util/StackLocator.class you get the root version.

If using Java 9 JVM, you get the /META-INF/versions/9/org/apache/logging/log4j/util/StackLocator.class version.

The code used to test:

private static void findResource(URLClassLoader classLoader, String name)
{
    System.out.printf("Resource[%s] = %s%n", name, classLoader.findResource(name));
}

The output seen:

java.version=1.8.0_144
java.vm.specification.version=1.8
Resource[org/apache/logging/log4j/util/StackLocator.class] = 
    jar:file:/tmp/jars/log4j-api-2.9.0.jar!/org/apache/logging/log4j/util/StackLocator.class

----
java.version=9
java.vm.specification.version=9
Resource[org/apache/logging/log4j/util/StackLocator.class] =  
    jar:file:/tmp/jars/log4j-api-2.9.0.jar!/META-INF/versions/9/org/apache/logging/log4j/util/StackLocator.class

Interestingly, if you use the URLClassLoader.findResources(String name) to find all occurrences of a resources, some magic occurs ...

The code

private static void findResources(URLClassLoader classLoader, String name) throws IOException
{
    Enumeration<URL> resources = classLoader.findResources(name);
    System.out.printf("Resources[%s]%n", name);
    while(resources.hasMoreElements())
    {
        URL url = resources.nextElement();
        System.out.printf("  = %s%n", url);
    }
    System.out.println();
}

The output

java.version=1.8.0_144
java.vm.specification.version=1.8
Resources[org/apache/logging/log4j/util/StackLocator.class]
  = jar:file:/tmp/jars/log4j-api-2.9.0.jar!/org/apache/logging/log4j/util/StackLocator.class

----
java.version=9
java.vm.specification.version=9
Resources[org/apache/logging/log4j/util/StackLocator.class]
  = jar:file:/tmp/jars/log4j-api-2.9.0.jar!/META-INF/versions/9/org/apache/logging/log4j/util/StackLocator.class

When running on Java 9, the root level resource is not returned on a findResources() call.

The Java 9+ use case for this is to have our bytecode scanning use the same override logic that JEP238 uses to not scan the root level class if the /META-INF/versions/9/... version of the same class exists.

However, a interesting wrinkle on this behavior is exposed by the log4j-api-2.9.0.jar file.

$ jar -tvf log4j-api-2.9.0.jar  | grep -E "StackLocator"
  1849 Sat Aug 26 13:37:16 GMT 2017 org/apache/logging/log4j/util/StackLocator$PrivateSecurityManager.class
  6336 Sat Aug 26 13:37:16 GMT 2017 org/apache/logging/log4j/util/StackLocator.class
  7237 Sat Aug 26 13:37:10 GMT 2017 META-INF/versions/9/org/apache/logging/log4j/util/StackLocator.class

The META-INF/versions/9/... version overrides the Java 1.8 StackLocator.class, but there is also a StackLocator$PrivateSecurityManager.class inner class. Should that inner class be skipped if the parent class exists in META-INF/versions/9/... ?

If you trust the Java 9 URLClassLoader.findResource(String name) behavior, then yes, that inner class in the root of the JAR is exposed and available to be used by the Java 9 JVM, even though the Java 9 bytecode version in META-INF/versions/9/... has no mention of it.

@joakime
Copy link
Contributor

joakime commented Sep 12, 2017

@gregw bring up questions about ignoring the root level class efficiently during a bytecode scan.

I wonder what we do when we encounter a duplicate class in our bytecode/annotation scanning now?

Eg:

  • foo.jar has a class com.blah.Special.class
  • baz.jar is also present and also has com.blah.Special.class

If we assume the scan occurs with foo.jar!/com/blah/Special.class then baz.jar!/com/blah/Special.class, what happens?

Does the second appearance of com/blah/Special.class in baz.jar get skipped? or does it replace the first appearance at foo.jar ?

If we replace, what do we do when the relationships in the class change (the baz.jar version has different interfaces it supports)

I would assume we have logic for this.

If we have this kind of safety net, then the JEP238 root replacement logic doesn't seem complicated.
If the class is /META-INF/versions/<vm>/... then it has a higher priority and always replaces a previously encountered class of the same name.
If the class is in the root / and is already present in the 'scanned already' list from a /META-INF/versions/<vm>/.. source, then we skip and don't replace it.

@joakime
Copy link
Contributor

joakime commented Sep 12, 2017

Thinking about this some more, and interesting impact of any kind of "selection" of duplicate classes during bytecode scanning is that the behavior has to match what the ClassLoader later performs.

In the foo.jar and baz.jar scenario mention in the prior message, what does the WebAppClassLoader do when there is a need to load com/blah/Special.class? Which one does it use?

Does this imply that the bytecode scanning results should be communicated to the WebAppClassLoader so that it uses the same classes as were selected by the bytecode scanning?

@joakime
Copy link
Contributor

joakime commented Sep 12, 2017

Perhaps the bytecode scanning should LOG.warn() when a duplicate class situation is detected?

@phax
Copy link
Author

phax commented Sep 12, 2017

Maybe the Log4J inner class StackLocator$PrivateSecurityManager.class is missing in the versions/9 folder by accident? Maybe this is something they missed in packaging???

@janbartel
Copy link
Contributor

There is no code already in place to try and second guess what the class loader will do with duplicate classes. The servlet spec only provides jar ordering/exclusion as a mechanism to control annotation scanning. This problem is another level of granularity - within the jar itself - and the spec has no means to deal with it.

@gregw
Copy link
Contributor

gregw commented Sep 12, 2017

Ideally we would not have to second guess the jvm's classloader and could just call a library method to access the jvm's logic... even if we have to put that code in a versioned class ourselves.

However, at this stage, the experiments that Jan and I have done indicate that the java 9 versioned JarFile class is broken... or at least does not do what the javadoc says it will do. So currently I think we will have to duplicate the logic in our own code.

However, short term, let's just apply the current PR for jetty-9.4.7, as this will allow multi-version jars to be deployed on java 8 and run correctly. We then need to do the work so that a jetty 9 can run on java 9 and that we will scan the correct version (may need to wait for a new version of ASM).

joakime added a commit that referenced this issue Sep 12, 2017
Issue #1797 - Safety checks for Java 8 and Multi-Release JARs during bytecode scanning
@gregw
Copy link
Contributor

gregw commented Sep 13, 2017

I think the handling of inner classes is indeed going to be difficult. I've posted to the servlet EG and reached out to a CDI contact to seek clarification.

The problem as I see it is that a multi versioned jar might contain something like:

org/example/Foo.class
org/example/Foo$Bar.class
META-INF/versions/9/org/example/Foo.class

It is clear that there is a java 9 version of Foo. But what is unclear is the inner class Foo$Bar? Is that only used by the base Foo version? or does the java 9 version also use the Foo$Bar inner class, but it didn't use any java 9 features, so the base version is able to be used??

So it looks like we are going to need to analyse the actual Foo class version used to see if Foo$Bar is referenced and only then scan Foo$Bar for annotations (and recursive analysis for Foo$Bar$Bob class )! It means that given the index only of a jar, it is impossible to come up with the list of classes that will be provided by that jar for a given JVM! The only alternative would be if there was an enforced convention that any versioned class would also version all of it's inner classes - which may be a reasonable assumption given that they would be compiled together, but we see nothing in the specifications that force a jar to be assembled that way.

@joakime
Copy link
Contributor

joakime commented Sep 13, 2017

Link to Servlet EG mailing list about this topic (that @gregw mentioned)

https://javaee.groups.io/g/servlet-spec/topic/annotation_scanning_with_java/6012186

@janbartel
Copy link
Contributor

Link to bug raised with Oracle: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8187528

@sbordet
Copy link
Contributor

sbordet commented Sep 14, 2017

@janbartel see comments at https://bugs.openjdk.java.net/browse/JDK-8187528, apparently it's not a bug ?

@janbartel
Copy link
Contributor

Those comments make no sense.

The javadoc for jdk-9 for JarFile.getJarEntry(String name) says:

If this JarFile is a multi-release jar file and is configured to be processed as such, then a search is performed to find and return a JarEntry that is the latest versioned entry associated with the given entry name. The returned JarEntry is the versioned entry corresponding to the given base entry name prefixed with the string "META-INF/versions/{n}/", for the largest value of n for which an entry exists. If such a versioned entry does not exist, then the JarEntry for the base entry is returned, otherwise null is returned if no entries are found. The initial value for the version n is the maximum version as returned by the method getVersion().

The observed behaviour from a test case based on the log4j-api-2.9.0.jar is that the base version only of a versioned class is returned.

Furthermore, code inspection of the implementation of JarFile.getJarEntry method shows that it searches the base first and returns it if it exists. It only ever examines versions if there is no base.

@sbordet
Copy link
Contributor

sbordet commented Sep 14, 2017

@janbartel can you comment on the OpenJDK issue tracker ?

@janbartel
Copy link
Contributor

@sbordet no, apparently not. It seems you have to be an "Author", where that is defined here: http://openjdk.java.net/bylaws#author

@sbordet
Copy link
Contributor

sbordet commented Sep 14, 2017

@janbartel I guess the next action is to write an email to a jdk9 mailing list referencing the bug.

@joakime
Copy link
Contributor

joakime commented Sep 14, 2017

Some tests of JarFile ...

$ jar -tvf log4j-api-2.9.0.jar | grep StackLocator.class
  6336 Sat Aug 26 13:37:16 GMT 2017 org/apache/logging/log4j/util/StackLocator.class
  7237 Sat Aug 26 13:37:10 GMT 2017 META-INF/versions/9/org/apache/logging/log4j/util/StackLocator.class

New Java 9 JarFile Constructor:

Using the new JarFile Constructor from Java 9, in example code like this ...

File file = new File("log4j-api-2.9.0.jar");
String javaVersion = System.getProperty("java.version");
Runtime.Version runtimeVersion = Runtime.Version.parse(javaVersion);
System.out.println("java.version=" + javaVersion);
System.out.println("Runtime.Version=" + runtimeVersion);

try (JarFile jarfile = new JarFile(file, true, ZipFile.OPEN_READ, runtimeVersion))
{
    JarEntry entry = jarfile.getJarEntry("org/apache/logging/log4j/util/StackLocator.class");
    System.out.println("JarEntry: " + entry);
    System.out.println("Compressed Size: " + entry.getCompressedSize());
    System.out.println("           Size: " + entry.getSize());
}

Output is:

java.version=9
Runtime.Version=9
JarEntry: org/apache/logging/log4j/util/StackLocator.class
Compressed Size: 2518
           Size: 7237

Which is the correct META-INF/versions/9/ sourced JarEntry

Default JarFile Constructor:

However, if we change to use default JarFile constructor ...

try (JarFile jarfile = new JarFile(file))
{
    JarEntry entry = jarfile.getJarEntry("org/apache/logging/log4j/util/StackLocator.class");
    System.out.println("JarEntry: " + entry);
    System.out.println("Compressed Size: " + entry.getCompressedSize());
    System.out.println("           Size: " + entry.getSize());
}

Then we get the root level JarEntry when running in both Java 9 and Java 8.

JarEntry: org/apache/logging/log4j/util/StackLocator.class
Compressed Size: 3257
           Size: 6336

Gotchas:

To be fair, this "get versioned entry" behavior seems to only be supported via the direct .getJarEntry(String name) or .getEntry(String name) calls.

Iterating through the entries with JarFile.entries() results in seeing both the root and versioned entries, regardless of the constructor used.

Interesting Notes:

Of note in this discovery, the META-INF/MANIFEST.MF has an entry that we should be aware of ...

$ jar -xvf log4j-api-2.9.0.jar META-INF/MANIFEST.MF
 inflated: META-INF/MANIFEST.MF
$ grep -i Multi-Release META-INF/MANIFEST.MF
Multi-Release: true

@joakime
Copy link
Contributor

joakime commented Sep 14, 2017

Our current org.eclipse.jetty.webapp.JarScanner will have to be changed to use JarFile, as JarInputStream is now insufficient to support Multi-Release JARs (JarInputStream does not support Multi-Release JARs).

Heck, we can even use Multi-Release JAR ourselves in the jetty-webapp.jar to have an alternate constructor for Java 9's JarFile 😉

@jglick
Copy link
Contributor

jglick commented Sep 15, 2017

It seems multirelease JARs are causing lots of mayhem for tools (mojohaus/animal-sniffer#32 etc.), for pretty marginal benefit: you can generally get the same effect with a small amount of reflection, or @IgnoreJRERequirement.

gregw pushed a commit that referenced this issue Oct 17, 2017
…bytecode scanning

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
gregw added a commit that referenced this issue Oct 17, 2017
Converted jetty-util to be a multi release jar
Added org/eclipse/jetty/util/MultiReleaseJarFile as botha  java8 and java9 version
deprecated jarScanner
updated AnnotationParser to use MultiReleaseJarFile
gregw added a commit that referenced this issue Oct 17, 2017
gregw added a commit that referenced this issue Oct 18, 2017
gregw added a commit that referenced this issue Oct 18, 2017
@janbartel
Copy link
Contributor

Changes made for jetty-9.3.x, 9.4.x and 10 to support multirelease jars and asm6.

janbartel added a commit that referenced this issue Oct 25, 2017
@gregw
Copy link
Contributor

gregw commented Nov 2, 2017

reopened due to NPE discovered in #1892

@gregw gregw reopened this Nov 2, 2017
gregw added a commit that referenced this issue Nov 2, 2017
@gregw gregw closed this as completed Nov 2, 2017
sbordet added a commit that referenced this issue Nov 8, 2017
Made MultiReleaseJarFile closeable and using try-with-resources in
AnnotationParser to avoid leaking file descriptors.

Made few code simplifications.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
sbordet added a commit that referenced this issue Nov 8, 2017
…ng. (#1951)

Made MultiReleaseJarFile closeable and using try-with-resources in
AnnotationParser to avoid leaking file descriptors.

Made few code simplifications.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
gregw added a commit that referenced this issue Jan 15, 2018
…ng. Fixed for java 10

Signed-off-by: Greg Wilkins <gregw@webtide.com>
@vinayapp
Copy link

Thanks. Similar issue here: https://stackoverflow.com/questions/49492583/sprint-boot-web-application-running-on-google-app-engine-throws-jetty-exceptio . So then how can we run a Spring Boot application (.war) with Gradle build on App Engine Standard.

@joakime
Copy link
Contributor

joakime commented Mar 26, 2018

@vinayapp As pointed out to you in #1892 ... search your WEB-INF/lib/*.jar files for entries that have module-info.class files or start with META-INF/versions/* and downgrade those artifacts to ones that don't use the Java 9 features.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants