(自用)配置文件优先级、SpringBoot原理、Maven私服
配置优先级
之前介绍过SpringBoot中支持三类配置文件.properties、.yml和.yaml,他们三者之间也是有着优先级顺序的,为.properties➡.yml➡.yaml。
同时SpringBoot为了增强程序的拓展性,除了支持配置文件属性配置,还支持Java系统属性和命令行参数的方式进行属性配置。
//java系统属性"-D"是固定的
-Dkey=value
-Dserver.port=8000
//命令行参数"--"是固定的
--key=value
--server.port=8000
我们可以在可视化配置界面来配置这两个属性:
然后在Modify options下拉列表选中Add VM options和Program arguments,即可编写相关属性。
两者之间,命令行参数的优先级更高,会优先生效,也就是说如图的配置中,实际运行的端口为9999。
这是在IDEA中配置,但如果项目已经打包上线的话,配置相关属性的方法就又不一样了。我们上文的两项配置清空,再来运行maven生命周期中的打包程序,系统会将jar包存放到target目录下(如果版本过老,则需要引入spring-boot-maven-plugin依赖):
直接在该目录下的路径框中输入cmd,输入java即可获取相关指令的介绍:
其中options代表java系统属性,args代表命令行参数。
然后输入java -jar 再按下Tab即可补齐jar包名字,按下回车运行,此时端口号因为尚未配置,仍为8080。按下ctrl+c关闭程序,再来配置相关的属性,注意Java系统属性应在前,命令行参数应该在后:
java -Dserver.port=9000 -jar chnApplication-0.0.1-SNAPSHOT.jar --server.port=8000
也就是说共有五种配置方式:
- application.yaml 文件配置
- application.yml文件配置
- application.properties文件配置
- java系统属性即可视化界面配置 (-Dxxx=xxx)
- 命令行参数 (–xxx=xxx)
优先级则为从下往上,命令行优先级最高。
bean管理
获取bean
默认情况下,Spring项目在启动时会自动创建IOC容器(spring容器),并自动创建bean放在IOC容器中,主动获取这些bean可以通过如下方式:
- 根据bean名称获取:Object getBean(String name)
- 根据bean类型获取:<T>T getBean(Class requiredType)
- 根据bean名称和类型获取:<T>T getBean(String name, Class<T> requiredType)
//IOC容器对象
@Autowired
private ApplicationContext applicationContext;
//获取bean对象
@Test
public void testGetBean(){
//根据bean的名称获取
//获取的对象为Object类,需强转
DeptController bean1 = (DeptController) applicationContext.getBean("deptController");
System.out.println(bean1);
//根据bean的类型获取
DeptController bean2 = applicationContext.getBean(DeptController.class);
System.out.println(bean2);
//根据bean的名称 及 类型获取
DeptController bean3 = applicationContext.getBean("deptController", DeptController.class);
System.out.println(bean3);
这就是三种获取bean的方式,但如果IOC容器中只有一个DeptController类型的bean,那这三个获取到的都是同一个bean,因为在Spring中bean默认是单例的。
我没获取的bean是单例还是多例,实际上取决于bean的作用域。
bean作用域
在Spring中bean支持五种作用域,后三种在web环境下生效,因此我们主要看前两种:
- singleton(单例):默认作用域。容器内同名称的bean只有一个实例。
- prototype(原型):每次请求Bean时都会创建一个新的Bean实例。
- request(请求):每个请求范围中,每个Bean只会有一个实例。
- session(会话):每个会话范围中,每个Bean只会有一个实例。
- application(应用):每个应用范围中,每个Bean只会有一个实例。
设置作用域则可以借助注解@Scope("prototype"),内部为不同的属性。
当使用默认的作用域(singleton)修饰类时,该Bean会在容器启动时就已实例化并放到IOC容器中。我们还可以通过注解@Lazy使bean延迟初始化,那么该Bean会在第一次使用时才会初始化。
使用注解@Scope("prototype")时,每次请求该Bean时Spring容器都会创建一个新的Bean实例。即Bean实例是在第一次使用时创建的。该bean会在容器启动时就已实例化并放到IOC容器中,并且bean会在第一次使用时都进行初始化。
第三方bean
之前使用的bean都是我们在项目中定义的,声明bean只需在类上加上注解@Component、 @Repository、@Service和@Controller(或者说@RestController)。但在实际开发中可能出现某个类是依赖提供的,或者说文件为只读属性无法修改等等特殊情况,这就要用到另外一个注解@Bean。
当你需要将第三方库的类实例化并注册为Spring容器中的Bean时,可以使用@Bean注解。这些类可能不是用Spring的注解@Component及其衍生注解标注的,因此需要通过@Bean手动注册。
我们可以定义一个方法,返回我们所需的类型的对象,并在该方法上加上@Bean,这样该类的实例就会自动注入到IOC之中。如果有多个外部bean,我们可以通过注解@Configuration声明一个配置类,在该类中定义方法实现多个bean注入。
如果未指定bean的名称,则bean名称就是方法名(首字母小写)。
如果指定bean的名称,则bean名称为自定义的名称。
如果声明bean时需要依赖注入(就像某个类被@RestController修饰,类内部使用注解@Autowired注入其他bean的实例),则需要在方法中指定对应的方法形参即可。
@Configuration
public class Config {
// 默认Bean声明
@Bean
public Default defaultBean() {
return new Default();}
// 指定名称的Bean声明
@Bean(name = "customBean")
public Custom customBean() {
return new Custom();}
// 依赖注入的Bean声明
@Bean
public Dependent dependentBean(Default defaultBean, Custom customBean) {
return new Dependent(defaultBean, customBean);}
}
SpringBoot原理
之前讲过:Spring是目前市面上最流行的java框架,其可以帮助我们快速的构建java项目,而在spring家族中有非常多优秀的框架,而所有的框架都是基于基础框架:Spring Framework实现的。
如果我们直接基于Spring Framework框架进行开发会比较繁琐,因此我们可以通过SpringBoot来简化spring框架的开发(注意是简化而非替代)。其之所以简化了开发,是因为底层框架实现了起步依赖和自动配置。而我们要介绍的原理就是这两大功能的原理。
起步依赖
如果使用spring框架进行开发,则需引入大量依赖如webmvc、servlet、jackson等等,还需要保证这些依赖的版本需相互匹配,否则会出现版本冲突等问题。而使用了SpringBoot框架后,只需要引入spring-boot-starter-web依赖即可,其他依赖会因为Maven的依赖传递自动引入。
自动配置
自动配置是指应用程序在启动时,一些配置类、bean对象就自动的存入到IOC容器当中,不需要我们手动去开发,从而简化了开发,省去了繁琐的配置操作。
原理
我们来看一个例子,在B项目中需要引入A项目下的某个bean,按照之前的方法编写完成后却失败了,这是因为@Component和其他三注解需要被spring的组件扫描到才能生效,而在SpringBoot项目中,主方法上的@SpringBootApplication注解虽具备包扫描的作用,但只能扫描到当前包及其子包。而A项目下的某个bean属于第三方依赖提供的,无法扫描到。
解决此问题可以有多种方法,我们依次来介绍:
@ComponentScan
我们可以借助注解@ComponentScan({"当前包","第三方包"}),因为该注解会覆盖掉原本默认扫描的包,因此我们不仅要指定第三方包,还需指定当前包。
@ComponentScan({"org.example1","org.example2"})
@ServletComponentScan
@SpringBootApplication
public class ChnApplication {
public static void main(String[] args) {
SpringApplication.run(ChnApplication.class, args);
}
}
但之前我们引入过大量第三方依赖,如果全部这样引入不仅代码臃肿,执行效率还低。这就要提到第二种方案:import导入。
@Import
使用@Import导入的类会被spring加载到IOC容器中,导入方式有很多,我们依次来看:导入普通类、导入配置类、导入ImportSelector接口实现类。其导入的都是数组,前两个导入的为类名,后一个导入的为String形式的全类名。
ImportSelector接口有一个selectImports方法,其返回值为一个数组,该数组中封装了类名,我们可以将需要交给IOC容器管理的类的全类名封装在这个数组中。
//导入普通类——————————————————————————————————
//单个类
@Import(A.class)
//多个类
@Import({A1.class,A2.class})
@SpringBootApplication
public class ChnApplication {//主方法
public static void main(String[] args) {
SpringApplication.run(ChnApplication.class, args);
}
//导入配置类——————————————————————————————————
//单个类
@Import(Config.class)
//多个类
@Import({Config1.class,Config2.class})
@SpringBootApplication
public class ChnApplication {//主方法
public static void main(String[] args) {
SpringApplication.run(ChnApplication.class, args);
}
//导入ImportSelector接口实现类————————————————
@SpringBootApplication
@Import(MyImportSelector.class) // 导入ImportSelector实现类
public class ChnApplication {
public static void main(String[] args) {
SpringApplication.run(ChnApplication.class, args);
}
}
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// 返回需要导入的类的全类名数组
return new String[]{"org.example.chnapplication.controller.Class1",
"org.example.chnapplication.controller.Class2"};
}
}
这样虽然相对来说简单些,但有时我们并不了解第三方包,想要准确的引入也很困难。我们可以让第三方依赖自己来指定,即使用第三方提供的注解,该注解一般为@EnableXXX,以Enable开头,该注解中封装了@Import注解,再在@Import注解后指定要导入的类。例如@EnableAutoConfiguration注解就是Spring Boot自动配置的关键所在。
回想之前我们启动SpringBoot项目时,都是通过启动类,或者说引导类来启动的,该类上有着注解@SpringBootApplication,我们来看该注解的源码:
//@SpringBootApplication注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
@AliasFor(annotation = Configuration.class)
boolean proxyBeanMethods() default true;
}
前四个注解为元注解,是修饰注解的注解,因此不需要看。
@SpringBootConfiguration注解封装了@Configuration注解和@Indexed注解,而@Indexed注解是用来加速应用启动的,无需了解。因此@SpringBootConfiguration注解和@Configuration注解相同,都是声明了当前类是一个配置类。因此我们可以直接在启动类中来声明第三方的bean对象。
@ComponentScan包扫描,默认扫描当前包及其子包。
@EnableAutoConfiguration,为核心注解,该注解封装了@Import(AutoConfigurationImportSelector.class),而括号中的AutoConfigurationImportSelector就是ImportSelector接口的实现类。
在该实现类中,实现了String[] selectImports()方法,其会一直读取到对应依赖META-INF目录下的spring.factories文件和org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。两文件中包含了大量配置类的全类名。方法返回对应的类名,将其这些类的bean交给Spring的IOC进行管理。
因此我们可以通过@bean注解声明bean对象,springboot项目在启动时就会加载配置文件中的配置类。
在Spring Boot 2.7及之前的版本中,spring.factories是主要的自动配置注册方式。在Spring Boot 2.7到3.0的过渡期间,同时兼容spring.factories和新的自动配置导入方式。但在Spring Boot 3.0及以后的版本中,虽然仍然可以支持spring.factories,但官方更推荐使用新的自动配置导入方式。
文件中有很多类,实际项目启动时因为注解@ConditionalXXX的限制,并不是所有的bean都注入IOC容器当中。
@Conditional
按照一定的条件进行判断,满足条件后才会注册对应的bean对象到IOC容器当中。其可以加在方法上或者类上,分别代表对当前这个方法声明的bean生效,和对整个配置类生效。
@Conditional是一个父注解,基于其衍生出了大量的子注解,我们主要介绍三个:
- @ConditionalOnClass: 判断环境中是否有对应的类,可以通过name或者value属性来判断,通常使用全类名(不用也行)。有则注册 bean 到 IOC 容器。
- @ConditionalOnMissingBean: 判断环境中没有对应的 bean,有则注册 bean 到 IOC 容器。默认根据声明的bean的类型来判断。其一般用来声明默认的bean,即有新定义的用新的,没定义则用默认的。
- @ConditionalOnProperty: 判断配置文件中有对应属性和值,有则注册 bean 到 IOC 容器。
//@ConditionalOnClass—————————————————————————————————
//环境中存在该类才会将该bean加入IOC容器中
@Configuration
@ConditionalOnClass(name = "com.example.MyDependency")//使用name参数需要全类名
@ConditionalOnClass(value = {"com.example.ClassOne", "com.example.ClassTwo"})//使用value参数
public class MyConfig {
}
//@ConditionalOnMissingBean———————————————————————————
@Configuration
@ConditionalOnMissingBean//默认
@ConditionalOnMissingBean(MyBean.class)// 基于Bean类型
@ConditionalOnMissingBean(name = "myCustomBeanName")//基于bean的名称
@ConditionalOnMissingBean(type = "MyBean.class", name = "myCustomBeanName")//同时指定Bean类型和名称
public class MyConfig {
}
//@ConditionalOnProperty——————————————————————————————
@Configuration
@ConditionalOnProperty(prefix = "myapp", name = "enabled", havingValue = "true")//基本使用
//这个配置类 MyConfig 只有在 myapp.enabled 属性值为 true 时才会被加载。
@ConditionalOnProperty(prefix = "myapp", name = {"feature1.enabled", "feature2.enabled"}, havingValue = "true")//多个属性
//这个配置类 MyConfig 只有在 myapp.feature1.enabled 和 myapp.feature2.enabled 属性值都为 true 时才会被加载。
@ConditionalOnProperty(prefix = "myapp", name = "feature.enabled")//不指定 havingValue
//在这个例子中,只要 myapp.feature.enabled 属性存在且不是 false,MyConfig 就会被加载。
@ConditionalOnProperty(name = "feature.enabled", havingValue = "true")
public class MyFeatureConfig {
}
使用全类名,还是类名?
- 类名:当你使用 value 参数并传递一个类对象时,你可以直接使用类名,因为编译器能够解析它,并且类对象在编译时是已知的。
- 全类名:当你使用 name 参数时,你必须提供全类名,因为这里你传递的是一个字符串,编译器无法直接解析类名,需要完整的包路径来定位类。
案例(自定义依赖)
在实际的开发中,我们可能会用到很多第三方技术,并不是所有第三方技术都提供了与springboot整合start起步依赖。我们可以定义一些公共组件,并将其封装为SpringBoot的starter。
starter包实现依赖管理功能,而autoconfigure实现自动配置功能。在项目中进行功能开发只需引入对应的起步依赖即可。
在之前的案例中,我们使用aliyunoss需要很多步骤:1、pom.xml文件中引入依赖2、参照官方SDK改造工具类3、yml文件中配置相关参数4、通过实体类加载yml中的配置项5、工具类获取参数6、将工具类交给IOC容器管理7、注入对应的bean。
封装之后,我们只需要引入依赖,注入对应的bean两步即可。
我们来以一个例子来示范:自定义aliyun-oss-spring-boot-starter,完成阿里云OSS操作工具类AliyunOSSUtils的自动配置。引入起步依赖之后,要想使用阿里云OSS,注入AliyunOSSUtils直接使用即可。
共可分为三步:
- 创建 aliyun-oss-spring-boot-starter 模块
- 创建 aliyun-oss-spring-boot-autoconfigure 模块,在starter中引入该模块
- 在 aliyun-oss-spring-boot-autoconfigure 模块中的定义自动配置功能,并定义自动配置文件 META-INF/spring/xxx.imports
一、创建 aliyun-oss-spring-boot-starter 模块
因为该文件仅仅作为依赖管理,所以无用模块都可以删除,只保留aliyun-oss-spring-boot-starter.iml文件和pom.xml文件。
没有aliyun-oss-spring-boot-starter.iml文件解决办法:
1.鼠标放到springboot模块上连按两次Ctrl
2.点击右侧Project选择要操作的模块
3.在中间输入框输入mvn idea:module并回车执行
稍等片刻即可出现.iml文件
二、创建 aliyun-oss-spring-boot-autoconfigure 模块
同样保留aliyun-oss-spring-boot-starter.iml文件和pom.xml文件,但不同的是还需保留src文件。src中主方法、配置文件、测试类都为无用文件,可删除,下图为删除无用文件后的目录:
在starter的pom.xml文件中中引入该模块
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-oss-spring-boot-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
三、在 aliyun-oss-spring-boot-autoconfigure 模块中的定义自动配置功能
和之前一样,创建AliOSSProperties类获取信息,创建AliOSSUtils类使用信息。会报错MutilpartFile类找不到,该类是springbootweb提供的,因此在aliyun-oss-spring-boot-autoconfigure 模块下的pom.xml文件中添加依赖:
<!--阿里云依赖-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
<!--如果使用的是Java 9及以上的版本,则需要添加JAXB相关依赖。添加JAXB相关依赖示例代码如下:-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.3</version>
</dependency>
<!--yml文件配置aliyun时出提示-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<!--web开发起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
不建议使用@Data,手动添加getset方法。
同时因为不应该使其转化为bean,因此之前的@Component、@Autowired都不能再使用了。又因为题目要求"想使用阿里云OSS,注入AliyunOSSUtils直接使用即可",也就是说最终还需将AliOSSUtils类交给IOC来管理,这就需要创建自动配置类AliOSSConfig,在该类中声明AliOSSUtils类的bean。
再回过来看,AliOSSUtils类中调用了aliOSSProperties.getEndpoint();但因无@Autowired,其内部并无实际值,我们需要为其创建getset方法,并在自动配置类中为其赋值。
同时赋值需要调用aliOSSProperties.getEndpoint()中的属性,但此时aliOSSProperties.getEndpoint()已不再是bean,我们无法直接使用,可通过@EnableConfigurationProperties(AliOSSProperties.class)直接将该类转为bean放入到IOC容器中。该注解是Enable开头,不难想到其底层封装了@Import注解。同时该注解只能放在声明了bean的方法上或者配置类上。
此时需要获取该bean,获取第三方bean的对象可以可以直接在方法形参中指定,因此直接将AliOSSProperties aliOSSProperties写入AliOSSUtils类的主方法形参中。
//AliOSSProperties类获取数据——————————————————————————
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
//相关getset方法略
}
//AliOSSUtils类执行方法———————————————————————————————
public class AliOSSUtils {
private AliOSSProperties aliOSSProperties;//没有自动注入,需自行定义getset方法
public AliOSSProperties getAliOSSProperties() {
return aliOSSProperties;}
public void setAliOSSProperties(AliOSSProperties aliOSSProperties) {
this.aliOSSProperties = aliOSSProperties;}
//实现上传图片到OSS
public String upload(MultipartFile file) throws IOException {
// 获取阿里云OSS参数
String endpoint = aliOSSProperties.getEndpoint();
String accessKeyId = aliOSSProperties.getAccessKeyId();
String accessKeySecret = aliOSSProperties.getAccessKeySecret();
String bucketName = aliOSSProperties.getBucketName();
// 获取上传的文件的输入流
InputStream inputStream = file.getInputStream();
// 避免文件覆盖
String originalFilename = file.getOriginalFilename();
String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
//上传文件到 OSS
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ossClient.putObject(bucketName, fileName, inputStream);
//文件访问路径
String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
// 关闭ossClient
ossClient.shutdown();
return url;// 把上传到oss的路径返回
}
}
//自动配置类AliOSSConfig——————————————————————————————
@Configuration//转为自动配置类
@EnableConfigurationProperties(AliOSSProperties.class)//导入AliOSSProperties类的bean
public class AliOSSConfig {
@Bean
public AliOSSUtils aliOSSUtils(AliOSSProperties aliOSSProperties) {
AliOSSUtils aliOSSUtils = new AliOSSUtils();
aliOSSUtils.setAliOSSProperties(aliOSSProperties);//赋值,原值从aliyunoss前缀的配置项封装过来
return aliOSSUtils;
}
}
再定义自动配置文件 META-INF/spring/xxx.imports
在resources目录下创建META-INF/spring两级目录。并在META下创建文件org.springframework.boot.autoconfigure.AutoConfiguration.imports,内部写上自动配置类的全类名com.aliyun.oss.aliyunoss.AliOSSConfig。
此时就已完成封装,我们在其他包中新建一个类,并在pom.xml中引入aliyun-oss-spring-boot-starter,因为依赖传递,其又会自动引入aliyun-oss-spring-boot-autoconfigure依赖,autoconfigure依赖又引入了aliyunOSS的相关依赖,所需依赖都依次传递下来了,同时为了给public class AliOSSProperties类中的各属性赋值,我们还需配置该类所在的yml文件:
<!-- 自行封装的依赖-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-oss-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
#阿里云配置yml文件
aliyun:
oss:
endpoint: https://oss-cn-beijing.aliyuncs.com # 阿里云OSS的端点
accessKeyId: LTAI5t6nivgHXQ1rnBt3dudV # 阿里云OSS的Access Key ID
accessKeySecret: EhrGz86soycvHNnK0V4PuZoDYgu4tm # 阿里云OSS的Access Key Secret
bucketName: chn-webapp # 阿里云OSS的Bucket名称
//其他包的类调用bean
@RestController
public class TestController {
@Autowired
public AliOSSUtils aliOSSUtils;
@PostMapping("/upload")
public String upload(MultipartFile file) throws IOException {
//调用文件上传方法到阿里云OSS,返回值为获取图片的url
String url= aliOSSUtils.upload(file);
return url;
}
}
此时再打开postman发起请求就可得到url:
总结
之前所学知识的分类图:
而SpringMVC就是Spring框架中的Web开发模块, SpringMVC、Spring框架、Mybatis共同组成了SSM,但因效率较低,可以直接基于SpringBoot进行开发。
Maven高级
之前我们了解了Maven的基本使用,这足已完成对于一些简单的项目的构建和管理,但需要开发一些中大型项目,这些知识便稍显不足。所以我们还需要学习一些Maven的高级功能。
分模块设计与开发
顾名思义就是在设计java项目时将其拆分成多个模块进行开发,以方便模块间的互相调用和资源共享。
回过头来看之前的员工管理系统,我们可以将各个功能都封装成一个模块,例如chnApp-pojo模块、chnApp-utils模块......然后将Controller、Saervice、Mapper三层封装成一个并引用其他模块的依赖即可。我们将原本的项目复制一份来进行操作。
新建模块chnApp-pojo,注意该模块仅存放实体类,所以不应选择基于SpringBoot,只需创建一个Maven模块即可:
下一步中有一选项Parent先选择为none,后文会介绍。
再将原本项目中的pojo包下的所有类复制到新项目中,建议包结构保持一致:
然后再在pom.xml文件中添加lombok依赖。此时该模块就已完成,原模块下的pojo包可以删除了。此时原项目会因为找不到对应文件而报错,我们需在原项目的pom.xml文件中引入依赖:
<!--原项目pojo文件中引入依赖-->
<dependency>
<groupId>com.chnMaven</groupId>
<artifactId>chn-pojo</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
此时相关工作就已完成,我们再以相同的方法新建模块chnApp-utils,这就完成了简单的模块拆分。
注意这只是为了演示如何进行模块拆分,实际开发中分模块设计应先分模块,再进行代码编写,而非像这样对已完成的项目进行拆分。
继承和聚合
此时已拆分为多模块,但问题也随之增多,为解决这些问题,就需要用到继承和聚合。
继承
模块之间的继承与java里类与类的继承类似,接下来我们来看如何实现。
在上文三个模块中,我们都引入了lombok的相关依赖,如果在大型项目中每个模块都配置一次该依赖,会非常臃肿。我们可以创建一个父工程,原先的三个工程都继承自该工程,此时三个工程中共有的依赖就可以定义在父工程之中,这就是继承。
继承在Maven中指的是父项目能够将配置信息传递给它的子项目。这允许子项目共享相同的配置,比如依赖版本、插件配置、属性等。实现需要在各个子工程的pom.xml文件中定义一个<parent>...</parent>来描述其父工程。
打开之前的SpringBoot工程,我们可以发现其pom.xml文件中都定义了一个父工程:
<!--SpringBoot项目共有的父工程-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
一、 创建Maven模块作为父工程,设置打包方式为pom(未设置的话默认为jar)。
创建与上文相同,只有名称不同,不再赘述,创建完成后在pom.xml文件中<version>下添加语句<packaging>pom</packaging>以设置其打包方式为pom。
Maven中常见的打包方式
- jar:普通模块打包,springboot项目基本都是jar包(内嵌tomcat运行)
- war:普通web程序打包,需要部署在外部的tomcat服务器中运行
- pom:父工程或聚合工程,该模块不写代码,仅进行依赖管理
原先的SpringBoot模块chnApplication已经默认继承了spring-boot-starter-parent,我们还需要使其继承新创建的模块chn-parent,但其和java一样只能单继承不能继承多个模块,我们可以和java一样通过多级继承来解决此问题,即chnApplication继承chn-parent模块,chn-parent模块再继承spring-boot-starter-parent模块。
在chn-parent的pom.xml文件中添加spring-boot-starter-parent工程依赖,其中<relativePath>代表父工程的相对路径,如果为默认属性则代表会从本地仓库中进行查找。
<!--父亲工程的pom.xml文件-->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.chnMaven</groupId>
<artifactId>chn-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>23</maven.compiler.source>
<maven.compiler.target>23</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
同时因为父工程只起到依赖管理的作用, 不写代码,所以src文件也可以删除。
二、在各个子工程的pom.xml文件中配置继承关系
上文提到过<relativePath>代表父工程的相对路径,而../代表从该文件向外退两层再寻找后文中指定的路径:
<parent>
<groupId>com.chnMaven</groupId>
<artifactId>chn-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../chn-parent/pom.xml</relativePath>
</parent>
因为groupId会继承自父类,因此在配置完继承关系后,原模块的groupId即可删除,其默认为父类的groupId。
三、在父工程中配置各个工程共有的依赖
目前只有lombok依赖重复,在父工程中添加该依赖,并删除子工程中的该依赖即可。
如果父工程和子工程中配置了相同的依赖但版本号不同,此时会以子工程中的版本号为准。
因为该案例中我们先有了项目,然后才进行模块区分,为方便理解将夫、父工程和子工程平级,在实际开发中,应保持子工程在父工程下一级目录中以彰显继承关系。
版本锁定
如果父工程中有依赖为部分几个模块共有而非全部模块共有,那么该依赖需要在每个模块中单独定义相同版本的依赖,如果需要更换依赖版本,因为模块过多很容易有所纰漏,这就要用到Maven的版本锁定功能来使所有的版本保持一致。
我们可以在父工程中定义标签<dependencyManagement>来统一管理模块,内部填写依赖相关信息。但要注意,这里仅仅只是指定该依赖的版本号,并未引入依赖,在子工程中还需引入该依赖,但无需再填写版本号,因为父工程中已指定版本号。
我们以utils中jwt的相关依赖为例:
<!--父工程-->
<dependencyManagement>
<dependencies>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
......
</dependencies>
</dependencyManagement>
<!--utils子工程-->
<dependencies>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
......
</dependencies>
此时如果需要更改版本号只需修改父类中的版本号即可,确保了整个所有子模块中该依赖的版本都相同。
但因为依赖过多,即使都在父类中,也很难快速找到我们想要修改的依赖,这就可以用到自定义属性/引用属性,其和application.properties的操作方法类似,在父模块中定义标签<properties>来统一管理依赖,内部填写依赖的版本号,子类中以${自定义名}来代替版本号,建议自定义名体现出与依赖的关系。
找到父工程中的<properties>标签,其已包含指定jdk版本号和编码格式的两条属性,我们再来添加需要添加的属性,然后修改父模块依赖中的版本号:
<properties>
<maven.compiler.source>23</maven.compiler.source>
<maven.compiler.target>23</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Lombok 版本 -->
<lombok.version>1.18.36</lombok.version>
<!-- JWT 版本 -->
<jjwt.version>0.9.1</jjwt.version>
......略
</properties>
<dependencyManagement>
<dependencies>
<!-- JWT 依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
......略
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
</dependencies>
同时原本的spring-boot-starter-web依赖在chn-parent的父模块spring-boot-starter-parent的父模块spring-boot-dependencies中已经进行了版本管理,因此我们无需再对其进行版本控制。
- <dependencyManagement>:统一管理依赖的版本号,而不实际引入这些依赖,还需在子工程中引入所需依赖(无需指定版本)。通常在多模块项目的父 pom.xml 中使用,以统一管理子模块的依赖版本。
- <dependencies>:直接依赖,如果在父工程中配置了依赖,则子工程会直接继承下来。
聚合
在分模块开发后,如果直接打包主程序chnApplication系统会报错,缺少对应的jar包,此时我们需要先将父模块和与其相关的子模块进行安装(可选择Maven面板上方的禁止图标以跳过测试),然后再对主程序进行打包。大型项目中有大量模块,各模块之间的关系错综复杂,此时操作就会变的非常繁琐。Maven的聚合即可解决该问题,实现项目的一键构建,包括编译、打包、安装等。
Maven 聚合是指将多个模块组合在一起进行统一构建的过程。在 Maven 中,这通常通过创建一个 “聚合模块”(也称为 “父模块”)来实现,要求该模块不包含实际的代码,而是用来管理一组相关的子模块。
在聚合模块中通过<modules>标签来指定所需聚合的模块,其中加上<module>内部写上需要聚合的模块,书写方式和<relativePath>中的路径相同。
<!--聚合其他模块-->
<modules>
<module>../chn-pojo</module>
<module>../chn-utils</module>
<module>../chnApplication</module>
</modules>
此时在Maven聚合模块chn-parent下执行package,其余子模块也会执行对应的命令。
私服
之前介绍过我们所拆分的模块是可以在各个项目组之间进行资源共享的,这个功能就需要通过Maven的私服来实现。
实现步骤就是A项目组开发一jar包,将其上传到私服,其他项目组按需下载,其相当于远程仓库。各项目组查找依赖的顺序:本地仓库-私服(远程仓库)-中央仓库。一般情况下,一个公司/项目只需一个私服,因此我们无需自己搭建,只需知道怎样使用即可。
私服一般包含很多仓库
- maven-central:代理仓库,用于从中央仓库或镜像仓库拉取jar包。
- maven-public:仓库组,默认包含其他几个Java仓库。
- maven-releases:正式发行版,用于存储正式发布的jar包。
- maven-snapshots:快照版,用于存储还未正式发布的jar包。
在pom.xml文件中我们可以看到我们创建的模块默认为SNAPSHOT快照版,如果将SNAPSHOT改为RELEASE或者不写,其都会上传到RELEASE仓库中。
<artifactId>chn-app</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>chn-app</name>
上传
想要上传,首先需要将模块打包成jar包存放到本地仓库,然后通过Maven的指令deploy将jar包从本地仓库中的jar包发布到私服当中。
首先访问私服需要一定的访问权限,我们需要在Maven中配置访问私服的用户名和密码,并在项目中指定上传资源的位置(url地址)。
打开maven的settings.xml配置文件中,做如下配置:
一、servers标签中,配置访问私服的个人凭证(访问的用户名和密码)
<servers>
<server>
<id>maven-releases</id>
<username>admin</username>
<password>admin</password>
</server>
<server>
<id>maven-snapshots</id>
<username>admin</username>
<password>admin</password>
</server>
</servers>
二、在mirrors标签中配置我们自己私服的连接地址
<mirror>
<id>maven-public</id>
<mirrorOf>*</mirrorOf>
<url>http://192.168.150.101:8081/repository/maven-public/</url>
</mirror>
三、在profiles标签中,增加如下配置,来允许使用snapshot快照版本的依赖,
<profile>
<id>allow-snapshots</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<repositories>
<repository>
<id>maven-public</id>
<url>http://192.168.150.101:8081/repository/maven-public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>
此时上传所需的相关配置都已实现。
下载
想要从私服中下载jar包到本地仓库,我们需要首先知道私服的地址,即在Maven中配置私服的地址(url地址),这样项目便可直接调用本地仓库中的jar包。
在项目的父模块的pom.xml文件中,增加如下配置,来配置项目发布的地址(也就是私服的地址):
<distributionManagement>
<!-- release版本的发布地址 -->
<repository>
<id>maven-releases</id>
<url>http://192.168.150.101:8081/repository/maven-releases/</url>
</repository>
<!-- snapshot版本的发布地址 -->
<snapshotRepository>
<id>maven-snapshots</id>
<url>http://192.168.150.101:8081/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
注意,这里的两个仓库id需与servers标签中,访问私服的个人凭证中的仓库id相同。
然后直接运行 deploy 生命周期即可完成上传(上传之前建议跳过单元测试)