【源码解析】Nacos配置热更新的实现原理

news2024/11/26 3:20:28

使用入门

  1. 使用@RefreshScope+@Value,实现动态刷新
@RestController
@RefreshScope
public class TestController {

    @Value("${cls.name}")
    private String clsName;

}
  1. 使用ConfigurationProperties,通过@Autowired注入使用
@Data
@ConfigurationProperties(prefix = "redis")
@Component
public class RedisProperties {

    private String userName;

    private String password;

    private String url;
}

源码解析

CacheData

CacheData#checkListenerMd5,当发现监听到的数据和本地配置不一致,会进行提醒。核心点在于会执行listener.receiveConfigInfo(contentTmp);

    void checkListenerMd5() {
        for (ManagerListenerWrap wrap : listeners) {
            if (!md5.equals(wrap.lastCallMd5)) {
                safeNotifyListener(dataId, group, content, type, md5, encryptedDataKey, wrap);
            }
        }
    }

    private void safeNotifyListener(final String dataId, final String group, final String content, final String type,
            final String md5, final String encryptedDataKey, final ManagerListenerWrap listenerWrap) {
        final Listener listener = listenerWrap.listener;
        
        Runnable job = new Runnable() {
            @Override
            public void run() {
                ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();
                ClassLoader appClassLoader = listener.getClass().getClassLoader();
                try {
                    if (listener instanceof AbstractSharedListener) {
                        AbstractSharedListener adapter = (AbstractSharedListener) listener;
                        adapter.fillContext(dataId, group);
                        LOGGER.info("[{}] [notify-context] dataId={}, group={}, md5={}", name, dataId, group, md5);
                    }
                    // 执行回调之前先将线程classloader设置为具体webapp的classloader,以免回调方法中调用spi接口是出现异常或错用(多应用部署才会有该问题)。
                    Thread.currentThread().setContextClassLoader(appClassLoader);
                    
                    ConfigResponse cr = new ConfigResponse();
                    cr.setDataId(dataId);
                    cr.setGroup(group);
                    cr.setContent(content);
                    cr.setEncryptedDataKey(encryptedDataKey);
                    configFilterChainManager.doFilter(null, cr);
                    String contentTmp = cr.getContent();
                    listener.receiveConfigInfo(contentTmp);
                    
                    // compare lastContent and content
                    if (listener instanceof AbstractConfigChangeListener) {
                        Map data = ConfigChangeHandler.getInstance()
                                .parseChangeData(listenerWrap.lastContent, content, type);
                        ConfigChangeEvent event = new ConfigChangeEvent(data);
                        ((AbstractConfigChangeListener) listener).receiveConfigChange(event);
                        listenerWrap.lastContent = content;
                    }
                    
                    listenerWrap.lastCallMd5 = md5;
                    LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ", name, dataId, group, md5,
                            listener);
                } catch (NacosException ex) {
                    LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} errCode={} errMsg={}",
                            name, dataId, group, md5, listener, ex.getErrCode(), ex.getErrMsg());
                } catch (Throwable t) {
                    LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} tx={}", name, dataId,
                            group, md5, listener, t.getCause());
                } finally {
                    Thread.currentThread().setContextClassLoader(myClassLoader);
                }
            }
        };
        
        final long startNotify = System.currentTimeMillis();
        try {
            if (null != listener.getExecutor()) {
                listener.getExecutor().execute(job);
            } else {
                job.run();
            }
        } catch (Throwable t) {
            LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} throwable={}", name, dataId,
                    group, md5, listener, t.getCause());
        }
        final long finishNotify = System.currentTimeMillis();
        LOGGER.info("[{}] [notify-listener] time cost={}ms in ClientWorker, dataId={}, group={}, md5={}, listener={} ",
                name, (finishNotify - startNotify), dataId, group, md5, listener);
    }
    

NacosContextRefresher

NacosContextRefresher#onApplicationEvent,系统启动的时候,会注册监听器。该监听器重写了innerReceive方法。

	public void onApplicationEvent(ApplicationReadyEvent event) {
		// many Spring context
		if (this.ready.compareAndSet(false, true)) {
			this.registerNacosListenersForApplications();
		}
	}

	private void registerNacosListenersForApplications() {
		if (isRefreshEnabled()) {
			for (NacosPropertySource propertySource : NacosPropertySourceRepository
					.getAll()) {
				if (!propertySource.isRefreshable()) {
					continue;
				}
				String dataId = propertySource.getDataId();
				registerNacosListener(propertySource.getGroup(), dataId);
			}
		}
	}

	private void registerNacosListener(final String groupKey, final String dataKey) {
		String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
		Listener listener = listenerMap.computeIfAbsent(key,
				lst -> new AbstractSharedListener() {
					@Override
					public 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);
		}
	}

AbstractSharedListener执行receiveConfigInfo方法,会调用innerReceive方法。

public abstract class AbstractSharedListener implements Listener {
    private volatile String dataId;
    private volatile String group;

    public AbstractSharedListener() {
    }

    public final void fillContext(String dataId, String group) {
        this.dataId = dataId;
        this.group = group;
    }

    public final void receiveConfigInfo(String configInfo) {
        this.innerReceive(this.dataId, this.group, configInfo);
    }

    public Executor getExecutor() {
        return null;
    }

    public abstract void innerReceive(String var1, String var2, String var3);
}

RefreshEventListener

RefreshEventListener监听到RefreshEvent,会执行this.refresh.refresh();

    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationReadyEvent) {
            this.handle((ApplicationReadyEvent)event);
        } else if (event instanceof RefreshEvent) {
            this.handle((RefreshEvent)event);
        }

    }    

	public void handle(RefreshEvent event) {
        if (this.ready.get()) {
            log.debug("Event received " + event.getEventDesc());
            Set<String> keys = this.refresh.refresh();
            log.info("Refresh keys changed: " + keys);
        }

    }

ContextRefresher

ContextRefresher#refresh,会发布EnvironmentChangeEvent事件。ContextRefresher#addConfigFilesToEnvironment启动了一个spring applicaiton程序去加载了一次配置,将变化后的配置重新加载进来了,然后进行this.scope.refreshAll();

	public synchronized Set<String> refresh() {
		Set<String> keys = refreshEnvironment();
		this.scope.refreshAll();
		return keys;
	}

	public synchronized Set<String> refreshEnvironment() {
		Map<String, Object> before = extract(
				this.context.getEnvironment().getPropertySources());
		addConfigFilesToEnvironment();
		Set<String> keys = changes(before,
				extract(this.context.getEnvironment().getPropertySources())).keySet();
		this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
		return keys;
	}

	ConfigurableApplicationContext addConfigFilesToEnvironment() {
		ConfigurableApplicationContext capture = null;
		try {
			StandardEnvironment environment = copyEnvironment(
					this.context.getEnvironment());
			SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
					.bannerMode(Mode.OFF).web(WebApplicationType.NONE)
					.environment(environment);
			// Just the listeners that affect the environment (e.g. excluding logging
			// listener because it has side effects)
			builder.application()
					.setListeners(Arrays.asList(new BootstrapApplicationListener(),
							new ConfigFileApplicationListener()));
			capture = builder.run();
			if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
				environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE);
			}
			MutablePropertySources target = this.context.getEnvironment()
					.getPropertySources();
			String targetName = null;
			for (PropertySource<?> source : environment.getPropertySources()) {
				String name = source.getName();
				if (target.contains(name)) {
					targetName = name;
				}
				if (!this.standardSources.contains(name)) {
					if (target.contains(name)) {
						target.replace(name, source);
					}
					else {
						if (targetName != null) {
							target.addAfter(targetName, source);
							// update targetName to preserve ordering
							targetName = name;
						}
						else {
							// targetName was null so we are at the start of the list
							target.addFirst(source);
							targetName = name;
						}
					}
				}
			}
		}
		finally {
			ConfigurableApplicationContext closeable = capture;
			while (closeable != null) {
				try {
					closeable.close();
				}
				catch (Exception e) {
					// Ignore;
				}
				if (closeable.getParent() instanceof ConfigurableApplicationContext) {
					closeable = (ConfigurableApplicationContext) closeable.getParent();
				}
				else {
					break;
				}
			}
		}
		return capture;
	}

RefreshScope#refreshAll,会发布RefreshScopeRefreshedEvent事件。

	@ManagedOperation(description = "Dispose of the current instance of all beans "
			+ "in this scope and force a refresh on next method execution.")
	public void refreshAll() {
		super.destroy();
		this.context.publishEvent(new RefreshScopeRefreshedEvent());
	}

GenericScope#destroy(),清理GenericScope缓存。

    protected boolean destroy(String name) {
        GenericScope.BeanLifecycleWrapper wrapper = this.cache.remove(name);
        if (wrapper != null) {
            Lock lock = ((ReadWriteLock)this.locks.get(wrapper.getName())).writeLock();
            lock.lock();

            try {
                wrapper.destroy();
            } finally {
                lock.unlock();
            }

            this.errors.remove(name);
            return true;
        } else {
            return false;
        }
    }

获取Bean对象

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {

	/**
	 * @see Scope#proxyMode()
	 * @return proxy mode
	 */
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}

AbstractBeanFactory#doGetBean,获取Bean。如果该类有@Refresh注解,获取对应的scope,执行scope.get来获取Bean对象。

	protected <T> T doGetBean(
			String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
			throws BeansException {

		String beanName = transformedBeanName(name);
		Object bean;

		// Eagerly check singleton cache for manually registered singletons.
		Object sharedInstance = getSingleton(beanName);
		if (sharedInstance != null && args == null) {
				//...
		}

		else {
			// ...

			try {
				// ...

				// Create bean instance.
				if (mbd.isSingleton()) {
					sharedInstance = getSingleton(beanName, () -> {
						try {
							return createBean(beanName, mbd, args);
						}
						catch (BeansException ex) {
							// Explicitly remove instance from singleton cache: It might have been put there
							// eagerly by the creation process, to allow for circular reference resolution.
							// Also remove any beans that received a temporary reference to the bean.
							destroySingleton(beanName);
							throw ex;
						}
					});
					bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
				}

				else if (mbd.isPrototype()) {
					// It's a prototype -> create a new instance.
					Object prototypeInstance = null;
					try {
						beforePrototypeCreation(beanName);
						prototypeInstance = createBean(beanName, mbd, args);
					}
					finally {
						afterPrototypeCreation(beanName);
					}
					bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
				}

				else {
					String scopeName = mbd.getScope();
					if (!StringUtils.hasLength(scopeName)) {
						throw new IllegalStateException("No scope name defined for bean ´" + beanName + "'");
					}
					Scope scope = this.scopes.get(scopeName);
					if (scope == null) {
						throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
					}
					try {
						Object scopedInstance = scope.get(beanName, () -> {
							beforePrototypeCreation(beanName);
							try {
								return createBean(beanName, mbd, args);
							}
							finally {
								afterPrototypeCreation(beanName);
							}
						});
						bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
					}
					catch (IllegalStateException ex) {
						throw new BeanCreationException(beanName,
								"Scope '" + scopeName + "' is not active for the current thread; consider " +
								"defining a scoped proxy for this bean if you intend to refer to it from a singleton",
								ex);
					}
				}
			}
			catch (BeansException ex) {
				cleanupAfterBeanCreationFailure(beanName);
				throw ex;
			}
		}

		// Check if required type matches the type of the actual bean instance.
		if (requiredType != null && !requiredType.isInstance(bean)) {
			try {
				T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType);
				if (convertedBean == null) {
					throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
				}
				return convertedBean;
			}
			catch (TypeMismatchException ex) {
				if (logger.isTraceEnabled()) {
					logger.trace("Failed to convert bean '" + name + "' to required type '" +
							ClassUtils.getQualifiedName(requiredType) + "'", ex);
				}
				throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
			}
		}
		return (T) bean;
	}

GenericScope#get,获取缓存中的对象

    public Object get(String name, ObjectFactory<?> objectFactory) {
        GenericScope.BeanLifecycleWrapper value = this.cache.put(name, new GenericScope.BeanLifecycleWrapper(name, objectFactory));
        this.locks.putIfAbsent(name, new ReentrantReadWriteLock());

        try {
            return value.getBean();
        } catch (RuntimeException var5) {
            this.errors.put(name, var5);
            throw var5;
        }
    }

StandardScopeCache#put,如果旧值不存在,存放成功,返回当前value值;如果旧值存在,返回旧值。

    public Object put(String name, Object value) {
        Object result = this.cache.putIfAbsent(name, value);
        return result != null ? result : value;
    }

GenericScope.BeanLifecycleWrapper#getBean,调用objectFactory.getObject()。也就是创建Bean。

        public Object getBean() {
            if (this.bean == null) {
                synchronized(this.name) {
                    if (this.bean == null) {
                        this.bean = this.objectFactory.getObject();
                    }
                }
            }

            return this.bean;
        }

ConfigurationPropertiesRebinderAutoConfiguration

ConfigurationPropertiesRebinderAutoConfiguration注入了ConfigurationPropertiesBeansConfigurationPropertiesRebinder

@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(ConfigurationPropertiesBindingPostProcessor.class)
public class ConfigurationPropertiesRebinderAutoConfiguration
		implements ApplicationContextAware, SmartInitializingSingleton {

	private ApplicationContext context;

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) {
		this.context = applicationContext;
	}

	@Bean
	@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
	public static ConfigurationPropertiesBeans configurationPropertiesBeans() {
		return new ConfigurationPropertiesBeans();
	}

	@Bean
	@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
	public ConfigurationPropertiesRebinder configurationPropertiesRebinder(
			ConfigurationPropertiesBeans beans) {
		ConfigurationPropertiesRebinder rebinder = new ConfigurationPropertiesRebinder(
				beans);
		return rebinder;
	}
}

ConfigurationPropertiesBeans实现了BeanPostProcessor,启动的时候会执行postProcessBeforeInitialization。寻找带有ConfigurationProperties注解的Bean对象,封装成ConfigurationPropertiesBean,存放到ConfigurationPropertiesBeansbeans属性中。

	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName)
			throws BeansException {
		if (isRefreshScoped(beanName)) {
			return bean;
		}
		ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean
				.get(this.applicationContext, bean, beanName);
		if (propertiesBean != null) {
			this.beans.put(beanName, propertiesBean);
		}
		return bean;
	}

	public static ConfigurationPropertiesBean get(ApplicationContext applicationContext, Object bean, String beanName) {
		Method factoryMethod = findFactoryMethod(applicationContext, beanName);
		return create(beanName, bean, bean.getClass(), factoryMethod);
	}

	private static ConfigurationPropertiesBean create(String name, Object instance, Class<?> type, Method factory) {
		ConfigurationProperties annotation = findAnnotation(instance, type, factory, ConfigurationProperties.class);
		if (annotation == null) {
			return null;
		}
		Validated validated = findAnnotation(instance, type, factory, Validated.class);
		Annotation[] annotations = (validated != null) ? new Annotation[] { annotation, validated }
				: new Annotation[] { annotation };
		ResolvableType bindType = (factory != null) ? ResolvableType.forMethodReturnType(factory)
				: ResolvableType.forClass(type);
		Bindable<Object> bindTarget = Bindable.of(bindType).withAnnotations(annotations);
		if (instance != null) {
			bindTarget = bindTarget.withExistingValue(instance);
		}
		return new ConfigurationPropertiesBean(name, instance, annotation, bindTarget);
	}

ConfigurationPropertiesRebinder实现了ApplicationListener<EnvironmentChangeEvent>,当监听到EnvironmentChangeEvent,会销毁Bean对象,再重新生成Bean对象。

    public void onApplicationEvent(EnvironmentChangeEvent event) {
        if (this.applicationContext.equals(event.getSource()) || event.getKeys().equals(event.getSource())) {
            this.rebind();
        }

    }

	@ManagedOperation
	public void rebind() {
		this.errors.clear();
		for (String name : this.beans.getBeanNames()) {
			rebind(name);
		}
	}

	@ManagedOperation
	public 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/571
					if (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;
	}

总结一下

  • 所有的RefreshScope注解修饰的bean都保存在GenericScope的缓存里,RefreshScope#refreshAll会将缓存全部清空,当spring获取这些Bean的时候,会重新生成保存到缓存中。
  • ConfigurationProperties注解的Bean在监听到EnvironmentChangeEvent,会进行销毁和重新初始化的操作。
  • 如果想要所有的@Value都可以热更新,可以看一下dynamic-config-starter这个项目的实现。

在这里插入图片描述

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

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

相关文章

警惕AI换脸技术:近期诈骗事件揭示的惊人真相

大家好&#xff0c;我是可夫小子&#xff0c;《小白玩转ChatGPT》专栏作者&#xff0c;关注AIGC、读书和自媒体。 目录 1. deepswap 2. faceswap 3. swapface 总结 &#x1f4e3;通知 近日&#xff0c;包头警方公布了一起用AI进行电信诈骗的案件&#xff0c;其中福州科技公…

医院PACS系统:三维多平面重建操作使用

三维多平面重建&#xff08;MPR\CPR&#xff09;界面工具栏&#xff1a; 按钮1&#xff1a;点击此按钮&#xff0c;用鼠标拖动正交的MPR定位线&#xff0c;可以动态浏览MPR图像。 按钮2&#xff1a;点击此按钮&#xff0c;按下鼠标左键在图像上作任意勾边&#xff0c;弹起鼠标…

python3.8安装rpy2

python3.8安装rpy2 rpy2是一个可以让r和python交互的库&#xff0c;非常强大&#xff0c;但是安装过程有些坎坷。 安装r语言 安装时首先需要安装r语言。 官网下载链接&#xff1a;https://www.r-project.org/ 选择与自己电脑相应的版本就好。 安装rpy2 然后需要安装rpy2库…

Radxa ROCK 5A RK3588S 开箱 vs 树莓派

Rock5 Model A 是一款高性能的单板计算机&#xff0c;采用了 RK3588S (8nm LP 制程&#xff09;处理器&#xff0c;具有 4 个高达 2.4GHz 的 ARM Cortex-A76 CPU 核心、4 个高达 1.8GHz 的 Cortex-A55 内核和 Mali-G610 MP4 GPU&#xff0c;支持 8K 60fps 视频播放&#xff0c…

光力转债上市价格预测

光力转债 基本信息 转债名称&#xff1a;光力转债&#xff0c;评级&#xff1a;A&#xff0c;发行规模&#xff1a;4.0亿元。 正股名称&#xff1a;光力科技&#xff0c;今日收盘价&#xff1a;22.53元&#xff0c;转股价格&#xff1a;21.46元。 当前转股价值 转债面值 / 转股…

Redis的常用数据结构之字符串类型

redis中的数据结构是根据value的值来进行区别的&#xff0c;主要分了String、Hash、List、Set&#xff08;无序集合&#xff09;、Zset&#xff08;有序集合&#xff09; 字符串&#xff08;String&#xff09; String类型是redis中最基础的数据结构&#xff0c;也可以理解为…

Java基础面试题突击系列6

&#x1f469;&#x1f3fb; 作者&#xff1a;一只IT攻城狮 &#xff0c;关注我不迷路 ❤️《java面试核心知识》突击系列&#xff0c;持续更新… &#x1f490; 面试必知必会学习路线&#xff1a;Java技术栈面试系列SpringCloud项目实战学习路线 &#x1f4dd;再小的收获x365天…

一、CNNs网络架构-基础网络架构(LeNet、AlexNet、ZFNet)

目录 1.LeNet 2.AlexNet 2.1 激活函数&#xff1a;ReLU 2.2 随机失活&#xff1a;Droupout 2.3 数据扩充&#xff1a;Data augmentation 2.4 局部响应归一化&#xff1a;LRN 2.5 多GPU训练 2.6 论文 3.ZFNet 3.1 网络架构 3.2 反卷积 3.3 卷积可视化 3.4 ZFNet改…

Integer源码

介绍 Integer是int类型的包装类&#xff0c;继承自Number抽象类&#xff0c;实现了Comparable接口。提供了一些处理int类型的方法&#xff0c;比如int到String类型的转换方法或String类型到int类型的转换方法&#xff0c;当然也包含与其他类型之间的转换方法。 Comparable提供…

3ds MAX 基本体建模,长方体、圆柱体和球体

3ds MAX基本页面如下&#xff1a; 生成新的几何体在右侧&#xff1a; 选择生成的对象类型即可&#xff0c;以下为例子&#xff1a; 1、长方体建模 选择建立的对象类型为长方形 在 任意一个窗口绘制&#xff0c;鼠标滑动 这里选择左上角的俯视图 松开鼠标后&#xff0c;可以…

单片机GD32F303RCT6 (Macos环境)开发 (二十九)—— GD32通过蓝牙透传模块 IAP升级

GD32通过蓝牙透传模块 IAP升级 1、思路 上一节手机App可以通过HC-08模块控制mcu的开锁&#xff0c;关锁的动作&#xff0c;那么我们是不是可以将mcu的升级文件通过hc-08模块发送给gd32&#xff0c;完成gd32程序的自升级呢&#xff1f; 2、命令协议 蓝牙透传模块每次只能发2…

Selenium的使用

一、基础 1、特点 selenium 是web中基于UI的自动化测试工具&#xff0c;它支持多平台、多语言、多浏览器&#xff0c;还有丰富的API。 2、原理 自动化脚本代码会创建一个http请求发送给浏览器驱动进行解析&#xff0c;浏览器驱动会操控浏览器执行测试&#xff0c;浏览器接着…

AirServer电脑通用版下载及使用教程

AirServer 是一款功能十分强大的投屏软件&#xff0c;支持并适用于 Windows和Mac。AirServer 是接收方&#xff0c;而不是发送方。 AirServer 只允许您接收镜像或流媒体内容&#xff0c;反之则不行。AirServer虽然功能十分强大&#xff0c;但是整体操作和使用都十分简单&#x…

如何在华为OD机试中获得满分?Java实现【知识图谱新词挖掘1】一文详解!

✅创作者&#xff1a;陈书予 &#x1f389;个人主页&#xff1a;陈书予的个人主页 &#x1f341;陈书予的个人社区&#xff0c;欢迎你的加入: 陈书予的社区 &#x1f31f;专栏地址: Java华为OD机试真题&#xff08;2022&2023) 文章目录 1. 题目描述2. 输入描述3. 输出描述…

【STL】list的模拟实现

目录 前言 结构解析 默认成员函数 构造函数 拷贝构造 赋值重载 析构函数 迭代器 const迭代器 数据修改 insert erase 尾插尾删头插头删 容量查询 源码 前言 &#x1f349;list之所以摆脱了单链表尾插麻烦&#xff0c;只能单向访问等缺点&#xff0c;正是因为其…

日常 - HttpURLConnection 网络请求 TLS 1.2

文章目录 环境前言HTTPS 请求流程服务端支持JDK 验证资源 环境 JDK 8 Hutool 4.5.1 前言 应供应商 DD 的 TLS 版本升级通知&#xff0c;企业版接口升级后 TLS 1.0 及 1.1 版本请求将无法连接&#xff0c;仅支持 TLS 1.2 及以上版本的客户端发起请求。 当前项目使用 Hutool …

有序表2:跳表

跳表是一个随机化的数据结构&#xff0c;可以被看做二叉树的一个变种&#xff0c;它在性能上和红黑树&#xff0c;AVL树不相上下&#xff0c;但是跳表的原理非常简单&#xff0c;目前在Redis和LeveIDB中都有用到。 它采用随机技术决定链表中哪些节点应增加向前指针以及在该节点…

找不到“SqlServer”模块-- 在此计算机上找不到任何 SQL Server cmdlet。

https://github.com/PowerShell/PowerShell/releases/tag/v7.2.2SQL Server Management Studio 18 启动触发器报错 标题: 找不到“SqlServer”模块 --------------- 在此计算机上找不到任何 SQL Server cmdlet。 在 https://powershellgallery.com/packages/SqlServer 上获取“…

PyTorch深度学习实战(1)——神经网络与模型训练过程详解

PyTorch深度学习实战&#xff08;1&#xff09;——神经网络与模型训练过程详解 0. 前言1. 传统机器学习与人工智能2. 人工神经网络基础2.1 人工神经网络组成2.2 神经网络的训练 3. 前向传播3.1 计算隐藏层值3.2 执行非线性激活3.3 计算输出层值3.4 计算损失值3.5 实现前向传播…

Linux——应用层之序列号与反序列化

TCP协议通讯流程 tcp是面向连接的通信协议,在通信之前,需要进行3次握手,来进行连接的建立。 当tcp在断开连接的时候,需要释放连接,4次挥手 服务器初始化: 调用socket, 创建文件描述符; 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了…