Кажущееся состояние гонки Spring Boot, приводящее к дублированию регистрации springSecurityFilterChain

У меня есть REST-полный веб-сервис, реализованный с помощью Spring Boot 1.2.0-RELEASE, который иногда выдает следующее исключение при запуске.

03-Feb-2015 11:42:23.697 SEVERE [localhost-startStop-1] org.apache.catalina.core.ContainerBase.addChildInternal ContainerBase.addChild: start: 
 org.apache.catalina.LifecycleException: Failed to start component [StandardEngine[Catalina].StandardHost[localhost].StandardContext[]]
        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:154)
        at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:725)
...
Caused by: java.lang.IllegalStateException: Duplicate Filter registration for 'springSecurityFilterChain'. Check to ensure the Filter is only configured once.
        at org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer.registerFilter(AbstractSecurityWebApplicationInitializer.java:215)
        at org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer.insertSpringSecurityFilterChain(AbstractSecurityWebApplicationInitializer.java:147)
...

Когда я говорю «время от времени», я имею в виду, что простой перезапуск сервера Tomcat (версия 8.0.17) приведет либо к этому исключению, либо к успешной загрузке без проблем.

Это приложение Servlet 3.0, созданное на основе Spring Boot, поэтому у нас нет традиционного файла web.xml. Вместо этого мы инициализируем наш сервлет с использованием Java.

package com.v.dw.webservice;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;

public class WebXml extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(ApplicationConfig.class);
    }
}

Мы также используемmvn spring-boot:run команда во время разработки, и это состояние гонки еще не появилось при запуске таким образом. Корень нашей конфигурации и основной метод, используемый maven, находятся в одном классе:

package com.v.dw.webservice;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.ManagementSecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;

@SpringBootApplication
@EnableAutoConfiguration(exclude = {ManagementSecurityAutoConfiguration.class, SecurityAutoConfiguration.class})
public class ApplicationConfig {

    public static void main(String[] args) {
        SpringApplication.run(ApplicationConfig.class, args);
    }

    @Value("${info.build.version}")
    private String apiVersion;

    @Bean
    @Primary
    @ConfigurationProperties(prefix="datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

}

Я попытался упростить нашу логику аутентификации, чтобы использовать для тестирования собственного провайдера аутентификации в памяти. Насколько я могу судить, это единственный пользовательский поставщик аутентификации на пути к классам, и мы не импортируем никакие классы конфигурации вне корневого пакета приложения.

К сожалению, выходные данные журналов, предоставляемые Spring и Tomcat, не помогают обеспечить контекст вокруг ошибки, поэтому я попытался получить источник AbstractSecurityWebApplictionInitializer отсюда:

https://raw.githubusercontent.com/spring-projects/spring-security/rb3.2.5.RELEASE/web/src/main/java/org/springframework/security/web/context/AbstractSecurityWebApplicationInitializer.java

И я изменилregisterFilter(...) метод в попытке генерировать некоторые полезные выходные данные отладки, добавивSystem.out звонки.

private final void registerFilter(ServletContext servletContext, boolean insertBeforeOtherFilters, String filterName, Filter filter) {
    System.out.println(">>>>>> Registering filter '" + filterName + "' with: " + filter.getClass().toString());
    Dynamic registration = servletContext.addFilter(filterName, filter);
    if(registration == null) {
        System.out.println(">>>>>> Existing filter '" + filterName + "' as: " + servletContext.getFilterRegistration(filterName).getClassName());
        throw new IllegalStateException("Duplicate Filter registration for '" + filterName +"'. Check to ensure the Filter is only configured once.");
    }
    registration.setAsyncSupported(isAsyncSecuritySupported());
    EnumSet<DispatcherType> dispatcherTypes = getSecurityDispatcherTypes();
    registration.addMappingForUrlPatterns(dispatcherTypes, !insertBeforeOtherFilters, "/*");
}

Когда это терпит неудачу, выходные данные отладки генерируются только один раз прямо перед исключением. Это указывает наregisterFilter(...) метод вызывается только один раз и относительно поздно в процессе загрузки Spring:

>>>>>> Registering filter 'springSecurityFilterChain' with: class org.springframework.web.filter.DelegatingFilterProxy
>>>>>> Existing filter 'springSecurityFilterChain' as: org.springframework.security.web.FilterChainProxy

Когда это работает, выходные данные отладки выглядят так:

>>>>>> Registering filter 'springSecurityFilterChain' with: class org.springframework.web.filter.DelegatingFilterProxy

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.0.RELEASE)

Это говорит о том, что конфигурация безопасности происходит намного раньше в процессе загрузки, когда она работает по сравнению с неудачной.

Ответы на вопрос(2)

Ваш ответ на вопрос