2

I have a spring-boot project with Java 11. Project has dependency on redis, so I included spring-boot-starter-data-redis dependency in pom.xml. The spring-data-redis jar has a Class named JedisClientUtils which has a default access modifier at class level.

When I run this project using mvn spring-boot:run, I get an error NoClassDefFound error for JedisClientUtils class. While debugging the issue I found that the same project runs successfully while using Java 8. My pom.xml has plugin as follows:

<build>
        <finalName>${war.name}</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
            <source>11</source>
            <target>11</target>
        </configuration>
                <dependencies>
                    <dependency>
                        <!-- update compiler plugin dependency on ASM for Java 11 compatibility -->
                        <groupId>org.ow2.asm</groupId>
                        <artifactId>asm</artifactId>
                        <version>6.2</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

Is there anything else required to build a java 11 project with default access classes Logs for reference :

java.lang.reflect.InvocationTargetException at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0 (Native Method) at jdk.internal.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62) at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke (Method.java:566) at org.springframework.boot.maven.AbstractRunMojo$LaunchRunner.run (AbstractRunMojo.java:558) at java.lang.Thread.run (Thread.java:834) Caused by: java.lang.NoClassDefFoundError: Could not initialize class org.springframework.data.redis.connection.jedis.JedisClientUtils at org.springframework.data.redis.connection.jedis.JedisConnection.isQueueing (JedisConnection.java:339)

spring-boot-data-redis verison : 2.1.1.RELEASE jedis.version : 2.9.0

puneet_0303
  • 197
  • 1
  • 11
  • You might need to update to a compatible version. `spring-boot-starter-data-redis` and `redis.clients.jedis` must be upgraded – Mebin Joe Jul 10 '19 at 09:30
  • Possible duplicate of [Why Getting NoClassDefFound error for JedisConnection when using Spring Redis](https://stackoverflow.com/questions/33128318/why-getting-noclassdeffound-error-for-jedisconnection-when-using-spring-redis) – ValerioMC Jul 10 '19 at 09:37
  • Also please add what versions of Spring Boot you are using. – Darren Forsythe Jul 10 '19 at 09:39
  • Already checked the possible duplicate. But did not work..with most of the research I can infer that issue is related to java 11 packaging – puneet_0303 Jul 10 '19 at 09:56

2 Answers2

1

I had a similar issue, but there are not enough details in your question to be sure it was the same.

However, if this can be helpful, here is what I had:
ForkJoinPool implementation was changed somewhere after java 8 and it can affect some of your code. In general, when a new thread in Java is created, it either takes specific classloader in a constructor or reuses parent's one. ForkJoinPool of Java 8 version didn't specify class loader for its threads (i.e. parent's one was used), but ForkJoinPool of Java 11 specifies system class loader. Now, when you try to load a class by name somewhere in a thread of ForkJoinPool without specifying correct class loader (like JedisClientUtils does), it will use system class loader by default which is not aware of your application classes. CompletableFuture and parallel Streams use ForkJoinPool thus are potential points for this issue. Here is a failing sample:

@Repository
public interface RedisRepo extends CrudRepository<Statistic, Long> {
}

@SpringBootApplication
public class Start {
    ...
    public static void main(String[] args) {
        new SpringApplication(Start.class).run(args);
        CompletableFuture.runAsync(() -> {
            redisRepo.deleteAll();
        });
    }
}

Possible fix:

public static void main(String[] args) {
    new SpringApplication(Start.class).run(args);
    CompletableFuture.runAsync(() -> {
        ClassLoader contextClassLoaderBackup = Thread.currentThread().getContextClassLoader();
        try {
            Thread.currentThread().setContextClassLoader(Start.class.getClassLoader());
            redisRepo.deleteAll();
        } finally {
            Thread.currentThread().setContextClassLoader(contextClassLoaderBackup);
        }
    });
}
niebaraka
  • 101
  • 1
  • 3
1

You COULD configure the ForkJoinPool's thread factory to set the classloader you expect application wide instead of setting the context Classloader every time a task is run. See CompletableFuture / ForkJoinPool Set Class Loader (The non dirty answer).

One caveat is that this breaking change seems to have been prompted by the assertion that one cannot rely on the common pool initializing threads at a particular point in the application's lifecycle (eg. common pool threads could be initialized before the classloader initializing your spring application is created).

For Spring apps specifically it seems prudent to configure a custom ForkJoinPool and submit tasks to that instead of relying on the commonPool given the breaking change introduced in Java 9 and the lack of a guarantee that the common pool will be initialized with the classloader your spring application needs even with a custom factory. Something like:

@Bean
public ForkJoinPool myForkJoinPool() {
    int threads = Runtime.getRuntime().availableProcessors();
    return new ForkJoinPool(threads, makeFactory("MyFactory"), null, false);
}

private ForkJoinWorkerThreadFactory makeFactory(String prefix) {
    return pool -> {
        final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
        worker.setName(prefix + worker.getPoolIndex());
        worker.setContextClassLoader(Application.class.getClassLoader());
        return worker;
    };
}

Where your pool is injected into components performing parallel operations, and is invoked:

myForkJoinPool.submit(myTask)
  • This worked for me, although PMD complained about `Application.class.getClassLoader()`. I changed it as recommended and it still works for me because the current thread is a spring thread. See here: https://stackoverflow.com/questions/34787419/pmd-rule-use-proper-class-loader-explaination – aradil Dec 10 '19 at 20:33