Правильный способ использования @ComponentScan в многомодульном приложении Java-Config Spring MVC

Я только что начал новый весенний проект, и на этот раз я хочу сделать все «правильно». В последнем проекте у меня были проблемы с многократной регистрацией определенных классов из-за нескольких аннотаций @ComponentScan. (т.е. все классы обслуживания были зарегистрированы дважды)

В основном я использую следующий макет:

WebAppInitializer:

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[] { RootConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[] { WebMvcConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

}

RootConfig:

@Configuration
@ComponentScan
public class RootConfig {
    /* ... */
}

WebMvcConfig:

@EnableWebMvc
@ComponentScan
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    /* ... */
}

DatabaseConfig:

@Configuration
@EnableJpaRepositories("my.base.class.path")
public class DataConfig {
    /* ... */
}

Первый основной вопрос: Какой класс должен искать какие классы/аннотации?

Должен ли только WebMvcConfig сканировать @Controller классы? Какой из них должен сканировать @Service, @Configuration и @Component?

Второй вопрос: Или мне просто использовать пакеты, чтобы сузить путь сканирования?

Пример:

rootpackage
rootpackage.config.RootConfig
rootpackage.config.DatabaseConfig
rootpackage.mvc.WebMvcConfig

а затем поместить все @Controller классы под rootpackage.mvc.*?

Третий вопрос: чаще разрешают RootConfig сканировать DatabaseConfig? Или мне следует поместить DatabaseConfig в метод getRootConfigClasses класса WebAppInitializer?

Последний вопрос: В многомодульном проекте: как организовать все это?

Пример. Если бы я выбрал способ, описанный во втором вопросе, я мог бы сказать, что каждый модуль приложения на самом деле будет состоять из нескольких разных модулей. Допустим, я хочу создать модуль X, в котором будет класс @Service и несколько классов @Controller, я мог бы поместить их в них в разных пакетах. Как это:

Maven Module X Service

rootpackage.services.x.XService
rootpackage.services.x.XServiceImpl

Maven Module X Controller

rootpackage.mvc.controller.x.X1Controller
rootpackage.mvc.controller.x.X2Controller
rootpackage.mvc.controller.x.X3Controller

А если предложить такой способ, то: Где размещать модели и репозитории (для доступа к БД)? Должен ли я создавать новый модуль для каждого из них?

Заранее спасибо!


person Benjamin M    schedule 14.01.2014    source источник


Ответы (2)


Я думаю, что нашел теперь довольно хороший макет проекта:

rootpackage.web.WebAppInitializer (see below)
rootpackage.web.SecurityWebAppInitializer (creates "springSecurityFilterChain")
rootpackage.web.WebMvcConfig (scans for everything in its own package and subpackages)
rootpackage.web.SecurityConfig (Spring Security config)

rootpackage.web.moduleA.SomeAController
rootpackage.web.moduleB.SomeBController

rootpackage.service.ServiceConfig (scans for everything in its own package and subpackages)
rootpackage.service.moduleA.AService
rootpackage.service.moduleA.AServiceImpl
rootpackage.service.moduleB.BService
rootpackage.service.moduleB.BServiceImpl
rootpackage.service.security.UserDetailsServiceImpl (for Spring Security)

rootpackage.model.DatabaseConfig (scans for everything in its own package and subpackages)
rootpackage.model.moduleA.SomeADomainObject
rootpackage.model.moduleB.SomeBDomainObject

Инициализатор веб-приложения:

@Order(2)
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[] {
            SecurityConfig.class,
            ServiceConfig.class,
            DatabaseConfig.class
        };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[] { WebMvcConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

}

Инициализатор SecurityWebApp:

@Order(1) // should always be registered in first place (= before WebAppInitializer)
public class SecurityWebAppInitializer extends AbstractSecurityWebApplicationInitializer {
    /* ... */
}

WebMvcConfig:

@Configuration
@EnableWebMvc
@ComponentScan // scans for everything in its own package and subpackages
               // so it only finds SomeAController.class and SomeBController.class
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    /* ... */
}

Конфигурация безопасности:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /* ... */
}

Конфигурация службы:

@Configuration
@ComponentScan // scans for everything in its own package and subpackages
               // so it only finds AServiceImpl.class and BServiceImpl.class
public class ServiceConfig {
    /* ... */   
}

Я поместил некоторый «System.out.println» в конструктор всех этих классов, чтобы увидеть, как часто они регистрируются/загружаются. Каждый конструктор выполняется ровно один раз!

Что Вы думаете об этом? Есть улучшения?

person Benjamin M    schedule 14.01.2014
comment
Просто примечание к Spring Security. Вы инициализируете springSecurityFilterChain (SSFC) в веб-модуле, пока все в порядке. Проблема в том, что у вас может быть только один SSFC в приложении, т.е. вы не можете зарегистрировать другой SSFC в каком-то другом модуле (дочернем контексте). Недавно я наткнулся на это в одном проекте, который я хочу модульизировать. К сожалению, я не знаю, как решить эту проблему. - person Jakub Jirutka; 15.01.2014
comment
Извините, я не совсем понимаю... :-( Какое решение лучше для большей гибкости? Должен ли я разместить по одному для каждого модуля? Я пока не так много работал с Spring Security. - person Benjamin M; 15.01.2014
comment
Ваша текущая конфигурация прекрасна и разумна. Я просто говорю, что у вас возникнут проблемы, если вы добавите другой (диспетчерский) контекст сервлета (то есть дочерний контекст), возможно, с REST API, который также будет защищен с помощью Spring Security. Причина в том, что вы не можете зарегистрировать еще один springSecurityFilterChain внутри контекста второго сервлета, может быть только один экземпляр. Единственный известный мне обходной путь — это зарегистрировать SSFC в корневом контексте, после чего его сможет использовать любой контекст сервлета (и сам корневой контекст), но это довольно ограниченный и негибкий способ. Просто знайте об этом ограничении. - person Jakub Jirutka; 15.01.2014
comment
Хорошо, теперь я немного лучше понимаю :-). Я думаю, что текущий макет проекта не будет иметь таких проблем, потому что это API только для REST. Никакого HTML, никакого JSP, ничего другого, только JSON (и, может быть, XML, если нас кто-то заставит...). - person Benjamin M; 15.01.2014
comment
Я рада. :) Если вы будете использовать Spring Security только в одном контексте, как сейчас, то беспокоиться не о чем. - person Jakub Jirutka; 15.01.2014
comment
Просто сам с этим борюсь. В итоге я не использовал пакеты, а указал сканированию, что именно включать/исключать. т. е. для моего ServiceConfig я использовал @ComponentScan(value = com.mypackage, excludeFilters = { @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class), @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Configuration.class) } ) Мой MvcConfig просто сканирует com.mypackage.controllers - person Bal; 12.03.2014
comment
Меня интересует ваша реализация. Согласно AbstractSecurityWebApplicationInitializer предлагает сначала добавить AbstractDispatcherServletInitializer. Но ваш @Order идет другим путем. - person end-user; 20.01.2015

С конфигурацией на основе XML у вас обычно будет 2 контекста: один родительский контекст, который будет загружать все ваши бизнес-сервисы, конфигурацию базы данных, репозитории, объекты домена и т. д., и веб-контекст для загрузки контроллеров и т. д.

Оба должны использовать пакеты, чтобы убедиться, что они не пытаются загрузить одни и те же компоненты дважды. Вы указываете оба в ContextLoaderListener, чтобы создать ApplicationContext.

Контекст веб-приложения знает о родителе (а не наоборот) и будет искать у родителя любые bean-компоненты, не найденные в его собственном контексте. Это означает, что ваши контроллеры могут получить доступ к вашим сервисам.

Я не делал этого в конфигурации Java, но я предполагаю, что подход тот же

person Romski    schedule 14.01.2014
comment
Спасибо. Да, он должен быть таким же, как в конфигурации XML. Это все тот же фреймворк, но просто другой способ описания конфигурации. ... Теперь мне просто нужно знать, где именно провести черту у этих пакетов: Вы использовали два раза etc. ;-). Не могли бы вы предоставить ссылку на какое-то хорошее объяснение или просто объяснить это более подробно? - person Benjamin M; 14.01.2014
comment
Это действительно зависит от того, как вы организуете свой код, но вы можете предоставить список пакетов для каждого, если компоненты разбросаны. Я добавлю больше деталей позже - person Romski; 15.01.2014
comment
Я создал новостной ответ на это со своим собственным решением. Кажется, пока все работает так, как ожидалось. Возможно, вы захотите это прокомментировать :) - person Benjamin M; 15.01.2014
comment
Да, это почти то же самое, что и с XML, только конфигурация на основе Java более гибкая. - person Jakub Jirutka; 15.01.2014