【全面教程】SpringBoot 多种读取配置文件中参数的方式
共 22672字,需浏览 46分钟
·
2021-02-04 07:33
👆点 Stephen 订 阅 关 注
作者:超级小豆丁
http://www.mydlq.club/article/61
系统环境:
SpringBoot 版本:2.2.2
一、简介
在日常开发使用 SpringBoot 框架时,经常有一些配置信息需要放置到配置文件中,我们需要手动读取这些配置到应用中进行一些逻辑,这里整理了一些常用读取配置的方法,简单介绍一下。
1、SpringBoot 中常用读取配置方法
SpringBoot 中常用的读取配置方法有:
使用 @Value 注解读取配置
使用 @ConfigurationProperties 注解读取配置
使用 Environment 对象读取配置
使用 PropertiesLoaderUtils 工具读取配置
2、@Value 和 @ConfigurationProperties 的区别
下面会详细介绍使用各个方法是如何读取配置信息。
二、使用 @Value 读取配置
1、@Value 读取配置参数
application.properties 配置文件内容:
my.name=mydlq
my.age=18
使用 @Value 读取配置文件
@Component
public class ReadProperties {
@Value("${my.name}")
private String name;
@Value("${my.age}")
private Integer age;
}
并且还可以设置一个默认值,放置未从配置文件中读取到该参数:
通过 @Value 读取配置文件
@Component
public class ReadProperties {
@Value("${my.name:默认姓名}")
private String name;
@Value("${my.age:18}")
private Integer age;
}
2、@Value 给参数设定值
使用 @Value 注解给参数设定值,达到跟“=”号一样的赋值效果:
@Component
public class ReadProperties {
@Value("#{'test value'}")
private String value;
}
3、@Value 读取系统属性
使用 @Value 注解读取系统环境参数:
@Component
public class ReadProperties {
@Value("#{systemProperties['os.name']}")
private String systemPropertiesName;
}
4、@Value 读取 Bean 的属性
测试用的实体对象:
@Data
public class User{
private String name;
private String age;
}
使用 @Value 注解读取 Bean 中对象的属性:
@Component
public class ReadProperties {
@Bean
public User user(){
User user = new User();
user.setName("测试");
user.setAge("18");
return user;
}
@Value("#{user.name}")
private String value;
}
5、@Value 使用 SpEL 表达式
在 @Value 注解中可以使用 SpEL 表达式,如下是使用 SpEL 表达式生成随机数:
@Component
public class ReadProperties {
@Value("#{ T(java.lang.Math).random() * 100.0 }")
private double random;
}
6、@Value 读取 Resource 资源文件
使用 @Value 可以读取资源文件进行一些操作:
@Component
public class ReadProperties {
@Value("classpath:application.properties")
private Resource resourceFile;
public void test(){
// 如果文件存在,就输出文件名称
if(resourceFile.exists()){
System.out.println(resourceFile.getFilename());
}
}
}
三、使用 @ConfigurationProperties 读取配置
1、@ConfigurationProperties 读取配置参数到 String 类型
application.properties 配置文件内容:
my.name=mydlq
使用 @ConfigurationProperties 注解读取对应配置:
@Configuration
@ConfigurationProperties(prefix = "my") //配置 prefix 来过滤对应前缀
public class ConfigurationReadConfig {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
注意:使用 @ConfigurationProperties 注解读取配置,则需要配置文件内容中的参数添加统一的前缀,在 @ConfigurationProperties 注解中配置该前缀的值,然后前缀后的属性名要与加 @ConfigurationProperties 注解的类中成员变量名称保持一致。
2、@ConfigurationProperties 读取 List 类型参数
application.properties 配置文件内容:
my.list[0]=a
my.list[1]=b
my.list[2]=c
使用 @ConfigurationProperties 注解读取对应配置:
@Configuration
@ConfigurationProperties(prefix = "my")
public class ConfigurationReadConfig {
private List list;
public List getList() {
return list;
}
public void setList(List list) {
this.list = list;
}
}
3、@ConfigurationProperties 读取 Map 类型参数
application.properties 配置文件内容:
my.map.name=xiao-li
my.map.sex=man
my.map.age=20
使用 @ConfigurationProperties 注解读取对应配置:
@Configuration
@ConfigurationProperties(prefix = "my")
public class ConfigurationReadConfig {
private Map map;
public Map getMap() {
return map;
}
public void setMap(Map map) {
this.map = map;
}
}
4、@ConfigurationProperties 读取 Time 类型参数
application.properties 配置文件内容:
my.time=20s
使用 @ConfigurationProperties 注解读取对应配置:
@Configuration
@ConfigurationProperties(prefix = "my")
public class ConfigurationReadConfig {
/**
* 设置以秒为单位
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration time;
public Duration getTime() {
return time;
}
public void setTime(Duration time) {
this.time = time;
}
}
5、@ConfigurationProperties 读取 DataSize 类型参数
application.properties 配置文件内容:
my.fileSize=10MB
使用 @ConfigurationProperties 注解读取对应配置:
@Configuration
@ConfigurationProperties(prefix = "my")
public class ConfigurationReadConfig {
/**
* 设置以 MB 为单位
*/
@DataSizeUnit(DataUnit.MEGABYTES)
private DataSize fileSize;
public DataSize getFileSize() {
return fileSize;
}
public void setFileSize(DataSize fileSize) {
this.fileSize = fileSize;
}
}
6、@ConfigurationProperties 读取 DataSize 类型参数
application.properties 配置文件内容:
my.fileSize=10MB
使用 @ConfigurationProperties 注解读取对应配置:
@Configuration
@ConfigurationProperties(prefix = "my")
public class ConfigurationReadConfig {
/**
* 设置以 MB 为单位
*/
@DataSizeUnit(DataUnit.MEGABYTES)
private DataSize fileSize;
public DataSize getFileSize() {
return fileSize;
}
public void setFileSize(DataSize fileSize) {
this.fileSize = fileSize;
}
}
7、@ConfigurationProperties 读取配置参数并进行 Valid 效验
application.properties 配置文件内容:
my.name=xiao-ming
my.age=20
使用 @ConfigurationProperties 注解读取对应配置:
@Validated // 引入效验注解
@Configuration
@ConfigurationProperties(prefix = "my")
public class ConfigurationReadConfigAndValid {
@NotNull(message = "姓名不能为空")
private String name;
@Max(value = 20L,message = "年龄不能超过 20 岁")
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
8、@ConfigurationProperties 读取配置到新建 Spring Bean 中
application.properties 配置文件内容:
user.name=mydlq
user.age=22
User 实体类
import lombok.Data;
@Data
public class User {
private String name;
private Integer age;
}
使用 @ConfigurationProperties 注解读取对应配置到新建的 Bean 对象中
@Configuration
public class ConfigurationReadObject {
@Bean("user")
@ConfigurationProperties(prefix = "user")
public User createUser(){
return new User();
}
}
9、从指定配置文件中读取参数
使用 @ConfigurationProperties 注解是默认从 application.properties 或者 application.yaml 中读取配置,有时候我们需要将特定的配置放到单独的配置文件中,这时候需要 @PropertySource 与 ConfigurationProperties 配置使用,使用 @PropertySource 注解指定要读取的文件,使用 @ConfigurationProperties 相关属性。
推荐:分享一套基于SpringBoot和Vue的企业级中后台开源项目,代码很规范!
测试文件:
测试文件名称:test.txt
测试文件编码方式:UTF-8
测试文件目录:resources/test.txt
测试文件内容:
my.name=mydlq
Java 中配置 @ConfigurationProperties 和 @PropertySource 注解读取对应配置:
@Configuration
@ConfigurationProperties(prefix = "my")
@PropertySource(encoding = "UTF-8", ignoreResourceNotFound = true, value = "classpath:test.txt")
public class ConfigurationReadConfig {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
四、使用 Environment 对象读取配置
application.properties 配置文件内容:
my.name=mydlq
使用 Environment 读取配置:
@Component
public class EnvironmentReadConfig {
private String name;
@Autowired
private Environment environment;
public String readConfig(){
name = environment.getProperty("my.name", "默认值");
}
}
五、使用 PropertiesLoaderUtils 读取配置
application.properties 配置文件内容:
my.name=mydlq
使用 Environment 读取配置:
public class PropertiesReadConfig {
private String name;
public void readConfig() {
try {
ClassPathResource resource = new ClassPathResource("application.properties");
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
name = properties.getProperty("my.name", "默认值");
} catch (IOException e) {
log.error("", e);
}
}
}
六、读取配置文件示例项目
1、Maven 引入相关依赖
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.2.RELEASEversion>
parent>
<groupId>com.aspirecngroupId>
<artifactId>springboot-read-config-exampleartifactId>
<version>0.0.1version>
<name>springboot-read-config-examplename>
<description>Spring Boot Read Configdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
2、测试的配置文件
application.properties
## 使用多种方法读取 String 参数
my1.name=xiao-ming
my1.sex=man
my1.age=20
## 使用 @ConfigurationProperties 读取 Map 参数
my2.map.name=xiao-li
my2.map.sex=man
my2.map.age=20
## 使用 @Value 读取 Map 参数
my3.map={name:"xiao-ming",sex:"man",age:"20"}
## 使用 @ConfigurationProperties 读取 List 参数
my4.list[0]=xiao-li
my4.list[1]=man
my4.list[2]=20
## 使用 @Value 读取 List 参数
my5.list=xiao-ming,man,20
## 使用 @ConfigurationProperties 读取 Time 参数
my6.time=20s
## 使用 @ConfigurationProperties 读取 DataSize 参数
my7.fileSize=10MB
## 使用 @ConfigurationProperties 读取参数并进行 @Valid 效验
my8.name=xiao-ming
my8.age=20
3、读取配置的多种方法
(1)、读取 String 配置的 Service
ConfigurationReadString
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
/**
* 通过 @PropertySource 指定读取的文件中 String 配置,通过 @ConfigurationProperties 过滤前缀
*/
@Data
@Configuration
@PropertySource(encoding = "UTF-8", ignoreResourceNotFound = true, value = "classpath:application.properties")
@ConfigurationProperties(prefix = "my1")
public class ConfigurationReadString {
private String name;
private String sex;
private String age;
public String readString(){
return name + "," + sex + "," + age;
}
}
EnvironmentReadString
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
/**
* 从环境对象 Environment 中读取 String 配置
*/
@Service
public class EnvironmentReadString {
@Autowired
private Environment environment;
public String readString(){
String name = environment.getProperty("my1.name", "");
String sex = environment.getProperty("my1.sex", "");
String age = environment.getProperty("my1.age", "18");
return name + "," + sex + "," + age;
}
}
PropertiesUtilReadString
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import java.io.IOException;
import java.util.Properties;
/**
* 通过 properties 工具读取 String 配置
*/
@Slf4j
public class PropertiesUtilReadString {
private PropertiesUtilReadString(){}
public static String readString() {
try {
ClassPathResource resource = new ClassPathResource("application.properties");
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
String name = properties.getProperty("my1.name", "");
String sex = properties.getProperty("my1.sex", "");
String age = properties.getProperty("my1.age", "18");
return name + "," + sex + "," + age;
} catch (IOException e) {
log.error("", e);
}
return "";
}
}
ValueReadString
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* 通过 @Value 读取 String 配置
*/
@Service
public class ValueReadString {
@Value("${my1.name}")
private String name;
@Value("${my1.sex}")
private String sex;
@Value("${my1.age:18}")
private String age;
public String readString() {
return name + "," + sex + "," + age;
}
}
(2)、读取 List 配置的 Service
ConfigurationReadList
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.util.List;
/**
* 通过 @ConfigurationProperties 方式读取文件中的 List 数据
*/
@Data
@Configuration
@PropertySource(encoding = "UTF-8", ignoreResourceNotFound = true, value = "classpath:application.properties")
@ConfigurationProperties(prefix = "my4")
public class ConfigurationReadList {
private List list;
public String readList() {
StringBuilder builder = new StringBuilder();
for (String str:list){
builder.append(str).append(",");
}
// 移除最后的“,”号
builder.delete(builder.length()-1,builder.length());
return builder.toString();
}
}
ValueReadList
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 通过 @Value 方式读取文件中的 List 数据
*/
@Service
public class ValueReadList {
@Value("#{'${my5.list}'.split(',')}")
private List list;
public String readList() {
StringBuilder builder = new StringBuilder();
for (String str:list){
builder.append(str).append(",");
}
// 移除最后的“,”号
builder.delete(builder.length()-1,builder.length());
return builder.toString();
}
}
(3)、读取 Map 配置的 Service
ConfigurationReadMap
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.util.Map;
/**
* 通过 @ConfigurationProperties 方式读取文件中的 Map 数据
*/
@Data
@Configuration
@PropertySource(encoding = "UTF-8", ignoreResourceNotFound = true, value = "classpath:application.properties")
@ConfigurationProperties(prefix = "my2")
public class ConfigurationReadMap {
private Map map;
}
ValueReadMap
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* 通过 @Value 方式读取文件中的 Map 数据
*/
@Service
public class ValueReadMap {
@Value("#{${my3.map}}")
private Map map;
public String readMap(){
return map.get("name") + "," + map.get("sex") + "," + map.get("age");
}
}
(4)、读取 Time 的 Service
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
/**
* 通过 @ConfigurationProperties 读取 time 参数
*/
@Data
@Configuration
@PropertySource(encoding = "UTF-8", ignoreResourceNotFound = true, value = "classpath:application.properties")
@ConfigurationProperties(prefix = "my6")
public class ConfigurationReadTime {
/**
* 设置以秒为单位
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration time;
public String readTime() {
return String.valueOf(time.getSeconds());
}
}
常见单位如下:
B for bytes KB for kilobytes MB for megabytes GB for gigabytes TB for terabytes
(5)、读取 DataSize 的 Service
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DataSizeUnit;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.util.unit.DataSize;
import org.springframework.util.unit.DataUnit;
/**
* 通过 @ConfigurationProperties 读取 time 参数
*/
@Data
@Configuration
@PropertySource(encoding = "UTF-8", ignoreResourceNotFound = true, value = "classpath:application.properties")
@ConfigurationProperties(prefix = "my7")
public class ConfigurationReadDatasize {
/**
* 设置以秒为单位
*/
@DataSizeUnit(DataUnit.MEGABYTES)
private DataSize fileSize;
public String readDatasize() {
return String.valueOf(fileSize.toMegabytes());
}
}
常用单位如下:
ns (纳秒)
us (微秒)
ms (毫秒)
s (秒)
m (分)
h (时)
d (天)
(6)、读取配置并进行 valie 效验的 service
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
/**
* 通过 @ConfigurationProperties 读取配置并进行 valid 效验
*/
@Data
@Validated // 效验注解
@Configuration
@PropertySource(encoding = "UTF-8", ignoreResourceNotFound = true, value = "classpath:application.properties")
@ConfigurationProperties(prefix = "my8")
public class ConfigurationReadConfigAndValid {
@NotNull(message = "姓名不能为空")
private String name;
@Max(value = 20L,message = "年龄不能超过 20 岁")
private Integer age;
public String readString() {
return name + "," + age;
}
}
(7)、读取配置到 Spring Bean 对象中
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 通过 @ConfigurationProperties 读取到 Spring Bean 对象中
*/
@Configuration
public class ConfigurationReadObject {
/**
* 测试的实体类对象
*/
@Data
public static class User {
private String name;
private Integer age;
}
/**
* 读取以 my9 为前缀的参数的值,到新建的对象中
*/
@Bean("user")
@ConfigurationProperties(prefix = "my9")
public User readObjectData(){
return new User();
}
}
4、配置测试用的 Controller
(1)、读取 String 的 Controller
import club.mydlq.service.string.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 读取 String Controller
*/
@RestController
@RequestMapping("/string")
public class ReadStringController {
@Autowired
private ValueReadString valueReadConfig;
@Autowired
private EnvironmentReadString environmentReadConfig;
@Autowired
private ConfigurationReadString configurationReadConfig;
@GetMapping("/value")
public String valueReadConfig(){
return valueReadConfig.readString();
}
@GetMapping("/env")
public String envReadConfig(){
return environmentReadConfig.readString();
}
@GetMapping("/util")
public String utilReadConfig(){
return PropertiesUtilReadString.readString();
}
@GetMapping("/configuration")
public String configurationReadConfig(){
return configurationReadConfig.readString();
}
}
(2)、读取 List 的 Controller
import club.mydlq.service.list.ConfigurationReadList;
import club.mydlq.service.list.ValueReadList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 读取 List Controller
*/
@RestController
@RequestMapping("/list")
public class ReadListController {
@Autowired
private ValueReadList vlueReadList;
@Autowired
private ConfigurationReadList configurationReadList;
@GetMapping("/value")
public String valueReadList(){
return vlueReadList.readList();
}
@GetMapping("/configuration")
public String configReadList(){
return configurationReadList.readList();
}
}
(3)、读取 Map 的 Controller
import club.mydlq.service.map.ConfigurationReadMap;
import club.mydlq.service.map.ValueReadMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 读取 Map Controller
*/
@RestController
@RequestMapping("/map")
public class ReadMapController {
@Autowired
private ConfigurationReadMap configurationReadMap;
@Autowired
private ValueReadMap valueReadMap;
@GetMapping("/configuration")
public String configReadMap(){
return configurationReadMap.getMap().get("name") + "," + configurationReadMap.getMap().get("sex") + "," + configurationReadMap.getMap().get("age");
}
@GetMapping("/value")
public String valueReadMap(){
return valueReadMap.readMap();
}
}
(4)、读取 Time 的 Controller
import club.mydlq.service.time.ConfigurationReadTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 读取 time Controller
*/
@RestController
@RequestMapping("/time")
public class ReadTimeController {
@Autowired
private ConfigurationReadTime configurationReadTime;
@GetMapping("/configuration")
public String configReadList(){
return configurationReadTime.readTime();
}
}
(5)、读取 Datasize 的 Controller
import club.mydlq.service.datasize.ConfigurationReadDatasize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 读取 Datasize Controller
*/
@RestController
@RequestMapping("/data")
public class ReadDatasizeController {
@Autowired
private ConfigurationReadDatasize configurationReadDatasize;
@GetMapping("/configuration")
public String configReadList(){
return configurationReadDatasize.readDatasize();
}
}
(6)、读取参数进行 valid 效验的 Controller
import club.mydlq.service.valid.ConfigurationReadConfigAndValid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 读取参数进行 valid 效验的 Controller
*/
@RestController
@RequestMapping("/valid")
public class ReadValidController {
@Autowired
private ConfigurationReadConfigAndValid configurationReadConfigAndValid;
@GetMapping("/configuration")
public String configReadList(){
return configurationReadConfigAndValid.readString();
}
}
(7)、读取参数到 Spring Bean 对象的 Controller
import club.mydlq.service.string.ConfigurationReadObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 读取参数到对象
*/
@RestController
@RequestMapping("/object")
public class ReadObjectController {
@Autowired
private ConfigurationReadObject.User user;
@GetMapping("/value")
public Object valueReadConfig(){
return user;
}
}
5、启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
参考地址:
https://www.jianshu.com/p/7f75936b573b
https://github.com/my-dlq/blog-example/tree/master/springboot/springboot-read-config-example
喜欢就三连呀
关注 Stephen,一起学习,一起成长。