Nacos源码解读11——客户端怎么读取最新的配置信息

项目启动怎么读取的配置信息

自动装配

SpringBoot 自动装配机制 加载 WEB/INF spring.factories
会将如下几个Bean加载到ioc 容器中

@Bean@ConditionalOnMissingBeanpublic NacosConfigProperties nacosConfigProperties() {return new NacosConfigProperties();}@Bean@ConditionalOnMissingBeanpublic NacosConfigManager nacosConfigManager(NacosConfigProperties nacosConfigProperties) {return new NacosConfigManager(nacosConfigProperties);}@Beanpublic NacosPropertySourceLocator nacosPropertySourceLocator(NacosConfigManager nacosConfigManager) {return new NacosPropertySourceLocator(nacosConfigManager);}/*** Compatible with bootstrap way to start.* @param beans configurationPropertiesBeans* @return configurationPropertiesRebinder*/@Bean@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)@ConditionalOnNonDefaultBehaviorpublic ConfigurationPropertiesRebinder smartConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {// If using default behavior, not use SmartConfigurationPropertiesRebinder.// Minimize te possibility of making mistakes.return new SmartConfigurationPropertiesRebinder(beans);}

在SpringBoot启动的时候会将触发 PropertySourceLocator的locate方法去读取配置 而NacosPropertySourceLocator实现了PropertySourceLocator接口所以会触发NacosPropertySourceLocator的locate方法

	@Overridepublic PropertySource<?> locate(Environment env) {//设置 Environment(上下文环境)nacosConfigProperties.setEnvironment(env);//获取远程调用服务 ConfigServiceConfigService configService = nacosConfigManager.getConfigService();if (null == configService) {log.warn("no instance of config service found, can't load config from nacos");return null;}//超时时间 3000long timeout = nacosConfigProperties.getTimeout();nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,timeout);String name = nacosConfigProperties.getName();//应用名String dataIdPrefix = nacosConfigProperties.getPrefix();if (StringUtils.isEmpty(dataIdPrefix)) {dataIdPrefix = name;}if (StringUtils.isEmpty(dataIdPrefix)) {dataIdPrefix = env.getProperty("spring.application.name");}CompositePropertySource composite = new CompositePropertySource(NACOS_PROPERTY_SOURCE_NAME);//共享配置 shared-configsloadSharedConfiguration(composite);//扩展配置 extension-configsloadExtConfiguration(composite);//本地配置  dataIdloadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);return composite;}

共享配置加载

	private void loadSharedConfiguration(CompositePropertySource compositePropertySource) {List<NacosConfigProperties.Config> sharedConfigs = nacosConfigProperties.getSharedConfigs();//如果配置了共享配置		if (!CollectionUtils.isEmpty(sharedConfigs)) {//检查共享配置checkConfiguration(sharedConfigs, "shared-configs");//加载配置信息loadNacosConfiguration(compositePropertySource, sharedConfigs);}}

扩展配置加载

	private void loadExtConfiguration(CompositePropertySource compositePropertySource) {List<NacosConfigProperties.Config> extConfigs = nacosConfigProperties.getExtensionConfigs();//如果配置了扩展配置	if (!CollectionUtils.isEmpty(extConfigs)) {//扩展配置检查checkConfiguration(extConfigs, "extension-configs");//加载配置信息loadNacosConfiguration(compositePropertySource, extConfigs);}}
	private void loadNacosConfiguration(final CompositePropertySource composite,List<NacosConfigProperties.Config> configs) {//遍历配置信息		for (NacosConfigProperties.Config config : configs) {//获取配置内容的数据格式  比如yaml 之类的String fileExtension = config.getFileExtension();//如果是空if (StringUtils.isEmpty(fileExtension)) {//从DataId里面获取fileExtension = NacosDataParserHandler.getInstance().getFileExtension(config.getDataId());}//加载配置信息loadNacosDataIfPresent(composite, config.getDataId(), config.getGroup(),fileExtension, config.isRefresh());}}

本地配置加载

	private void loadApplicationConfiguration(CompositePropertySource compositePropertySource, String dataIdPrefix,NacosConfigProperties properties, Environment environment) {//文件扩展名 .ymal/properties    (spring.cloud.nacos.config.file-extension)String fileExtension = properties.getFileExtension();//GroupString nacosGroup = properties.getGroup();// load directly once by default//加载 配置文件名的文件loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,fileExtension, true);// load with suffix, which have a higher priority than the default//加载 配置文件名.yml/Property的文件loadNacosDataIfPresent(compositePropertySource,dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);// Loaded with profile, which have a higher priority than the suffix//根据 .dev/.prd 进行逐项加载for (String profile : environment.getActiveProfiles()) {String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,fileExtension, true);}}

加载配置信息

	private void loadNacosDataIfPresent(final CompositePropertySource composite,final String dataId, final String group, String fileExtension,boolean isRefreshable) {if (null == dataId || dataId.trim().length() < 1) {return;}if (null == group || group.trim().length() < 1) {return;}//获取属性源     NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,fileExtension, isRefreshable);this.addFirstPropertySource(composite, propertySource, false);}private NacosPropertySource loadNacosPropertySource(final String dataId,final String group, String fileExtension, boolean isRefreshable) {//是否配置了刷新 每次判断会触发一次回调if (NacosContextRefresher.getRefreshCount() != 0) {//不支持动态刷新 从本地缓存拿if (!isRefreshable) {return NacosPropertySourceRepository.getNacosPropertySource(dataId,group);}}//远程拿数据return nacosPropertySourceBuilder.build(dataId, group, fileExtension,isRefreshable);}

本地获取数据

当不支持动态刷新的时候直接从本地缓存拿数据

	public static NacosPropertySource getNacosPropertySource(String dataId,String group) {return NACOS_PROPERTY_SOURCE_REPOSITORY.get(getMapKey(dataId, group));}

从服务端获取数据

	NacosPropertySource build(String dataId, String group, String fileExtension,boolean isRefreshable) {//加载服务端配置Map<String, Object> p = loadNacosData(dataId, group, fileExtension);//构建配置信息NacosPropertySource nacosPropertySource = new NacosPropertySource(group, dataId,p, new Date(), isRefreshable);//保存本地缓存NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);return nacosPropertySource;}
	private List<PropertySource<?>> loadNacosData(String dataId, String group,String fileExtension) {String data = null;.....//获取服务信息data = configService.getConfig(dataId, group, timeout);.....		return Collections.emptyList();}
 @Overridepublic String getConfig(String dataId, String group, long timeoutMs) throws NacosException {return getConfigInner(namespace, dataId, group, timeoutMs);}
    private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {group = null2defaultGroup(group);ParamUtils.checkKeyParam(dataId, group);ConfigResponse cr = new ConfigResponse();cr.setDataId(dataId);cr.setTenant(tenant);cr.setGroup(group);// 1.优先使用本地缓存配置String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);if (content != null) { LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),dataId, group, tenant, ContentUtils.truncateContent(content));cr.setContent(content);configFilterChainManager.doFilter(null, cr);content = cr.getContent();return content;}try {//2.委派给worker从远程服务器获取String[] ct = worker.getServerConfig(dataId, group, tenant, timeoutMs);cr.setContent(ct[0]);configFilterChainManager.doFilter(null, cr);content = cr.getContent();return content;} catch (NacosException ioe) {if (NacosException.NO_RIGHT == ioe.getErrCode()) {throw ioe;}LOGGER.warn("[{}] [get-config] get from server error, dataId={}, group={}, tenant={}, msg={}",agent.getName(), dataId, group, tenant, ioe.toString());}LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),dataId, group, tenant, ContentUtils.truncateContent(content));// 本地快照目录拿content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);cr.setContent(content);configFilterChainManager.doFilter(null, cr);content = cr.getContent();return content;}

本地缓存文件读取

    public static String getFailover(String serverName, String dataId, String group, String tenant) {//从本地文件中获取File localPath = getFailoverFile(serverName, dataId, group, tenant);if (!localPath.exists() || !localPath.isFile()) {return null;}try {//读文件return readFile(localPath);} catch (IOException ioe) {LOGGER.error("[" + serverName + "] get failover error, " + localPath, ioe);return null;}}

从Nacos服务端获取数据

    public ConfigResponse getServerConfig(String dataId, String group, String tenant, long readTimeout, boolean notify)throws NacosException {//分组等于空设置默认分组 if (StringUtils.isBlank(group)) {group = Constants.DEFAULT_GROUP;}//配置信息读取return this.agent.queryConfig(dataId, group, tenant, readTimeout, notify);}
@Override
public ConfigResponse queryConfig(String dataId, String group, String tenant, long readTimeouts, boolean notify)throws NacosException {ConfigQueryRequest request = ConfigQueryRequest.build(dataId, group, tenant);request.putHeader(NOTIFY_HEADER, String.valueOf(notify));RpcClient rpcClient = getOneRunningClient();if (notify) {CacheData cacheData = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));if (cacheData != null) {rpcClient = ensureRpcClient(String.valueOf(cacheData.getTaskId()));}}// 发送一个grpc请求读取配置信息ConfigQueryResponse response = (ConfigQueryResponse) requestProxy(rpcClient, request, readTimeouts);ConfigResponse configResponse = new ConfigResponse();......
}

服务端读取配置信息

ConfigQueryRequestHandler

@Override
@TpsControl(pointName = "ConfigQuery", parsers = {ConfigQueryGroupKeyParser.class, ConfigQueryGroupParser.class})
@Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
public ConfigQueryResponse handle(ConfigQueryRequest request, RequestMeta meta) throws NacosException {try {return getContext(request, meta, request.isNotify());} catch (Exception e) {return ConfigQueryResponse.buildFailResponse(ResponseCode.FAIL.getCode(), e.getMessage());}}

这里太长懒得贴 反正就是会先用groupKey=dataid+group+namespace 服务端对每一个配置文件都有服务端的内存缓存,这里使用读写锁获取锁,为了避免并发修改的情况
然后会判断当前模式是不是单机模式 并且不使用mysql 做数据存储的则从内置的数据库拿数据 如果配的是mysql 则从mysql中拿数据返回

缓存配置信息到本地

	public static void collectNacosPropertySource(NacosPropertySource nacosPropertySource) {NACOS_PROPERTY_SOURCE_REPOSITORY.putIfAbsent(getMapKey(nacosPropertySource.getDataId(),nacosPropertySource.getGroup()), nacosPropertySource);}

配置信息动态刷新

springboot自动注入的时候会去注入一个 NacosConfigAutoConfiguration 然后再这里面会注入一个 NacosContextRefresher进行数据的刷新 当spring启动的时候 会发送一个ApplicationReadyEvent事件而NacosContextRefresher会监听这个事件

	@Overridepublic void onApplicationEvent(ApplicationReadyEvent event) {// many Spring contextif (this.ready.compareAndSet(false, true)) {this.registerNacosListenersForApplications();}}
	private void registerNacosListenersForApplications() {//是否开启了刷新  配置文件中 spring.cloud.nacos.config.refresh-enabled = true/false if (isRefreshEnabled()) {//开启了遍历 外部化配置的属性元for (NacosPropertySource propertySource : NacosPropertySourceRepository.getAll()) { //判断这个属性元是否开启了 Refreshableif (!propertySource.isRefreshable()) {continue;}String dataId = propertySource.getDataId();//注册监听事件registerNacosListener(propertySource.getGroup(), dataId);}}}

注册监听事件

当配置发生变更的时候会回调到这个监听事件中去发送一个刷新事件
具体怎么判断数据变更看上一张的safeNotifyListener方法 这个方法中会有一个innerReceive方法的调用最终会到这里

	private void registerNacosListener(final String groupKey, final String dataKey) {String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);Listener listener = listenerMap.computeIfAbsent(key,lst -> new AbstractSharedListener() {//当NacosContextRefresher.getRefreshCount() != 0 判断配置发生变化发生回调@Overridepublic void innerReceive(String dataId, String group,String configInfo) {refreshCountIncrement();//添加刷新历史记录nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);// todo feature: support single refresh for listening//发布事件applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config"));if (log.isDebugEnabled()) {log.debug(String.format("Refresh Nacos config group=%s,dataId=%s,configInfo=%s",group, dataId, configInfo));}}});try {configService.addListener(dataKey, groupKey, listener);}catch (NacosException e) {log.warn(String.format("register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,groupKey), e);}}

监听刷新事件

RefreshEventListener会监听到刷新事件进行处理

	@Overridepublic void onApplicationEvent(ApplicationEvent event) {if (event instanceof ApplicationReadyEvent) {handle((ApplicationReadyEvent) event);}else if (event instanceof RefreshEvent) {handle((RefreshEvent) event);}}
	public void handle(RefreshEvent event) {//是否就绪if (this.ready.get()) { // don't handle events before app is readylog.debug("Event received " + event.getEventDesc());Set<String> keys = this.refresh.refresh();log.info("Refresh keys changed: " + keys);}}
	public synchronized Set<String> refresh() {Set<String> keys = refreshEnvironment();//重新刷新beanthis.scope.refreshAll();return keys;}
	public synchronized Set<String> refreshEnvironment() {Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());addConfigFilesToEnvironment();//得到发生变更的keySet<String> keys = changes(before,extract(this.context.getEnvironment().getPropertySources())).keySet();//发布EnvironmentChange事件this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));return keys;}

Environment变更事件监听

ConfigurationPropertiesRebinder 监听到事件重新进行数据的绑定

	@Overridepublic void onApplicationEvent(EnvironmentChangeEvent event) {if (this.applicationContext.equals(event.getSource())// Backwards compatible|| event.getKeys().equals(event.getSource())) {//数据绑定rebind();}}
	@ManagedOperationpublic void rebind() {this.errors.clear();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) {// TODO: determine a more general approach to fix this.// see https://github.com/spring-cloud/spring-cloud-commons/issues/571if (getNeverRefreshable().contains(bean.getClass().getName())) {return false; // ignore}//先卸载this.applicationContext.getAutowireCapableBeanFactory().destroyBean(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;}

总结

springboot启动的时候怎么加载的nacos中的配置

1.springboot启动的时候会通过自动注入 将NacosPropertySourceLocator注入到bean容器中 然后会调用到locate方法中进行配置的读取
2.配置读取会先判断是否配置了自动刷新 如果没有配置则直接从缓存中读取
3.如果配置了自动刷新 会先从本地快照中读取 如果读取到了就返回并加入到本地缓存中
4.如果快照中也没有读取到 则通过grpc请求从服务端读取 从服务端读取的时候会先生成一个读写锁防止有问题 他会判断是集群还是单机启动 如果是单机启动并且没有使用mysql 则从内嵌的数据库里读取 如果是集群并且配置了mysql则从mysql 读取返回给客户端
5.如果从服务端也没读取到则从本地磁盘读取

怎么实现动态刷新配置

1.springboot启动的时候会通过自动注入 会注入一个NacosContextRefresher到bean容器中 这里面会注册一个监听器用来监听事件变更的一个回调
2.当服务端有配置变更之后会推送给客户端进行配置的修改并触发这个回调
3.然后回调中会触发一个刷新事件
4.当准备完毕之后会去发送一个spring中配置修改的一个事件
5.这个事件中会触发bean的重新绑定事件 实际上就是将老的bean给卸载将新的bean重新加载来实现配置的实时变更

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

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

相关文章

【算法Hot100系列】两数之和

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

【rabbitMQ】模拟work queue,实现单个队列绑定多个消费者

上一篇&#xff1a; springboot整合rabbitMQ模拟简单收发消息 https://blog.csdn.net/m0_67930426/article/details/134904766?spm1001.2014.3001.5502 在这篇文章的基础上进行操作 基本思路&#xff1a; 1.在rabbitMQ控制台创建一个新的队列 2.在publisher服务中定义一个…

MySQL中的数据类型

MySQL中的数据类型 大家好&#xff0c;我是微赚淘客系统的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天我们将探讨MySQL中的数据类型&#xff0c;这是数据库设计中至关重要的一部分。数据库作为程序的底层支持&#xff0c;数据类型的选择…

[python]利用whl轮子文件python3.12安装talib

ta-lib目前很多人使用&#xff0c;网上也有很多人下载whl文件直接pip安装即可&#xff0c;但是最新版本3.12没有出来&#xff0c;因此本人独家制作python 3.12版本whl文件&#xff0c;从源码开始编译生成。TA-Lib-0.4.28-cp312-cp312-win-amd64.whl &#xff0c;注意这个whl文件…

Java 多线程下的单例模式

单例对象&#xff08;Singleton&#xff09;是一种常用的设计模式。在Java应用中&#xff0c;单例对象能保证在一个JVM中&#xff0c;该对象只有一个实例存在。正是由于这个特 点&#xff0c;单例对象通常作为程序中的存放配置信息的载体&#xff0c;因为它能保证其他对象读到一…

JWT的原理

在谈及jwt原理前,我们其实对jwt并不陌生,对于有经验的码农,大都听过或者实践过,对于一些初学者,凡是谈及安全方面的问题,总是觉得很复杂,感觉不是自己能搞得懂得,但其实无非也是加密解密的过程,不要想的太复杂,我们先说一说JWT在生产上的应用 JWT在生产上的应用 传递用户身份信…

Android系统中使用Cunit测试C/C++接口

Android系统中使用Cunit测试C/C接口 Cunit是C/C语言的单元测试框架&#xff0c;但常用于Windows和Linux开发中。 Android系统中经常有jni、so库、hal service等都是C/C实现&#xff0c;本文讲解如何将Cunit嵌入Android中&#xff0c;用于测试一些C/C api。 Cunit简介 Cunit是很…

全面解析“由于找不到hid.dll,无法继续执行代码”的4个解决方法

在计算机使用过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“找不到hid.dll”。这个问题通常出现在尝试运行某个程序或访问某个设备时。那么&#xff0c;当我们遇到这个问题时&#xff0c;应该如何解决呢&#xff1f;本文将详细介绍找不到hid.dll的解…

高校需要哪些大数据实训平台?

当前&#xff0c;数据已成为重要的生产要素&#xff0c;大数据产业作为以数据生成、采集、存储、加工、分析、服务为主的战略性新兴产业&#xff0c;是激活数据要素潜能的关键支撑&#xff0c;是加快经济社会发展质量变革、效率变革、动力变革的重要引擎。 泰迪大数据实验…

Angular 14带来了类型化表单和独立组件

独立组件通过减少对ngmodule的需求&#xff0c;有望简化Angular应用的开发。 介绍 Angular 14是谷歌开发的、基于typescript的web应用框架的最新版本&#xff0c;它以输入表单和独立组件的开发者预览版为特色。 其特性包括&#xff1a; 一个基于组件的框架&#xff0c;用于构…

Fortran读取netcdf文件/WRF中的文件读取

一直很好奇WRF到底如何通过netcdf库读取netcdf文件&#xff0c;正巧有个机会&#xff0c;试了下fortran读取nc文件&#xff0c;总结一下。 netcdf库 Fortran读取nc文件需要依赖netcdf外部库。安装该库以后&#xff0c;会有专门写给ffortran函数声明的头文件&#xff1a;netcd…

数据类型·

定义 数据类型是指在编程语言中&#xff0c;能够表示不同种类的数据值并对其进行操作的集合。在不同的编程语言中&#xff0c;数据类型可能有所不同&#xff0c;但通常包括基本数据类型和复合数据类型两种。 基本数据类型通常包括整数、浮点数、布尔值、字符等。这些类型的数…

231210 刷题日报

单调栈&#xff1a; 为啥需要单调栈&#xff1f;因为栈的后入先出特性方便从栈顶删除刚入栈的元素 496. 下一个更大元素 I 739. 每日温度 单调对列&#xff1a; 为啥要用单调对列&#xff1f;因为像滑动窗口这种题目&#xff0c;窗口两端都需要插入和删除&#xff0c;所以需…

Python满屏飘字表白代码

​ 目录 系列文章 写在前面 Turtle入门 满屏飘字 写在后面 系列文章 序号文章目录直达链接表白系列1浪漫520表白代码https://want595.blog.csdn.net/article/details/1306668812满屏表白代码https://want595.blog.csdn.net/article/details/1297945183跳动的爱心https://…

CF1898B Milena and Admirer(贪心)

题目链接 题目大意 有一个长度为 n 的数组 做操作使这个数组不递减&#xff1a; 把一个数分成两个数&#xff0c;例如&#xff1a;x 分为 a 和 b&#xff0c; x a b 求最小操作次数 思路 见注释 代码 #include<bits/stdc.h> #define int long long using names…

Shutter的安装及使用

概要&#xff1a;本篇主要讲述截图软件Shutter的安装和使用&#xff0c;操作系统是Ubuntu22.04 一、安装 sudo apt install shutter 二、区域截图 1、打开Shutter&#xff0c;点击Selection 2、提示信息 3、框选矩形区域 按住鼠标左键&#xff0c;拖动鼠标&#xff0c;松…

IT行业最被低估的六项技术,再加上一项尚未消亡的技术

2023年&#xff0c;生成式人工智能——更具体地说是ChatGPT——吸引了业界的广泛关注&#xff0c;深得董事会、首席执行官和其他高管的一致赞赏&#xff08;也不乏害怕情绪&#xff09;。当然&#xff0c;他们的热情是有道理的&#xff0c;多项研究发现&#xff0c;人工智能正在…

Electron[4] Electron最简单的打包实践

1 背景 前面三篇已经完成通过Electron搭建的最简单的HelloWorld应用了&#xff0c;虽然这个应用还没添加任何实质的功能&#xff0c;但是用来作为打包的案例&#xff0c;足矣。下面再分享下通过Electron-forge来将应用打包成安装包。 2 依赖 在Electron[2] Electron使用准备…

[山东大学操作系统课程设计]实验四+实验五

0.写在前面&#xff1a; 为什么这次把两个实验放在一起写了&#xff0c;因为实验五的要求就是在实验四的基础上完成实现的。但是我得实现说明&#xff0c;我的实验四虽然完成了要求&#xff0c;但是无法在我自己的实验四的基础上完成实验五&#xff0c;这是一个很大的问题&…

软考考前背过-软件设计师

今年5月份开始准备考&#xff0c;没想到会突然改革&#xff0c;还好刷题刷的多&#xff0c;也过了。 跟着B站up主的视频学的&#xff0c;都学了一遍之后才开始刷题&#xff0c;平时要上班&#xff0c;也就下班和周末能学&#xff0c;时间可能拉的比较长&#xff0c;学完前面的内…