专栏名称: 芋道源码
纯 Java 源码分享公众号,目前有「Dubbo」「SpringCloud」「Java 并发」「RocketMQ」「Sharding-JDBC」「MyCAT」「Elastic-Job」「SkyWalking」「Spring」等等
目录
相关文章推荐
芋道源码  ·  如何应对消息堆积? ·  7 小时前  
芋道源码  ·  Cloudflare ... ·  7 小时前  
芋道源码  ·  某公司新招了个牛逼的架构师后... ·  2 天前  
芋道源码  ·  疯传Java界,堪称最强! ·  2 天前  
51好读  ›  专栏  ›  芋道源码

Spring Boot模块化开发:@Import注解的应用

芋道源码  · 公众号  · Java  · 2025-03-19 22:05

正文

👉 这是一个或许对你有用 的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入 芋道快速开发平台 知识星球。 下面是星球提供的部分资料:

👉 这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、 商城 、支付、工作流、大屏报表、微信公众号、 ERP CRM AI 大模型 等等功能:

  • Boot 多模块架构:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 微服务架构:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 双版本

来源:juejin.cn/post/
7352792335904047131


在使用Spring Boot开发后端应用程序时,很多时候我们使用四层架构来完成对单体应用程序的开发。虽然四层架构在SSM单体应用程序中能够很清晰明了地划分每一层,从数据到功能,最后到API。

但是随着我们单体项目功能的增加,项目仍然会变得更加臃肿,比如说当我们再打开很久之前的项目,或者接手其它项目时,看到侧边栏一长条的 xxxDAO 或者 xxxService 时,很多时候一时半会也是缓不过神的:

当然,这仅仅是一个还未开发完成的小型单体项目,如果功能再复杂一点,那么其臃肿程度我们将无法想象,也使得我们继续开发、维护变得困难。

这时我们可以对项目分模块,即按照功能拆分为多个Maven模块,然后可以通过依赖或者配置的方式将多个功能集成在主要模块上,甚至还可以控制是否启用某个功能。

需要注意的是,这里的应用拆分并不是把应用拆分成Spring Cloud分布式微服务多模块,而是仅对一个单体项目而言,它仍然是单体项目,但是每一个功能放在每个模块中,而不再是所有功能放在一个Spring Boot工程中。

要想实现Spring Boot模块化开发,我们可以借助@Import注解,实现在一个模块中,导入另一个模块中的类并将其也初始化为Bean注册到IoC容器。

下面,我们就通过一个简单的例子来学习一下。

1,再看@ComponentScan

在学习今天的内容之前,我们可以先回顾一下关于IoC容器扫描组件的基本知识。

1) IoC容器的扫描起点

相信大家对 @SpringBootApplication 这个注解并不陌生,我们创建的每个Spring Boot工程主类都长这样差不多:

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.classargs);
 }

}

从初学Spring Boot开始,我们就知道要想让一个类被扫描并实例化成Bean交给IoC容器托管,除了给那些类标注相关的注解(比如 @Component )之外,还需要将其放在主类(也就是标注了 @SpringBootApplication 的类)所在的软件包或者其子包层级下,这样在IoC容器初始化时,我们的类才会被扫描到。

可见 @SpringBootApplication 事实上标注了IoC容器创建Bean时扫描的起点,不过 @SpringBootApplication 是一个复杂的复合注解,它是下列注解的组合:

而事实上,真正起到标注扫描起点作用的注解是 @ComponentScan ,当该注解标注在一个类上时,这个类就会被标记为IoC容器的扫描起点,相信大家初学Spring时都写过这样类似的入门示例:

package com.gitee.swsk33.springdemo;

import com.gitee.swsk33.springdemo.service.MessageService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.ApplicationContext;

@ComponentScan
public class Main {

    public static void main(String[] args) {
        // 创建基于注解的上下文容器实例,并传入配置类Main以实例化其它标注了Bean注解的类
        ApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
        // 利用Spring框架,取出Bean:MessageService对象
        MessageService messageService = context.getBean(MessageService.class);
        // 这时,就可以使用了!
        messageService.printMessage();
    }

}

可见上述是我的主类,其标注了 @ComponentScan 注解,主类位于软件包 com.gitee.swsk33.springdemo ,那么IoC容器初始化时,就会递归扫描位于软件包 com.gitee.swsk33.springdemo 中及其下所有子包中标注了相关注解(例如 @Component @Service )的类,并将它们实例化为Bean放入IoC容器托管,上述代码中, MessageService 位于 com.gitee.swsk33.springdemo 中的子包service下且标注了相关注解,因此能够被实例化为Bean并放入IoC容器,后续我们可以取出。

事实上,无论是 @ComponentScan 还是 @SpringBootApplication 注解,都是可以指定扫描位置的,比如说:

@SpringBootApplication(scanBasePackages = "com.gitee.swsk33.mainmodule")
public class MainModuleApplication {
 // ...
}

这表示启动程序时指定扫描软件包 com.gitee.swsk33.mainmodule 中及其所有子包下对应的类,只不过平时大多数时候我们都缺省这个参数,这样默认情况下, @ComponentScan 或者 @SpringBootApplication 就是以自身为起点向下扫描当前包以及所有的子包中的类了。

2) 导入其它模块作为依赖?

首先假设现在有一个Maven多模块工程,其中有三个Spring Boot工程如下:

上述是一个按照功能拆分的Spring Boot多模块的项目示例, main-module 工程是主功能,而另外两个是两个子功能模块,主功能模块需要以Maven依赖的形式导入子功能模块,它们才能组成一个完整的系统。

如果说现在在上述主功能中,将功能1以Maven依赖形式引入,启动主功能,功能1模块中的 FunctionOneService 类也会被扫描到并实例化为Bean吗?

很显然并不会。因为主功能中主类位于软件包 com.gitee.swsk33.mainmodule 中,那么启动时就会扫描该软件包及其子包下的类,不可能说扫描到功能1中的软件包 com.gitee.swsk33.functionone 了。

当然,这个问题很好解决,我们可以在 @SpringBootApplication 注解中指定 scanBasePackages 字段将两个子模块的包路径加进去就行了,这样确实没有问题,但是好像总觉得不是很优雅:如果我需要按需停用或者启用功能,那就需要修改这个主类的注解中传入的参数。

有没有别的办法呢?当然, @Import 注解也可以实现这个功能。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

2,@Import注解的基本使用

@Import 注解通常标注在配置类上,它可以在IoC容器初始化当前配置类的同时,将其它的指定类也引入进来并初始化为Bean,例如:

@Configuration
@Import(DemoFunction.class)
public class FunctionImportConfig 
{
}

可见上述 FunctionImportConfig 是一个配置类,该类会在IoC容器初始化时被扫描并初始化为Bean,那么在IoC容器扫描这个 FunctionImportConfig 的同时,也会读取到它上面的@Import注解,而 @Import 注解中指定了类 DemoFunction ,这就可以使得 DemoFunction 类也被加入扫描的候选类,最终也被实例化为Bean并交给IoC容器。

事实上,无论被标注 @Import 的类放在哪里,主要这个类能被扫描到,且标注了 @Configuration 等注解、能被实例化为Bean,那么其上的 @Import 注解中指定的类也会被连带着加入扫描以及初始化为Bean的候选。

当然,上述这个被导入的 DemoFunction 类也是有要求的,它必须是一个配置类,分下面两种情况讨论:

  • 被导入的 DemoFunction @Configuration 标注的类: Spring会将这个 DemoFuntion 配置类初始化为Bean并加载到IoC容器中,这意味着只有该配置类本身、以及其中显示声明的Bean才会被加载到容器中,其他未声明的bean则不会被加载
  • 被导入的 DemoFunction @ComponentScan 标注的类: Spring则会在导入该配置类同时,还会根据 @ComponentScan 指定的扫描包路径,扫描其指定的全部包下对应的类(标注了 @Component 等等注解的)并初始化为Bean,默认则是将该类及其所在包的所有子包下的相关类初始化为Bean

回到上面的多模块项目场景中,可见我们只需要使用 @Import 注解不就可以在主模块中,把功能1模块中的类全部导入并初始化为Bean吗?

下面,我们就来尝试一下。

1) 导入其它模块的@ComponentScan类

大家可以根据上述工程结构创建一个多模块Maven项目,先是创建一个父模块的pom.xml,然后主模块、功能模块1和功能模块2都继承这同一个父项目,这样它们之间可以相互引用。

首先我们来看功能模块1,该模块作为一个功能,不需要作为一个完整的Spring Boot应用程序启动,因此该模块中不需要主类,只编写起点配置类和功能代码(比如Service层的类)即可,删除功能模块1的全部依赖,然后只加一个 spring-boot-starter 作为一些注解的基本支持即可:

然后删除功能模块1的主方法main,并将 @SpringBootApplication 改成 @ComponentScan ,仅作为扫描起点类即可,该类位于功能模块1最顶层软件包中,其中内容如下:

package com.gitee.swsk33.functionone;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class FunctionOneApplication {
}

然后再给功能模块1开发一个Service类,内容如下:

package com.gitee.swsk33.functionone.service;

import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class FunctionOneService {

 @PostConstruct
 private void init() {
  log.info("功能1,启动!");
 }

}

可见使其被初始化为Bean时打印一句话,让我们知道该类被扫描并且被初始化即可。

现在回到主模块,在其中将功能模块1以依赖形式引入:

然后在主模块中创建一个配置类,使用 @Import 导入功能模块1中的扫描起点(标注了 @ComponentScan 的类):

package com.gitee.swsk33.mainmodule.config;

import com.gitee.swsk33.functionone.FunctionOneApplication;
import com.gitee.swsk33.functiontwo.FunctionTwoApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * 用于导入其它模块的配置,使得其它模块中的Bean也能够交给IoC托管
 */

@Configuration
@Import(FunctionOneApplication.class)
public class FunctionImportConfig 
{
}

事实上, @Import 可以导入多个类,传入数组形式即可,这里我们只导入模块1的起点类。

现在,启动主模块,可见模块1中的服务类也被成功扫描到并初始化为Bean了:

可见当我们的主模块启动时:

  • 首先初始化主模块中的配置类 FunctionImportConfig ,同时读取到该配置类上的 @Import 注解中指定的模块1中的类 FunctionOneApplication






请到「今天看啥」查看全文