Spring配置那些事

一、引言

配置是一个项目中不那么起眼,但却有非常重要的东西。在工程项目中,我们一般会将可修改、易变、不确定的值作为配置项,在配置文件/配置中心中设置。

比方说,不同环境有不同的数据库地址、不同的线程池大小等,可以通过每个环境单独配置文件的方式,实现不修改代码的情况下修改配置项。

再比方说,我们有一个功能上线,可能存在兼容性问题,我们需要在开始的时候开关打开,执行旧的代码逻辑,待一些操作执行结束之后,再将开关关闭,执行新的代码逻辑。那么我们可以把开关写到配置里面,通过配置中心修改配置的方式,在不停机的情况下,热更新配置,从而实现开关的修改。

那么,Spring应用是如何管理配置的呢?对于热更新的一些场景,我们在实际开发中需要做哪些事情呢?本文将对这些问题进行介绍。

二、Spring配置使用

本章节将简单介绍Spring对于配置的使用。

2.1 读配置

比如,我们在配置文件或者配置中心(如Apollo)中添加了一个配置,Spring应用可以通过以下几种方式取出配置。

x:y:z: 1

1. @Value

通过注解@Value+配置占位符,可以实现配置注入。对于需要默认值的情况,可以在配置项(x.y.z)后添加然后跟上默认值(1

@Component
public class MyComponent {// @Value("${x.y.z}") // 无默认值的情况@Value("${x.y.z:1}")private int z;
}

2. @ConfigurationProperties

为了方便配置管理,也经常会将配置放到单独的Properties类中。通过@ConfigurationProperties 可以指定配置项前缀(x.y),这个前缀后面的所有配置会反序列化到该类上。

@Data
@ConfigurationProperties("x.y")
public class MyProperties {private int z = 1;
}

为了让这个配置可以作为Spring bean被使用,一般可以直接在类上添加@Component注解

@Data
@Component
@ConfigurationProperties("x.y")
public class MyProperties {private int z = 1;
}

对于一些自动配置情况,需要在满足条件的情况下,才将Properties加载到Spring容器。那么这个时候,可以在自动配置类上添加配置@EnableConfigurationProperties,在满足条件的情况下会将Properties类引入。

@EnableConfigurationProperties({MyProperties.class})
//@ConditionOnXXX("")  //满足条件的才自动装配
public class SnowflakeAutoConfiguration {// ...
}

另外,还有一个提升我们开发效率和体验的小技巧。我们在改配置文件的时候,发现Spring官方提供的配置,编辑的时候会有自动提示,但是我们自己的配置没有自动提示。

我们可以pom.xml添加以下依赖。添加依赖之后,在前端编译的时候(也就是编译class文件的时候),会自动将@ConfigurationProperties的配置类的信息提取成json格式的元数据,保存在类路径的META-INF/spring-configuration-metadata.json文件中。这样IDE就可以通过元数据文件实现配置编辑的自动提示。

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional>
</dependency>

spring-configuration-metadata.json内容如下,不需要手动编写。

{"groups": [{"name": "x.y","type": "ltd.dujiabao.configtests.config.MyProperties","sourceType": "ltd.dujiabao.configtests.config.MyProperties"}],"properties": [{"name": "x.y.z","type": "java.lang.Integer","sourceType": "ltd.dujiabao.configtests.config.MyProperties","defaultValue": 1}],"hints": []
}

3. EnvironmentAware

通过实现EnvironmentAware接口,可以获取Environment的实现类,从而取出需要的配置。

这种方式的获取配置比较常见的使用场景是,在生成BeanDefinition阶段,需要取出一些配置值,上面提到的两种方式,bean都还没生成,没办法通过上面提到的方式拿到配置。需要直接拿到专门用于管理应用配置的接口Environment,直接取出所需的配置。对于Environment,后续会在第三章第一节详细介绍。

getProperty方法指定配置键名称,从而获取配置。

public class MyImport implements EnvironmentAware {private Environment environment;@Overridepublic void setEnvironment(Environment environment) {this.environment = environment;String z = environment.getProperty("x.y.z");}
}

通过Binder指定配置前缀,将配置前缀后的所有配置都绑定到指定类中。

public class MyImport implements EnvironmentAware {private Environment environment;@Overridepublic void setEnvironment(Environment environment) {this.environment = environment;MyProperties myProperties = Binder.get(environment).bind("x.y", MyProperties.class).get();}
}

2.2 配置的多环境使用

1. profile

对于不同环境,可能会有不同的配置,比如说线程池大小、连接池大小。可以通过配置profile去控制当前使用的是哪个环境,用哪个配置。

比如,当前有dev、uat环境。

dev的配置文件为application-dev.yml

x:y:z: 2

uat的配置文件为application-uat.yml

x:y:z: 3

在application.yml中,可以选择profile,从而选择对应的配置。也可以在启动服务时,通过命令行的方式传入。当spring.profiles.active=uat,会使用application-uat.yml,当spring.profiles.active=dev会使用application-dev.yml。

spring:profiles:active: uat
java -Dspring.profiles.active=dev -jar xxx.jar

三、Spring配置原理

第二章中,介绍了Spring配置日常的基本使用。在本章节,将从配置组件、配置注入、配置热更新三个方面详细介绍Spring配置的原理及使用。

1. 配置组件

本章节,将介绍Spring配置中重要的几个组件,并通过介绍组件,将Spring对于配置管理逻辑进行介绍。

1.1 Environment

1.1.1 Environment

在Spring中,配置最重要的组件就是Environment,它集成了Spring应用的所有配置。

我们可以简单看下Environment的源码。Environment主要包括两部分,一部分是Profile,另一部分是Property。Profile表示当前进程激活了哪个环境,用了哪个环境的配置;Property表示当前进程的配置项。

方法getActiveProfiles获取当前激活的Profile;getDefaultProfiles获取默认的Profile;acceptsProfiles判断是否满足所有Profile。

public interface Environment extends PropertyResolver {String[] getActiveProfiles();String[] getDefaultProfiles();@Deprecatedboolean acceptsProfiles(String... profiles);boolean acceptsProfiles(Profiles profiles);
}

containsProperty判断是否包含某个配置项;getProperty获取配置项的值;getRequiredProperty获取配置项的值,当配置项不存在抛出IllegalStateExceptionresolvePlaceholdersresolveRequiredPlaceholders主要用于处理${..}占位符

public interface PropertyResolver {boolean containsProperty(String key);@NullableString getProperty(String key);String getProperty(String key, String defaultValue);@Nullable<T> T getProperty(String key, Class<T> targetType);<T> T getProperty(String key, Class<T> targetType, T defaultValue);String getRequiredProperty(String key) throws IllegalStateException;<T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;String resolvePlaceholders(String text);String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;
}
1.1.2 ConfigurableEnvironment

ConfigurableEnvironment,顾名思义提供了可配置的Environment接口,它继承了Environment

可通过方法setActiveProfilesaddActiveProfilesetDefaultProfiles 修改激活、默认的Profile;通过getPropertySources获取PropertySource列表,并且对PropertySource列表进行修改;通过getSystemPropertiesgetSystemEnvironment可以获取一些和系统参数相关的map。

public interface ConfigurableEnvironment extends Environment, ConfigurablePropertyResolver {void setActiveProfiles(String... profiles);void addActiveProfile(String profile);void setDefaultProfiles(String... profiles);MutablePropertySources getPropertySources();Map<String, Object> getSystemProperties();Map<String, Object> getSystemEnvironment();
}
1.1.3 AbstractEnvironment

接口Environment的默认实现类是AbstractEnvironment,我们简单分析它的实现原理。

public abstract class AbstractEnvironment implements ConfigurableEnvironment {//...
}
1.1.2.1 成员变量

AbstractEnvironment包含两个重要的成员:

  1. propertySources:维护所有配置来源PropertySource的一个集合类
  2. propertyResolver:用于提供一些读配置的方法,比如说获取配置值、通过占位符获取配置值等,propertySources会传入作为配置来源

我们这里引出了一个很重要的组件PropertySource,可以简单理解为每一个配置来源都有一个PropertySource,将在1.2介绍。

	private final MutablePropertySources propertySources = new MutablePropertySources();private final ConfigurablePropertyResolver propertyResolver =new PropertySourcesPropertyResolver(this.propertySources);
1.1.2.2 构造方法

构造方法将成员变量propertySources传入方法customizePropertySources,为子类提供一个可以自定义PropertySource并加入到的propertySources方法。

	public AbstractEnvironment() {customizePropertySources(this.propertySources);}protected void customizePropertySources(MutablePropertySources propertySources) {}

Spring应用默认的Environment实现类StandardEnvironment,它会继承AbstractEnvironment,重写方法customizePropertySources。我们可以看到,它添加了两个PropertySourcesystemProperties是系统属性的来源,systemEnvironment是系统环境变量的来源。

比方说,在启动服务时传入设置系统属性property_name,那么这个系统属性会因为systemPropertiesEnvironment管理,可以直接通过第二章介绍的方式获取该值。

java -Dproperty_name=value -jar your_application.jar

比方说,在Linux环境下,设置了环境变量VARIABLE_NAME,那么它也会因为systemEnvironmentEnvironment管理,可以直接通过第二章介绍的方式获取该值。

export VARIABLE_NAME="value"
public class StandardEnvironment extends AbstractEnvironment {public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";@Overrideprotected void customizePropertySources(MutablePropertySources propertySources) {propertySources.addLast(new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));}
}
@Override@SuppressWarnings({"rawtypes", "unchecked"})public Map<String, Object> getSystemProperties() {try {return (Map) System.getProperties();}catch (AccessControlException ex) {return (Map) new ReadOnlySystemAttributesMap() {@Override@Nullableprotected String getSystemAttribute(String attributeName) {try {return System.getProperty(attributeName);}catch (AccessControlException ex) {if (logger.isInfoEnabled()) {logger.info("Caught AccessControlException when accessing system property '" +attributeName + "'; its value will be returned [null]. Reason: " + ex.getMessage());}return null;}}};}}@Override@SuppressWarnings({"rawtypes", "unchecked"})public Map<String, Object> getSystemEnvironment() {if (suppressGetenvAccess()) {return Collections.emptyMap();}try {return (Map) System.getenv();}catch (AccessControlException ex) {return (Map) new ReadOnlySystemAttributesMap() {@Override@Nullableprotected String getSystemAttribute(String attributeName) {try {return System.getenv(attributeName);}catch (AccessControlException ex) {if (logger.isInfoEnabled()) {logger.info("Caught AccessControlException when accessing system environment variable '" +attributeName + "'; its value will be returned [null]. Reason: " + ex.getMessage());}return null;}}};}}
1.1.2.3 getProperty

通过getProperty取出配置项的值。我们可以看到这个方法实际上用的就是propertyResolver。

	private final MutablePropertySources propertySources = new MutablePropertySources();private final ConfigurablePropertyResolver propertyResolver =new PropertySourcesPropertyResolver(this.propertySources);@Override@Nullablepublic String getProperty(String key) {return this.propertyResolver.getProperty(key);}

我们通过源码可以找到propertyResolver获取配置的位置,简单来说就是遍历所有PropertySource,第一个找到值的就直接返回。因此PropertySource的顺序还有一个优先级问题,排前面的优先使用。

	@Nullableprotected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {if (this.propertySources != null) {// 遍历所有PropertySourcefor (PropertySource<?> propertySource : this.propertySources) {if (logger.isTraceEnabled()) {logger.trace("Searching for key '" + key + "' in PropertySource '" +propertySource.getName() + "'");}Object value = propertySource.getProperty(key);if (value != null) {if (resolveNestedPlaceholders && value instanceof String) {value = resolveNestedPlaceholders((String) value);}logKeyFound(key, propertySource, value);return convertValueIfNecessary(value, targetValueType);}}}if (logger.isTraceEnabled()) {logger.trace("Could not find key '" + key + "' in any property source");}return null;}
1.1.2.4 getActiveProfiles

顾名思义,方法就是用来获取当前被激活的Profile

从方法中可以看到,获取激活的Profile的基本逻辑就是,在没有初始化的情况下,从配置项spring.profiles.active中获取,随后保存到成员变量activeProfiles中;之后可以直接从activeProfiles获取。

	public static final String ACTIVE_PROFILES_PROPERTY_NAME = "spring.profiles.active";private final Set<String> activeProfiles = new LinkedHashSet<>();public String[] getActiveProfiles() {return StringUtils.toStringArray(doGetActiveProfiles());}protected Set<String> doGetActiveProfiles() {synchronized (this.activeProfiles) {if (this.activeProfiles.isEmpty()) {String profiles = getProperty(ACTIVE_PROFILES_PROPERTY_NAME);if (StringUtils.hasText(profiles)) {setActiveProfiles(StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(profiles)));}}return this.activeProfiles;}}

1.2 PropertySource

简单来说就是对配置来源的抽象,也就是说每一种配置来源都有一个PropertySource。比如说,配置文件的配置来源是OriginTrackedMapPropertySource,Apollo的配置来源是ConfigPropertySource的对象。而如果我们想自定义配置来源,也可以通过继承PropertySource来实现。

1.2.1 PropertySource

首先介绍一下抽象类PropertySource。成员主要由几部分组成,name配置来源的名称,source来源的实体。最重要的方法getProperty是抽象方法,由子类实现查询配置的逻辑。

public abstract class PropertySource<T> {protected final Log logger = LogFactory.getLog(getClass());protected final String name;protected final T source;public PropertySource(String name, T source) {Assert.hasText(name, "Property source name must contain at least one character");Assert.notNull(source, "Property source must not be null");this.name = name;this.source = source;}@SuppressWarnings("unchecked")public PropertySource(String name) {this(name, (T) new Object());}public String getName() {return this.name;}public T getSource() {return this.source;}public boolean containsProperty(String name) {return (getProperty(name) != null);}@Nullablepublic abstract Object getProperty(String name);public static PropertySource<?> named(String name) {return new ComparisonPropertySource(name);}
}

1.3 ConfigFileApplicationListener

接下来,我们将介绍ConfigFileApplicationListener,通过它可以了解到配置文件是如何变成PropertySource的,并且可以了解到如何自定义PropertySource,自定义的PropertySource如何被发现并使用。

我们可以看到,ConfigFileApplicationListener实现了三个接口EnvironmentPostProcessorSmartApplicationListenerOrdered

public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {//..
}
1.3.1 Ordered

简单来说Ordered是用来表示多个同类组件之间顺序,后续在处理所有EnvironmentPostProcessor时会用到这个顺序。

	public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;private int order = DEFAULT_ORDER;@Overridepublic int getOrder() {return this.order;}
1.3.2 SmartApplicationListener

SmartApplicationListener简单来说就是可以同时监听多种应用事件ApplicationEventConfigFileApplicationListener会监听ApplicationEnvironmentPreparedEventApplicationPreparedEvent这两个事件,针对这两个事件,分别会执行onApplicationEnvironmentPreparedEventonApplicationPreparedEvent这两个方法。

	@Overridepublic boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType)|| ApplicationPreparedEvent.class.isAssignableFrom(eventType);}@Overridepublic void onApplicationEvent(ApplicationEvent event) {if (event instanceof ApplicationEnvironmentPreparedEvent) {onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);}if (event instanceof ApplicationPreparedEvent) {onApplicationPreparedEvent(event);}}

在Spring应用启动的前期,会创建并准备一个应用的Environment,完成准备之后会发布一个ApplicationEnvironmentPreparedEvent事件。这个事件会触发执行

ConfigFileApplicationListener的方法onApplicationEnvironmentPreparedEvent,对一系列PropertySource进行加载并注册到Environment中。

我们可以看到,这个方法做的事情主要是将所有EnvironmentPostProcessor加载进来,随后按照设定的顺序逐一执行。

	private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {// 加载所有EnvironmentPostProcessorList<EnvironmentPostProcessor> postProcessors = loadPostProcessors();// 把当前对象也加入到处理器列表中postProcessors.add(this);// 根据Ordered设置的顺序进行排序AnnotationAwareOrderComparator.sort(postProcessors);// EnvironmentPostProcessor逐一执行for (EnvironmentPostProcessor postProcessor : postProcessors) {postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());}}// 通过Spring Factory的机制加载所有EnvironmentPostProcessorList<EnvironmentPostProcessor> loadPostProcessors() {return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, getClass().getClassLoader());}

我们通过方法loadPostProcessors可以看出,Spring Boot为开发者提供了扩展接口。开发者可以自定义EnvironmentPostProcessor,然后在META-INF/spring.factories中将该自定义类进行注册。SpringFactoriesLoader会通过扫描每个jar包类路径的文件META-INF/spring.factoriesEnvironmentPostProcessor的实现类找出,然后将它们进行实例化。

因此,如果我们想自定义配置来源PropertySource,可以先实现EnvironmentPostProcessor,EnvironmentPostProcessor中将PropertySource加入到Environment中,然后将这个类写到文件META-INF/spring.factories中

org.springframework.boot.env.EnvironmentPostProcessor=ltd.dujiabao.configtests.config.CustomEnvironmentPostProcessor
1.3.3 EnvironmentPostProcessor

在Spring应用生成Environment之后,会通过调用EnvironmentPostProcessor,对Environment进行进一步增强。也就是说,如果我们想添加自定义的PropertySource,可以通过实现这个接口,然后通过spring.factories进行注册。比如,Apollocom.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer

ConfigFileApplicationListener自身就是EnvironmentPostProcessor的实现类,这个实现方法会将向Environment添加若干个PropertySource,包括基于配置文件的PropertySource。下面我们将详细介绍这个过程。

	@Overridepublic void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {addPropertySources(environment, application.getResourceLoader());}protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {// 添加RandomValuePropertySourceRandomValuePropertySource.addToEnvironment(environment);// 加载new Loader(environment, resourceLoader).load();}

首先,将RandomValuePropertySource添加到Environment,简单来说就是我们取配置的时候可以通过配置项random.intrandom.longrandom.uuid取出随机值,比较简单,不再赘述。

之后,通过内部类Loader进行加载。也就是说,加载PropertySource的核心逻辑在Loader

1.3.4 ConfigFileApplicationListener.Loader
1.3.4.1 成员变量&构造方法

我们先来看下Loader的成员变量:

  1. environment:当前Spring应用的Environment
  2. placeholdersResolver:用于解析占位符,从Environment中取出值
  3. resourceLoader:用于从文件系统中读取配置文件
  4. propertySourceLoaders:包含所有用于将配置文件加载为PropertySourcePropertySourceLoader
  5. profiles:保存当前待处理的激活的Profile,这是一个队列。一开始的时候,会有一个默认的Profile,并且在读入配置文件的时候,可以增加Profile。循环从队列中取出Profile,直到队列为空。
  6. processedProfiles:保存所有被处理过的Profile
  7. activatedProfiles:是否已取出被激活的Profile列表。意思是只会读取spring.profiles.active一次,先被读取的优先级高,会被采纳;其他不会被采纳。
  8. loaded:map保存每个Profile的PropertySource
  9. loadDocumentsCache:缓存读入的文件,避免需要每次都从文件系统中读入

从构造方法中,我们可以看出PropertySourceLoader也提供了可扩展的spi。构造方法中,通过SpringFactoriesLoader查出所有PropertySourceLoader。我们可以通过实现PropertySourceLoader,自定义解析配置文件的方法。

	private class Loader {private final Log logger = ConfigFileApplicationListener.this.logger;// 当前Spring应用的`Environment`private final ConfigurableEnvironment environment;// 用于解析占位符,从`Environment`中取出值private final PropertySourcesPlaceholdersResolver placeholdersResolver;// 用于从文件系统中读取配置文件private final ResourceLoader resourceLoader;// 包含所有用于将配置文件加载为PropertySource的PropertySourceLoaderprivate final List<PropertySourceLoader> propertySourceLoaders;// 保存当前待处理的激活的`Profile`,这是一个队列。一开始的时候,会有一个默认的`Profile`,并且在读入配置文件的时候,可以增加Profile。循环从队列中取出`Profile`,直到队列为空。private Deque<Profile> profiles;// 保存所有被处理过的`Profile`private List<Profile> processedProfiles;// 是否已取出被激活的`Profile`列表。意思是只会读取`spring.profiles.active`一次,先被读取的优先级高,会被采纳;其他不会被采纳。private boolean activatedProfiles;// map保存每个Profile的`PropertySource`private Map<Profile, MutablePropertySources> loaded;// 缓存读入的文件,避免需要每次都从文件系统中读入private Map<DocumentsCacheKey, List<Document>> loadDocumentsCache = new HashMap<>();Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {this.environment = environment;// 传入environment,构造PropertySourcesPlaceholdersResolverthis.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);// 创建资源加载器this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(null);// 从Spring Loader中取出配置加载器列表this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,getClass().getClassLoader());}}
1.3.4.2 Loader#load

接下来介绍加载配置的方法。FilteredPropertySource.apply 里面实际上没做什么,我们就直接忽略。我们直接看最后的lambda表达式即可。

基本逻辑就是:

  1. 从现有的PropertySource初始化profiles队列。也就是从环境变量、系统变量中取出。

  2. profiles队头取出Profile,然后从文件系统读入该Profile的配置文件。并且若配置文件中有指定spring.profiles.active,并且之前未激活过,则将这些Profile加入到队列中。循环读,直到队列为空。

因此,下面主要介绍两个方法:initializeProfilesload

void load() {FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,(defaultProperties) -> {this.profiles = new LinkedList<>();this.processedProfiles = new LinkedList<>();this.activatedProfiles = false;this.loaded = new LinkedHashMap<>();// 初始化Profile列表initializeProfiles();// 取出当前Profile,扫描配置文件while (!this.profiles.isEmpty()) {Profile profile = this.profiles.poll();if (isDefaultProfile(profile)) {// 将非默认Profile加入到EnvironmentaddProfileToEnvironment(profile.getName());}// 加载配置文件load(profile, this::getPositiveProfileFilter,addToLoaded(MutablePropertySources::addLast, false));this.processedProfiles.add(profile);}load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));addLoadedPropertySources();applyActiveProfiles(defaultProperties);});}
1.3.4.3 Loader#initializeProfiles

初始化成员变量profiles,基本逻辑是:

  1. 默认添加一个null,后续会读入文件application.yml或者其他application.文件
  2. 从现有的PropertySource中读入激活的Profile,并将其加入到队列后
  3. 若未指定激活的Profile,则添加一个叫defaultProfile
private void initializeProfiles() {// 默认添加一个null,后续会读入文件application.yml或者其他application.文件this.profiles.add(null);Binder binder = Binder.get(this.environment);// 从现有的PropertySource中读入spring.profiles.activeSet<Profile> activatedViaProperty = getProfiles(binder, ACTIVE_PROFILES_PROPERTY);// 从现有的PropertySource中读入spring.profiles.includeSet<Profile> includedViaProperty = getProfiles(binder, INCLUDE_PROFILES_PROPERTY);// 从environment中读入其他active的Profile,可能是硬编码指定的List<Profile> otherActiveProfiles = getOtherActiveProfiles(activatedViaProperty, includedViaProperty);this.profiles.addAll(otherActiveProfiles);this.profiles.addAll(includedViaProperty);// 添加激活的ProfileaddActiveProfiles(activatedViaProperty);// 若没有指定,那添加一个default的Profile,后续会读入文件application-default.yml或者其他application-default.文件if (this.profiles.size() == 1) {for (String defaultProfileName : this.environment.getDefaultProfiles()) {Profile defaultProfile = new Profile(defaultProfileName, true);this.profiles.add(defaultProfile);}}}void addActiveProfiles(Set<Profile> profiles) {if (profiles.isEmpty()) {return;}// 只允许添加一次激活的Profileif (this.activatedProfiles) {if (this.logger.isDebugEnabled()) {this.logger.debug("Profiles already activated, '" + profiles + "' will not be applied");}return;}// 添加激活的Profilethis.profiles.addAll(profiles);if (this.logger.isDebugEnabled()) {this.logger.debug("Activated activeProfiles " + StringUtils.collectionToCommaDelimitedString(profiles));}// 设置标识位this.activatedProfiles = true;// 删除默认的profile defaultremoveUnprocessedDefaultProfiles();}
1.3.4.4 Loader#load(Profile, DocumentFilterFactory, DocumentConsumer)

基本逻辑为:

  1. 获取配置文件的的路径位置,通过配置项spring.config.location。若没有则默认用这些目录,classpath:/classpath:/config/file:./file:./config/*/file:./config/
  2. 遍历每个路径,在每个路径下搜索配置文件。配置文件的文件名从配置项spring.config.name获取。若没有则默认用,application
		private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {// 遍历所有配置文件的路径,加载配置文件getSearchLocations().forEach((location) -> {boolean isDirectory = location.endsWith("/");// 获取配置文件名前缀Set<String> names = isDirectory ? getSearchNames() : NO_SEARCH_NAMES;// 加载names.forEach((name) -> load(location, name, profile, filterFactory, consumer));});}private Set<String> getSearchLocations() {// 获取额外的配置文件路径,spring.config.additional-locationSet<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);// 获取配置文件文件路径,spring.config.location,如果没有指定,则用默认值if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {locations.addAll(getSearchLocations(CONFIG_LOCATION_PROPERTY));}else {locations.addAll(// 默认从这些路径搜索文件classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));}return locations;}private Set<String> getSearchNames() {// 获取配置文件前缀名,spring.config.nameif (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);Set<String> names = asResolvedSet(property, null);names.forEach(this::assertValidConfigName);return names;}// 若没有设置,默认为applicationreturn asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);}
1.3.4.5 Loader#load(String, Profile, DocumentFilterFactory, DocumentConsumer)

基本逻辑就是:

  1. 若传进来的location是文件,遍历所有PropertySourceLoader,对文件进行加载
  2. 若传进来的location是文件夹,遍历所有PropertySourceLoader,对所有可能的文件进行尝试加载
		private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,DocumentConsumer consumer) {// 当传进来的location是文件,不是文件夹,name为空,直接进入下面的加载逻辑if (!StringUtils.hasText(name)) {// 遍历所有PropertySourceLoader,只有支持文件后缀的能加载for (PropertySourceLoader loader : this.propertySourceLoaders) {if (canLoadFileExtension(loader, location)) {load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);return;}}throw new IllegalStateException("File extension of config file location '" + location+ "' is not known to any PropertySourceLoader. If the location is meant to reference "+ "a directory, it must end in '/'");}// 当传进来的location是文件夹Set<String> processed = new HashSet<>();// 遍历所有PropertySourceLoader,获取该加载器支持的文件后缀,然后拼接成路径,对文件进行加载for (PropertySourceLoader loader : this.propertySourceLoaders) {for (String fileExtension : loader.getFileExtensions()) {if (processed.add(fileExtension)) {loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,consumer);}}}}
1.3.4.6 Loader#loadForFileExtension

这个方法的逻辑比较复杂,一般来说有用的只有注释的那两处。

  1. Profile不为空时,拼接文件名 prefix + “-” + profile + fileExtension,随后在文件系统查找并加载文件。
  2. Profile为空时,拼接文件名 prefix + fileExtension,随后在文件系统查找并加载文件。
		private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);if (profile != null) {String profileSpecificFile = prefix + "-" + profile + fileExtension;// 在Profile不为null时,一般会通过这个方法加载配置文件load(loader, profileSpecificFile, profile, defaultFilter, consumer);load(loader, profileSpecificFile, profile, profileFilter, consumer);for (Profile processedProfile : this.processedProfiles) {if (processedProfile != null) {String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;load(loader, previouslyLoaded, profile, profileFilter, consumer);}}}// 在在Profile为null时,一般会通过这个方法加载配置文件load(loader, prefix + fileExtension, profile, profileFilter, consumer);}
1.3.4.7 Loader#load(PropertySourceLoader, String, Profile, DocumentFilter, DocumentConsumer)

基本逻辑就是将文件读进Document,随后将DocumentPropertySource 插入到loaded中,这样就完成了从配置文件到PropertySource的转换

private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,DocumentConsumer consumer) {// 通过路径查找资源Resource[] resources = getResources(location);for (Resource resource : resources) {try {// 文件不存在,直接返回if (resource == null || !resource.exists()) {if (this.logger.isTraceEnabled()) {StringBuilder description = getDescription("Skipped missing config ", location, resource,profile);this.logger.trace(description);}continue;}// 文件后缀为空,直接返回if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {if (this.logger.isTraceEnabled()) {StringBuilder description = getDescription("Skipped empty config extension ", location,resource, profile);this.logger.trace(description);}continue;}// 包含一些隐藏的元素,不重要。。if (resource.isFile() && isPatternLocation(location) && hasHiddenPathElement(resource)) {if (this.logger.isTraceEnabled()) {StringBuilder description = getDescription("Skipped location with hidden path element ",location, resource, profile);this.logger.trace(description);}continue;}// 将文件加载为Document列表String name = "applicationConfig: [" + getLocationName(location, resource) + "]";List<Document> documents = loadDocuments(loader, name, resource);if (CollectionUtils.isEmpty(documents)) {if (this.logger.isTraceEnabled()) {StringBuilder description = getDescription("Skipped unloaded config ", location, resource,profile);this.logger.trace(description);}continue;}List<Document> loaded = new ArrayList<>();// 一般我们不会配filter,认为考虑满足的情况就好了for (Document document : documents) {if (filter.match(document)) {addActiveProfiles(document.getActiveProfiles());addIncludedProfiles(document.getIncludeProfiles());loaded.add(document);}}Collections.reverse(loaded);// 将文档转换为if (!loaded.isEmpty()) {loaded.forEach((document) -> consumer.accept(profile, document));if (this.logger.isDebugEnabled()) {StringBuilder description = getDescription("Loaded config file ", location, resource,profile);this.logger.debug(description);}}}catch (Exception ex) {StringBuilder description = getDescription("Failed to load property source from ", location,resource, profile);throw new IllegalStateException(description.toString(), ex);}}}private DocumentConsumer addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>> addMethod,boolean checkForExisting) {return (profile, document) -> {if (checkForExisting) {for (MutablePropertySources merged : this.loaded.values()) {if (merged.contains(document.getPropertySource().getName())) {return;}}}// 将文档的PropertySource加入到loaded里面MutablePropertySources merged = this.loaded.computeIfAbsent(profile,(k) -> new MutablePropertySources());addMethod.accept(merged, document.getPropertySource());};}
1.3.4.8 Loader#addLoadedPropertySources

1.3.4.2 在完成加载之后,会将加载成功的所有PropertySource加入到Environment

		private void addLoadedPropertySources() {MutablePropertySources destination = this.environment.getPropertySources();List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());Collections.reverse(loaded);String lastAdded = null;Set<String> added = new HashSet<>();// 遍历所有被load的PropertySourcefor (MutablePropertySources sources : loaded) {for (PropertySource<?> source : sources) {if (added.add(source.getName())) {// 将其加入到environment中addLoadedPropertySource(destination, lastAdded, source);lastAdded = source.getName();}}}}private void addLoadedPropertySource(MutablePropertySources destination, String lastAdded,PropertySource<?> source) {if (lastAdded == null) {if (destination.contains(DEFAULT_PROPERTIES)) {destination.addBefore(DEFAULT_PROPERTIES, source);}else {destination.addLast(source);}}else {destination.addAfter(lastAdded, source);}}

至此,终于介绍完Spring Boot加载配置文件至Environment的逻辑。

2. 配置注入

本小节主要介绍@Value@ConfigurationProperties是如何从Environment中拿到配置的。

2.1 @Value 原理

简单来说,就是在构建bean的时候,在处理自动注入时,解析@Value的占位符之后,从所有PropertySource中找到配置值。

详见https://juejin.cn/post/7043315611744600094

image-20240615164546265.png

截屏2024-06-15 18.28.35.png

截屏2024-06-15 18.33.51.png

截屏2024-06-15 18.34.32.png

截屏2024-06-15 18.35.05.png

2.2 @ConfigurationProperties

简单来说,在创建标注了@ConfigurationProperties的bean之后,会遍历所有BeanPostProcessor执行postProcessBeforeInitialization方法。BeanPostProcessor有一个实现类ConfigurationPropertiesBindingPostProcessor专门负责将配置值绑定到bean上。

绑定的逻辑也就是从PropertySource中取出配置值,随后设置到bean的字段上。详见org.springframework.boot.context.properties.bind.Binder

截屏2024-06-15 21.57.07.png

截屏2024-06-15 18.51.14.png

截屏2024-06-15 18.55.05.png

截屏2024-06-15 18.55.23.png

截屏2024-06-15 18.55.39.png

截屏2024-06-15 18.56.21.png

截屏2024-06-15 18.56.37.png

截屏2024-06-15 18.57.11.png

截屏2024-06-15 19.54.01.png

截屏2024-06-15 19.54.20.png

org.springframework.boot.context.properties.bind.Binder#findProperty我们可以看出实际上就是从ConfigurationPropertySource中取出配置值。

截屏2024-06-15 19.21.20.png

四、配置热更新的实践

考虑到Apollo是比较常见的配置中心,我们将以Apollo为例介绍如何实现热更新的Spring应用的配置的。

1. @Value

apollo-client 默认支持热更新 @Value的字段值,无需额外配置或开发。

原理可见 com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener

Apollo 上更新配置之后,AutoUpdateConfigChangeListener会收到消息,随后从消息中拿出被修改的key,重新查询最新的值,通过反射对字段值进行重新设置。

public class AutoUpdateConfigChangeListener implements ConfigChangeListener{@Overridepublic void onChange(ConfigChangeEvent changeEvent) {// 获取所有修改的keySet<String> keys = changeEvent.changedKeys();if (CollectionUtils.isEmpty(keys)) {return;}for (String key : keys) {// 查出key对应的SpringValue,SpringValue存储Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);if (targetValues == null || targetValues.isEmpty()) {continue;}// 通过反射更新值for (SpringValue val : targetValues) {updateSpringValue(val);}}}private void updateSpringValue(SpringValue springValue) {try {// 查出最新的值,若有需要对值进行转换Object value = resolvePropertyValue(springValue);// 通过反射更新springValue.update(value);logger.info("Auto update apollo changed value successfully, new value: {}, {}", value,springValue);} catch (Throwable ex) {logger.error("Auto update apollo changed value failed, {}", springValue.toString(), ex);}}private Object resolvePropertyValue(SpringValue springValue) {// value will never be null, as @Value and @ApolloJsonValue will not allow thatObject value = placeholderHelper.resolvePropertyValue(beanFactory, springValue.getBeanName(), springValue.getPlaceholder());if (springValue.isJson()) {value = parseJsonValue((String)value, springValue.getGenericType());} else {if (springValue.isField()) {// org.springframework.beans.TypeConverter#convertIfNecessary(java.lang.Object, java.lang.Class, java.lang.reflect.Field) is available from Spring 3.2.0+if (typeConverterHasConvertIfNecessaryWithFieldParameter) {value = this.typeConverter.convertIfNecessary(value, springValue.getTargetType(), springValue.getField());} else {value = this.typeConverter.convertIfNecessary(value, springValue.getTargetType());}} else {value = this.typeConverter.convertIfNecessary(value, springValue.getTargetType(),springValue.getMethodParameter());}}return value;}
}
public class SpringValue {public void update(Object newVal) throws IllegalAccessException, InvocationTargetException {if (isField()) {injectField(newVal);} else {injectMethod(newVal);}}private void injectField(Object newVal) throws IllegalAccessException {Object bean = beanRef.get();if (bean == null) {return;}boolean accessible = field.isAccessible();field.setAccessible(true);field.set(bean, newVal);field.setAccessible(accessible);}private void injectMethod(Object newVal)throws InvocationTargetException, IllegalAccessException {Object bean = beanRef.get();if (bean == null) {return;}methodParameter.getMethod().invoke(bean, newVal);}
}

2. @ConfigurationProperties

@ConfigurationProperties默认是不能自动更新的,但是我们从上一小节可以看出,当Apollo配置更新的时候,会通知监听器ConfigChangeListener。我们可以通过自定义一个ConfigChangeListener,在出现配置更新的时候,触发@ConfigurationProperties bean的自动更新。

首先引入依赖,用于发布EnvironmentChangeEvent,以及发布EnvironmentChangeEvent之后自动更新@ConfigurationProperties的bean。

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-context</artifactId>
</dependency>

之后,实现一个ConfigChangeListener,监听配置变更,发布事件EnvironmentChangeEvent,至此就可以实现ConfigurationProperties bean的热更新。

@Configuration
public class ConfigurationPropertiesLiveRefresher implements ConfigChangeListener, ApplicationRunner {@Autowiredprivate ApplicationEventPublisher publisher;@Value("#{'${apollo.bootstrap.namespaces:}'.split(',')}")private List<String> namespaces;private static final Logger log = LoggerFactory.getLogger(ConfigurationPropertiesLiveRefresher.class);@Overridepublic void run(ApplicationArguments args) {// 启动时,注册监听器,将当前类注册进去for (String namespace : namespaces) {ConfigService.getConfig(namespace).addChangeListener(this);log.info("Successfully added config change listener to namespace {}", namespace);}}@Overridepublic void onChange(ConfigChangeEvent changeEvent) {// 当存在配置更新时,发布一个EnvironmentChangeEvent事件publisher.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));log.info("Successfully changed config change event {}", changeEvent.changedKeys());}
}

我们可以通过源码分析一下EnvironmentChangeEvent触发更新的原理。

当发布事件EnvironmentChangeEvent之后,监听器ConfigurationPropertiesRebinder监听到事件之后,会触发bean到重新绑定。这样就实现了ConfigurationProperties bean的重新绑定。重新绑定里面会调用到方法initializeBean,这个方法又会走到刚刚2.2小节提到的配置绑定逻辑。

@Component
@ManagedResource
public class ConfigurationPropertiesRebinderimplements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {// 所有ConfigurationPropertie的bean的容器private ConfigurationPropertiesBeans beans;@Overridepublic void onApplicationEvent(EnvironmentChangeEvent event) {if (this.applicationContext.equals(event.getSource())|| event.getKeys().equals(event.getSource())) {// 重新绑定rebind();}} @ManagedOperationpublic void rebind() {this.errors.clear();// 遍历所有ConfigurationPropertie的bean,进行重新绑定for (String name : this.beans.getBeanNames()) {rebind(name);}}@ManagedOperationpublic boolean rebind(String name) {if (!this.beans.getBeanNames().contains(name)) {return false;}if (this.applicationContext != null) {try {Object bean = this.applicationContext.getBean(name);if (AopUtils.isAopProxy(bean)) {bean = ProxyUtils.getTargetObject(bean);}if (bean != null) {// 对bean执行销毁方法this.applicationContext.getAutowireCapableBeanFactory().destroyBean(bean);// 对bean重新初始化this.applicationContext.getAutowireCapableBeanFactory().initializeBean(bean, name);return true;}}catch (RuntimeException e) {this.errors.put(name, e);throw e;}catch (Exception e) {this.errors.put(name, e);throw new IllegalStateException("Cannot rebind to " + name, e);}}return false;}
}
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactoryimplements AutowireCapableBeanFactory {@Overridepublic Object initializeBean(Object existingBean, String beanName) {return initializeBean(beanName, existingBean, null);}protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {if (System.getSecurityManager() != null) {AccessController.doPrivileged((PrivilegedAction<Object>) () -> {invokeAwareMethods(beanName, bean);return null;}, getAccessControlContext());}else {invokeAwareMethods(beanName, bean);}Object wrappedBean = bean;if (mbd == null || !mbd.isSynthetic()) {// 这里!!又重新进入这个方法,对bean的值进行重新绑定!wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);}try {invokeInitMethods(beanName, wrappedBean, mbd);}catch (Throwable ex) {throw new BeanCreationException((mbd != null ? mbd.getResourceDescription() : null),beanName, "Invocation of init method failed", ex);}if (mbd == null || !mbd.isSynthetic()) {wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);}return wrappedBean;}
}

3. 依赖@ConfigurationProperties的bean更新

但是,还有另一个问题。有些bean的字段值是根据ConfigurationProperties bean的配置值而生成的。当Configuration bean的配置值更新之后,使用这个配置值的bean的字段也需要更新。

比如说MyPropertiesUsage依赖MyProperties的配置值z,生成自身的字段值myValue

@Component
@ConfigurationProperties("x.y")
@Data
public class MyProperties {private String z = "";
}
@Data
@Component
public class MyPropertiesUsage {@Autowiredprivate MyProperties myProperties;private String myValue;@PostConstructpublic void init() {myValue = "my-" + myProperties.getZ();}
}

为了在更新MyProperties之后,触发MyPropertiesUsage的更新,主要有几个思路。

  1. MyProperties 添加初始化方法(比如实现接口InitializingBean、注解@PostConstruct指定),调用方法MyPropertiesUsage.init(),触发MyPropertiesUsage重新初始化。缺点是不够优雅,没有做到依赖反转,不够通用。
  2. MyProperties 添加初始化方法(比如实现接口InitializingBean、注解@PostConstruct指定),调用发布自定义的事件MyPropertiesChangedEventMyPropertiesUsage监听事件MyPropertiesChangedEvent,重新执行初始化方法。缺点是不够通用,每次有相似的需求时,都需要进行额外的改造。
  3. 自定义注解RefreshAfterConfigurationPropertiesChanged,标注在需要在配置变化时更新的bean上。当监听到配置发生变化时,自动将所有标注了该注解的bean重新初始化。

第三个思路比较通用,并且开发成本也比较低。我们可以代码实现:

自定义注解RefreshAfterConfigurationPropertiesChanged

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RefreshAfterConfigurationPropertiesChanged {
}

MyPropertiesUsage添加注解RefreshAfterConfigurationPropertiesChanged

@Data
@Component
@RefreshAfterConfigurationPropertiesChanged
public class MyPropertiesUsage {@Autowiredprivate MyProperties myProperties;private String myValue;@PostConstructpublic void init() {myValue = "my-" + myProperties.getZ();}
}

修改ConfigurationPropertiesLiveRefresher,添加方法refreshBeansDependsOnConfigurationProperties,在监听到配置变更事件,并且配置已重新绑定之后,对标注了ConfigurationPropertiesLiveRefresher对bean进行重新初始化。

@Configuration
public class ConfigurationPropertiesLiveRefresher implements ConfigChangeListener, ApplicationRunner, ApplicationContextAware {@Autowiredprivate ApplicationEventPublisher publisher;@Value("#{'${apollo.bootstrap.namespaces:}'.split(',')}")private List<String> namespaces;@Autowiredprivate ApplicationContext applicationContext;private static final Logger log = LoggerFactory.getLogger(ConfigurationPropertiesLiveRefresher.class);@Overridepublic void run(ApplicationArguments args) {for (String namespace : namespaces) {ConfigService.getConfig(namespace).addChangeListener(this);log.info("Successfully added config change listener to namespace {}", namespace);}}@Overridepublic void onChange(ConfigChangeEvent changeEvent) {publisher.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));// 新增方法,刷新beanrefreshBeansDependsOnConfigurationProperties();log.info("Successfully changed config change event {}", changeEvent.changedKeys());}private void refreshBeansDependsOnConfigurationProperties() {// 从容器中拿到所有标注了RefreshAfterConfigurationPropertiesChanged的beanMap<String, Object> beans = applicationContext.getBeansWithAnnotation(RefreshAfterConfigurationPropertiesChanged.class);// 对所有bean先进行销毁,再对bean进行初始化for (Map.Entry<String, Object> entry : beans.entrySet()) {this.applicationContext.getAutowireCapableBeanFactory().destroyBean(entry.getValue());this.applicationContext.getAutowireCapableBeanFactory().initializeBean(entry.getValue(), entry.getKey());}}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;}
}

四、总结

本文在第二章中介绍了Spring配置的基本使用、第三章中介绍了Spring配置原理、第四章中介绍了日常开发中配置热更新的一些实践。

五、参考资料

  1. Spring Framework源码
  2. Apollo Client源码
  3. Spring Environment介绍
  4. Apollo 源码解析 —— 客户端配置 Spring 集成(一)之 XML 配置
  5. 自定义EnvironmentPostProcessor

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/28610.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【计算机毕业设计】241外卖微信小程序

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

jadx+android studio+雷电模拟器 动态调试apk

# 环境准备 1.雷电模拟器&#xff0c;开启root 2.jadx&#xff1a; https://sourceforge.net/projects/jadx.mirror/files/v1.5.0/jadx-gui-1.5.0-with-jre-win.zip/download 3.java jdk 11 https://www.oracle.com/cn/java/technologies/javase/jdk11-archive-downloads.…

【文献阅读】Adaptive Arrays

符号表 由于论文内符号繁杂&#xff0c;这里写了一个符号表 符号含义 μ k l \mu_{kl} μkl​kl协方差项 n k n_k nk​K通道的复包络 n l n_l nl​L通道的复包络 μ l k \mu_{lk} μlk​kl协方差项的共轭 α \alpha α定义信号的幅度和时间变化 s k s_k sk​k信道中的信号 w …

什么是Dubbo?

文章目录 1、Dubbo介绍1.1 什么是Dubbo1.2 Dubbo关键特性1.3 什么是RPC1.4 实现RPC的方式1.5 Dubbo中的五大核心组件 1、Dubbo介绍 Apache Dubbo是一款高性能、轻量级的开源微服务开发框架&#xff0c;它提供了RPC通信与微服务治理两大关键能力。这意味着&#xff0c;使用Dubbo…

【Linux】进程_6

文章目录 五、进程8. 进程地址空间 未完待续 五、进程 8. 进程地址空间 上图可能很多人都看过了&#xff0c;这里再来验证一下&#xff1a; 验证位置&#xff1a; 验证堆栈的生长方向&#xff1a; 在上面的空间布局图中&#xff0c;有一个疑问&#xff0c;画的空间是 内存…

【RabbitMQ】初识 RabbitMQ

初识 RabbitMQ 1.认识 RabbitMQ1.1 介绍1. 2.使用场景1.2.1 推送通知1.2.2 异步任务1.2.3 多平台应用的通信1.2.4 消息延迟1.2.5 远程过程调用 1.3 特性 2.基本概念2.1 生产者、消费者和代理2.2 消息队列2.3 交换机2.3.1 direct2.3.2 topic2.3.3 headers2.3.4 fanout 2.4 绑定2…

docker hub仓库被禁用,镜像加速器站点替换

整理 站点整理之前用的daemon.json,现更改镜像加速地址替换自己的docker加速器daemon.json前面加https:// 站点整理 之前用的daemon.json,现更改镜像加速地址 vim /etc/docker/daemon.json{"registry-mirrors": ["https://4xgbe4ey.mirror.aliyuncs.com",…

计算缺失msvcr120.dll文件怎么办,msvcr120.dll丢失的解决方法分享

在计算机使用过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“找不到msvcr120.dll”。那么&#xff0c;msvcr120.dll到底是什么&#xff1f;为什么计算机会找不到它&#xff1f;它会对计算机产生什么具体影响&#xff1f;如何解决这个问题&#xff1f;…

vue框架学习------框架概述

框架 在基础语言之上,对各种基础功能进行封装 什么是框架 框架&#xff08;Framework&#xff09;是整个或部分系统的可重用设计&#xff0c;表现为一组抽象构件及构件实例间交互的方法; 另一种定义认为&#xff0c;框架是可被应用开发者定制的应用骨架。前者是从应用方面而…

正能量情感语录热门素材文案去哪里找?文案素材网站分享

正能量情感语录热门素材文案去哪里找&#xff1f;文案素材网站分享 想为你的作品注入正能量和情感温度&#xff1f;不知如何获取热门情感语录素材&#xff1f;别担心&#xff0c;今天我将为大家推荐一些海外知名的素材网站&#xff0c;让你轻松找到受欢迎的文案素材&#xff…

ffmpeg解封装rtsp并录制视频-(1)解封装rtsp断网或摄像机重启后自动重连处理

头文件&#xff1a; xtools.h #pragma once #include <thread> #include <iostream> #include <mutex> //日志级别 DEBUG INFO ERROR FATAL enum XLogLevel {XLOG_TYPE_DEBUG,XLOG_TYPE_INFO,XLOG_TPYE_ERROR,XLOG_TYPE_FATAL }; #define LOG_MIN_LEVEL XLO…

dp练习题

先来一个简单dp练习 class Solution { public:int rob(vector<int>& nums) {int n nums.size();vector<int> a(n 1);int ans nums[0]; a[0] nums[0];if (n 1) return ans;a[1] max(nums[0], nums[1]);ans max(ans, a[1]);if (n 2) return ans;for (i…

xss+csrf项目实例

项目背景&#xff1a; 如下&#xff1a;我们是在一个类似文章管理系统的网站上面发现的该漏洞。我们将其运行在本地的phpstudy集成环境上面。 源码地址下载链接&#xff1a;https://pan.baidu.com/s/1MpnSAq7a_oOcGh4XgPE-2w 提取码&#xff1a;4444 考察内容&#xff1a; …

10 C++11

10 C11 1、类型推导1.1 auto关键字1.2 auto类型推断本质 2、类型计算2.1 类型计算分类2.2 类型计算的四种规则2.3 返回值类型计算 3、列表初始化4、Lambda表达式4.1 前置知识4.2 Lambda表达式4.3 捕获表 5、右值引用5.1 概念5.2 左值引用和右值引用 6、移动语义 1、类型推导 1…

嵌入式复古游戏项目开发与实现

大家好,今天看到一个火柴盒项目,非常的小巧,分享给大家,感兴趣的话,可以复刻一个玩一玩。 MicroByte 是一款微型主机,能够运行 NES、GameBoy、GameBoy Color、Game Gear 和 Sega Master 系统的游戏,所有元器件都设计在这 78 x 17 x 40 mm 的封装中。尽管成品尺寸很小,但…

AI预测体彩排3采取888=3策略+和值012路或胆码测试6月16日升级新模型预测第1弹

根据前面的预测效果&#xff0c;我对模型进行了重新优化&#xff0c;因为前面的模型效果不是很好。熟悉我的彩友比较清楚&#xff0c;我之前的主要精力是对福彩3D进行各种模型的开发和预测&#xff0c;排三的预测也就是最近1个月才开始搞的。3D的预测&#xff0c;经过对模型的多…

Java面向对象-接口

Java面向对象-接口 一、JDK1.8之前二、接口的作用三、JDK1.8之后&#xff0c;新增非抽象方法四、静态方法 一、JDK1.8之前 1、类是类&#xff0c;接口是接口&#xff0c;它们是同一层次的概念 2、接口中没有构造器 3、接口如何声明&#xff1a;interface 4、在jdk1.8之前&…

【字符串函数】

1.strlen的使⽤和模拟实现 size_t strlen ( const char * str ); 1.字符串以 \0 作为结束标志&#xff0c;strlen函数返回的是在字符串中 \0 前⾯出现的字符个数&#xff08;不包 含 \0 )。 2.参数指向的字符串必须要以 \0 结束。 3.注意函数的返回值为size_t&#xff0c;是⽆…

力扣148. 排序链表

给你链表的头结点 head &#xff0c;请将其按 升序 排列并返回 排序后的链表 。 示例 1&#xff1a; 输入&#xff1a;head [4,2,1,3] 输出&#xff1a;[1,2,3,4] 示例 2&#xff1a; 输入&#xff1a;head [-1,5,3,4,0] 输出&#xff1a;[-1,0,3,4,5] 示例 3&…

23 华三(自动获取的IP地址)

华三交换机 DHCP 配置 #version 7.1.070, Alpha 7170 //设备的版本信息 #sysname sw1 //修改设备的名字 #irf mac-address persistent timerirf auto-update enableundo irf link-delayirf member 1 priority 1#dhcp enable //开启DHCP 服务dhcp server forbidden-ip 192.168.…