WebMvcConfigurer не применяется при создании нескольких пользовательских контекстов в SpringBoot Tomcat

Вопрос или проблема

ЗАМЕТКА: версия springboot 3.3.5

В моей конфигурации мне нужно развернуть на отдельной базе веб-контекста tomcat (и я читаю имена контекстов из конфигурационного файла, поэтому я убрал стандартный ROOT-контекст). Я настроил Tomcat с помощью бина tomcatFactory и использовал AnnotationConfigWebApplicationContext для сканирования моих RESTControllers (что работает).

Однако я заметил 2 аномалии с этим подходом:

  1. Хотя мой WebMvcConfigurer экземпляр создан (я записал вызов конструктора), функция addInterceptors не выполняется.

  2. Мои бины инициализируются дважды, например, у меня есть этот пользовательский бин DataSource, который инициализируется дважды. Это, вероятно, связано с тем, что Spring ApplicationContext и AnnotationConfigWebApplicationContext сканируются отдельно (просто предполагаю, потому что он инициализируется только дважды, даже когда я добавляю 3 контекста), но в AnnotationConfigWebApplicationContext я уже явно отключил сканирование пространства имен datasource, а также даже пытался вызвать setParent для моего AnnotationConfigWebApplicationContext, но это не помогает: appContext.setParent(applicationContext);. При доступе к бину из моих REST Controllers используется только первый экземпляр.

Ниже приведен мой код:

@SpringBootApplication(exclude = ServletWebServerFactoryAutoConfiguration.class)
@ComponentScan({"mynamespace"})
@EnableAutoConfiguration(exclude = {QuartzAutoConfiguration.class, WebMvcAutoConfiguration.class})
public class MySpringBootContainer {

    private static final Logger log = LoggerFactory.getLogger(MySpringBootContainer.class);

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(SpringBootContainer.class);
        app.setDefaultProperties(Map.of(
                "spring.config.import", "file:./config.yml")
        );
        app.run(args);
    }

    @Bean
    public TomcatServletWebServerFactory tomcatFactory() {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory() {

            @Override
            protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
                Context webContext = tomcat.addContext("/test1", null);
                Context webContext2 = tomcat.addContext("/test2", null);

                AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
                appContext.setServletContext(webContext.getServletContext());
                appContext.scan("mynamespace");
                appContext.refresh();

                tomcat.getHost().removeChild(tomcat.getHost().findChild("")); // удалить стандартный контекст приложения

                var dispatcherServlet = new DispatcherServlet(appContext);
                Tomcat.addServlet(webContext, "default", dispatcherServlet);
                webContext.addServletMappingDecoded("/*", "default");

                appContext.setServletContext(webContext2.getServletContext()); // это обязательно
                var dispatcherServlet2 = new DispatcherServlet(appContext);
                Tomcat.addServlet(webContext2, "default", dispatcherServlet2);
                webContext2.addServletMappingDecoded("/*", "default");

                return super.getTomcatWebServer(tomcat);
            }
        });
        return factory;
    }
}

Ответ или решение

WebMvcConfigurer не применяется при создании нескольких пользовательских контекстов в Spring Boot на Tomcat

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

Проблема с WebMvcConfigurer

Проблема с тем, что метод addInterceptors из вашего WebMvcConfigurer не вызывается, может быть связана с тем, что Spring не находит ваш WebMvcConfigurer в контексте. Когда вы создаете новый экземпляр AnnotationConfigWebApplicationContext для каждого контекста, идентификаторы всех конфигурационных классов и бинов могут не пересекаться должным образом.

Решение:

  1. Объедините контексты. Вместо создания нового AnnotationConfigWebApplicationContext для каждого контекста, можно создать один родительский ApplicationContext и установить его для дочерних контекстов. Таким образом, ваши WebMvcConfigurer и другие бины будут доступны во всех контекстах.

    ApplicationContext parentContext = new AnnotationConfigWebApplicationContext();
    parentContext.scan("mynamespace");
    parentContext.refresh();
    
    // Создание дочерних контекстов
    AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext();
    webContext.setParent(parentContext); // Установка родителя
    webContext.setServletContext(tomcatContext.getServletContext());
    webContext.refresh();
  2. Обновите конфигурацию. Убедитесь, что все ваши конфигурационные классы помечены необходимыми аннотациями, такими как @Configuration и @EnableWebMvc.

Проблема с многократной инициализацией бинов

Дублирующаяся инициализация бинов, вероятно, происходит из-за того, что каждый контекст инициализирует свои собственные экземпляры бинов. Поскольку у вас два контекста, все, что вы создаете в одном, создается и в другом.

Решение:

  1. Управление с помощью родительского контекста. Как и в предыдущем пункте, создание родительского контекста и установка его для дочерних позволит избежать повторной инициализации. Все бины, объявленные в родительском контексте, будут доступны и в дочерних без повторной инициализации.

  2. Кэширование бинов. Если по какой-то причине вам необходимо использовать отдельные контексты, рассмотрите возможность внедрения кэширования для ваших бинов или реализации Singleton для ваших конфигурационных классов.

Обновленный пример кода

Вот более оптимизированный пример вашего кода, который учитывает предложения выше:

@SpringBootApplication(exclude = ServletWebServerFactoryAutoConfiguration.class)
@ComponentScan({"mynamespace"})
@EnableAutoConfiguration(exclude = {QuartzAutoConfiguration.class, WebMvcAutoConfiguration.class})
public class MySpringBootContainer {

    private static final Logger log = LoggerFactory.getLogger(MySpringBootContainer.class);

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(MySpringBootContainer.class);
        app.setDefaultProperties(Map.of("spring.config.import", "file:./config.yml"));
        app.run(args);
    }

    @Bean
    public TomcatServletWebServerFactory tomcatFactory() {
        return new TomcatServletWebServerFactory() {
            @Override
            protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
                AnnotationConfigWebApplicationContext parentContext = new AnnotationConfigWebApplicationContext();
                parentContext.scan("mynamespace");
                parentContext.refresh();

                Context webContext1 = tomcat.addContext("/test1", null);
                Context webContext2 = tomcat.addContext("/test2", null);

                setupServlet(webContext1, parentContext);
                setupServlet(webContext2, parentContext);

                tomcat.getHost().removeChild(tomcat.getHost().findChild("")); // Удалить контекст ROOT
                return super.getTomcatWebServer(tomcat);
            }

            private void setupServlet(Context context, ApplicationContext parent) {
                AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
                appContext.setParent(parent);
                appContext.setServletContext(context.getServletContext());
                appContext.refresh();

                DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext);
                Tomcat.addServlet(context, "default", dispatcherServlet);
                context.addServletMappingDecoded("/*", "default");
            }
        };
    }
}

Заключение

Эти изменения помогут вам решить проблемы, с которыми вы столкнулись при работе с несколькими контекстами в Spring Boot на Tomcat. Следуя предложенным подходам, вы сможете добиться более стабильной и предсказуемой работы вашего приложения. Если у вас остались вопросы или нужны дополнительные пояснения, не стесняйтесь задавать их.

Оцените материал
Добавить комментарий

Капча загружается...