【源码解析】SpringBoot使用Nacos配置中心和使用 @NacosValue 进行热更新

news2025/1/11 18:30:00

SpringBoot使用Nacos

引入依赖

<dependency>
	<groupId>com.alibaba.boot</groupId>
	<artifactId>nacos-config-spring-boot-starter</artifactId>
	<version>0.2.12</version>
</dependency>

增加本地配置

nacos:
  config:
    server-addr: 127.0.0.1:8848
    bootstrap:
      enable: true
      log:
        enable: true
    data-id: cls-service
    type: yaml
    auto-refresh: true # 开启自动刷新

增加远程配置

cls:
  cname: chars11

使用@NacosValue

@RestController
@RequestMapping("test")
public class NacosValueController {

    @NacosValue(value = "${cls.cname}", autoRefreshed = true)
    private String userName;

    @GetMapping("t1")
    public String getUserName() {
        return userName;
    }

}

NacosValue原理解析

NacosConfigEnvironmentProcessor

EnvironmentPostProcessorApplicationListener实现了SmartApplicationListener,系统启动的时候,会执行EnvironmentPostProcessorApplicationListener#onApplicationEvent。获取容器中所有的EnvironmentPostProcessor,执行对应的postProcessEnvironment

    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationEnvironmentPreparedEvent) {
            this.onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent)event);
        }

        if (event instanceof ApplicationPreparedEvent) {
            this.onApplicationPreparedEvent();
        }

        if (event instanceof ApplicationFailedEvent) {
            this.onApplicationFailedEvent();
        }

    }

    private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
        ConfigurableEnvironment environment = event.getEnvironment();
        SpringApplication application = event.getSpringApplication();
        Iterator var4 = this.getEnvironmentPostProcessors(application.getResourceLoader(), event.getBootstrapContext()).iterator();

        while(var4.hasNext()) {
            EnvironmentPostProcessor postProcessor = (EnvironmentPostProcessor)var4.next();
            postProcessor.postProcessEnvironment(environment, application);
        }

    }

NacosConfigEnvironmentProcessor#postProcessEnvironment,如果nacos.config.bootstrap.logEnable=true,开启预加载。系统添加了NacosConfigApplicationContextInitializer初始化器。

	public void postProcessEnvironment(ConfigurableEnvironment environment,
			SpringApplication application) {
		application.addInitializers(new NacosConfigApplicationContextInitializer(this));
		nacosConfigProperties = NacosConfigPropertiesUtils
				.buildNacosConfigProperties(environment);
		if (enable()) {
			System.out.println(
					"[Nacos Config Boot] : The preload log configuration is enabled");
			loadConfig(environment);
			NacosConfigLoader nacosConfigLoader = NacosConfigLoaderFactory.getSingleton(nacosConfigProperties, environment, builder);
			LogAutoFreshProcess.build(environment, nacosConfigProperties, nacosConfigLoader, builder).process();
		}
	}

	boolean enable() {
		return nacosConfigProperties != null
				&& nacosConfigProperties.getBootstrap().isLogEnable();
	}

NacosConfigEnvironmentProcessor#loadConfig,调用NacosConfigLoader进行加载。

	private void loadConfig(ConfigurableEnvironment environment) {
		NacosConfigLoader configLoader = new NacosConfigLoader(nacosConfigProperties,
				environment, builder);
		configLoader.loadConfig();
		// set defer NacosPropertySource
		deferPropertySources.addAll(configLoader.getNacosPropertySources());
	}

NacosConfigLoader#loadConfig,调用了NacosUtils.getContent来获取远程配置的内容。

	public void loadConfig() {
		MutablePropertySources mutablePropertySources = environment.getPropertySources();
		List<NacosPropertySource> sources = reqGlobalNacosConfig(globalProperties,
				nacosConfigProperties.getType());
		for (NacosConfigProperties.Config config : nacosConfigProperties.getExtConfig()) {
			List<NacosPropertySource> elements = reqSubNacosConfig(config,
					globalProperties, config.getType());
			sources.addAll(elements);
		}
		if (nacosConfigProperties.isRemoteFirst()) {
			for (ListIterator<NacosPropertySource> itr = sources.listIterator(sources.size()); itr.hasPrevious();) {
				mutablePropertySources.addAfter(
						StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, itr.previous());
			}
		} else {
			for (NacosPropertySource propertySource : sources) {
				mutablePropertySources.addLast(propertySource);
			}
		}
	}

	private List<NacosPropertySource> reqGlobalNacosConfig(Properties globalProperties,
			ConfigType type) {
		List<String> dataIds = new ArrayList<>();
		// Loads all data-id information into the list in the list
		if (!StringUtils.hasLength(nacosConfigProperties.getDataId())) {
			final String ids = environment
					.resolvePlaceholders(nacosConfigProperties.getDataIds());
			dataIds.addAll(Arrays.asList(ids.split(",")));
		}
		else {
			dataIds.add(nacosConfigProperties.getDataId());
		}
		final String groupName = environment
				.resolvePlaceholders(nacosConfigProperties.getGroup());
		final boolean isAutoRefresh = nacosConfigProperties.isAutoRefresh();
		return new ArrayList<>(Arrays.asList(reqNacosConfig(globalProperties,
				dataIds.toArray(new String[0]), groupName, type, isAutoRefresh)));
	}

	private NacosPropertySource[] reqNacosConfig(Properties configProperties,
			String[] dataIds, String groupId, ConfigType type, boolean isAutoRefresh) {
		final NacosPropertySource[] propertySources = new NacosPropertySource[dataIds.length];
		for (int i = 0; i < dataIds.length; i++) {
			if (!StringUtils.hasLength(dataIds[i])) {
				continue;
			}
			// Remove excess Spaces
			final String dataId = environment.resolvePlaceholders(dataIds[i].trim());
			final String config = NacosUtils.getContent(builder.apply(configProperties),
					dataId, groupId);
			final NacosPropertySource nacosPropertySource = new NacosPropertySource(
					dataId, groupId,
					buildDefaultPropertySourceName(dataId, groupId, configProperties),
					config, type.getType());
			nacosPropertySource.setDataId(dataId);
			nacosPropertySource.setType(type.getType());
			nacosPropertySource.setGroupId(groupId);
			nacosPropertySource.setAutoRefreshed(isAutoRefresh);
			logger.info("load config from nacos, data-id is : {}, group is : {}",
					nacosPropertySource.getDataId(), nacosPropertySource.getGroupId());
			propertySources[i] = nacosPropertySource;
			DeferNacosPropertySource defer = new DeferNacosPropertySource(
					nacosPropertySource, configProperties, environment);
			nacosPropertySources.add(defer);
		}
		return propertySources;
	}

NacosUtils#getContent,获取远程配置,调用核心类ConfigService

	public static String getContent(ConfigService configService, String dataId,
			String groupId) {
		String content = null;
		try {
			content = configService.getConfig(dataId, groupId, DEFAULT_TIMEOUT);
		}
		catch (NacosException e) {
			if (logger.isErrorEnabled()) {
				logger.error("Can't get content from dataId : " + dataId + " , groupId : "
						+ groupId, e);
			}
		}
		return content;
	}

NacosConfigApplicationContextInitializer

NacosConfigApplicationContextInitializer初始化,会添加自动刷新的监听器。

public class NacosConfigApplicationContextInitializer
		implements ApplicationContextInitializer<ConfigurableApplicationContext> {
	// ...

	@Override
	public void initialize(ConfigurableApplicationContext context) {
		singleton.setApplicationContext(context);
		environment = context.getEnvironment();
		nacosConfigProperties = NacosConfigPropertiesUtils
				.buildNacosConfigProperties(environment);
		final NacosConfigLoader configLoader = NacosConfigLoaderFactory.getSingleton(
				nacosConfigProperties, environment, builder);
		if (!enable()) {
			logger.info("[Nacos Config Boot] : The preload configuration is not enabled");
		}
		else {

			// If it opens the log level loading directly will cache
			// DeferNacosPropertySource release

			if (processor.enable()) {
				processor.publishDeferService(context);
				configLoader
						.addListenerIfAutoRefreshed(processor.getDeferPropertySources());
			}
			else {
				configLoader.loadConfig();
				configLoader.addListenerIfAutoRefreshed();
			}
		}

		final ConfigurableListableBeanFactory factory = context.getBeanFactory();
		if (!factory
				.containsSingleton(NacosBeanUtils.GLOBAL_NACOS_PROPERTIES_BEAN_NAME)) {
			factory.registerSingleton(NacosBeanUtils.GLOBAL_NACOS_PROPERTIES_BEAN_NAME,
					configLoader.getGlobalProperties());
		}
	}

	private boolean enable() {
		return processor.enable() || nacosConfigProperties.getBootstrap().isEnable();
	}

}

NacosConfigLoader#addListenerIfAutoRefreshed(List<NacosConfigLoader.DeferNacosPropertySource>),调用NacosPropertySourcePostProcessor添加监听器。

	public void addListenerIfAutoRefreshed(
			final List<DeferNacosPropertySource> deferNacosPropertySources) {
		for (DeferNacosPropertySource deferNacosPropertySource : deferNacosPropertySources) {
			NacosPropertySourcePostProcessor.addListenerIfAutoRefreshed(
					deferNacosPropertySource.getNacosPropertySource(),
					deferNacosPropertySource.getProperties(),
					deferNacosPropertySource.getEnvironment());
		}
	}

NacosPropertySourcePostProcessor#addListenerIfAutoRefreshed,当监听到配置变化,直接替换env的配置数据。

	public static void addListenerIfAutoRefreshed(
			final NacosPropertySource nacosPropertySource, final Properties properties,
			final ConfigurableEnvironment environment) {

		if (!nacosPropertySource.isAutoRefreshed()) { // Disable Auto-Refreshed
			return;
		}

		final String dataId = nacosPropertySource.getDataId();
		final String groupId = nacosPropertySource.getGroupId();
		final String type = nacosPropertySource.getType();
		final NacosServiceFactory nacosServiceFactory = getNacosServiceFactoryBean(
				beanFactory);

		try {

			ConfigService configService = nacosServiceFactory
					.createConfigService(properties);

			Listener listener = new AbstractListener() {

				@Override
				public void receiveConfigInfo(String config) {
					String name = nacosPropertySource.getName();
					NacosPropertySource newNacosPropertySource = new NacosPropertySource(
							dataId, groupId, name, config, type);
					newNacosPropertySource.copy(nacosPropertySource);
					MutablePropertySources propertySources = environment
							.getPropertySources();
					// replace NacosPropertySource
					propertySources.replace(name, newNacosPropertySource);
				}
			};

			if (configService instanceof EventPublishingConfigService) {
				((EventPublishingConfigService) configService).addListener(dataId,
						groupId, type, listener);
			}
			else {
				configService.addListener(dataId, groupId, listener);
			}

		}
		catch (NacosException e) {
			throw new RuntimeException(
					"ConfigService can't add Listener with properties : " + properties,
					e);
		}
	}

使用DelegatingEventPublishingListener对监听器进行包装。

	public void addListener(String dataId, String group, String type, Listener listener)
			throws NacosException {
		Listener listenerAdapter = new DelegatingEventPublishingListener(configService,
				dataId, group, type, applicationEventPublisher, executor, listener);
		addListener(dataId, group, listenerAdapter);
	}

DelegatingEventPublishingListener#receiveConfigInfo,当接收到事件后,会调用内置的监听器处理,以及发布NacosConfigReceivedEvent事件。

	@Override
	public void receiveConfigInfo(String content) {
		onReceived(content);
		publishEvent(content);
	}

	private void publishEvent(String content) {
		NacosConfigReceivedEvent event = new NacosConfigReceivedEvent(configService,
				dataId, groupId, content, configType);
		applicationEventPublisher.publishEvent(event);
	}

	private void onReceived(String content) {
		delegate.receiveConfigInfo(content);
	}

NacosValueAnnotationBeanPostProcessor

NacosValueAnnotationBeanPostProcessor实现了BeanPostProcessor,启动的时候执行NacosValueAnnotationBeanPostProcessor#postProcessBeforeInitialization。将带有@NacosValue注解的属性和方法加入到一个Map中。

	@Override
	public Object postProcessBeforeInitialization(Object bean, final String beanName)
			throws BeansException {

		doWithFields(bean, beanName);

		doWithMethods(bean, beanName);

		return super.postProcessBeforeInitialization(bean, beanName);
	}

	private void doWithFields(final Object bean, final String beanName) {
		ReflectionUtils.doWithFields(bean.getClass(),
				new ReflectionUtils.FieldCallback() {
					@Override
					public void doWith(Field field) throws IllegalArgumentException {
						NacosValue annotation = getAnnotation(field, NacosValue.class);
						doWithAnnotation(beanName, bean, annotation, field.getModifiers(),
								null, field);
					}
				});
	}

	private void doWithMethods(final Object bean, final String beanName) {
		ReflectionUtils.doWithMethods(bean.getClass(),
				new ReflectionUtils.MethodCallback() {
					@Override
					public void doWith(Method method) throws IllegalArgumentException {
						NacosValue annotation = getAnnotation(method, NacosValue.class);
						doWithAnnotation(beanName, bean, annotation,
								method.getModifiers(), method, null);
					}
				});
	}

	private void doWithAnnotation(String beanName, Object bean, NacosValue annotation,
			int modifiers, Method method, Field field) {
		if (annotation != null) {
			if (Modifier.isStatic(modifiers)) {
				return;
			}

			if (annotation.autoRefreshed()) {
				String placeholder = resolvePlaceholder(annotation.value());

				if (placeholder == null) {
					return;
				}

				NacosValueTarget nacosValueTarget = new NacosValueTarget(bean, beanName,
						method, field, annotation.value());
				put2ListMap(placeholderNacosValueTargetMap, placeholder,
						nacosValueTarget);
			}
		}
	}

	private <K, V> void put2ListMap(Map<K, List<V>> map, K key, V value) {
		List<V> valueList = map.get(key);
		if (valueList == null) {
			valueList = new ArrayList<V>();
		}
		valueList.add(value);
		map.put(key, valueList);
	}

该类同样还实现了ApplicationListener<NacosConfigReceivedEvent>,当监听到NacosConfigReceivedEvent,通过反射用新的值替换Bean中的属性值。

	@Override
	public void onApplicationEvent(NacosConfigReceivedEvent event) {
		// In to this event receiver, the environment has been updated the
		// latest configuration information, pull directly from the environment
		// fix issue #142
		for (Map.Entry<String, List<NacosValueTarget>> entry : placeholderNacosValueTargetMap
				.entrySet()) {
			String key = environment.resolvePlaceholders(entry.getKey());
			String newValue = environment.getProperty(key);

			if (newValue == null) {
				continue;
			}
			List<NacosValueTarget> beanPropertyList = entry.getValue();
			for (NacosValueTarget target : beanPropertyList) {
				String md5String = MD5Utils.md5Hex(newValue, "UTF-8");
				boolean isUpdate = !target.lastMD5.equals(md5String);
				if (isUpdate) {
					target.updateLastMD5(md5String);
					Object evaluatedValue = resolveNotifyValue(target.nacosValueExpr, key, newValue);
					if (target.method == null) {
						setField(target, evaluatedValue);
					}
					else {
						setMethod(target, evaluatedValue);
					}
				}
			}
		}
	}


	private void setMethod(NacosValueTarget nacosValueTarget, Object propertyValue) {
		Method method = nacosValueTarget.method;
		ReflectionUtils.makeAccessible(method);
		try {
			method.invoke(nacosValueTarget.bean,
					convertIfNecessary(method, propertyValue));

			if (logger.isDebugEnabled()) {
				logger.debug("Update value with {} (method) in {} (bean) with {}",
						method.getName(), nacosValueTarget.beanName, propertyValue);
			}
		}
		catch (Throwable e) {
			if (logger.isErrorEnabled()) {
				logger.error("Can't update value with " + method.getName()
						+ " (method) in " + nacosValueTarget.beanName + " (bean)", e);
			}
		}
	}

	private void setField(final NacosValueTarget nacosValueTarget,
			final Object propertyValue) {
		final Object bean = nacosValueTarget.bean;

		Field field = nacosValueTarget.field;

		String fieldName = field.getName();

		try {
			ReflectionUtils.makeAccessible(field);
			field.set(bean, convertIfNecessary(field, propertyValue));

			if (logger.isDebugEnabled()) {
				logger.debug("Update value of the {}" + " (field) in {} (bean) with {}",
						fieldName, nacosValueTarget.beanName, propertyValue);
			}
		}
		catch (Throwable e) {
			if (logger.isErrorEnabled()) {
				logger.error("Can't update value of the " + fieldName + " (field) in "
						+ nacosValueTarget.beanName + " (bean)", e);
			}
		}
	}

在这里插入图片描述

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

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

相关文章

通过 ChatGPT 制作一个短视频

图文&#xff0c;生成视频 当通过 ChatGPT 生成连贯的 prompt 时&#xff0c;除了连环画&#xff0c;我们理所当然还可能畅想更激进的场景——生成动画视频。目前 AIGC 社区确实在生成视频方面有一定的尝试。比如 Deforum 可以通过多条 prompt&#xff0c;配合具体的切换时间点…

Centos7中mysql安装配置

前提&#xff1a;先关闭防火墙或开启tcp的3306端口 1、查看服务器上是否有现成的安装包 yum list mysql* 2、去mysql官网的yum资源库找到对应的rpm文件的下载链接 确定系统版本 cat /etc/redhat-release 到mysql官网复制对应版本的资源下载链接 MySQL :: Download MySQL Yum…

chatgpt赋能python:Python长连接详解:优化用户体验和节约资源的有效方式

Python长连接详解&#xff1a;优化用户体验和节约资源的有效方式 Python语言具备多样性和灵活性&#xff0c;是内容和功能极其丰富的一种编程语言。对于网站或者应用程序的开发&#xff0c;在Python语言的基础上可以实现长连接&#xff0c;优化用户体验和节约资源&#xff0c;…

网站部署与上线(1)虚拟机

文章目录 .1 虚拟机简介2 虚拟机的安装 本章将搭建实例的生产环境&#xff0c;将所有的代码搭建在一台Linux服务器中&#xff0c;并且测试其能否正常运行。 使用远程服务器进行连接&#xff1b; 基本的Linux命令&#xff1b; 使用Nginx搭建Node.js服务器&#xff1b; 在服务器端…

Admin.NET管理系统(vue3等前后端分离)学习笔记--持续更新

我的学习笔记 - 9iAdmin.NET 欢迎学习交流&#xff08;一&#xff09;前端笔记1.1 关于.env的设置1.2 关于路由模式问题1.3 关于 vue.config.ts1.4 关于 打包&#xff08;pnpm run build&#xff09;溢出问题1.5 关于 打包&#xff08;pnpm run build&#xff09;后部署到IIS重…

你知道网速的发展史吗? 80年代的我们是这样上网的!

&#x1f680; 个人主页 极客小俊 ✍&#x1f3fb; 作者简介&#xff1a;web开发者、设计师、技术分享博主 &#x1f40b; 希望大家多多支持一下, 我们一起进步&#xff01;&#x1f604; &#x1f3c5; 如果文章对你有帮助的话&#xff0c;欢迎评论 &#x1f4ac;点赞&#x1…

C++:征服C指针:指针(一)

关于指针 1.看一个简单的程序&#xff0c;来接触下指针2. 常见疑问&#xff1a;指针就是地址&#xff0c;那么int的指针和double的指针有什么区别 了3. 常见疑问&#xff1a;指针运算4. 为什么存在奇怪的指针运算符5. 试图将数组作为函数的参数进行传递。6. 什么是空指针5.1 声…

怎样用一周时间研究 ChatGPT

我是怎样用一周时间研究 ChatGPT 的&#xff1f; 上周大概开了 20 多个会&#xff0c;其中有一些是见了觉得今年可能会比较活跃出手的机构&#xff0c;其余见的绝大多数是和 ChatGPT 相关。 我后面就以 ChatGPT 为例&#xff0c;讲下我是如何快速一周 cover 一个赛道的&#x…

GDB 基础使用与多进程调试

​ GDB 全称“GNU symbolic debugger”是 Linux 下常用的程序调试器&#xff0c;当下的 GDB 支持调试多种编程语言编写的程序&#xff0c;包括 C、C、Go、Objective-C、OpenCL、Ada 等。 01 GDB 基础调试 1.1 基础使用 安装工具 # 安装 gcc sudo yum install gcc # 安装 g s…

记录一次el-table动态添加删除列导致表格样式错误(或不聚集)问题

记录一次el-table动态添加删除列导致表格样式错误问题 需求背景出现的问题解决方案理论&#xff1a;在el-table中设置key值&#xff0c;重新赋值表格数据之后&#xff0c;更新key值&#xff0c;达到动态更新效果 需求背景 一个电商类商品管理平台&#xff08;类似shopify产品编…

2023 华为 Datacom-HCIE 真题题库 06--含解析

多项选择 1.[试题编号&#xff1a;190185] &#xff08;多选题&#xff09;如图所示&#xff0c;PE 1和PE2之间通过Loopback0接口建立MP-BGP邻居关系&#xff0c;在配置完成之后&#xff0c;发现CE1和CE2之间无法互相学习路由&#xff0c;以下哪些项会导致该问题出现? A、PE1…

初识linux之简单了解TCP协议与UDP协议

目录 一、理解源IP地址和目的IP地址 二、端口号 1. 为什么要有端口号 2. 理解端口号 3. 源端口号和目的端口号 三、初步了解TCP协议和UDP协议 1. 初步认识TCP协议 2. 初步认识UDP协议 3. 可靠传输与不可靠传输 四、网络字节序 1. 网络字节序的概念 2. 如何形成网络…

python+django电子笔记交易系统vue

编码使用python&#xff08;我的pycharm版本是2021.3.3&#xff09;&#xff0c;数据库使用mysql&#xff08;我的mysql版本5.5&#xff09;。网站点击能够跳转各个页面&#xff0c;不用部署服务器&#xff0c;本地运行即可。 题目&#xff1a;基于django的电子笔记交易系统 功…

并发编程的三大特性之有序性

有序性的概念 Java文件在被cpu执行前会进行编译成cpu可以执行的指令&#xff0c;为了提高cpu的执行效率会对其中的一些语句进行重排序。Java指令最终是乱序执行的目的是为了提高cpu的执行效率&#xff0c;发挥cpu的性能 单例模式由于指令重排可能会出现上述的问题&#xff0…

ASP.NET Core

1. 入口文件 一个应用程序总有一个入口文件&#xff0c;是应用启动代码开始执行的地方&#xff0c;这里往往也会涉及到应用的各种配置。当我们接触到一个新框架的时候&#xff0c;可以从入口文件入手&#xff0c;了解入口文件&#xff0c;能够帮助我们更好地理解应用的相关配置…

SOC与MCU的区别及汽车电子未来发展以及展望

SOC与MCU的区别及汽车电子未来发展以及展望 MCU与SOC的区别 CPU&#xff08;Central Processing Unit&#xff09;&#xff0c;是一台计算机的运算核心和控制核心。CPU由运算器、控制器和寄存器及实现它们之间联系的数据、控制及状态的总线构成。差不多所有的CPU的运作原理可…

【PHP】问题已解决:宝塔面板搭建php网站无法上传图片或是文件(保姆级图文)

目录 问题情况原因和解决方法总结 『PHP』分享PHP环境配置到项目实战个人学习笔记。 欢迎关注 『PHP』 系列&#xff0c;持续更新中 欢迎关注 『PHP』 系列&#xff0c;持续更新中 问题情况 宝塔面板搭建php网站无法上传图片或是文件。 原因和解决方法 检查你的php里是否安装…

老板让你写个PPT没有头绪?没事,ChatGPT来帮你!

文章目录 前言一、先确定写什么——准备内容二、再看看能用吗——自动生成PPT三、最后再改改——看个人喜好写在最后 前言 自从人工智能横空而出&#xff0c;它在人们的生活中产生了巨大的影响。尤其在企业办公领域&#xff0c;借助人工智能的力量&#xff0c;能够迅速产出丰富…

千乎万唤始出来,支持gpt3和gpt4支持画图,的在线gpt应用接入案例开源上线啦

了解OPEN AI 平台用户一直在说&#xff0c;这个接口要怎么对接&#xff0c;如何在体验。 由于我一直忙于接口中台开发&#xff0c;所以在线基于OPEN AI 接口实例例子就一直没有写。现在终于写完了。 基于纯HTMLCSSJS 小白也能轻松上手部署。代码简单清晰。 这里不多做其他赘述…

tensorflow及其keras如何保存模型

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…