Статьи

Избегайте нежелательного сканирования компонентов конфигурации Spring

Я столкнулся с  интересной проблемой переполнения стека.  У Бретта Райана была проблема, что конфигурация Spring Security была инициализирована дважды. Когда я изучал его код, я обнаружил проблему. Позвольте мне показать, показать код.

У него довольно стандартное Spring приложение (не использующее Spring Boot). Использует более современную конфигурацию сервлета Java, основанную на Spring  AbstractAnnotationConfigDispatcherServletInitializer.

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
 
public class AppInitializer extends
        AbstractAnnotationConfigDispatcherServletInitializer {
 
 
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{SecurityConfig.class};
    }
 
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }
 
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
 
}

Как видите, есть два класса конфигурации:

  • SecurityConfig — содержит конфигурацию Spring Security
  • WebConfig — основная конфигурация контейнера IoC Spring
package net.lkrnac.blog.dontscanconfigurations;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
 
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        System.out.println("Spring Security init...");
        auth
                .inMemoryAuthentication()
                .withUser("user").password("password").roles("USER");
    }
 
}
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "net.lkrnac.blog.dontscanconfigurations")
public class WebConfig extends WebMvcConfigurerAdapter {
 
}

Обратите внимание на компонент сканирования в  WebConfig. Это сканирующий пакет, где расположены все три класса. Когда вы запускаете это в контейнере сервлета, текст  «Spring Security init…»  записывается в консоль дважды. Это означает, что  SecurityConfig конфигурация загружается дважды. Было загружено

  1. При создании корневого контекста в методе AppInitializer.getRootConfigClasses()
  2. По компоненту сканирования в классе WebConfig. Этот экземпляр создается как часть создания контекста сервлета в методе. AppInitializer.getServletConfigClasses().

Зачем? Я нашел это  объяснение в документации Spring :

Помните , что  @Configuration классы  мета-аннотированный  с  @Component, поэтому они являются кандидатами для компонентного сканирования!

Так что это особенность Spring, и поэтому мы хотим избежать сканирования компонентов Spring,  @Configurationиспользуемого конфигурацией сервлета. Бретт Райан независимо нашел эту проблему и показал свое решение в упомянутом вопросе переполнения стека:

@ComponentScan(basePackages = "com.acme.app",
               excludeFilters = {
                   @Filter(type = ASSIGNABLE_TYPE,
                           value = {
                               WebConfig.class,
                               SecurityConfig.class
                           })
               })

Мне  не нравится это решение. Аннотация слишком многословна для меня. Также некоторые разработчики могут создать новый   класс и забыть включить его в этот фильтр. Я бы предпочел указать специальный пакет, который будет исключен из сканирования компонентов Spring.@Configuration

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

Я создал  пример проекта на Github,  чтобы вы могли поиграть с ним.