3pacccccc opened a new pull request, #23481:
URL: https://github.com/apache/kafka/pull/23481

   ## Problem
   
   `Utils.loadClass()` uses the thread context class loader (TCCL) whenever
   it is non-null. If that loader cannot find a configured class, the
   lookup fails even when Kafka's defining class loader can load it.
   
   For example, this string-valued producer configuration:
   
   ```properties
   key.serializer=org.apache.kafka.common.serialization.StringSerializer
   ```
   
   can fail before the producer connects to a broker:
   
   ```text
   ConfigException: Invalid value
   org.apache.kafka.common.serialization.StringSerializer for configuration
   key.serializer: Class
   org.apache.kafka.common.serialization.StringSerializer
   could not be found.
   ```
   
   ## Real-world trigger
   
   The issue was observed when the first `KafkaTemplate.send()` call ran in
   an asynchronous completion callback:
   
   ```java
   CompletableFuture
       .supplyAsync(() -> handleMessage(message), applicationExecutor)
       .whenCompleteAsync((result, exception) ->
           kafkaTemplate.send(topic, key, serialize(result)))
       .exceptionally(exception -> null);
   ```
   
   The first stage uses an explicit application executor. However,
   `whenCompleteAsync()` has no executor argument, so its callback uses the
   default asynchronous execution facility, normally
   `ForkJoinPool.commonPool()`.
   
   The producer is created lazily by `KafkaTemplate`. If the callback makes
   the first send, Kafka parses the serializer class name on a common-pool
   worker. In a Spring Boot executable JAR, that worker's TCCL may be
   unable to see classes loaded from `BOOT-INF/lib`, while Kafka's defining
   class loader can load `StringSerializer`.
   
   The resulting path is:
   
   ```text
   CompletableFuture.whenCompleteAsync
     -> ForkJoinPool.commonPool worker
     -> KafkaTemplate.send
     -> DefaultKafkaProducerFactory.createProducer
     -> KafkaProducer constructor
     -> ConfigDef.parseType(Type.CLASS)
     -> Utils.loadClass
     -> ClassNotFoundException
   ```
   
   The trailing `exceptionally()` may consume the exception, but it does
   not cause the class-loading failure.
   
   The original failure was observed with `kafka-clients` 3.0.0. The
   class-loading behavior and the minimal reproduction are also present on
   Kafka trunk at commit `56baca2b65251318a17154034a638b6f37654c26`.
   
   ## Minimal reproduction
   
   The regression is isolated without Spring Boot or a broker:
   
   1. Confirm that Kafka's defining class loader can load
      `StringSerializer`.
   2. Install a non-null TCCL that deliberately rejects only the
      `StringSerializer` class name.
   3. Parse that class name through `ConfigDef.parseType(..., Type.CLASS)`.
   4. Restore the original TCCL in a `finally` block.
   
   ```java
   ClassLoader original = Thread.currentThread().getContextClassLoader();
   String className = StringSerializer.class.getName();
   
   ClassLoader rejectingLoader = new ClassLoader(original) {     @Override
   public Class<?> loadClass(String name) throws ClassNotFoundException {
   if (className.equals(name)) {             throw new
   ClassNotFoundException(name);         }         return
   super.loadClass(name);     } };
   
   try {
       Thread.currentThread().setContextClassLoader(rejectingLoader);
       ConfigDef.parseType("key.serializer", className,
   ConfigDef.Type.CLASS);
   } finally {
       Thread.currentThread().setContextClassLoader(original);
   }
   ```
   
   Before this change, step 3 throws `ConfigException`, wrapping a
   `ClassNotFoundException`. The class remains loadable through
   `Utils.class.getClassLoader()`.
   
   ## Change
   
   Update `Utils.loadClass()` to:
   
   1. Try the TCCL first, preserving its precedence and alias support.
   2. If the initial lookup throws `ClassNotFoundException`, retry with
      Kafka's defining class loader when it is non-null and different.
   3. Initialize the resolved class using the loader that found it.
   
   If both lookups fail, the second exception retains the first as a
   suppressed exception. Initialization, linkage, and type-check failures
   do not trigger the fallback.
   
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to