Apollo架构篇 - 客户端架构
前言
本文基于 Apollo 1.8.0 版本展开分析。
客户端
使用
Apollo 支持 API 方式和 Spring 整合两种方式。
API 方式
API 方式是最简单、高效使用使用 Apollo 配置的方式,不依赖 Spring 框架即可使用。
获取命名空间的配置
// 1、获取默认的命名空间的配置
Config config = ConfigService.getAppConfig();// 2、获取properties格式的命名空间的配置
// String somePublicNamespace = "CAT";
// Config config = ConfigService.getConfig(somePublicNamespace);// 3、获取yaml/yml格式的命名空间的配置
// Config config = ConfigService.getConfig("application.yml");// 4、获取其它格式的命名空间的配置
// ConfigFile configFile = ConfigService.getConfigFile("test", ConfigFileFormat.XML);
// String content = configFile.getContent();String someKey = "someKeyFromDefaultNamespace";
String someDefaultValue = "someDefaultValueForTheKey";
String value = config.getProperty(someKey, someDefaultValue);
通过 Config 的 getProperty 方法可以获取到指定属性对应的属性值。
监听配置变化事件
Config config = ConfigService.getAppConfig();
config.addChangeListener(new ConfigChangeListener() {@Overridepubic void onChange(ConfigChangeEvent changeEvent) {System.out.println("Changes for namespace " + changeEvent.getNamespace());for (String key : changeEvent.changedKeys()) {ConfigChange change = changeEvent.getChange(key);System.out.println(String.format("Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType()));}}
});
希望配置发生变化时得到通知。通过 Config 的 addChangeListener 方法添加一个 ConfigChangeListener 监听器。
Spring整合方式
可以在代码中直接使用,如:@Value("${someKeyFromApollo:someDefaultValue}")
。
也可以在配置文件中使用,如:spring.datasource.url:${someKeyFromApollo:someDefaultValue}
。
甚至可以直接托管 Spring 中的配置。如:在 Apollo 中直接配置 spring.datasource.url=jdbc:mysql://localhost:3306/somedb
。
支持 Spring Boot 的 @ConfigurationProperties
方式。
也支持结合 API 方式使用,如:@ApolloConfig private Config config;
。
基于 Spring 的配置
注入默认的命名空间的配置到 Spring 中
@Configuration
@EnableApolloConfig
public class AppConfig {}
注入多个命名空间的配置到 Spring 中
@Configuration
@EnableApolloConfig({"FX.apollo", "application.yml"})
public class AnotherAppConfig {}
注入多个命名空间的配置到 Spring 中,并且指定顺序
在 @EnableApolloConfig 注解中的 order 属性指定顺序,值越小则顺序越靠前。
@Configuration
@EnableApolloConfig(order = 2)
public class AppConfig {}@Configuration
@EnableApolloConfig(value = {"FX.apollo", "application.yml"}, order = 1)
public class AnotherAppConfig {}
基于 Spring Boot 的配置
额外支持通过 application.properties / bootstrap.properties 进行配置,该方式可以使配置在更早的阶段注入,比如使用 @ConditionalOnProperty
的场景或者有一些 spring-boot-starter 在启动阶段就需要读取配置然后做一些事情。
# 启动阶段注入application命名空间的配置
apollo.bootstrap.enabled = true
也支持注入多个命名空间的配置。
# 启动阶段注入application,FX.apollo,application.yml命名空间的配置
apollo.bootstrap.enabled = true
apollo.bootstrap.namespaces = application,FX.apollo,application.yml
可以让 Apollo 的加载顺序在日志系统之前,比如希望把日志相关的配置(logging.level.root=info
或者 logback-spring.xml 中的参数)也交给 Apollo 管理。
# 启动阶段注入application命名空间的配置
apollo.bootstrap.enabled = true
# 让 Apollo 的加载顺序在日志系统之前
apollo.bootstrap.eagerLoad.enabled = true
原理
1、客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。
2、客户端还会定时从服务端拉取应用的最新配置。
- 这是一个 fallback 机制,为了防止推送机制失效导致配置不更新。
- 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回 304 - Not Modified。
- 定时频率默认为每 5 分钟拉取一次,客户端也可以通过运行时指定
apollo.refreshInterval
系统参数来覆盖,单位为分钟。
3、客户端从服务端获取到应用的最新配置后,会保存在内存中。
4、客户端会把从服务端获取到的配置在本地文件系统缓存一份。在遇到服务不可用,或者网络不通的时候,依然能从本地恢复配置。
5、应用程序可以从 Apollo 客户端获取最新的配置、订阅配置更新通知。
源码分析
RemoteConfigRepository
在 ConfigService 的 getConfig 方法中会调用 RemoteConfigRepository 的构造器方法。
先来看 RemoteConfigRepository 构造器方法。
public RemoteConfigRepository(String namespace) {m_namespace = namespace;m_configCache = new AtomicReference<>();m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);m_httpUtil = ApolloInjector.getInstance(HttpUtil.class);m_serviceLocator = ApolloInjector.getInstance(ConfigServiceLocator.class);remoteConfigLongPollService = ApolloInjector.getInstance(RemoteConfigLongPollService.class);m_longPollServiceDto = new AtomicReference<>();m_remoteMessages = new AtomicReference<>();m_loadConfigRateLimiter = RateLimiter.create(m_configUtil.getLoadConfigQPS());m_configNeedForceRefresh = new AtomicBoolean(true);m_loadConfigFailSchedulePolicy = new ExponentialSchedulePolicy(m_configUtil.getOnErrorRetryInterval(),m_configUtil.getOnErrorRetryInterval() * 8);// 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中,同时持久化到磁盘中// 应用程序可以从客户端获取最新的配置、订阅配置更新通知this.trySync();// 客户端定时调度处理this.schedulePeriodicRefresh();// 客户端长轮询处理this.scheduleLongPollingRefresh();
}
1、trySync 方法
首先看下 trySync 方法的处理逻辑。
protected boolean trySync() {try {sync();return true;} catch (Throwable ex) {Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex));logger.warn("Sync config failed, will retry. Repository {}, reason: {}", this.getClass(), ExceptionUtil.getDetailMessage(ex));}return false;
}
进入 sync 方法内部一窥究竟。
@Override
protected synchronized void sync() {Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig");try {// 从 m_configCache 缓存中获取本地配置ApolloConfig previous = m_configCache.get();// 从服务端加载远程配置ApolloConfig current = loadApolloConfig();// 如果本地配置与远程配置不一致,即远程配置发生了变化if (previous != current) {logger.debug("Remote Config refreshed!");// 更新 m_configCache 缓存m_configCache.set(current);// 回调所有 RepositoryChangeListener 的 onRepositoryChange 方法this.fireRepositoryChange(m_namespace, this.getConfig());}if (current != null) {Tracer.logEvent(String.format("Apollo.Client.Configs.%s", current.getNamespaceName()),current.getReleaseKey());}transaction.setStatus(Transaction.SUCCESS);} catch (Throwable ex) {transaction.setStatus(ex);throw ex;} finally {transaction.complete();}
}
不难看出 sync 方法实际上对应原理 3 - 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中。
2、schedulePeriodicRefresh 方法
初始延迟 5 分钟,之后每隔 5 分钟重复调度一次 trySync 方法。
private void schedulePeriodicRefresh() {logger.debug("Schedule periodic refresh with interval: {} {}",m_configUtil.getRefreshInterval(), m_configUtil.getRefreshIntervalTimeUnit());// 默认初始延迟5分钟,之后每隔5分钟重复调度一次m_executorService.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {Tracer.logEvent("Apollo.ConfigService", String.format("periodicRefresh: %s", m_namespace));logger.debug("refresh config for namespace: {}", m_namespace);trySync();Tracer.logEvent("Apollo.Client.Version", Apollo.VERSION);}}, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(),m_configUtil.getRefreshIntervalTimeUnit());
}
不难看出 schedulePeriodicRefresh 方法实际上对应原理 2 - 客户端还会定时从服务端拉取应用的最新配置。
3、scheduleLongPollingRefresh 方法
客户端向服务端发起长轮询请求。实际上对应原理 1 - 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。
private void scheduleLongPollingRefresh() {// 交给 RemoteConfigLongPollService 处理remoteConfigLongPollService.submit(m_namespace, this);
}
接着看下 RemoteConfigLongPollService 的 submit 方法是如何处理的。
public boolean submit(String namespace, RemoteConfigRepository remoteConfigRepository) {// 更新 m_longPollNamespaces 缓存boolean added = m_longPollNamespaces.put(namespace, remoteConfigRepository);// 更新 m_notifications 缓存m_notifications.putIfAbsent(namespace, INIT_NOTIFICATION_ID);if (!m_longPollStarted.get()) {// 执行 startLongPolling 方法startLongPolling();}return added;
}
接着看下 startLongPolling 方法的处理逻辑。
private void startLongPolling() {if (!m_longPollStarted.compareAndSet(false, true)) {return;}try {final String appId = m_configUtil.getAppId();final String cluster = m_configUtil.getCluster();final String dataCenter = m_configUtil.getDataCenter();final String secret = m_configUtil.getAccessKeySecret();// 默认 2000 毫秒final long longPollingInitialDelayInMills = m_configUtil.getLongPollingInitialDelayInMills();m_longPollingService.submit(new Runnable() {@Overridepublic void run() {if (longPollingInitialDelayInMills > 0) {try {logger.debug("Long polling will start in {} ms.", longPollingInitialDelayInMills);TimeUnit.MILLISECONDS.sleep(longPollingInitialDelayInMills);} catch (InterruptedException e) {//ignore}}// 执行 doLongPollingRefresh 方法doLongPollingRefresh(appId, cluster, dataCenter, secret);}});} catch (Throwable ex) {m_longPollStarted.set(false);ApolloConfigException exception =new ApolloConfigException("Schedule long polling refresh failed", ex);Tracer.logError(exception);logger.warn(ExceptionUtil.getDetailMessage(exception));}
}
接着看下 doLongPollingRefresh(…) 方法的处理逻辑。
private void doLongPollingRefresh(String appId, String cluster, String dataCenter, String secret) {final Random random = new Random();ServiceDTO lastServiceDto = null;while (!m_longPollingStopped.get() && !Thread.currentThread().isInterrupted()) {// 限流判断if (!m_longPollRateLimiter.tryAcquire(5, TimeUnit.SECONDS)) {try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {}}Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "pollNotification");String url = null;try {if (lastServiceDto == null) {// 向服务端发起一个 /services/config 的 GET 请求,从响应体中得到 ServiceDTO 列表List<ServiceDTO> configServices = getConfigServices();// 采用随机策略从 ServiceDTO 列表中选出一个 ServiceDTO 实例lastServiceDto = configServices.get(random.nextInt(configServices.size()));}// 组装请求的 URLurl = assembleLongPollRefreshUrl(lastServiceDto.getHomepageUrl(), appId, cluster, dataCenter,m_notifications);logger.debug("Long polling from {}", url);HttpRequest request = new HttpRequest(url);// 默认读操作的超时时间为 90 秒request.setReadTimeout(LONG_POLLING_READ_TIMEOUT);if (!StringUtils.isBlank(secret)) {Map<String, String> headers = Signature.buildHttpHeaders(url, appId, secret);request.setHeaders(headers);}transaction.addData("Url", url);// 1、底层采用 HttpURLConnection 向服务端发起一个 /notifications/v2 的 GET 请求final HttpResponse<List<ApolloConfigNotification>> response =m_httpUtil.doGet(request, m_responseType);logger.debug("Long polling response: {}, url: {}", response.getStatusCode(), url);if (response.getStatusCode() == 200 && response.getBody() != null) {// 2、更新客户端本地 m_notifications 缓存updateNotifications(response.getBody());// 3、更新客户端本地 m_remoteNotificationMessages 缓存updateRemoteNotifications(response.getBody());transaction.addData("Result", response.getBody().toString());// 4、对比客户端本地缓存与服务端返回的配置信息,如果发生变化,则更新本地缓存并触发监听器回调notify(lastServiceDto, response.getBody());}if (response.getStatusCode() == 304 && random.nextBoolean()) {lastServiceDto = null;}m_longPollFailSchedulePolicyInSecond.success();transaction.addData("StatusCode", response.getStatusCode());transaction.setStatus(Transaction.SUCCESS);} catch (Throwable ex) {lastServiceDto = null;Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex));transaction.setStatus(ex);long sleepTimeInSecond = m_longPollFailSchedulePolicyInSecond.fail();logger.warn("Long polling failed, will retry in {} seconds. appId: {}, cluster: {}, namespaces: {}, long polling url: {}, reason: {}",sleepTimeInSecond, appId, cluster, assembleNamespaces(), url, ExceptionUtil.getDetailMessage(ex));try {TimeUnit.SECONDS.sleep(sleepTimeInSecond);} catch (InterruptedException ie) {//ignore}} finally {transaction.complete();}}
}
默认的监听器是 LocalFileConfigRepository。
LocalFileConfigRepository
看下 onRepositoryChange 方法,它是如何处理的。
@Override
public void onRepositoryChange(String namespace, Properties newProperties) {if (newProperties.equals(m_fileProperties)) {return;}Properties newFileProperties = propertiesFactory.getPropertiesInstance();newFileProperties.putAll(newProperties);// 1、将配置持久化到磁盘中updateFileProperties(newFileProperties, m_upstream.getSourceType());// 2、触发监听器回调this.fireRepositoryChange(namespace, newProperties);
}
1、updateFileProperties 方法
private synchronized void updateFileProperties(Properties newProperties, ConfigSourceType sourceType) {this.m_sourceType = sourceType;if (newProperties.equals(m_fileProperties)) {return;}this.m_fileProperties = newProperties;persistLocalCacheFile(m_baseDir, m_namespace);
}
进入 persistLocalCacheFile 方法一窥究竟。
void persistLocalCacheFile(File baseDir, String namespace) {if (baseDir == null) {return;}// 文件名为 ${appId} + ${cluster} + ${namespace}.propertiesFile file = assembleLocalCacheFile(baseDir, namespace);OutputStream out = null;Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "persistLocalConfigFile");transaction.addData("LocalConfigFile", file.getAbsolutePath());try {out = new FileOutputStream(file);// 底层调用 Properties 的 store 方法将配置持久化到磁盘中m_fileProperties.store(out, "Persisted by DefaultConfig");transaction.setStatus(Transaction.SUCCESS);} catch (IOException ex) {ApolloConfigException exception =new ApolloConfigException(String.format("Persist local cache file %s failed", file.getAbsolutePath()), ex);Tracer.logError(exception);transaction.setStatus(exception);logger.warn("Persist local cache file {} failed, reason: {}.", file.getAbsolutePath(),ExceptionUtil.getDetailMessage(ex));} finally {if (out != null) {try {out.close();} catch (IOException ex) {//ignore}}transaction.complete();}
}
简单看下 Properties 的 store 方法是如何持久化的。
public void store(OutputStream out, String comments)throws IOException
{store0(new BufferedWriter(new OutputStreamWriter(out, "8859_1")),comments,true);
}private void store0(BufferedWriter bw, String comments, boolean escUnicode)throws IOException
{if (comments != null) {writeComments(bw, comments);}bw.write("#" + new Date().toString());bw.newLine();synchronized (this) {for (Enumeration<?> e = keys(); e.hasMoreElements();) {String key = (String)e.nextElement();String val = (String)get(key);key = saveConvert(key, true, escUnicode);val = saveConvert(val, false, escUnicode);bw.write(key + "=" + val);bw.newLine();}}bw.flush();
}
可以看出底层使用 BufferedWriter 的 write 方法将数据写入到磁盘中。
而本地磁盘文件的路径在 findLocalCacheDir 方法中进行定义。
LocalFileConfigRepository 构造器方法中会调用该方法。
private File findLocalCacheDir() {try {String defaultCacheDir = m_configUtil.getDefaultLocalCacheDir();Path path = Paths.get(defaultCacheDir);if (!Files.exists(path)) {Files.createDirectories(path);}if (Files.exists(path) && Files.isWritable(path)) {// 一种是通过参数指定的文件路径return new File(defaultCacheDir, CONFIG_DIR);}} catch (Throwable ex) {//ignore}// 另一种是应用程序的上下文路径return new File(ClassLoaderUtil.getClassPath(), CONFIG_DIR);
}
简单看下第一种通过参数指定的文件路径。
public String getDefaultLocalCacheDir() {String cacheRoot = getCustomizedCacheRoot();if (!Strings.isNullOrEmpty(cacheRoot)) {return cacheRoot + File.separator + getAppId();}cacheRoot = isOSWindows() ? "C:\\opt\\data\\%s" : "/opt/data/%s";return String.format(cacheRoot, getAppId());
}
进入 getCustomizedCacheRoot 方法一窥究竟。
private String getCustomizedCacheRoot() {// 1. Get from System PropertyString cacheRoot = System.getProperty("apollo.cacheDir");if (Strings.isNullOrEmpty(cacheRoot)) {// 2. Get from OS environment variablecacheRoot = System.getenv("APOLLO_CACHEDIR");}if (Strings.isNullOrEmpty(cacheRoot)) {// 3. Get from server.propertiescacheRoot = Foundation.server().getProperty("apollo.cacheDir", null);}if (Strings.isNullOrEmpty(cacheRoot)) {// 4. Get from app.propertiescacheRoot = Foundation.app().getProperty("apollo.cacheDir", null);}return cacheRoot;
}
2、fireRepositoryChange 方法
实际上对应原理 5 - 应用程序可以从 Apollo 客户端获取最新的配置、订阅配置更新通知。
protected void fireRepositoryChange(String namespace, Properties newProperties) {for (RepositoryChangeListener listener : m_listeners) {try {listener.onRepositoryChange(namespace, newProperties);} catch (Throwable ex) {Tracer.logError(ex);logger.error("Failed to invoke repository change listener {}", listener.getClass(), ex);}}
}
默认的监听器是 DefaultConfig。
DefaultConfig
看下 DefaultConfig 是如何回调处理的。
@Override
public synchronized void onRepositoryChange(String namespace, Properties newProperties) {if (newProperties.equals(m_configProperties.get())) {return;}ConfigSourceType sourceType = m_configRepository.getSourceType();Properties newConfigProperties = propertiesFactory.getPropertiesInstance();newConfigProperties.putAll(newProperties);Map<String, ConfigChange> actualChanges = updateAndCalcConfigChanges(newConfigProperties, sourceType);if (actualChanges.isEmpty()) {return;}// 监听器回调处理this.fireConfigChange(new ConfigChangeEvent(m_namespace, actualChanges));Tracer.logEvent("Apollo.Client.ConfigChanges", m_namespace);
}
进入 fireConfigChange 方法一窥究竟。
protected void fireConfigChange(final ConfigChangeEvent changeEvent) {for (final ConfigChangeListener listener : m_listeners) {if (!isConfigChangeListenerInterested(listener, changeEvent)) {continue;}m_executorService.submit(new Runnable() {@Overridepublic void run() {String listenerName = listener.getClass().getName();Transaction transaction = Tracer.newTransaction("Apollo.ConfigChangeListener", listenerName);try {// 触发监听器回调listener.onChange(changeEvent);transaction.setStatus(Transaction.SUCCESS);} catch (Throwable ex) {transaction.setStatus(ex);Tracer.logError(ex);logger.error("Failed to invoke config change listener {}", listenerName, ex);} finally {transaction.complete();}}});}
监听器默认是 AutoUpdateConfigChangeListener。
AutoUpdateConfigChangeListener
看下 AutoUpdateConfigChangeListener 是如何回调处理的。
@Override
public void onChange(ConfigChangeEvent changeEvent) {// 获取发生变化的属性集合Set<String> keys = changeEvent.changedKeys();if (CollectionUtils.isEmpty(keys)) {return;}for (String key : keys) {Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);if (targetValues == null || targetValues.isEmpty()) {continue;}// 更新应用程序使用到的属性对应的属性值for (SpringValue val : targetValues) {updateSpringValue(val);}}
}
进入 updateSpringValue 方法一窥究竟。
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);}
}
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 方法更新属性值field.set(bean, newVal);field.setAccessible(accessible);
}private void injectMethod(Object newVal)throws InvocationTargetException, IllegalAccessException {Object bean = beanRef.get();if (bean == null) {return;}// 底层使用 Method 的 invoke 方法更新属性值methodParameter.getMethod().invoke(bean, newVal);
}
相关文章:
Apollo架构篇 - 客户端架构
前言 本文基于 Apollo 1.8.0 版本展开分析。 客户端 使用 Apollo 支持 API 方式和 Spring 整合两种方式。 API 方式 API 方式是最简单、高效使用使用 Apollo 配置的方式,不依赖 Spring 框架即可使用。 获取命名空间的配置 // 1、获取默认的命名空间的配置 C…...
JVM调优最全面的成长 :参数详解+垃圾算法+示例展示+类文件到源码+面试问题
目录1.优秀的Java开发者1.1 什么是Java?1.2 编程语言1.3 计算机[硬件]能够懂的语言1.3.1 计算机发展史1.3.2 计算机体系结构1.3.3 计算机处理数据过程1.3.4 机器语言1.3.5 不同厂商的CPU1.3.6 操作系统1.3.7 汇编语言1.3.8 高级语言1.3.9 编译型和解释型1.3.9.1 编译…...
linux驱动常用函数
以下为一些常见用户态函数在内核中的替代,包括头文件和函数声明:1、动态申请内存:linux/vmalloc.hvoid *vmalloc(unsigned long size);void vfree(const void *addr);2、字符串操作:linux/string.hvoid * memset(void *,int,__ker…...
Flowable进阶学习(九)数据对象DataObject、租户Tenant、接收任务ReceiveTask
文章目录一、数据对象DataObject二、租户 Tenant三、接收任务 ReceiveTask案例一、数据对象DataObject DataObject可以⽤来定义⼀些流程的全局属性。 绘制流程图,并配置数据对象(不需要选择任意节点) 2. 编码与测试 /*** 部署流程*/ Test…...
C语言实现五子棋(n子棋)
五子棋的历史背景: 五子棋起源于中国,是全国智力运动会竞技项目之一,是一种两人对弈的纯策略型棋类游戏。双方分别使用黑白两色的棋子,下在棋盘直线与横线的交叉点上,先形成五子连珠者获胜。五子棋容易上手,…...
OpenStack云平台搭建(2) | 安装Keystone
目录 1、登录数据库配置 2、数据库导入Keystone表 3、配置http服务 4、创建域、用户 5、创建脚本 Keystone(OpenStack Identity Service)是 OpenStack 框架中负责管理身份验证、服务访问规则和服务令牌功能的组件。下面我们进行Keystone的安装部署 1…...
基于javaFX的固定资产管理系统
1. 总体设计 本系统分为登录模块、资产管理模块、资产登记模块和信息展示模块共四个模块。 登录模块的主要功能是:管理员通过登录模块登录本系统; 资产管理模块的主要功能有:修改、删除系统中的固定资产; 在资产登记模块中&#…...
板子登录和挂载问题记录
ubuntu登录板子问题 ssh登录ssh 10.1.3.15,显示No route to host 则尝试在板子上ping 本机ip 试一下 挂载 本地机器vim /etc/export编辑此内容并保存 /exports_0209/tda4_build *(rw,no_root_squash,nohide,insecure,no_subtree_check,async)1.挂载nfs方法 mou…...
二、Linux文件 - Open函数讲解实战
目录 1.Open函数讲解 2.open函数实战 2.1 man 1 ls 查询Shell命令 2.2 man 2 open 查看系统调用函数 2.3项目实战 2.3.1O_RDWR和O_CREAT 2.3.2O_APPEND的用法 1.Open函数讲解 高频使用的Linux系统调用:open write read close Linux自带的工具…...
源码分析Spring解决循环依赖的过程
循环依赖是之前很爱问的一个面试题,最近不咋问了,但是梳理Spring解决循环依赖的源码,会让我们对Spring创建bean的流程有一个清晰的认识,有必要搞一搞。开始搞之前,先参考了这个老哥写的文章,对Spring处理循…...
LabVIEW中加载.NET 2.0,3.0和3.5程序集
LabVIEW中加载.NET 2.0,3.0和3.5程序集已使用.NETFramework 2.0,3.0或3.5创建了.NET程序集,但是当尝试在构造函数节点中加载这些程序集时,却收到LabVIEW消息显示: 所选文件不是.NET程序集,所属类型库或自动化可执行文件。所以想确认是否可以在…...
Fluent Python 笔记 第 2 章 序列构成的数组
2.1 内置类型序列概览 容器序列(能存放不同类型的数据):(作者分的类) list、tuple 和 collections.deque扁平序列(只能容纳一种类型): str、byes、bytearray、memoryview 和 array.array可变:…...
句子扩充法
人,物,时,地,事 什么人和什么物在什么时间什么地点发生了什么事。 思维导图:以人为中心,人具有客观能动性。 例如:秋燕南飞。 扩展为: 盘旋在洞庭湖上方的大雁渐渐消失了。“它们都…...
Java并发编程概述
在学习并发编程之前,我们需要稍微回顾以下线程相关知识:线程基本概念程序:静态的代码,存储在硬盘中进程:运行中的程序,被加载在内存中,是操作系统分配内存的基本单位线程:是cpu执行的…...
Java常见数据结构的排序与遍历(包括数组,List,Map)
数组遍历与排序 数组定义 //定义 int a[] new int[5]int[] a new int[5];//带初始值定义 int b[] {1,2,3,4,5};赋值 //定义时赋值 int b[] {1,2,3,4,5};//引用赋值 a[6] 1 a[9] 9 //未赋值为空取值 //通过下表取值,从0开始 b[1] 1 b[2] 2遍历 Test p…...
数据结构|绪论
🔥Go for it!🔥 📝个人主页:按键难防 📫 如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步👀 📖系列专栏:数据结构与算法 ὒ…...
内网渗透(十二)之内网信息收集-内网端口扫描和发现
系列文章第一章节之基础知识篇 内网渗透(一)之基础知识-内网渗透介绍和概述 内网渗透(二)之基础知识-工作组介绍 内网渗透(三)之基础知识-域环境的介绍和优点 内网渗透(四)之基础知识-搭建域环境 内网渗透(五)之基础知识-Active Directory活动目录介绍和使用 内网渗透(六)之基…...
RabbitMq相关面试题
文章目录消息队列有没有接触过? 简单介绍一下?消息中间件模式分类 ?使用MQ有什么好处?MQ如何选型 ?你们项目中用到过 MQ 吗?谈谈你对 MQ 的理解?MQ消费者消费消息的顺序一致性问题?R…...
树莓派开机自启动Python脚本或者应用程序
树莓派开机自启动Python脚本或者应用程序前言一、对于Python脚本的自启动方法1、打开etc/rc.local文件2、编辑输入需要启动的指令3、重启树莓派验证二、对于需要读写配置文件的应用程序的自启前言 在树莓派上写了一些Python脚本,还有一个java 的jar包想要在树莓派上…...
全国青少年编程等级考试scratch四级真题2022年9月(含题库答题软件账号)
青少年编程等级考试scratch真题答题考试系统请点击电子学会-全国青少年编程等级考试真题Scratch一级(2019年3月)在线答题_程序猿下山的博客-CSDN博客_小航答题助手1、运行下列程序,说法正确的是?( )A.列表…...
NodeJS与npm版本不一致时降级npm的方法
首先查看 Node.js 与 npm 版本对应关系:Node.js与npm版本查看。 安装 cnpm: npm install -g cnpm 查看一下 npm 和 cnpm 的镜像: npm config get registry cnpm config get registry 2 如果不是 https://registry.npm.taobao.org/ 的话就修…...
《C++ Primer Plus》第16章:string类和标准模板库(8)
关联容器 关联容器(associative container)是对容器概念的另一个改进。关联容器将值与键关联在一起,并使用键来查找值。例如,值可以表示雇员信息(如姓名、地址、办公室号码、家庭电话和工作电话、健康计划等ÿ…...
Linux安装达梦8数据库
Linux安装达梦8数据库 服务器系统:centos7 数据库版本:达梦8 先获取安装包:https://eco.dameng.com/download/?_blank 选择相应版本下载,下载完解压之后会得到一个iso文件,把他上传到服务器上,建议上传到/opt目录下…...
[数据库]初识数据库
●🧑个人主页:你帅你先说. ●📃欢迎点赞👍关注💡收藏💖 ●📖既选择了远方,便只顾风雨兼程。 ●🤟欢迎大家有问题随时私信我! ●🧐版权:本文由[你帅…...
Redis的缓存雪崩、击穿、穿透和解决方案
2.5 缓存穿透问题的解决思路 缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。 常见的解决方案有两种: 缓存空对象 优点:实现简单,维护…...
52000000
选择题(共52题,合计52.0分) 1. 敏捷团队在项目执行过程中会用到一种叫做“看板”的可视化工具,它可显示WIP, 帮助识别瓶颈和过度承诺, 从而使团队能够优化工作流。请从下列选项中选择WIP的最佳解释?() A 等待初步加工的材料的库存 B 目前正…...
内网资源探测
✅作者简介:CSDN内容合伙人、信息安全专业在校大学生🏆 🔥系列专栏 :内网安全 📃新人博主 :欢迎点赞收藏关注,会回访! 💬舞台再大,你不上台,永远是…...
Java后端内部面试题(前一部分)
面试题 基础篇 1、Java语言有哪些特点 1、简单易学、有丰富的类库 2、面向对象(Java最重要的特性,让程序耦合度更低,内聚性更高) 2、面向对象和面向过程的区别 面向过程:是分析解决问题的步骤,然后用函数把…...
关于如何抄引擎源码
前两天,后台有网友发私信给我,问我如何抄引擎源码。我一愣,感觉像吃饭喝水一样自然。 抄源码的好处就不说了,抄之前不懂的内容,抄完后就懂了,至少懂一部分了。当然也可以只读不抄,不过ÿ…...
差分模拟信号转单端输出电路设计
需求分析: 1.差分输入0~16V -Vpp电压量; 2.输入频率0~1.2KHz; 3.单端对应输出0~3V的模拟量; 4.输出频率对应0~1.2KHz; 5.供电范围3~5V。 针对以上需求,设计如下图所示电路。 1.电路功能: …...
建网站的书籍/互联网营销主要学什么
经常有小伙伴做了一段时间功能测试后,想转做接口测试,但是又没有头绪。今天我们就来聊聊如何学习接口测试。 其实,我们都知道,学完软件测试的前三年,我们大致能做的工作方向就这么几个:功能测试、接口测试…...
网站建设与管理模拟题1/小型培训机构管理系统
从HTML被发明开始,样式就以各种形式存在。不同的浏览器结合它们各自的样式语言为用户提供页面效果的控制。最初的HTML只包含很少的显示属性。随着HTML的成长,为了满足页面设计者的要求,HTML添加了很多显示功能。但是随着这些功能的增加&#…...
网站备案可以国际域名/搜索引擎优化的主要内容
转自:https://www.cnblogs.com/songhaipeng/p/3323541.html 在J2EE框架下开发web网站,这种问题经常遇到,只要我们网上搜一下,就可以看到很多版本的,我整理一下: 第一种可能性解决:看看我的项目&…...
深圳 营销型网站公司/青岛seo公司
第一周:做点计算1.1 第一个程序Eclipse是绝大多数人的唯一选择;如何在Eclipse中编辑、编译和运行程序;详解第一个程序:程序框架、输出、出错怎么办;做点计算:如何让程序输出算术结果1.2 数据是用变量来表示…...
net网站开发教学视频/韶山百度seo
字符串的方法 charAt();返回字符串指定索引的字符;concat();连接两个或多个字符串;indexOf();返回字符串中检索指定字符第一次出现的位置;lastIndexOf();返回字符串中检索指定字符最后一次出现的位置;subString();提取字符串中两个…...
wordpress theme demo bar/网站维护一般都是维护什么
1JMS与MQ 1.1JMSJMS(Java Messaging Service)是Java平台上有关面向消息中间件(MOM)的技术规范,它便于消息系统中的Java应用程序进行消息交换,并且通过提供标准的产生、发送、接收消息的接口简化企业应用的开…...