成功运行较长时间后,Spring AOP NullPointerException

时间:2019-05-20 19:04:50

标签: spring-boot nullpointerexception spring-aop

这个问题困扰着我自己和我的两个同事几天了。

在我们的春季启动微服务运行了几分钟到几小时,并且收到了几百到几千个请求后,我们收到了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由我们的代理设置,因此它必须在请求中可用(我们添加了日志记录以验证此语句为true;是)。

我们不知道可能导致NPE开始被触发的确切流量模式。一旦触发,所有后续请求也会产生NPE。

在此项目中,我们还有其他几个请求范围的Bean。也可以使用customer-id选择它们。在进行更改之前的几个月中,该项目中已经存在多个上述对象。他们没有出现这个问题。

我们认为userLdapRepoPerRequest()databaseAccountUserRepositoryPerRequest()方法正常运行-至少在命中方法时,接收正确的customer-id,返回正确的对象等……。这是通过在这些方法的主体中添加日志记录来确定的-进入记录参数的方法后立即显示一条日志消息,验证customer-id的一条日志消息,并在返回之前立即记录一条记录该值的日志消息这将被退回。注意:我们的日志记录设置在每条消息上都有一个关联ID,因此我们可以跟踪对应同一请求的消息。

Spring似乎正在丢失一些代理的豆子。

任何人都对正在发生的事情有任何想法,或者您希望我们尝试什么?任何线索都非常感谢。

0 个答案:

没有答案