jmockit / jmockit1

Advanced Java library for integration testing, mocking, faking, and code coverage
Other
461 stars 239 forks source link

Using @Capturing on field producing classloader issue #707

Open bilak opened 3 years ago

bilak commented 3 years ago

We are using @Capturing on field for long time. It was working correctly until recent changes in tomcat. Here is a bug that I've reported and here is the application on which it can be tested.

2021-03-29 10:43:13.352 ERROR 143417 --- [o-auto-1-exec-1] o.a.c.c.C.[Tomcat].[localhost]           : Exception Processing /hello

java.lang.LinkageError: loader 'app' attempted duplicate class definition for org.apache.catalina.authenticator.jaspic.AuthConfigFactoryImpl. (org.apache.catalina.authenticator.jaspic.AuthConfigFactoryImpl is in unnamed module of loader 'app')
    at java.base/java.lang.ClassLoader.defineClass1(Native Method) ~[na:na]
    at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1017) ~[na:na]
    at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:174) ~[na:na]
    at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:800) ~[na:na]
    at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:698) ~[na:na]
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:621) ~[na:na]
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:579) ~[na:na]
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178) ~[na:na]
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522) ~[na:na]
    at org.apache.catalina.authenticator.AuthenticatorBase.findJaspicProvider(AuthenticatorBase.java:1416) ~[tomcat-embed-core-9.0.43.jar:9.0.43]
    at org.apache.catalina.authenticator.AuthenticatorBase.getJaspicProvider(AuthenticatorBase.java:1409) ~[tomcat-embed-core-9.0.43.jar:9.0.43]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:533) ~[tomcat-embed-core-9.0.43.jar:9.0.43]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) ~[tomcat-embed-core-9.0.43.jar:9.0.43]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.43.jar:9.0.43]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-9.0.43.jar:9.0.43]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:346) ~[tomcat-embed-core-9.0.43.jar:9.0.43]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) ~[tomcat-embed-core-9.0.43.jar:9.0.43]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.43.jar:9.0.43]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:887) ~[tomcat-embed-core-9.0.43.jar:9.0.43]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1684) ~[tomcat-embed-core-9.0.43.jar:9.0.43]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.43.jar:9.0.43]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.43.jar:9.0.43]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

Do you think this can be fixed on jmockit site?

rliesenfeld commented 3 years ago

Fix on JMockit's side? Probably not, if it would require me digging into a lot of unknown code... If a reproduceable, self-contained, test case can be shown here, I can take a look.

bilak commented 3 years ago

Here is my example application. If you change tomcat version to 9.0.41 then it works without any issue.

Saljack commented 3 years ago

Hi I spend wholed day with debuging and trying to resolve it and now I know where is the issue. But I do not know how to fix it in general. The problem happens when this is called in Startup.getClassIfLoaded:

@SuppressWarnings("ConstantConditions") Class<?>[] loadedClasses = instrumentation.getAllLoadedClasses();

I do not know why. I suspect classloaders. Here is the new code in Tomcat which is problematic:

factory = AccessController.doPrivileged(
  (PrivilegedExceptionAction<AuthConfigFactory>) () -> {
      // Load this class with the same class loader as used for
      // this class. Note that the Thread context class loader
      // should not be used since that would trigger a memory leak
      // in container environments.
      if (className.equals("org.apache.catalina.authenticator.jaspic.AuthConfigFactoryImpl")) {
          return new org.apache.catalina.authenticator.jaspic.AuthConfigFactoryImpl();
      } else {
          Class<?> clazz = Class.forName(className);
          return (AuthConfigFactory) clazz.getConstructor().newInstance();
      }
  });

and old one which works:

factory = AccessController.doPrivileged(
        new PrivilegedExceptionAction<AuthConfigFactory>() {
    @Override
    public AuthConfigFactory run() throws ReflectiveOperationException,
            IllegalArgumentException, SecurityException {
        // Load this class with the same class loader as used for
        // this class. Note that the Thread context class loader
        // should not be used since that would trigger a memory leak
        // in container environments.
        if (className.equals("org.apache.catalina.authenticator.jaspic.AuthConfigFactoryImpl")) {
            return new org.apache.catalina.authenticator.jaspic.AuthConfigFactoryImpl();
        } else {
            Class<?> clazz = Class.forName(className);
            return (AuthConfigFactory) clazz.getConstructor().newInstance();
        }
    }
});

So there is only replacement of the annonynmous class with the lambda expression.

I found two workarounds:

  1. Update JMockit CapturedType.isNotToBeCaptured and add there another non eligible class org.apache.catalina.authenticator.jaspic.AuthConfigFactoryImpl. Maybe there could be some configuration file for it where users can define there non eligable classes.
  2. The second workaround is pretty stupid. Just add a field of type AuthConfigFactoryImpl into a test with @Capturing.

    AuthConfigFactoryImpl hack;
    
    @Capturing
    private TestService testService;

From my point of view the issue is in JDK and java.lang.instrument.Instrumentation.getAllLoadedClasses()

Saljack commented 3 years ago

And here is exactly the same issue with ASM: https://stackoverflow.com/questions/47169763/asm-5-2-java-lang-linkage-error-when-instrumenting-apachhttpclient