Using @Import Annotation for Modular Development in Spring Boot
This article explains how to use Spring Boot's @Import annotation to modularize a monolithic application by splitting functionality into Maven modules, configuring component scanning, creating custom annotations, and leveraging ImportSelector and ImportBeanDefinitionRegistrar for dynamic bean registration and conditional loading.
When developing Spring Boot back‑end applications, a four‑layer architecture works well for small monoliths, but as the project grows the codebase becomes cluttered with many xxxDAO and xxxService classes, making maintenance difficult.
To keep a single‑module project manageable, you can split the functionality into separate Maven modules. Each module contains its own Spring components, and the main module imports the others via the @Import annotation, without turning the whole system into a distributed micro‑service architecture.
The entry point for a module can be a class annotated with @ComponentScan . When this class is imported, Spring treats it as a new scan start point, recursively scanning its package and sub‑packages for beans such as @Component , @Service , etc.
Example of a main class using @SpringBootApplication and the underlying @ComponentScan :
package com.gitee.swsk33.mainmodule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MainModuleApplication {
public static void main(String[] args) {
SpringApplication.run(MainModuleApplication.class, args);
}
}In a multi‑module Maven project, the main module can import a functional module by declaring it as a dependency and adding an @Import configuration:
package com.gitee.swsk33.mainmodule.config;
import com.gitee.swsk33.functionone.FunctionOneApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import(FunctionOneApplication.class)
public class FunctionImportConfig {
}To make the import more declarative, you can create a custom annotation that itself uses @Import :
package com.gitee.swsk33.mainmodule.annotation;
import com.gitee.swsk33.functionone.FunctionOneApplication;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(FunctionOneApplication.class)
public @interface EnableFunctionOne {
}Applying @EnableFunctionOne on the main application class automatically brings in the functional module’s beans.
For more flexible imports, implement ImportSelector to decide at runtime which classes to load:
package com.gitee.swsk33.mainmodule.selector;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class DemoImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
System.out.println("Importing class: " + importingClassMetadata.getClassName());
return new String[]{
"com.gitee.swsk33.functionone.FunctionOneApplication",
"com.gitee.swsk33.functiontwo.FunctionTwoApplication"
};
}
}Register the selector with @Import in a configuration class, and Spring will invoke selectImports to import the specified classes.
Alternatively, implement ImportBeanDefinitionRegistrar to programmatically register bean definitions:
package com.gitee.swsk33.mainmodule.selector;
import com.gitee.swsk33.functionone.FunctionOneApplication;
import com.gitee.swsk33.functiontwo.FunctionTwoApplication;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
public class DemoImportRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
GenericBeanDefinition beanOne = new GenericBeanDefinition();
beanOne.setBeanClass(FunctionOneApplication.class);
GenericBeanDefinition beanTwo = new GenericBeanDefinition();
beanTwo.setBeanClass(FunctionTwoApplication.class);
registry.registerBeanDefinition("functionOneComponentScan", beanOne);
registry.registerBeanDefinition("functionTwoComponentScan", beanTwo);
}
}Both approaches allow dynamic, conditional loading of modules. You can further control the import with Spring’s conditional annotations, e.g., @ConditionalOnProperty , so a module is only loaded when a specific property (such as com.gitee.swsk33.function-one.enabled=true ) is set.
In summary, the @Import annotation, together with custom annotations, ImportSelector , and ImportBeanDefinitionRegistrar , provides a powerful mechanism for Spring Boot modular development, enabling clean separation of features, optional activation, and flexible bean registration without converting the monolith into a full micro‑service architecture.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.