Lambda - ClassNotFoundException

8

Aqui está a aparência do meu código e não está claro como / por executorService.submit(work::get)que isso causaria um ClassNotFoundExceptionerro na classe anônima em questão. Isso não acontece o tempo todo, mas, quando essa exceção é encontrada, ela não parece se recuperar - as solicitações subsequentes são atendidas com as mesmas exceções. Alguém sabe o que poderia estar causando isso?

Edição: Posso confirmar que todas as chamadas para este método funcionam, ou nenhuma, em uma sessão VM - não é como alguns suceder, enquanto outros falham devido à referida exceção.

Edição adicional: https://bugs.openjdk.java.net/browse/JDK-8148560 é exatamente o bug que estou enfrentando, mas esse foi fechado porque não era reproduzível e / ou o repórter não respondeu. De alguma forma, parece que o tipo anônimo resultante da expressão lambda é lixo coletado antes do executor executar a expressão, mas obviamente nem sempre. O jdk em uso é 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
    }
}

Aqui está a aparência do stacktrace de exceção (os números de linha não corresponderão, pois o código acima é apenas um protótipo):

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
fonte
Isso acontece no espaço de trabalho local ou nos ambientes prod / pre-prod?
Subir Kumar Sao
@SubirKumarSao ambientes sem produtos (não locais), mas isso pode muito bem acontecer também nos produtos.
mystarrocks
Algum motivo específico para ter o método anotado @Transactionalusando também o transactionTemplateinside?
Bond - Java Bond

Respostas:

5

É o caso do método sintético gerado pelo lambda, que não consegue encontrar a classe necessária (ou seja, TransactionCallback ) e, portanto, o erro abaixo

Causado por: java.lang.NoClassDefFoundError: com / ab / cde / ct / service / impl / IngestionService $$ Lambda $ 53 em com.ab.cde.ct.service.impl.IngestionService $$ Lambda $ 53 / 812375226.get $ Lambda (Fonte desconhecida)

O código específico que causa esse problema é

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

Para superar isso, modifique o código como abaixo

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

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

Se acima ainda não funcionar, use abaixo da solução alternativa

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);

Informe nos comentários se mais alguma informação é necessária.

PS: Não há necessidade de @Transactionalse transactionTemplateestá sendo usado, pois ambos servem essencialmente ao mesmo propósito.

Referências:

  1. Compilação Lambda aqui e aqui
  2. Métodos sintéticos em java
Obrigação - Java Bond
fonte
Obrigado - eu percebi isso neste link , mas não deveria sempre falhar? Por que funciona na maioria das vezes e falha apenas em determinadas sessões de VM?
mystarrocks
Quanto ao transactionTemplate, este método contém interações que usam os @Transactionalcomportamentos padrão , bem como aqueles que usam o objeto de modelo personalizado. Eu deixei de fora todo esse código por questões de concisão.
mystarrocks
Talvez valha a pena mencionar - parece que o TransactionCallbackitem ainda não foi carregado se esse método for o primeiro a usá-lo em uma determinada sessão da VM. Isso explica o comportamento?
mystarrocks
1
Correto, a sequência de carregamento de classe é essencial aqui porque ela falha em algum momento e nem sempre. A solução mencionada elimina isso forçando explicitamente o carregamento da classe antes da execução.
Bond - Java Bond
Isso ocorreu novamente, apesar das soluções alternativas, desfazendo a aceitação desta resposta. A julgar pelo rastreamento da pilha de erros, parece que a classe que está sendo reclamada como ausente é a classe anônima gerada a partir do próprio lambda; nenhum dos tipos mencionados no método da interface funcional, embora não tenha certeza. bugs.openjdk.java.net/browse/JDK-8148560 é basicamente o que estou experimentando.
mystarrocks
0

Eu já tive isso antes com problemas de DI e com erros de ambiguidade / problemas de configuração na resolução de pacotes. Estou supondo pela sua postagem que o erro ocorre após a inicialização bem-sucedida e exatamente na invocação dessa linha no método, e pode ser atingido no depurador.

Primeira sugestão:

Com o Gradle / Maven, verifique os pacotes dependentes para garantir que tudo tenha a versão necessária e você não está substituindo uma versão globalmente que pode afetar um pacote dependente que requer uma versão mais alta ou mais baixa dessa dependência.

Algumas frutas baixas para experimentar primeiro (se for fácil o suficiente escolher):

  • Atualize sua versão do JDK ou Java (ou veja se outro desenvolvedor da sua equipe possui uma versão diferente e eles podem reprovar o problema)
  • Atualize sua versão do spring (mesmo uma versão menor)
  • Atualize seu IDE
  • Adicione registro e verifique se o problema pode ser reproduzido em ambientes de liberação.

Em relação à injeção de dependência,

Eu recomendaria tentar algo como o seguinte .. e também é uma boa prática para injeção de dependência na primavera, pois fornece à primavera um mapa de dependência mais explícito e aumenta sua capacidade de depurar as dependências do aplicativo.

@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
    }
}

Existem algumas razões pelas quais eu recomendo:

  1. Em java, quando nenhum construtor é definido, está implícito que existe um construtor padrão e o compilador irá gerar um construtor para você. Na primavera, isso pode ser confuso e também diminuir o desempenho.
  2. A definição desse construtor diz explicitamente ao Spring: Estou contando com essas duas dependências que também configurei como Beans que serão não nulas e totalmente resolvidas na construção. Você deve inicializar essas dependências primeiro e passá-las antes que este possa ser um objeto válido.
  3. Isso ajuda na depuração, você pode definir um ponto de interrupção no construtor e validar o que está chegando.
  4. Se houver um problema com a configuração do bean para as dependências, o Spring explodirá. Os rastreamentos da pilha da mola nem sempre são os mais úteis, mas podem ajudá-lo a depurar qualquer problema em que você não esteja totalmente isolando e declarando beans dos quais depende da maneira correta.
  5. Ele permite que você elimine a possibilidade de qualquer problema com a injeção, tanto do ponto de vista do Spring Framework (difícil dizer o que acontece nos bastidores), quanto do ponto de vista lógico do aplicativo / domínio. Se ainda assim for nulo mais tarde, você poderá depurar o que foi passado no construtor - o que significa que veio nulo, foi desalocado mais tarde ou existe um problema de ambiguidade em que pode haver dois definidos e a primavera será passe no primeiro criado, mesmo que eventualmente haja vários executorServices criados.

Como essa deve ser uma definição de bean válida, desde que a classe seja incluída na varredura de componente da sua configuração, pode ser necessário definir explicitamente o bean em uma classe de configuração, especialmente se você tiver vários beans de cada tipo (o que também pode ser seu problema). )

Ex:

@Configuration
class SomeConfiguration {

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

Observe que, para a configuração, os parâmetros para o método bean serão enviados automaticamente até a primavera, assim que esses beans forem inicializados nessa configuração ou em outra configuração. Muito legal né?

Além disso, o nome do seu bean corresponde ao nome do método aqui e, se você tiver vários beans do mesmo tipo, spring pode passar como parâmetros, pode ser necessário informar ao spring qual nome do bean usar. Para fazer isso, você utilizaria a anotação @Qualifier.

Eu realmente espero que isso ajude, ou pelo menos validar a instanciação está acontecendo corretamente.

TheJeff
fonte