Hello,
We are using spring-batch in our project, but however we have no dependency to "org.springframework.orm.jpa.JpaTransactionManager".
In BasicBatchConfigurer class, JpaTransactionManager used directly. I think it should be delegated to inner class, for avoiding hard dependency on orm.
"org.springframework.validation.beanvalidation.LocalValidatorFactoryBean.HibernateValidatorDelegate" an example usage for this.
Thank you.
Comment From: wilkinsona
While JpaTransactionManager is used, it doesn't appear anywhere in the signature of the class or its methods. It's only used if you have an EntityManagerFactory bean available in the context. The only way that I can see that the problem would occur is if you are using JPA but you aren't using Spring Framework's JPA support.
To be sure that we're trying to solve the problem that you're seeing can you please provide a small sample that reproduces the problem?
Comment From: spring-projects-issues
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Comment From: okohub
@wilkinsona you can find a simple sample in this repo: https://github.com/okohub/jpaproblem
Comment From: snicoll
That sample doesn't show anything you've described here. And the error hasn't a single bit of JPA either (I suppose you guessed that by looking at the code...)
Spring Batch works by storing jobs details in a database so you need to provide one. I've added h2 and your sample started.
Having said that, the documentation should be updated to make that a bit more explicit so I've created #8271
Comment From: wilkinsona
The sample reproduces the problem once you add H2 or similar
Comment From: wilkinsona
For reasons that I don't yet fully understand, it fails due to the introduction of a private method that uses AbstractPlatformTransactionManager as its return type. Changing back to PlatformTransactionManager gets rid of the failure. I'd like to try and understand why before making that change.
Comment From: snicoll
To be clear, that samples doesn't fail for me once I add h2
Comment From: okohub
@snicoll I committed h2 dependency to sample repo, but problem still occurs. Could you show how you add h2?
Comment From: cemo
@wilkinsona I am curious that why it did not throw exception before too. The only explanation for me now, in terms of JVM perspective, having a class reference which is not available in classpath is fine if it is only used in method body. I would expect raising an exception but seems legit.
Comment From: wilkinsona
having a class reference which is not available in classpath is fine if it is only used in method body
That was my expectation too. My understanding is that as long as a type doesn't appear in the signature of a class (method return type, method argument, etc) then it will only be loaded when the code that references the type is executed. This problem shows that's not the case though.
Comment From: wilkinsona
This is the native stack when it tries to load JpaTransactionManager:
2555 Thread_14613559
+ 2555 thread_start (in libsystem_pthread.dylib) + 13 [0x7fff98d993ed]
+ 2555 _pthread_start (in libsystem_pthread.dylib) + 176 [0x7fff98d9bfd7]
+ 2555 _pthread_body (in libsystem_pthread.dylib) + 131 [0x7fff98d9c05a]
+ 2555 JavaMain (in java) + 2500 [0x103836931]
+ 2555 jni_CallStaticVoidMethod (in libjvm.dylib) + 349 [0x104d1d151]
+ 2555 jni_invoke_static(JNIEnv_*, JavaValue*, _jobject*, JNICallType, _jmethodID*, JNI_ArgumentPusher*, Thread*) (in libjvm.dylib) + 447 [0x104d243da]
+ 2555 JavaCalls::call_helper(JavaValue*, methodHandle*, JavaCallArguments*, Thread*) (in libjvm.dylib) + 1710 [0x104ced5e2]
+ 2555 ??? (in <unknown binary>) [0x105aa47a7]
+ 2555 ??? (in <unknown binary>) [0x105ad6bf9]
+ 2555 InterpreterRuntime::_new(JavaThread*, ConstantPool*, int) (in libjvm.dylib) + 94 [0x104ce9050]
+ 2555 InstanceKlass::initialize(Thread*) (in libjvm.dylib) + 61 [0x104cc1aeb]
+ 2555 InstanceKlass::initialize_impl(instanceKlassHandle, Thread*) (in libjvm.dylib) + 37 [0x104cc13c5]
+ 2555 InstanceKlass::link_class(Thread*) (in libjvm.dylib) + 59 [0x104cc138d]
+ 2555 InstanceKlass::link_class_impl(instanceKlassHandle, bool, Thread*) (in libjvm.dylib) + 929 [0x104cc102b]
+ 2555 Verifier::verify(instanceKlassHandle, Verifier::Mode, bool, Thread*) (in libjvm.dylib) + 272 [0x104fa6cf2]
+ 2555 ClassVerifier::verify_class(Thread*) (in libjvm.dylib) + 190 [0x104fa6096]
+ 2555 ClassVerifier::verify_method(methodHandle, Thread*) (in libjvm.dylib) + 2991 [0x104f8eee7]
+ 2555 ClassVerifier::verify_return_value(VerificationType, VerificationType, unsigned short, StackMapFrame*, Thread*) (in libjvm.dylib) + 345 [0x104f88261]
+ 2555 VerificationType::is_assignable_from(VerificationType const&, ClassVerifier*, bool, Thread*) const (in libjvm.dylib) + 319 [0x104f0848b]
+ 2555 VerificationType::is_reference_assignable_from(VerificationType const&, ClassVerifier*, bool, Thread*) const (in libjvm.dylib) + 450 [0x104f86cf6]
+ 2555 SystemDictionary::resolve_or_fail(Symbol*, Handle, Handle, bool, Thread*) (in libjvm.dylib) + 49 [0x104f3c023]
+ 2555 SystemDictionary::resolve_or_null(Symbol*, Handle, Handle, Thread*) (in libjvm.dylib) + 262 [0x104f3b970]
+ 2555 SystemDictionary::resolve_instance_class_or_null(Symbol*, Handle, Handle, Thread*) (in libjvm.dylib) + 1167 [0x104f3aeed]
+ 2555 SystemDictionary::load_instance_class(Symbol*, Handle, Thread*) (in libjvm.dylib) + 728 [0x104f3b754]
It looks to me like ClassVerifier is attempting to verify that JpaTransactionManager is an AbstractPlatformTransactionManager. For some reason that causes it to have to load JpaTransactionManager. When the return type is PlatformTransactionManager, so it has to verify that JpaTransactionManager is a PlatformTransactionManager, JpaTransactionManager isn't loaded for some reason and the failure does not occur.
I can also prevent the failure from occurring by running with -noverify so the difference in behaviour definitely appears to be related to the verifier. I still can't explain why @snicoll cannot reproduce the problem.
Comment From: cemo
@wilkinsona great work. Probably @snicoll has spent too much time on optimization issues and he might has -noverify in his environment by default?
Comment From: snicoll
@wilkinsona I can answer to that easy now thanks to your great analysis:
➜ echo $MAVEN_OPTS
-noverify -XX:CICompilerCount=1 -XX:TieredStopAtLevel=1
Comment From: wilkinsona
Ok, I understand the difference in behaviour now. Interfaces are given special treatment which means that JpaTransactionManager isn't loaded when createAppropriateTransactionManager returns an interface (PlatformTransactionManager in this case).
Comment From: cemo
🥇
Comment From: wilkinsona
And just to get to the bottom of the rabbit hole, the JLS states that "for assignments, interfaces are treated like Object" so this is working as specified. Lastly, here's an example that illustrates the behaviour:
package com.example;
import java.net.URLClassLoader;
public class VerificationQuirks {
public static void main(String[] args) throws ClassNotFoundException {
URLClassLoader someFooHidingClassLoader = new URLClassLoader(
((URLClassLoader) VerificationQuirks.class.getClassLoader()).getURLs(),
null) {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (name.equals("com.example.VerificationQuirks$SomeFoo")) {
throw new ClassNotFoundException(name);
}
return super.findClass(name);
}
};
// Works
System.out.println(Class.forName("com.example.VerificationQuirks$ReturnsFoo",
true, someFooHidingClassLoader));
// Fails
System.out.println(
Class.forName("com.example.VerificationQuirks$ReturnsAbstractFoo", true,
someFooHidingClassLoader));
}
public static class ReturnsFoo {
public Foo getFoo() {
if (ClassUtils.isPresent("com.example.VerificationQuirks$SomeFoo")) {
return new SomeFoo();
}
return null;
}
}
public static class ReturnsAbstractFoo {
public AbstractFoo getFoo() {
if (ClassUtils.isPresent("com.example.VerificationQuirks$SomeFoo")) {
return new SomeFoo();
}
return null;
}
}
public static interface Foo {
}
public static class AbstractFoo implements Foo {
}
public static class SomeFoo extends AbstractFoo {
}
public static class ClassUtils {
public static boolean isPresent(String name) {
try {
Class.forName(name);
return true;
}
catch (ClassNotFoundException ex) {
return false;
}
}
}
}