【JavaWeb】SpringBoot原理
1 配置优先级
在前面,已经学习了SpringBoot项目当中支持的三类配置文件:
application.properties
application.yml
application.yaml
在SpringBoot项目当中,我们要想配置一个属性,通过这三种方式当中的任意一种来配置都可以,那么如果项目中同时存在这三种配置文件,且都配置了同一个属性,如:Tomcat端口号,到底哪一份配置文件生效呢?
application.properties
server.port=8081
application.yml
server:port: 8083
application.yaml
server:port: 8082
我们启动SpringBoot程序,测试下三个配置文件中哪个Tomcat端口号生效:
properties、yaml、yml三种配置文件同时存在。 配置好了,启动服务,测试一下:
properties、yaml、yml三种配置文件,优先级最高的是properties
yaml、yml两种配置文件同时存在
yaml、yml 两种配置文件,优先级最高的是yml。
配置文件优先级排名(从高到低):
properties配置文件
yml配置文件
yaml配置文件
注:虽然springboot支持多种格式配置文件,但是在项目开发时,推荐统一使用一种格式的配置。(yml是主流)
在SpringBoot项目当中除了以上3种配置文件外,SpringBoot为了增强程序的扩展性,除了支持配置文件的配置方式以外,还支持另外两种常见的配置方式:
Java系统属性配置 (格式: -Dkey=value)
-Dserver.port=9000
命令行参数 (格式:--key=value)
--server.port=10010
那在idea当中运行程序时,如何来指定Java系统属性和命令行参数呢?
编辑启动程序的配置信息
打开之后,选择 Modify options , 选择 Add VM options, Program arguments
重启服务,同时配置Tomcat端口(application.properties、系统属性、命令行参数),测试哪个Tomcat端口号生效:
说明,命令行参数的优先级时最高的,同时配置的情况下,命令行参数的配置项生效。
删除命令行参数配置,重启SpringBoot服务:
五种配置方式的优先级: 命令行参数 > 系统属性参数 > properties参数 > yml参数 > yaml参数
思考:如果项目已经打包上线了,这个时候我们又如何来设置Java系统属性和命令行参数呢?
java -Dserver.port=9000 -jar XXXXX.jar --server.port=10010
下面我们来演示下打包程序运行时指定Java系统属性和命令行参数:
1. 执行maven打包指令package,把项目打成jar文件
2. 使用命令:java -jar 方式运行jar文件程序。
同时设置Java系统属性和命令行参数
注:Springboot项目进行打包时,需要引入插件
spring-boot-maven-plugin
(基于官网骨架创建项目,会自动添加该插件)
在SpringBoot项目当中,常见的属性配置方式有5种, 3种配置文件,加上2种外部属性的配置(Java系统属性、命令行参数)。通过以上的测试,我们也得出了优先级(从低到高):
application.yaml(忽略)
application.yml
application.properties
java系统属性(-Dxxx=xxx)
命令行参数(--xxx=xxx)
2 Bean的管理
在前面,我们已经讲过了我们可以通过Spring当中提供的注解@Component以及它的三个衍生注解(@Controller、@Service、@Repository)来声明IOC容器中的bean对象,同时我们也学习了如何为应用程序注入运行时所需要依赖的bean对象,也就是依赖注入DI。
我们今天主要学习IOC容器中Bean的其他使用细节,主要学习以下两方面:
bean的作用域配置
管理第三方的bean对象
接下来我们先来学习第一方面,Bean的作用域。
2.1 Bean的作用域
在前面我们提到的IOC容器当中,默认bean对象是单例的 (只有一个实例对象)。在Spring中支持五种作用域,后三种在web环境才生效:
作用域 | 说明 |
---|---|
singleton | 容器内同名称的bean只有一个实例(单例)(默认) |
prototype | 每次使用该bean时会创建新的实例(非单例) |
request | 每个请求范围内会创建新的实例(web环境中,了解) |
session | 每个会话范围内会创建新的实例(web环境中,了解) |
application | 每个应用范围内会创建新的实例(web环境中,了解) |
知道了bean的5种作用域了,我们要怎么去设置一个bean的作用域呢?
可以借助Spring中的@Scope注解来进行配置作用域
1). 测试一
控制器
//默认bean的作用域为:singleton (单例)
@RestController
@RequestMapping("/depts")
public class DeptController {@Autowiredprivate DeptService deptService;public DeptController(){System.out.println("DeptController constructor ....");}//省略其他代码...
}
测试类
@SpringBootTest
class SpringbootWebConfig2ApplicationTests {@Autowiredprivate ApplicationContext applicationContext; //IOC容器对象//bean的作用域@Testpublic void testScope(){for (int i = 0; i < 10; i++) {DeptController deptController = applicationContext.getBean(DeptController.class);System.out.println(deptController);}}
}
重启SpringBoot服务,运行测试方法,查看控制台打印的日志:
注意事项:
IOC容器中的bean默认使用的作用域:singleton (单例)
默认singleton的bean,在容器启动时被创建,可以使用@Lazy注解来延迟初始化(延迟到第一次使用时)
2). 测试二
修改控制器DeptController代码:
@Scope("prototype") //bean作用域为非单例
@RestController
@RequestMapping("/depts")
public class DeptController {@Autowiredprivate DeptService deptService;public DeptController(){System.out.println("DeptController constructor ....");}//省略其他代码...
}
重启SpringBoot服务,再次执行测试方法,查看控制台打印的日志:
注:
prototype的bean,每一次使用该bean的时候都会创建一个新的实例
实际开发当中,绝大部分的Bean是单例的,也就是说绝大部分Bean不需要配置scope属性
默认singleton的bean,在容器启动时被创建,可以使用@Lazy注解来延迟初始化(延迟到第一次使用时)
prototype的bean,每一次使用该bean的时候都会创建一个新的实例。
实际开发当中,绝大部分的bean是单例的,也就是说绝大部分bean不需要配置scope属性。
2.2 第三方的Bean
学习完bean的获取、bean的作用域之后,接下来我们再来学习第三方bean的配置。
之前我们所配置的bean,像controller、service,dao三层体系下编写的类,这些类都是我们在项目当中自己定义的类(自定义类)。当我们要声明这些bean,也非常简单,我们只需要在类上加上@Component以及它的这三个衍生注解(@Controller、@Service、@Repository),就可以来声明这个bean对象了。
但是在我们的项目开发过程当中,还有一种情况就是这个类它并不是我们自己编写的,而是在我们引入的第三方依赖当中提供的,那么此时我们是无法使用 @Component 以及他的衍生注解来声明bean的,这种情况下就需要使用@Bean注解来声明bean 了。
演示1:
在启动类中直接声明这个Bean。比如:我们可以将我们之前使用的阿里云OSS操作的工具类,基于@Bean注解的方式来声明Bean。
import org.example.utils.AliyunOSSOperator;
import org.example.utils.AliyunOSSProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;@ServletComponentScan
@EnableScheduling
@SpringBootApplication
public class TliasWebManagementApplication {public static void main(String[] args) {SpringApplication.run(TliasWebManagementApplication.class, args);}@Beanpublic AliyunOSSOperator aliyunOSSOperator(AliyunOSSProperties ossProperties) {return new AliyunOSSOperator(ossProperties);}
}
测试:
package org.example;import cn.hutool.core.io.FileUtil;
import org.example.utils.AliyunOSSOperator;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import java.io.File;
import java.util.List;
import java.util.Set;@SpringBootTest
class TliasWebManagementApplicationTests {@Autowiredprivate AliyunOSSOperator aliyunOSSOperator;@Testvoid testUploadFiles() throws Exception {// 上传文件, 本地文件 C:\Users\deng\Pictures\6.jpgbyte[] content = FileUtil.readBytes(new File("C:\\Users\\deng\\Pictures\\6.jpg"));String url = aliyunOSSOperator.upload(content, "6.jpg");System.out.println(url);}@Testvoid testListFiles() throws Exception {// 获取文件列表List<String> objectNameList = aliyunOSSOperator.listFiles();objectNameList.forEach(System.out::println);}@Testvoid testDelFiles() throws Exception {// 删除文件aliyunOSSOperator.deleteFile("2024/06/43b48632-3a05-4e1d-a00d-96f2d57b2a84.jpg");}}
演示2:
如果要管理的第三方 bean 对象,建议对这些bean进行集中分类配置,可以通过 @Configuration 注解声明一个配置类。【推荐】
package org.example.config;import org.example.utils.AliyunOSSOperator;
import org.example.utils.AliyunOSSProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class OSSConfig {@Beanpublic AliyunOSSOperator aliyunOSSOperator(AliyunOSSProperties ossProperties) {return new AliyunOSSOperator(ossProperties);}
}
通过@Bean注解的name 或 value属性可以声明bean的名称,如果不指定,默认bean的名称就是方法名。
如果第三方bean需要依赖其他bean对象,直接在bean定义方法中设置形参即可,容器会根据类型自动装配。
3 SpringBoot原理
经过前面的学习,大家会发现基于SpringBoot进行web程序的开发是非常简单、非常高效的。
SpringBoot使我们能够集中精力地去关注业务功能的开发,而不用过多地关注框架本身的配置使用。而我们前面所讲解的都是面向应用层面的技术,接下来我们开始学习SpringBoot的原理,这部分内容偏向于底层的原理分析。
在剖析SpringBoot的原理之前,我们先来快速回顾一下我们前面所讲解的Spring家族的框架。
Spring是目前世界上最流行的Java框架,它可以帮助我们更加快速、更加容易的来构建Java项目。而在Spring家族当中也提供了很多优秀的框架,而所有的框架都是基于一个基础框架的也就是著名的SpringFramework(也就是Spring框架)。而前面我们也提到,如果我们直接基于Spring框架进行项目的开发,会比较繁琐。
这个繁琐主要体现在两个地方:
在pom.xml中依赖配置比较繁琐,在项目开发时,需要自己去找到对应的依赖,还需要找到依赖它所配套的依赖以及对应版本,否则就会出现版本冲突问题。
在使用Spring框架进行项目开发时,需要在Spring的配置文件中做大量的配置,这就造成Spring框架入门难度较大,学习成本较高。
基于Spring存在的问题,官方在Spring框架4.0版本之后,又推出了一个全新的框架:SpringBoot。
通过 SpringBoot来简化Spring框架的开发(是简化不是替代)。我们直接基于SpringBoot来构建Java项目,会让我们的项目开发更加简单,更加快捷。
SpringBoot框架之所以使用起来更简单更快捷,是因为SpringBoot框架底层提供了两个非常重要的功能:一个是起步依赖,一个是自动配置。
通过SpringBoot所提供的起步依赖,就可以大大的简化pom文件当中依赖的配置,从而解决了Spring框架当中依赖配置繁琐的问题。
通过自动配置的功能就可以大大的简化框架在使用时bean的声明以及bean的配置。我们只需要引入程序开发时所需要的起步依赖,项目开发时所用到常见的配置都已经有了,我们直接使用就可以了。
简单回顾之后,接下来我们来学习下SpringBoot的原理。其实学习SpringBoot的原理就是来解析SpringBoot当中的起步依赖与自动配置的原理。我们首先来学习SpringBoot当中起步依赖的原理。
3.1 起步依赖
假如我们没有使用SpringBoot,用的是Spring框架进行web程序的开发,此时我们就需要引入web程序开发所需要的一些依赖。
当我们引入了 spring-boot-starter-web 之后,maven会通过依赖传递特性,将web开发所需的常见依赖都传递下来。
所以,起步依赖的原理就是Maven的依赖传递。
在SpringBoot给我们提供的这些起步依赖当中,已提供了当前程序开发所需要的所有的常见依赖(官网地址:https://docs.spring.io/spring-boot/docs/2.7.7/reference/htmlsingle/#using.build-systems.starters)。
比如:springboot-starter-web,这是web开发的起步依赖,在web开发的起步依赖当中,就集成了web开发中常见的依赖:json、web、webmvc、tomcat等。我们只需要引入这一个起步依赖,其他的依赖都会自动的通过Maven的依赖传递进来。
3.2 自动配置
SpringBoot的自动配置就是当spring容器启动后,一些配置类、bean对象就自动存入到了IOC容器中,不需要我们手动去声明,从而简化了开发,省去了繁琐的配置操作。
比如,在我们前面讲解AOP记录日志的那个案例中,我们要将一个对象转为json,直接注入一个Gson,然后就可以直接使用了。而我们在我们整个项目中,也并未配置Gson这个类型的bean,为什么可以直接注入使用呢? 原因就是因为这个bean,springboot中已经帮我们自动配置完毕了,我们是可以直接使用的。
那接下来,我们就要来解析,SpringBoot中到底是如何完成自动配置的。
3.2.1 实现方案
我们知道了什么是自动配置之后,接下来我们就要来剖析自动配置的原理。解析自动配置的原理就是分析在 SpringBoot项目当中,我们引入对应的依赖之后,是如何将依赖jar包当中所提供的bean以及配置类直接加载到当前项目的SpringIOC容器当中的。
接下来,我们就直接通过代码来分析自动配置原理。
准备工作:在Idea中导入"资料\03. 自动配置原理" 下的 itheima-utils 工程
SpringBoot项目 spring-boot-web-config 工程中,通过坐标引入itheima-utils依赖
1、引入的 itheima-utils 中配置如下:
@Component
public class TokenParser {public void parse(){System.out.println("TokenParser ... parse ...");}
}
2、在测试类中,添加测试方法
@SpringBootTest
public class AutoConfigurationTests {@Autowiredprivate ApplicationContext applicationContext;@Testpublic void testTokenParse(){System.out.println(applicationContext.getBean(TokenParser.class));}//省略其他代码...
}
3、执行测试方法
异常信息描述: 没有com.example.TokenParse类型的bean
说明:在Spring容器中没有找到com.example.TokenParse类型的bean对象
思考:引入进来的第三方依赖当中的bean以及配置类为什么没有生效?
-
原因在我们之前讲解IOC的时候有提到过,在类上添加@Component注解来声明bean对象时,还需要保证@Component注解能被Spring的组件扫描到。
-
SpringBoot项目中的@SpringBootApplication注解,具有包扫描的作用,但是它只会扫描启动类所在的当前包以及子包。
-
当前包:org.example, 第三方依赖中提供的包:com.example(扫描不到)
那么如何解决以上问题的呢?
-
方案1:@ComponentScan组件扫描
-
方案2:@Import 导入(使用@Import导入的类会被Spring加载到IOC容器中)
3.2.1.1 @ComponentScan组件扫描
@SpringBootApplication
@ComponentScan({"org.example","com.example"}) //指定要扫描的包
public class SpringbootWebConfigApplication {public static void main(String[] args) {SpringApplication.run(SpringbootWebConfigApplication.class, args);}
}
重新执行测试方法,控制台日志输出:
大家可以想象一下,如果采用以上这种方式来完成自动配置,那我们进行项目开发时,当需要引入大量的第三方的依赖,就需要在启动类上配置N多要扫描的包,这种方式会很繁琐。而且这种大面积的扫描性能也比较低。
缺点:
使用繁琐
性能低
结论:SpringBoot中并没有采用以上这种方案。
3.2.1.2 方案2:@Import 导入
@Import导入
导入形式主要有以下几种:
导入普通类
导入配置类
导入ImportSelector接口实现类
1). 使用@Import导入普通类:
@Import(TokenParser.class) //导入的类会被Spring加载到IOC容器中
@SpringBootApplication
public class SpringbootWebConfigApplication {public static void main(String[] args) {SpringApplication.run(SpringbootWebConfigApplication.class, args);}
}
重新执行测试方法,控制台日志输出全类名。
2). 使用@Import导入配置类:
配置类
@Configuration
public class HeaderConfig {@Beanpublic HeaderParser headerParser(){return new HeaderParser();}@Beanpublic HeaderGenerator headerGenerator(){return new HeaderGenerator();}
}
启动类:
@Import(HeaderConfig.class) //导入配置类
@SpringBootApplication
public class SpringbootWebConfig2Application {public static void main(String[] args) {SpringApplication.run(SpringbootWebConfig2Application.class, args);}
}
测试类:
@SpringBootTest
public class AutoConfigurationTests {@Autowiredprivate ApplicationContext applicationContext;@Testpublic void testHeaderParser(){System.out.println(applicationContext.getBean(HeaderParser.class));}@Testpublic void testHeaderGenerator(){System.out.println(applicationContext.getBean(HeaderGenerator.class));}//省略其他代码...
}
执行测试方法,看到控制台输出全类名。
3). 使用@Import导入ImportSelector接口实现类:
ImportSelector接口实现类
public class MyImportSelector implements ImportSelector {public String[] selectImports(AnnotationMetadata importingClassMetadata) {//返回值字符串数组(数组中封装了全限定名称的类)return new String[]{"com.example.HeaderConfig"};}
}
启动类:
@Import(MyImportSelector.class) //导入ImportSelector接口实现类
@SpringBootApplication
public class SpringbootWebConfig2Application {public static void main(String[] args) {SpringApplication.run(SpringbootWebConfig2Application.class, args);}
}
执行测试方法,看到控制台输出全类名。
我们使用@Import注解通过这三种方式都可以导入第三方依赖中所提供的bean或者是配置类。
思考:如果基于以上方式完成自动配置,当要引入一个第三方依赖时,是不是还要知道第三方依赖中有哪些配置类和哪些Bean对象?
答案:是的。 (对程序员来讲,很不友好,而且比较繁琐)
思考:当我们要使用第三方依赖,依赖中到底有哪些bean和配置类,谁最清楚?
答案:第三方依赖自身最清楚。
结论:我们不用自己指定要导入哪些bean对象和配置类了,让第三方依赖它自己来指定。
怎么让第三方依赖自己指定bean对象和配置类?
比较常见的方案就是第三方依赖给我们提供一个注解,这个注解一般都以@EnableXxxx开头的注解,注解中封装的就是@Import注解
4). 使用第三方依赖提供的 @EnableXxxxx注解
第三方依赖中提供的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)//指定要导入哪些bean对象或配置类
public @interface EnableHeaderConfig {
}
在使用时只需在启动类上加上@EnableXxxxx注解即可
@EnableHeaderConfig //使用第三方依赖提供的Enable开头的注解
@SpringBootApplication
public class SpringbootWebConfig2Application {public static void main(String[] args) {SpringApplication.run(SpringbootWebConfig2Application.class, args);}
}
执行测试方法,看到控制台输出全类名。
以上四种方式都可以完成导入操作,但是第4种方式会更方便更优雅,而这种方式也是SpringBoot当中所采用的方式。
3.2.2 自定义starter
3.2.2.1 分析
前面我们解析了SpringBoot中自动配置的原理,下面我们就通过一个自定义starter案例来加深大家对于自动配置原理的理解。首先介绍一下自定义starter的业务场景,再来分析一下具体的操作步骤。
所谓starter指的就是SpringBoot当中的起步依赖。在SpringBoot当中已经给我们提供了很多的起步依赖了,我们为什么还需要自定义 starter 起步依赖?
这是因为在实际的项目开发当中,我们可能会用到很多第三方的技术,并不是所有的第三方的技术官方都给我们提供了与SpringBoot整合的starter起步依赖,但是这些技术又非常的通用,在很多项目组当中都在使用。
业务场景:
我们前面案例当中所使用的阿里云OSS对象存储服务,现在阿里云的官方是没有给我们提供对应的起步依赖的,这个时候使用起来就会比较繁琐,我们需要引入对应的依赖。我们还需要在配置文件当中进行配置,还需要基于官方SDK示例来改造对应的工具类,我们在项目当中才可以进行使用。
大家想在我们当前项目当中使用了阿里云OSS,我们需要进行这么多步的操作。在别的项目组当中要想使用阿里云OSS,是不是也需要进行这么多步的操作,所以这个时候我们就可以自定义一些公共组件,在这些公共组件当中,我就可以提前把需要配置的bean都提前配置好。将来在项目当中,我要想使用这个技术,我直接将组件对应的坐标直接引入进来,就已经自动配置好了,就可以直接使用了。我们也可以把公共组件提供给别的项目组进行使用,这样就可以大大的简化我们的开发。
在SpringBoot项目中,一般都会将这些公共组件封装为SpringBoot当中的starter,也就是我们所说的起步依赖。
而在springboot中,官方提供的起步依赖 或 第三方提供的起步依赖,基本都会包含两个模块,如下所示:
其中,spring-boot-starter 或 xxx-spring-boot-starter 这个模块主要是依赖管理的功能。 而 spring-boot-autoconfigure 或 xxxx-spring-boot-autoconfigure 主要是起到自动配置的作用,自动配置的核心代码就在这个模块中编写。
SpringBoot官方starter命名: spring-boot-starter-xxxx
第三组织提供的starter命名: xxxx-spring-boot-starter
而自动配置模块的核心,就是编写自动配置的核心代码,然后将自动配置的核心类,配置在核心的配置文件 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中。 配置如下:
SpringBoot官方的自动配置依赖 spring-boot-autoconfiure 中就提供了配置类,并且也提供了springboot会自动读取的配置文件。当SpringBoot项目启动时,会读取到META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports配置文件中的配置类并加载配置类,生成相关bean对象注册到IOC容器中。
结果:我们可以直接在SpringBoot程序中使用自动配置的
在自定义一个起步依赖starter的时候,按照规范需要定义两个模块:
starter模块(进行依赖管理[把程序开发所需要的依赖都定义在starter起步依赖中])
autoconfigure模块(自动配置)
将来在项目当中进行相关功能开发时,只需要引入一个起步依赖就可以了,因为它会将autoconfigure自动配置的依赖给传递下来。
3.2.2.2 需求
上面我们简单介绍了自定义starter的场景,以及自定义starter时涉及到的模块之后,接下来我们就来完成一个自定义starter的案例。
需求:自定义aliyun-oss-spring-boot-starter,完成阿里云OSS操作工具类 AliyunOSSOperator 的自动配置。
目标:引入起步依赖引入之后,要想使用阿里云OSS,注入AliyunOSSOperator 直接使用即可。
之前我们的用法:
1). 在pom.xml中引入阿里云oss的所有依赖
<!--阿里云OSS-->
<dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.17.4</version>
</dependency>
<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>
2). application.yml 中配置阿里云OSS的配置信息
#阿里云oss配置
aliyun:oss:endpoint: https://oss-cn-beijing.aliyuncs.combucketName: ******
3). 定义实体类封装配置信息
package org.example.utils;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliyunOSSProperties {private String endpoint;private String bucketName;
}
4). 定义工具类AliyunOSSOperator
package org.example.utils;import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
import com.aliyun.oss.model.OSSObjectSummary;
import com.aliyun.oss.model.ObjectListing;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.io.ByteArrayInputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;@Component
public class AliyunOSSOperator {@Autowiredprivate AliyunOSSProperties aliyunOSSProperties;/*** 文件上传*/public String upload(byte[] content, String originalFilename) throws Exception {String endpoint = aliyunOSSProperties.getEndpoint();String bucketName = aliyunOSSProperties.getBucketName();// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();// 填写Object完整路径,例如202406/1.png。Object完整路径中不能包含Bucket名称。//获取当前系统日期的字符串,格式为 yyyy/MMString dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));//根据原始文件名originalFilename, 生成一个新的不重复的文件名String newFileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));String objectName = dir + "/" + newFileName;// 创建OSSClient实例。OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);//文件上传try {ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content));} finally {if (ossClient != null) {ossClient.shutdown();}}return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;}/*** 查询文件列表*/public List<String> listFiles() throws Exception {String endpoint = aliyunOSSProperties.getEndpoint();String bucketName = aliyunOSSProperties.getBucketName();// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();// 指定前缀,例如exampledir/object。String keyPrefix = null;// 创建OSSClient实例。OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);try {// 列举文件。如果不设置keyPrefix,则列举存储空间下的所有文件。如果设置keyPrefix,则列举包含指定前缀的文件。ObjectListing objectListing = ossClient.listObjects(bucketName, keyPrefix);List<OSSObjectSummary> sums = objectListing.getObjectSummaries();if(sums != null && !sums.isEmpty()){return sums.stream().map(OSSObjectSummary::getKey).collect(Collectors.toList());}} finally {if (ossClient != null) {ossClient.shutdown();}}return null;}/*** 删除指定对象*/public void deleteFile(String objectName) throws Exception {String endpoint = aliyunOSSProperties.getEndpoint();String bucketName = aliyunOSSProperties.getBucketName();// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();// 创建OSSClient实例。OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);try {// 删除文件或目录。如果要删除目录,目录必须为空。ossClient.deleteObject(bucketName, objectName);} finally {if (ossClient != null) {ossClient.shutdown();}}}}
5). 其他地方要使用阿里云OSS,注入工具类,再使用
@Slf4j
@RestController
public class UploadController {@Autowiredprivate AliyunOSSOperator aliyunOSSOperator;/*** 文件上传*/@PostMapping("/upload")public Result upload(MultipartFile file) throws Exception {log.info("上传文件:{}",file.getOriginalFilename());//调用aliyun OSS进行文件上传String url = aliyunOSSOperator.upload(file.getBytes(), file.getOriginalFilename());//返回结果return Result.success(url);}
}
我们可以看到,在项目中使用阿里云OSS,需要这么五步操作,而阿里云OSS这个云服务还是非常常见的,很多项目中都要使用。
大家再思考,现在我们使用阿里云OSS,需要做这么几步,将来大家在开发其他的项目的时候,你使用阿里云OSS,这几步你要不要做?当团队中其他小伙伴也在使用阿里云OSS的时候,步骤 不也是一样的。
所以这个时候我们就可以制作一个公共组件(自定义starter)。starter定义好之后,将来要使用阿里云OSS进行文件上传,只需要将起步依赖引入进来之后,就可以直接注入 AliyunOSSOperator 使用了。
3.2.2.3 实现
需求明确之后,接下来我们再来分析一下具体的实现步骤:
第1步:创建自定义starter模块 aliyun-oss-spring-boot-starter(进行依赖管理)
把阿里云OSS所有的依赖统一管理起来
第2步:创建autoconfigure模块 aliyun-oss-spring-boot-autoconfigure
在starter中引入autoconfigure (我们使用时只需要引入starter起步依赖即可)
第3步:在autoconfigure模块aliyun-oss-spring-boot-autoconfigure中完成自动配置
1. 定义一个自动配置类,在自动配置类中将所要配置的bean都提前配置好
2. 定义配置文件,把自动配置类的全类名定义在配置文件(META-INF/spring/xxxx.imports)中
我们分析完自定义阿里云OSS自动配置的操作步骤了,下面我们就按照分析的步骤来实现自定义starter。
首先我们先来创建两个Maven模块:
1). 创建 aliyun-oss-spring-boot-starter
选择springboot的版本,不需要勾选任何的依赖。直接点击 create 创建项目。
创建完starter模块后,删除多余的文件,只保留一个pom.xml文件。
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 https://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.5</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.aliyun.oss</groupId><artifactId>aliyun-oss-spring-boot-starter</artifactId><version>0.0.1-SNAPSHOT</version><name>aliyun-oss-spring-boot-starter</name><description>aliyun-oss-spring-boot-starter</description><properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-oss-spring-boot-autoconfigure</artifactId><version>0.0.1-SNAPSHOT</version></dependency></dependencies></project>
2). 创建 aliyun-oss-spring-boot-autoconfigure 模块
步骤大体同上
创建完starter模块后,删除多余的文件,只保留 src 和 pom.xml 。最终保留内容如下:
该模块的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 https://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.5</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.aliyun.oss</groupId><artifactId>aliyun-oss-spring-boot-autoconfigure</artifactId><version>0.0.1-SNAPSHOT</version><name>aliyun-oss-spring-boot-autoconfigure</name><description>aliyun-oss-spring-boot-autoconfigure</description><properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><!--阿里云依赖--><dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.17.4</version></dependency><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></dependencies></project>
按照我们之前的分析,是需要在starter模块中来引入autoconfigure这个模块的。打开starter模块中的pom文件:
前两步已经完成了,接下来最关键的就是第三步,也就是在aliyun-oss-spring-boot-autoconfigure模块当中来完成自动配置操作。
我们将之前案例中所使用的阿里云OSS部分的代码直接拷贝到autoconfigure模块下,然后进行改造就行了。
拷贝过来后,还缺失一些相关的依赖,需要把相关依赖也拷贝过来:
那此时,大家思考下,在类上添加的 @Component 注解还有用吗?
答案:没用了。 在SpringBoot项目中,并不会去扫描com.aliyun.oss这个包,不扫描这个包那类上的注解也就失去了作用。
@Component注解不需要使用了,可以从类上删除了。
1). 删除 AliyunOSSOperator 工具类上的 @Component 注解 和 @Autowired 注解。
2). 删除 AliyunOSSProperties 实体类上的 @Component 注解。
删除后报红色错误,暂时不理会,后面再来处理。
3). 既然不能用 @Component 注解声明bean,那就需要按照 starter 的定义规范,定义一个自动配置类,在自动配置类中声明bean。
下面我们就要定义一个自动配置类 AliOSSAutoConfiguration 了,在自动配置类当中来声明 AliOSSOperator 的bean对象。
具体代码如下:
package com.aliyun.oss;import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
@EnableConfigurationProperties(AliyunOSSProperties.class)
public class AliyunOSSAutoConfiguration {@Beanpublic AliyunOSSOperator aliyunOSSOperator(AliyunOSSProperties aliyunOSSProperties) {return new AliyunOSSOperator(aliyunOSSProperties);}}
AliyunOSSOperator 的代码中需要增加一个有参构造,将 AliyunOSSProperties 对象传递给工具类。代码改造如下:
4). 在 aliyun-oss-spring-boot-autoconfigure 模块中的resources下,新建自动配置文件 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
将自动配置类的全类名,配置在文件中,这样在springboot启动的时候,就会加载到这份文件,并加载到其中的配置类了。
配置内容如下:
com.aliyun.oss.AliyunOSSAutoConfiguration
到此,这个 aliyun-oss-spring-boot-stater 就定义好了,哪里要想使用,就可以直接导入依赖,直接注入使用了。
3.2.2.4 测试
阿里云OSS的starter我们刚才已经定义好了,接下来我们就来做一个测试。
今天的课程资料当中,提供了一个自定义starter的测试工程。我们直接打开文件夹,里面有一个测试工程。测试工程就是
springboot-autoconfiguration-test
,我们只需要将测试工程直接导入到Idea当中即可。
测试前准备:
1). 在导入的test工程中引入阿里云starter依赖
<dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-oss-spring-boot-starter</artifactId><version>0.0.1-SNAPSHOT</version>
</dependency>
2). 在导入的test工程中的 application.yml
中配置阿里云OSS的配置信息
aliyun:oss:endpoint: https://oss-cn-beijing.aliyuncs.combucketName: java422-web-ai
3). 在test工程中的 UploadController
类编写代码
package com.itheima.controller;import com.aliyun.oss.AliyunOSSOperator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;@RestController
public class UploadController {@Autowiredprivate AliyunOSSOperator aliyunOSSOperator;@PostMapping("/upload")public String upload(MultipartFile image) throws Exception {//上传文件到阿里云 OSSString url = aliyunOSSOperator.upload(image.getBytes(), image.getOriginalFilename());System.out.println(url);return url;}}
编写完代码后,我们启动当前的SpringBoot测试工程,使用Apifox工具进行文件上传:
这样,我们就完成了starter的定义。在其他项目中要想使用,只需要引入依赖,配置一下阿里云OSS的信息,就可以直接注入使用了 。