redis-start自动装配学习笔记:
废话不多,先上代码
获取自定义redis数据源配置文件配置
@ConfigurationProperties(prefix = "test.redis")
@Setter
@Getter
public class StarterRedisProperties {
/**
* 扩展RedisProperties构建多数据源映射表
*/
private Map<String, RedisProperties> redis;
}
配置文件:
test:
redis:
basic:
database: 0
password:
shutdown-timeout: 1000ms
socket-timeout: 2000ms
sentinel:
master:
nodes:
lettuce:
pool:
max-active: 100
min-idle: 20
max-idle: 100
max-wait: 2000ms
配置redis客户端链接
@Slf4j
public class TestRedisRegistrar implements EnvironmentAware, ImportBeanDefinitionRegistrar {
private Map<String, RedisConnectionFactory> registerBean = new ConcurrentHashMap<>(24);
private Environment environment;
private Binder binder;
private static final String PREFIX = "test.redis";
private static final String PREFIX_WITH_COMMA = "test.redis.";
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
binder = Binder.get(environment);
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
log.info("config redis start.");
Map<String, Map<String, Object>> redisConfigs = null;
try {
// 获取yaml中的配置文件
redisConfigs = binder.bind(PREFIX, Map.class).get();
} catch (NoSuchElementException e) {
log.error("Please config the redis prefix config.", e);
throw e;
}
boolean havePrimary = true;
for (String redisSourceName : redisConfigs.keySet()) {
//配置基础连接信息
RedisConfiguration redisConfiguration = getRedisConfiguration(redisSourceName);
//配置连接池信息
GenericObjectPoolConfig poolConfig = getRedisPoolConfig(redisSourceName);
//配置RedisConnectionFactory
RedisConnectionFactory redisConnectionFactory = getRedisConnectionFactory(redisSourceName, redisConfiguration, poolConfig);
//构造RedisConnectionFactory Supplier
Supplier<RedisConnectionFactory> supplier = () -> {
return redisConnectionFactory;
};
//构建RedisConnectionFactoryBean
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(getRedisConnectionFactoryClass(redisConnectionFactory), supplier);
AbstractBeanDefinition beanDefinition = builder.getRawBeanDefinition();
beanDefinition.setPrimary(havePrimary);
registry.registerBeanDefinition(getRedisConnectionFactoryBeanName(redisSourceName), beanDefinition);
//构建StringRedisTemplateBean
GenericBeanDefinition stringRedisTemplate = new GenericBeanDefinition();
stringRedisTemplate.setBeanClass(StringRedisTemplate.class);
ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
constructorArgumentValues.addIndexedArgumentValue(0, redisConnectionFactory);
stringRedisTemplate.setConstructorArgumentValues(constructorArgumentValues);
stringRedisTemplate.setAutowireMode(AutowireCapableBeanFactory.AUTOWIRE_BY_NAME);
registry.registerBeanDefinition(getStringRedisTemplateBeanName(redisSourceName), stringRedisTemplate);
//构建RedisTemplateBean
GenericBeanDefinition redisTemplate = new GenericBeanDefinition();
redisTemplate.setBeanClass(RedisTemplate.class);
redisTemplate.getPropertyValues().add("connectionFactory", redisConnectionFactory);
redisTemplate.setAutowireMode(AutowireCapableBeanFactory.AUTOWIRE_BY_NAME);
registry.registerBeanDefinition(getRedisTemplateBeanName(redisSourceName), redisTemplate);
//创建RedisRepositoryBean
GenericBeanDefinition redisRepository = new GenericBeanDefinition();
redisRepository.setBeanClass(RedisRepository.class);
ConstructorArgumentValues cav = new ConstructorArgumentValues();
cav.addIndexedArgumentValue(0, stringRedisTemplate);
redisRepository.setConstructorArgumentValues(cav);
redisRepository.setAutowireMode(AutowireCapableBeanFactory.AUTOWIRE_BY_NAME);
registry.registerBeanDefinition(getRedisRepositoryBeanName(redisSourceName), redisRepository);
if (havePrimary) {
havePrimary = false;
}
log.info("config success for key: {}", redisSourceName);
}
log.info("config redis end.");
}
/**
* 获取RedisConnetionFactory Class
*
* @param redisConnectionFactory
* @return
*/
private Class getRedisConnectionFactoryClass(RedisConnectionFactory redisConnectionFactory) {
Class clazz = null;
if (redisConnectionFactory instanceof LettuceConnectionFactory) {
clazz = LettuceConnectionFactory.class;
}
if (redisConnectionFactory instanceof JedisConnectionFactory) {
clazz = JedisConnectionFactory.class;
}
return clazz;
}
/**
* 配置 RedisConnectionFactory: 配置连接工厂。
* SpringData redis 默认提供JedisConnctionFactory 和 LettuceConnectionFactory 2种实现,
* 如果要支持RedissonConnectionFactory, 请参见redisson官方starter。
*
* @param redisSourceName
* @param redisConfiguration
* @param poolConfig
* @return
*/
private RedisConnectionFactory getRedisConnectionFactory(String redisSourceName, RedisConfiguration redisConfiguration, GenericObjectPoolConfig poolConfig) {
String connectionFactoryBeanName = getRedisConnectionFactoryBeanName(redisSourceName);
RedisConnectionFactory redisConnectionFactory = registerBean.get(connectionFactoryBeanName);
if (redisConnectionFactory != null) {
return redisConnectionFactory;
}
Duration socketTimeOut = null;
try {
socketTimeOut = binder.bind(PREFIX_WITH_COMMA + redisSourceName + ".socket-timeout", Duration.class).get();
} catch (NoSuchElementException e) {
throw new IllegalArgumentException("missing socket-timeout config.");
}
if (isClientJedis(redisSourceName)) {
redisConnectionFactory = createJedisConnectionFactory(redisSourceName, socketTimeOut, redisConfiguration, poolConfig);
}
if (isClientLettuce(redisSourceName)) {
redisConnectionFactory = createLettuceConnectionFactory(redisSourceName, socketTimeOut, redisConfiguration, poolConfig);
}
return redisConnectionFactory;
}
/**
* 创建LettuceConnectionFactory
*
* @param redisSourceName
* @param socketTimeOut
* @param redisConfiguration
* @param poolConfig
* @return
*/
private RedisConnectionFactory createLettuceConnectionFactory(String redisSourceName, Duration socketTimeOut, RedisConfiguration redisConfiguration, GenericObjectPoolConfig poolConfig) {
LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder = LettucePoolingClientConfiguration.builder();
Duration shutDownTimeOut = binder.bind(PREFIX_WITH_COMMA + redisSourceName + ".shutdown-timeout", Duration.class).get();
if (shutDownTimeOut != null) {
builder.shutdownTimeout(shutDownTimeOut);
}
if (socketTimeOut != null) {
builder.commandTimeout(socketTimeOut);
}
LettuceClientConfiguration lettuceClientConfiguration = builder.poolConfig(poolConfig).build();
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisConfiguration, lettuceClientConfiguration);
registerBean.put(getRedisConnectionFactoryBeanName(redisSourceName), lettuceConnectionFactory);
return lettuceConnectionFactory;
}
/**
* 创建JedisConnectionFactory
*
* @param redisSourceName
* @param socketTimeOut
* @param redisConfiguration
* @param poolConfig
* @return
*/
private RedisConnectionFactory createJedisConnectionFactory(String redisSourceName, Duration socketTimeOut, RedisConfiguration redisConfiguration, GenericObjectPoolConfig poolConfig) {
JedisClientConfiguration.DefaultJedisClientConfigurationBuilder builder = (JedisClientConfiguration.DefaultJedisClientConfigurationBuilder) JedisClientConfiguration.builder();
if (socketTimeOut != null) {
builder.connectTimeout(socketTimeOut);
builder.readTimeout(socketTimeOut);
}
builder.poolConfig(poolConfig);
JedisClientConfiguration jedisClientConfiguration = builder.build();
JedisConnectionFactory jedisConnectionFactory = null;
if (redisConfiguration instanceof RedisStandaloneConfiguration) {
jedisConnectionFactory = new JedisConnectionFactory((RedisStandaloneConfiguration) redisConfiguration, jedisClientConfiguration);
}
if (redisConfiguration instanceof RedisSentinelConfiguration) {
jedisConnectionFactory = new JedisConnectionFactory((RedisSentinelConfiguration) redisConfiguration, jedisClientConfiguration);
}
if (redisConfiguration instanceof RedisClusterConfiguration) {
jedisConnectionFactory = new JedisConnectionFactory((RedisClusterConfiguration) redisConfiguration, jedisClientConfiguration);
}
registerBean.put(getRedisConnectionFactoryBeanName(redisSourceName), jedisConnectionFactory);
return jedisConnectionFactory;
}
/**
* 配置redis连接池信息
*
* @param redisSourceName
* @return
*/
private GenericObjectPoolConfig getRedisPoolConfig(String redisSourceName) {
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
try {
//如果需要支持多客户端类型,请按优先级加载。比如优先Lettuce > Jedis > Redisson
RedisProperties.Pool pool = null;
if (isClientJedis(redisSourceName)) {
pool = binder.bind(PREFIX_WITH_COMMA + redisSourceName + ".jedis.pool", RedisProperties.Pool.class).get();
}
if (isClientLettuce(redisSourceName)) {
pool = binder.bind(PREFIX_WITH_COMMA + redisSourceName + ".lettuce.pool", RedisProperties.Pool.class).get();
}
poolConfig.setMaxIdle(pool.getMaxIdle());
poolConfig.setMaxTotal(pool.getMaxActive());
poolConfig.setMinIdle(pool.getMinIdle());
if (pool.getMaxWait() != null) {
poolConfig.setMaxWaitMillis(pool.getMaxWait().toMillis());
}
return poolConfig;
} catch (Exception e) {
log.error("Please config the redis pool info.", e);
throw new IllegalArgumentException("pool config error。");
}
}
/**
* 按优先级(单点-->哨兵-->集群)获取RedisConfiguration配置类型
*
* @param redisSourceName: redis数据源名字
* @return 1. RedisStandaloneConfiguration 单例
* 2. RedisSentinelConfiguration 哨兵
* 3. RedisClusterConfiguration 集群
*/
private RedisConfiguration getRedisConfiguration(String redisSourceName) {
Map<String, String> map = binder.bind(PREFIX_WITH_COMMA + redisSourceName, Map.class).get();
String host = map.get("host");
String password = map.get("password");
String configDataBase = String.valueOf(map.get("database"));
if (StringUtils.isBlank(configDataBase) || "null".equals(configDataBase)) {
configDataBase = "0"; //默认0库
}
Integer database = Integer.parseInt(configDataBase);
if (StringUtils.isNotBlank(host)) {
//配置单点连接基础信息
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(map.get("host"));
redisStandaloneConfiguration.setPort(Integer.parseInt(String.valueOf(map.get("port"))));
redisStandaloneConfiguration.setDatabase(database);
if (StringUtils.isNotBlank(password)) {
redisStandaloneConfiguration.setPassword(password);
}
return redisStandaloneConfiguration;
}
RedisProperties.Sentinel sentinel = null;
try {
sentinel = binder.bind(PREFIX_WITH_COMMA + redisSourceName + ".sentinel", RedisProperties.Sentinel.class).get();
} catch (NoSuchElementException e) {
//ignore
}
if (sentinel != null) {
//配置哨兵连接基础信息
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration();
redisSentinelConfiguration.master(sentinel.getMaster());
redisSentinelConfiguration.setSentinels(createSentinels(sentinel));
if (StringUtils.isNotBlank(password)) {
redisSentinelConfiguration.setPassword(RedisPassword.of(password));
}
redisSentinelConfiguration.setDatabase(database);
return redisSentinelConfiguration;
}
RedisProperties.Cluster cluster = null;
try {
cluster = binder.bind(PREFIX_WITH_COMMA + redisSourceName + ".cluster", RedisProperties.Cluster.class).get();
} catch (NoSuchElementException e) {
//ignore
}
if (cluster != null) {
//配置集群连接基础信息
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(cluster.getNodes());
if (cluster.getMaxRedirects() != null) {
redisClusterConfiguration.setMaxRedirects(cluster.getMaxRedirects());
}
if (StringUtils.isNotBlank(password)) {
redisClusterConfiguration.setPassword(RedisPassword.of(password));
}
return redisClusterConfiguration;
}
throw new IllegalArgumentException("No match configuration for " + redisSourceName);
}
/**
* 配置Sentinel节点列表
*
* @param sentinel
* @return
* @see org.springframework.boot.autoconfigure.data.redis.RedisConnectionConfiguration#createSentinels
*/
private List<RedisNode> createSentinels(RedisProperties.Sentinel sentinel) {
List<RedisNode> nodes = new ArrayList<>();
for (String node : sentinel.getNodes()) {
try {
String[] parts = org.springframework.util.StringUtils.split(node, ":");
Assert.state(parts.length == 2, "Must be defined as 'host:port'");
nodes.add(new RedisNode(parts[0], Integer.valueOf(parts[1])));
} catch (RuntimeException ex) {
throw new IllegalStateException("Invalid redis sentinel " + "property '" + node + "'", ex);
}
}
return nodes;
}
/**
* 此数据源是用的Jedis客户端吗?
*
* @param redisSourceName
* @return
*/
private boolean isClientJedis(String redisSourceName) {
try {
binder.bind(PREFIX_WITH_COMMA + redisSourceName + ".jedis", Map.class).get();
} catch (NoSuchElementException e) {
return false;
}
return true;
}
/**
* 此数据源是用的Lettuce客户端吗?
*
* @param redisSourceName
* @return
*/
private boolean isClientLettuce(String redisSourceName) {
try {
binder.bind(PREFIX_WITH_COMMA + redisSourceName + ".lettuce", Map.class).get();
} catch (NoSuchElementException e) {
return false;
}
return true;
}
/**
* 获取RedisConnectionFactoryBeanName
*
* @param redisSourceName
* @return
*/
private String getRedisConnectionFactoryBeanName(String redisSourceName) {
return redisSourceName + "-redis-connection-factory";
}
/**
* 获取StringRedisTemplateBeanName
*
* @param redisSourceName
* @return
*/
private String getStringRedisTemplateBeanName(String redisSourceName) {
return redisSourceName + "-string-redis-template";
}
/**
* 获取RedisTemplateBeanName
*
* @param redisSourceName
* @return
*/
private String getRedisTemplateBeanName(String redisSourceName) {
return redisSourceName + "-redis-template";
}
/**
* 获取RedisRepositoryBeanName
*
* @param redisSourceName
* @return
*/
private String getRedisRepositoryBeanName(String redisSourceName) {
return redisSourceName + "-repository";
}
}
配置META-INF/spring.factroies(重点)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.xxx.xxx.redis.TestRedisAutoConfiguration
配置spring-configuration-metadata.json(可选)
{
"groups": [
{
"name": "test.redis",
"type": "com.xxx.xxx.redis.TestRedisProperties",
"sourceType": "com.xxx.xxx.redis.TestRedisProperties"
}
],
"properties": [
{
"name": "test.redis",
"type": "java.util.Map<java.lang.String,org.springframework.boot.autoconfigure.data.redis.RedisProperties>",
"sourceType": "com.xxx.xxx.redis.TestRedisProperties"
}
],
"hints": []
}
自动装配“value类”
@Configuration
@EnableConfigurationProperties(TestRedisProperties.class)
@AutoConfigureBefore({RedisAutoConfiguration.class})
@Import(TestRedisRegistrar.class)
public class TestRedisAutoConfiguration {
}
看完所有代码之后,简单梳理下,首先就会有以下问题:
1、spring.factories文件是什么?
2、自动装配"value类"是什么?
3、自动装配如何实现?
4、spring-configuration-metadata.json有什么用,为什么可选?
下面一一解答这几个问题
1、spring.factories文件是什么?
在springboot启动的时候,会自动扫描所有的spring.factories文件,spring.factories文件的内容信息以kv存储,其中key和value对应的值就是简单的“=”获取,是springboot自动装配的核心配置
2、自动装配“value类是什么”?
是自己起的名字,相当于就是在springboot启动时,自动装配时用来加载(装配)的对应的value
3、自动装配如何实现?
上面说到spring.factories在springboot启动时加载,是springboot自动装配的核心配置,那么她为什么核心,怎么会自动扫描加载呢?我们一步一步看
1、springboot如何实现自动装配?
首先,说到springboot自动装配,离不开的就是springboot的启动,说到springboot的启动,那就不得不提在启动类上的关键注解__@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 {
......
}
各种各样的注解花里胡哨,但是本次的重点不是他们,直击核心,看到其中有一个注解是__@EnableAutoConfiguration__,翻译过来就是:启用自动装配,这个注解一听就是此次自动装配的核心。
2、@EnableAutoConfiguration做了什么?
先不着急,进入到__@EnableAutoConfiguration__注解中,看下里面有什么
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
......
}
又是这一大盘子注解…但是,不能被其他注解迷惑(也没什么迷惑的,有些一看就知道没关系),所谓__自动装配__少不了的就是__Auto Configuration__,那一下子需要把关注点放到__@Import(AutoConfigurationImportSelector.class)__上,那么这个__AutoConfigurationImportSelector.class__又是何方神圣,做了什么呢?废话不说,点进去看了才知道
3、AutoConfigurationImportSelector.class是何方神圣?
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
......
}
好家伙,我直接就是好家伙,上来就可以看到__AutoConfigurationImportSelector__实现这么多接口,既然来都来了,那就先简单看下每个实现的接口是干什么的吧(说简单看,就简单看)
DeferredImportSelector:
DeferredImportSelector__继承了__ImportSelector__类, ImportSelector__中的__selectImports__方法返回一组__bean,@EnableAutoConfiguration__注解借助__@Import__注解将这组__bean__注入到__spring__容器中,springboot正式通过这种机制来完成__bean__的注入的。
BeanClassLoaderAware:
分别获取Bean的类装载器和bean工厂
ResourceLoaderAware:
获取资源加载器,可以获得外部资源文件
BeanFactoryAware:
获得当前__bean Factory__,从而调用容器的服务
EnvironmentAware:
凡注册到Spring容器内的bean,实现了__EnvironmentAware__接口__重写setEnvironment__方法后,在工程启动时可以获得__application.properties__的配置文件配置的属性值。
Ordered:
处理相同接口实现类的优先级问题
简单的甚至有点笼统,但是都不重要,小学都知道,选择题不会就选长的,那我们就先看那个最长的吧(不重要我写那么多干嘛~),“注入”和“bean”,这一切都像极了__Spring IOC__,
“这样写,会让他看起很酷”
——沃兹基.索德
4、自动装配的实现
刚才说到__@EnableAutoConfiguration__会借助__DeferredImportSelector__搞些小动作,那么做了什么?得到了什么呢?
既然实现了接口,那必须要实现他的方法(是必须吗?Springboot可是传说中的jdk8哦~),实现了__selectImports__方法如下:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) { // 是否允许(开启)自动装配
return NO_IMPORTS; // 否,return
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata); // 方法名直译:获取自动装配条目
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
看翻译也知道,很关键的就在__getAutoConfigurationEntry(annotationMetadata)这个方法(毕竟还是离不开__Auto Configuration),点进去看一眼他在偷偷的帮我们干什么!
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) { // 是否允许(开启)自动装配
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); // 获取自动装配类名
configurations = removeDuplicates(configurations); // 去重
Set<String> exclusions = getExclusions(annotationMetadata, attributes); // 获取排除类
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions); // 去除排除类
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
根据我的注释不用我说了吧,点__getCandidateConfigurations(annotationMetadata, attributes)__就完事了
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); // 关键:加载所有的factory
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you"+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
如何加载所有的factory,接下来就是见证奇迹的时候!
// 这个东西,似曾相识?
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList()); //好事多磨,再点一下
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); // 重点
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
偶买噶!盘他!这里的逻辑就是读取所有的 META-INF/spring.factories 下的内容,获取需要自动装配的类,来进行自动装配,因此开头提到__spring.factories__是自动装配的关键就是在这,他帮助spring-boot注册其他的bean,且用来记录项目包外需要注册的bean类名。
以上就是自动装配的整体流程实现!
作者码字…(什么?还有个知识点?我看看~)
4、spring-configuration-metadata.json有什么用,为什么可选?
这个文件的主要作用就是,在你的配置你的配置文件的时候,无论是__yaml__还是__properties __,可以在IDE中,给出对应的配置,例如:spring配置指定端口的时候,在idea中就会给出对应的提示key
这是spring中对应的__spring-configuration-metadata.json__的对应配置
完结撒花
以上都是本人自己的一些理解和学习demo,如果有问题和不对的地方请私信或者留言指正
关于此starter如果在使用时有问题的话也可以留言一起学习