Lambda - ClassNotFoundException

8

Así es como se ve mi código, y no está claro cómo / por executorService.submit(work::get)qué arrojaría un ClassNotFoundExceptionsobre la clase anónima en cuestión. No sucede todo el tiempo, pero una vez que se encuentra esta excepción, no parece recuperarse; las solicitudes posteriores se cumplen con las mismas excepciones. ¿Alguien sabe qué podría estar causando que esto ocurra?

EDITAR: Puedo confirmar que todas las llamadas a este método funcionan, o ninguna funciona, en una sesión de VM: no es como que algunas tengan éxito mientras que otras fallan debido a dicha excepción.

Edición adicional: https://bugs.openjdk.java.net/browse/JDK-8148560 es exactamente el error que estoy experimentando, pero ese estaba cerrado ya que no era reproducible y / o el periodista no respondió. De alguna manera, parece que el tipo anónimo resultante de la expresión lambda es basura recolectada antes de que el ejecutor ejecute la expresión, pero obviamente no siempre. El jdk en uso es openjdk1.8.0_221.

package com.ab.cde.ct.service.impl;

@Service
public class IngestionService {
    @Autowired private TransactionTemplate transactionTemplate;
    @Autowired private AsyncTaskExecutor executorService;

    @Transactional
    public void ingest(Data data) {
        Supplier<Optional<String>> work = () -> transactionTemplate.execute(s -> {
            // actual work on the data object, enclosed in a try/catch/finally
        });
        executorService.submit(work::get); // this is where the exception gets thrown
    }
}

Así es como se ve la excepción stacktrace (los números de línea no se corresponden ya que el código anterior es solo un prototipo):

2019-10-23 19:11:35,267|[http-apr-26001-exec-10]|[B6AC864143092042BBB4A0876BB51EB6.1]|[]|[ERROR] web.error.ErrorServlet  [line:142] org.springframework.web.util.NestedServletException: Handler processing failed; nested exception is java.lang.NoClassDefFoundError: com/ab/cde/ct/service/impl/IngestionService$$Lambda$53
org.springframework.web.util.NestedServletException: Handler processing failed; nested exception is java.lang.NoClassDefFoundError: com/ab/cde/ct/service/impl/IngestionService$$Lambda$53
    at org.springframework.web.servlet.DispatcherServlet.triggerAfterCompletionWithError(DispatcherServlet.java:1275)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:951)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:867)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:951)
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:853)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:661)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:827)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
Caused by: java.lang.NoClassDefFoundError: com/ab/cde/ct/service/impl/IngestionService$$Lambda$53
    at com.ab.cde.ct.service.impl.IngestionService$$Lambda$53/812375226.get$Lambda(Unknown Source)
    at com.ab.cde.ct.service.impl.IngestionService.ingest(IngestionService.java:264)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:317)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:183)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:96)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:260)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:94)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
    at com.sun.proxy.$Proxy252.ingest(Unknown Source)
Caused by: java.lang.ClassNotFoundException: com.ab.cde.ct.service.impl.IngestionService$$Lambda$53
    at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1364)
    at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1185)
    ... 115 more
mystarrocks
fuente
¿Esto sucede en el espacio de trabajo local o en entornos prod / pre-prod?
Subir Kumar Sao
@SubirKumarSao entornos no prod (no local), pero esto también podría suceder en prod.
mystarrocks
¿Alguna razón en particular para anotar también el método @Transactionalusando el transactionTemplateinside?
Bond - Java Bond

Respuestas:

5

Este es el caso del método sintético generado por lambda que no puede encontrar la clase requerida (es decir, TransactionCallback ) y, por lo tanto, el siguiente error

Causado por: java.lang.NoClassDefFoundError: com / ab / cde / ct / service / impl / IngestionService $$ Lambda $ 53 en com.ab.cde.ct.service.impl.IngestionService $$ Lambda $ 53 / 812375226.get $ Lambda (Fuente desconocida)

El código particular que causa este problema es

Supplier<Optional<String>> work = () -> transactionTemplate.execute(s -> {
        // actual work on the data object, enclosed in a try/catch/finally
});

Para superar esto, modifique el código de la siguiente manera

TransactionCallback<Optional<String>> callback = transactionStatus -> {
      // your processing goes here  
      return Optional.of("some value"); 
};

Supplier<Optional<String>> work = () -> transactionTemplate.execute(callback);

Si lo anterior aún no funciona, use la solución alternativa debajo

Object callback = (TransactionCallback<Optional<String>>)transactionStatus -> {
     // your processing goes here     
     return Optional.of("some value");
};

Supplier<Optional<String>> work = () -> transactionTemplate.execute((TransactionCallback<Optional<String>>)callback);

Háganos saber en los comentarios si se requiere más información.

PD: No es necesario @Transactionalsi transactionTemplatese está utilizando, ya que ambos sirven esencialmente para el mismo propósito.

Referencias

  1. Compilación lambda aquí y aquí
  2. Métodos sintéticos en java
Bond - Java Bond
fuente
Gracias. Lo supuse desde este enlace , pero ¿no debería fallar siempre si es así? ¿Por qué funciona la mayor parte del tiempo y solo falla en ciertas sesiones de VM?
mystarrocks
En cuanto al transactionTemplate, este método contiene interacciones que usan los @Transactionalcomportamientos predeterminados , así como aquellos que usan el objeto de plantilla personalizada. Dejé todo ese código por brevedad.
mystarrocks
Quizás valga la pena mencionar: parece que TransactionCallbacktodavía no está cargado si este método es el primero que lo usa en una sesión de VM determinada. ¿Eso explica el comportamiento?
mystarrocks
1
Correcto, la secuencia de carga de clases es esencial aquí por qué falla en algún momento y no siempre. La solución mencionada elimina esto al forzar explícitamente la carga de la clase antes de la ejecución.
Bond - Java Bond
Esto ocurrió nuevamente a pesar de las soluciones alternativas, por lo que deshace la aceptación de esta respuesta. A juzgar por el seguimiento de la pila de errores, parece que la clase de la que se está perdiendo es la clase anónima generada desde la propia lambda; ninguno de los tipos mencionados en el método de interfaz funcional, aunque no estoy seguro. bugs.openjdk.java.net/browse/JDK-8148560 es básicamente lo que estoy experimentando.
mystarrocks
0

He tenido esto antes con problemas de DI y con errores de ambigüedad / problemas de configuración en la resolución del paquete. Supongo por su publicación que el error ocurre después de un inicio exitoso, y exactamente al invocar esa línea en el método, y puede ser golpeado en el depurador.

Primera sugerencia:

Con Gradle / Maven, verifique los paquetes dependientes para asegurarse de que todo tenga la versión que necesita, y no anule una versión global que pueda afectar a un paquete dependiente que requiera una versión superior o inferior de esa dependencia.

Algunas frutas bajas para probar primero (si es lo suficientemente fácil de recoger):

  • Actualice su versión JDK o Java (o vea si otro desarrollador de su equipo tiene una versión diferente y puede reprobar el problema)
  • Actualice su versión de spring (incluso una versión menor)
  • Actualiza tu IDE
  • Agregue el registro y verifique si el problema puede reproducirse en entornos de lanzamiento.

En cuanto a la inyección de dependencia,

Recomendaría probar algo como lo siguiente ... y también es una buena práctica para la inyección de dependencias en primavera, ya que le da a Spring un mapa de dependencias más explícito y aumenta su capacidad para depurar las dependencias de la aplicación.

@Service
public class IngestionService {

    private TransactionTemplate transactionTemplate;
    private AsyncTaskExecutor executorService;

    public IngestionService(TransactionTemplate transactionTemplate, AsyncTaskExecutor executorService) {
         this.transactionTemplate = transactionTemplate;
         this.executorService = executorService;
    }

    @Transactional
    public void ingest(Data data) {
        Supplier<Optional<String>> work = () -> transactionTemplate.execute(s -> {
            // actual work on the data object, enclosed in a try/catch/finally
        });
        executorService.submit(work::get); // this is where the exception gets thrown
    }
}

Hay algunas razones por las que recomiendo esto:

  1. En Java, cuando no se define un constructor, se da a entender que hay un constructor predeterminado, y el compilador generará un constructor para usted. En primavera, esto puede ser confuso y también disminuir el rendimiento.
  2. La definición de este constructor le dice explícitamente a Spring: confío en estas dos dependencias que también configuré como Beans que no serán nulas y se resolverán por completo en la construcción. Debe inicializar esas dependencias primero y pasarlas antes de que pueda ser un objeto válido.
  3. Esto ayuda en la depuración, puede establecer un punto de interrupción en el constructor y validar lo que está entrando.
  4. Si hay un problema con la configuración de su bean para las dependencias, Spring explotará. Las trazas de pila de resorte no siempre son las más útiles, pero pueden ayudarlo a depurar cualquier problema en el que no esté aislando y declarando completamente los beans de los que depende de la manera correcta.
  5. Le permite eliminar la posibilidad de cualquier problema con la inyección, tanto desde el punto de vista de Spring Framework (difícil de decir qué sucede detrás de escena) como desde el punto de vista lógico de su aplicación / dominio. Si aún así es nulo más tarde, habría podido depurar lo que se pasó en el constructor, lo que significa que fue nulo, se desasignó más tarde o hay un problema de ambigüedad en el que puede haber dos definidos y spring will Pase el primero creado a pesar de que eventualmente puede haber múltiples ejecutorServicios creados.

Como esta debería ser una definición de bean válida siempre que la clase esté incluida en el escaneo de componentes de su configuración, es posible que necesite definir el bean explícitamente en una clase de configuración, especialmente si tiene múltiples beans de cada tipo (lo que también podría ser su problema )

Ex:

@Configuration
class SomeConfiguration {

    @Bean
    public IngestionService myIngestionServiceDefaultBeanNameChangeMe(TransactionTemplate transactionTemplateParamSentBySpringAutomaticallyChangeMyName, AsyncTaskExecutor executorServiceSentBySpringAutomaticallyChangeMyName) {
         return new IngestionService(transactionTemplateParamSentBySpringAutomaticallyChangeMyName, executorServiceSentBySpringAutomaticallyChangeMyName);
    }
}

Tenga en cuenta que para la configuración, los parámetros para el método de bean se enviarán en primavera automáticamente una vez que esos beans se hayan inicializado dentro de esta configuración u otra configuración. Genial eh?

Además, el nombre de su bean corresponde con el nombre del método aquí, y si tiene múltiples beans del mismo tipo que spring podría pasar como parámetros, es posible que necesite decirle a spring qué nombre de bean usar. Para hacer esto, utilizaría la anotación @Qualifier.

Realmente espero que esto ayude, o al menos validar la instanciación está sucediendo correctamente.

TheJeff
fuente