Spring AOP NullPointerException после успешного выполнения в течение длительного периода времени - PullRequest
1 голос
/ 20 мая 2019

Это проблема, которая поставила в тупик меня и двух моих коллег уже несколько дней.

Мы получаем исключение NullPointerException после того, как наш подпружиненный микросервис работает без перебоев в течение нескольких минутдо нескольких часов и получил от нескольких сотен до нескольких тысяч запросов.Эта проблема возникла после того, как несколько bean-компонентов были изменены на область запросов из-за изменения требований.

Классы (все объекты автоматически подключаются / создаются при загрузке микросервиса):

// New class introduced to accommodate requirements change.
@Repository("databaseUserAccountRepo")
public class DatabaseAccountUserRepoImpl implements UserLdapRepo {

    private final DatabaseAccountUserRepository databaseAccountUserRepository;

    @Autowired
    public DatabaseAccountUserRepoImpl(
        @Qualifier("databaseAccountUserRepositoryPerRequest") final DatabaseAccountUserRepository databaseAccountUserRepository
    ) {
        this.databaseAccountUserRepository = databaseAccountUserRepository;
    }

    // ...snip...
}

// ==============================================================================

// New class introduced to accommodate requirements change.
@Repository("databaseAccountUserRepository")
public interface DatabaseAccountUserRepository
        extends org.springframework.data.repository.CrudRepository {
    // ...snip...
}

// ==============================================================================

@Repository("ldapUserAccountRepo")
public class UserLdapRepoImpl implements UserLdapRepo {
    // ...snip...
}

// ==============================================================================

@Component
public class LdapUtils {

    private final UserLdapRepo userLdapRepo;

    @Autowired
    public LdapUtils(
        @Qualifier("userLdapRepoPerRequest") final UserLdapRepo userLdapRepo
    ) {
        this.userLdapRepo = userLdapRepo;
    }

    // ...snip...

    public Object myMethod(/* whatever */) {
        // ...snip...
        return userLdapRepo.someMethod(/* whatever */);
    }
}

// ==============================================================================

// I have no idea why the original developer decided to do it this way.
// It's worked fine up until now so I see no reason to change it unless
// I really need to.
public class AuthenticationContext {

    private static final ThreadLocal<String> organizationNameThreadLocal = new ThreadLocal<>();

    // ...snip...

    public static void setOrganizationName(String organizationName) {
        organizationNameThreadLocal.set(organizationName);
    }

    public static String getOrganizationName() {
        return organizationNameThreadLocal.get();
    }

    public static void clear() {
        organizationNameThreadLocal.remove();
    }

    // ...snip...
}

// ==============================================================================

public class AuthenticationContextInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        AuthenticationContext.setOrganizationName(request.getHeader("customer-id"));
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
            Object handler, Exception ex) throws Exception {
        AuthenticationContext.clear();
    }
}

Код дляобласть действия запроса:

@Configuration
// We have some aspects in our codebase, so this might be relevant.
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class ServiceConfiguration {

    // ...snip...

    @Bean 
    @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) 
    public UserLdapRepo userLdapRepoPerRequest(   
        final Map<String, String> customerIdToUserLdapRepoBeanName    
    ) {   
        final String customerId = AuthenticationContext.getOrganizationName();
        final String beanName = customerIdToUserLdapRepoBeanName.containsKey(customerId)    
            ? customerIdToUserLdapRepoBeanName.get(customerId)  
            : customerIdToUserLdapRepoBeanName.get(null);       // default 
        return (UserLdapRepo) applicationContext.getBean(beanName); 
    }

    @Bean   
    public Map<String, String> customerIdToUserLdapRepoBeanName(  
        @Value("${customers.user-accounts.datastore.use-database}") final String[] customersUsingDatabaseForAccounts  
    ) {   
        final Map<String, String> customerIdToUserLdapRepoBeanName = new HashMap<>();   

        customerIdToUserLdapRepoBeanName.put(null, "ldapUserAccountRepo");     // default option   
        if (customersUsingDatabaseForAccounts != null && customersUsingDatabaseForAccounts.length > 0) {  
            Arrays.stream(customersUsingDatabaseForAccounts)  
                .forEach(customerId ->    
                    customerIdToUserLdapRepoBeanName.put(customerId, "databaseUserAccountRepo")   
                );    
        }

        return customerIdToUserLdapRepoBeanName;   
    }

    // Given a customer ID (taken from request header), returns the
    // DatabaseAccountUserRepository instance for that particular customer.
    // The DatabaseAccountUserRepositoryProvider is NOT request-scoped.
    // The DatabaseAccountUserRepositoryProvider is basically just a utility
    // wrapper around a map of String -> DatabaseAccountUserRepository.
    @Bean   
    @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) 
    public DatabaseAccountUserRepository databaseAccountUserRepositoryPerRequest( 
        final DatabaseAccountUserRepositoryProvider databaseAccountUserRepositoryProvider 
    ) {   
        final String customerId = AuthenticationContext.getOrganizationName();  
        return databaseAccountUserRepositoryProvider.getRepositoryFor(customerId);  
    }

    // ...snip...

}

Трассировка стека:

java.lang.NullPointerException: null
    at org.springframework.aop.framework.adapter.DefaultAdvisorAdapterRegistry.getInterceptors(DefaultAdvisorAdapterRegistry.java:81)
    at org.springframework.aop.framework.DefaultAdvisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(DefaultAdvisorChainFactory.java:89)
    at org.springframework.aop.framework.AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice(AdvisedSupport.java:489)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:659)
    at com.mycompany.project.persistence.useraccount.ldap.UserLdapRepoImpl$$EnhancerBySpringCGLIB$$b6378f51.someMethod(<generated>)
    at sun.reflect.GeneratedMethodAccessor304.invoke(Unknown Source)
    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:333)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
    at org.springframework.aop.support.DelegatingIntroductionInterceptor.doProceed(DelegatingIntroductionInterceptor.java:133)
    at org.springframework.aop.support.DelegatingIntroductionInterceptor.invoke(DelegatingIntroductionInterceptor.java:121)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
    at com.sun.proxy.$Proxy209.findByFederatedInfo(Unknown Source)
    at com.mycompany.project.util.LdapUtils.myMethod(LdapUtils.java:141)

Метод, в котором выбрасывается NPE, - это парень:

//////////////////////////////////////////
// This is a method in Spring framework //
//////////////////////////////////////////
@Override
public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
    List<MethodInterceptor> interceptors = new ArrayList<MethodInterceptor>(3);
    Advice advice = advisor.getAdvice();    // <<<<<<<<<< line 81
    if (advice instanceof MethodInterceptor) {
        interceptors.add((MethodInterceptor) advice);
    }
    for (AdvisorAdapter adapter : this.adapters) {
        if (adapter.supportsAdvice(advice)) {
            interceptors.add(adapter.getInterceptor(advisor));
        }
    }
    if (interceptors.isEmpty()) {
        throw new UnknownAdviceTypeException(advisor.getAdvice());
    }
    return interceptors.toArray(new MethodInterceptor[interceptors.size()]);
}

Наиболее важные зависимости:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.10.RELEASE</version>
    <relativePath/>
</parent>

<dependencies>
    <!-- this results in spring-aop:4.3.14.RELEASE -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
</dependencies>

Заголовок запроса customer-id устанавливается нашим прокси-сервером, поэтому он должен быть доступен по запросу (мы добавили запись в журнал, чтобы убедиться, что это утверждение верно; так оно и есть).

Мы не знаем точную схему движения, которая может вызвать запуск NPE.После запуска все последующие запросы также приводят к появлению NPE.

У нас есть несколько других bean-объектов в области запросов в этом проекте;они также выбираются с помощью customer-id.Несколько из указанных объектов существовали в этом проекте в течение нескольких месяцев до этого изменения.Они не демонстрируют эту проблему.

Мы считаем, что методы userLdapRepoPerRequest() и databaseAccountUserRepositoryPerRequest() работают правильно - получение правильного customer-id, возврат правильного объекта и т. Д. ... по крайней мере, когдаметоды поражены.Это было определено добавлением регистрации в тело этих методов - сообщение журнала сразу после ввода метода, в котором записывается параметр, одно сообщение журнала, проверяющее значение customer-id, и одно сообщение журнала непосредственно перед возвратом, который записывает значение, которое являетсябыть возвращенным.Примечание. В нашей настройке ведения журналов есть идентификатор корреляции, присутствующий в каждом сообщении, поэтому мы можем отслеживать, какие сообщения соответствуют одному и тому же запросу.

Это почти так, как будто Spring теряет отслеживание нескольких своих прокси-компонентов.

У кого-нибудь есть идеи о том, что происходит или что вы хотели бы, чтобы мы попробовали?Любые выводы очень ценятся.

...