Note: I have created a jira-issue + PR for hibernate: https://hibernate.atlassian.net/browse/HHH-13794
I have also created an issue at the jakarta jaxb-api github: https://github.com/eclipse-ee4j/jaxb-api/issues/121

While trying to set up the hibernate JPA metamodel generator for a hobby project it failed with very little explanation.

[INFO] diagnostic: Note: Hibernate JPA 2 Static-Metamodel Generator 5.4.10.Final
[WARNING] diagnostic: warning: Unable to parse persistence.xml: Unable to perform unmarshalling at line number 0 and column 0. Message: null

For context, I am invoking the JPA metamodel generator through the maven processor plugin during the generate-resources phase of my maven build. The metamodel generator uses JAXB to parse the persistence.xml and that’s where it failed.

There was no stack trace or anything and no easy way to get more information so eventually I cloned hibernate from Github and git grep-ed through the code to find where this logging statement was generated. It turned out that the metamodelgen swallows the exception that happened in XmlParserHelper:

catch ( JAXBException e ) {
	StringBuilder builder = new StringBuilder();
	builder.append( "Unable to perform unmarshalling at line number " );
	builder.append( handler.getLineNumber() );
	builder.append( " and column " );
	builder.append( handler.getColumnNumber() );
	builder.append( ". Message: " );
	builder.append( handler.getMessage() );
	throw new XmlParsingException( builder.toString(), e );
}

As you can see, the exception itself doesn’t get added to the message of the created XmlParsingException. (Only the message gets logged). I assume that’s because it’s presumed that the exception will be caused by a parser error. In this case, however, the exception originated during initialisation of the JAXB context, before any parsing was even being done, as I found out after adding the exception to the logmessage:

Caused by: javax.xml.bind.JAXBException: ClassCastException: attempting to cast jar:file:/home/haster/.m2/repository/javax/xml/bind/jaxb-api/2.3.1/jaxb-api-2.3.1.jar!/javax/xml/bind/JAXBContext.class to jar:file:/home/haster/.m2/repository/javax/xml/bind/jaxb-api/2.3.1/jaxb-api-2.3.1.jar!/javax/xml/bind/JAXBContext.class.  Please make sure that you are specifying the proper ClassLoader.    
  	at javax.xml.bind.ContextFinder.handleClassCastException(ContextFinder.java:157)
  	at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:300)
  	at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:286)
  	at javax.xml.bind.ContextFinder.find(ContextFinder.java:409)
  	at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:721)
  	at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:662)
  	at org.hibernate.jpamodelgen.util.xml.XmlParserHelper.getJaxbRoot(XmlParserHelper.java:122)
  	... 39 more

At first, I was confused by the word ClassCastException and I was trying to find out how we could have a classcast while trying to cast the exact same class from the exact same artifact at the exact same location to itself. It was only after a while that I realised it wasn’t an actual ClassCastException, it was a JAXBException masquerading as one!

What happens is that the JAXB ContextFinder detects an upcoming classcast and pre-emptively throws a JAXBException with a specific and somewhat confusing message:

	Object context = m.invoke(obj, classes, properties);
	if (!(context instanceof JAXBContext)) {
		// the cast would fail, so generate an exception with a nice message
		throw handleClassCastException(context.getClass(), JAXBContext.class);
	}
	return (JAXBContext) context;

//...

private static JAXBException handleClassCastException(Class originalType, Class targetType) {
  final URL targetTypeURL = which(targetType);

  return new JAXBException(Messages.format(Messages.ILLEGAL_CAST,
        // we don't care where the impl class is, we want to know where JAXBContext lives in the impl
        // class' ClassLoader
        
  getClassClassLoader(originalType).getResource("javax/xml/bind/JAXBContext.class"),
        targetTypeURL));
}

As you can see, JAXB tries to construct a context object and then checks if it is a instance of JAXBContext before returning. If it isn’t, the exception’s message explicitly ignores the actual implementation of the context in favor of logging the location of the actual and expected JAXBContext interface class. It does this because it assumed that if the actual context is not an instance of the expected JAXBContext, that is probably due to there being two versions of the JAXBContext class on the classpath.

This is not a bad assumption in general and I would usually expect to see such a class cast error involving two different artifacts (or different versions of the same artifact) that both contain the JAXBContext class.

In this case, that assumption turned out to be correct as well, but since it involved the exact same artifact the exception’s message threw me for a loop and I spent some time chasing down ghosts in my dependency tree.

After building modified versions of both the Hibernate metamodel generator and the javax JAXB api I managed to gather more information. I wrote a bit of code that looped through the class hierarchy of the created object and compared the result of an instanceof check with the result of the same check against the JAXBContext class that the ContextFinder wanted to use:

private static String getInstanceOfChecks(Object object, Class clazz) {
    String formatString = "object %s instanceof type %s: %b ; \n instanceof type %s : %b \n\n";

    Deque<Class> classQueue = new LinkedList<>();
    classQueue.add(object.getClass());

    StringBuffer buffer = new StringBuffer();

    while (!classQueue.isEmpty()) {
        Class curr = classQueue.pop();
        String whichClassString = which(curr).toString();
        String instanceOfCheckThisClass = String.format(formatString, object, whichClassString, (curr.isAssignableFrom(object.getClass())), which(clazz),
                (clazz.isAssignableFrom(object.getClass())));

        buffer.append(instanceOfCheckThisClass).append("\n");

        Class superClass = curr.getSuperclass();
        if (superClass != null)
            classQueue.add(superClass);

        for (Class iface : curr.getInterfaces()) {
            classQueue.addFirst(iface);
        }
    }
    return buffer.toString();
}

This gave me the final relevant clue:

object jar:file:/home/haster/.m2/repository/org/glassfish/jaxb/jaxb-runtime/2.3.1/jaxb-runtime-2.3.1.jar!/com/sun/xml/bind/v2/runtime/JAXBContextImpl.class Build-Id: 2.3.1
instanceof type jar:file:/home/haster/.m2/repository/javax/xml/bind/jaxb-api/2.3.2-SNAPSHOT/jaxb-api-2.3.2-SNAPSHOT.jar!/javax/xml/bind/JAXBContext.class: true ;
instanceof type jar:file:/home/haster/.m2/repository/javax/xml/bind/jaxb-api/2.3.2-SNAPSHOT/jaxb-api-2.3.2-SNAPSHOT.jar!/javax/xml/bind/JAXBContext.class : false

I was confused. From the logging, the JAXBContextImpl class object came from the umodified jaxb-runtime version 2.3.1. That class implemented, among others, the JAXBContext interface whose class object came from the modified jaxb-api version 2.3.2. As expected, the generated object was an instanceof this JAXBContext.

However, an instanceof check against JAXBContext directly (JAXBContext.class.isAssignableFrom(object)), not from the generated object’s class hierarchy, failed.

Wait, what? The created JAXBContextImpl both is and is not an instance of the JAXBcontext from the modified JAXB api?
b && !b ?

After thinking on it I came to the conclusion that the only way this could be true is if there were two separate instances of the class-object of the same JAXBContext class file. Which in turn can only be true if there are two separate classloaders involved.

Armed with this hypotheses I debugged my way through the hibernate metamodel generator. Now that I knew what I was looking for it was easy to find. The way the metamodel generator calls JAXB to try and parse the persistence.xml does in fact involve a potentially separate classloader.

In XmlParserHelper line 122, in the getJaxbRoot-method, it calls the newInstance factory method of JAXBContext without a classloader parameter. This means that eventually the ContextFinder will use the context-classloader to load the JAXB context. and this turns out to not be the same classloader as what loaded the JAXBContext-class for the current thread.

The context classloader is typically further up the chain of classloaders. Since the currently loaded JAXBContext class was loaded by a classloader further down the chain, it was not available to the context classloader. When the context classloader then tried to load the JAXBContextImpl class, it also had to load the JAXBContext class and that’s how we ended up with two instances of the same class object.

This is similar to what goes wrong in https://github.com/eclipse-ee4j/jaxb-api/issues/99, though there the end result is the inability to find the jaxb implementation, where in this case we do find the implementation but there is a classloading mismatch where we have two separate versions of the same class.

If we change line 122 in the XmlParserHelper to pass the current classloader along this will fix the problem, though since that exact method is not available as part of the API, we have to pass the package containing the ObjectFactory class, instead of the actual ObjectFactory class object, so it is slightly less pretty.

Leave a Reply

Your email address will not be published. Required fields are marked *