这个问题困扰着我自己和我的两个同事几天了。
在我们的春季启动微服务运行了几分钟到几小时,并且收到了几百到几千个请求后,我们收到了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似乎正在丢失一些代理的豆子。
任何人都对正在发生的事情有任何想法,或者您希望我们尝试什么?任何线索都非常感谢。