在使用Spring Boot开发后端应用程序时,很多时候我们使用四层架构来完成对单体应用程序的开发。虽然四层架构在SSM单体应用程序中能够很清晰明了地划分每一层,从数据到功能,最后到API。
但是随着我们单体项目功能的增加,项目仍然会变得更加臃肿,比如说当我们再打开很久之前的项目,或者接手其它项目时,看到侧边栏一长条的
xxxDAO
或者
xxxService
时,很多时候一时半会也是缓不过神的:
当然,这仅仅是一个还未开发完成的小型单体项目,如果功能再复杂一点,那么其臃肿程度我们将无法想象,也使得我们继续开发、维护变得困难。
这时我们可以对项目分模块,即按照功能拆分为多个Maven模块,然后可以通过依赖或者配置的方式将多个功能集成在主要模块上,甚至还可以控制是否启用某个功能。
需要注意的是,这里的应用拆分并不是把应用拆分成Spring Cloud分布式微服务多模块,而是仅对一个单体项目而言,它仍然是单体项目,但是每一个功能放在每个模块中,而不再是所有功能放在一个Spring Boot工程中。
要想实现Spring Boot模块化开发,我们可以借助@Import注解,实现在一个模块中,导入另一个模块中的类并将其也初始化为Bean注册到IoC容器。
下面,我们就通过一个简单的例子来学习一下。
在学习今天的内容之前,我们可以先回顾一下关于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.class, args);
}
}
从初学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
就是以自身为起点向下扫描当前包以及所有的子包中的类了。
首先假设现在有一个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/
@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吗?
下面,我们就来尝试一下。
大家可以根据上述工程结构创建一个多模块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