C++11 设计模式5. 原型模式

什么是原型模式?

        原型模式⼀种创建型设计模式,该模式的核⼼思想是基于现有的对象创建新的对象,⽽不是从头开始创建。在原型模式中,通常有⼀个原型对象,它被⽤作创建新对象的模板。新对象通过复制原型对象的属性和状态来创建,⽽⽆需知道具体的创建细节。

Prototype模式说简单点,就是提供了一个clone, 通过已存在对象进行新对象创建。clone()实现和具体的实现语言相关,在C++中我们通过拷贝构造函数实现。

那为啥要写clone的接口来实现这个目的呢?直接使用拷贝构造不香么,知乎中看到陈硕大佬对此的一个回答,觉得豁然开朗。

Prototype 的意义在于,你拿到一个 基类指针 Base* ,
它指向某个 派生类 Derived 对象,
你想克隆出 Derived对象,但代码中不写出 Derived 的具体类型,
因为有很多派生类,这种情况下你用构造函数是搞不定的。
switch-case 是 bad smells 。
另外,这里考虑 virtual 的性能损失是主次不分,
构造对象需要分配内存,这开销比一次虚函数调用大多了。
–陈硕在知乎中的回答

复习拷贝构造函数实现

在学习原型模式之前,我们先来看一下C++中深拷贝和先拷贝问题

UML 

两种角色:

1. prototype(抽象原型类) Monster类

2. ConcretePrototype 具体原型类:在clone方法中 return一个自己的对象

        M_undead,M_Element,M_Mechanic

那么什么时候才能使用这个原型对象呢?

我们以 魔兽争霸 这款游戏中的 分身系英雄为例,当 剑圣 出镜像的时候,5狗齐飞,分身斧,这些道具或者技能使用的时候,分裂出来的这些英雄都应该有当前英雄的这些属性,然后将属性微调就可以,例如,分身的攻击力只有原先的30%,受到伤害是原先的150%。就可以用 原型模型。

这里的核心是 魔兽争霸的英雄 属性太多了,还有6个框框的物品栏,分裂的瞬间,都要将这些属性拷贝过去。--也就是说:

如果对象的内部数据比较复杂多变,并且在创建对象的时候希望爆出对象的当前状态,那么用原型模式显然比用工厂方法模式更合适

工厂方法模式和原型模式在创建对象时的异同点

a。都不需要程序员知道所创建对象所属的类名

b.工厂方法模式中的createMonster仍旧属于根据类名来生成新对象

c 原型模式clone是根据现有对象来生成新对象,会调用 copy构造方法 。 

d 原型模型不像工厂模式,不需要额外的等级结构创建多个工厂类 

原型模式的优缺点:

如果创建新对象的背部数据比较复杂且多变,原型模式创建对象的效率可能高的多

需要在clone中完成对象的拷贝,特别是深拷贝和浅拷贝问题,但是一般都调用 copy构造方法

当前copy构造函数中也可以调用 operator= 的重写。

这样也可以使用 "= " 完成拷贝

原型模式和直接 M_undead(m_undead1) 有啥区别?

既然原型模式的 也是调用  copy构造函数,那么直接用 如下的代码不就行了吗?为啥还弄个原型模式?

M_undead m_undead2 = M_undead(m_undead1)

1. C++才有 copy构造,java ,C#并没有,设计模式是独立于变成 语言存在的,因此原型模式的存在是有意义的。

2.你拿到一个 基类指针 Base* ,
它指向某个 派生类 Derived 对象,
你想克隆出 Derived对象,但代码中不写出 Derived 的具体类型,
因为有很多派生类,这种情况下你用构造函数是搞不定的。
switch-case 是 bad smells 。

代码

在代码中还复习了 移动构造函数,和 移动operator=运算符的重写。

// 原型模型//原型模式⼀种创建型设计模式,
//该模式的核⼼思想是基于现有的对象创建新的对象,
//⽽不是从头开始创建。在原型模式中,通常有⼀个原型对象,
//它被⽤作创建新对象的模板。
//新对象通过复制原型对象的属性和状态来创建,⽽⽆需知道具体的创建细节。
//
//Prototype模式说简单点,就是提供了一个clone, 
//通过已存在对象进行新对象创建。
//clone()实现和具体的实现语言相关,
//在C++中我们通过拷贝构造函数实现。
//
//那为啥要写clone的接口来实现这个目的呢?
//直接使用拷贝构造不香么?
//
//Prototype 的意义在于,你拿到一个 基类指针 Base* ,
//它指向某个 派生类 Derived 对象,
//你想克隆出 Derived对象,但代码中不写出 Derived 的具体类型,
//因为有很多派生类,这种情况下你用构造函数是搞不定的。
//switch - case 是 bad smells 。
//另外,这里考虑 virtual 的性能损失是主次不分,
//构造对象需要分配内存,这开销比一次虚函数调用大多了。//
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include "stdlib.h"
#include "stdio.h"
#include "stdint.h"
using namespace std;//在学习原型模式之前,需要先复习一下 C++ 的 深拷贝浅拷贝问题。
//涉及到的知识点:拷贝构造函数,重写赋值运算符函数,移动构造函数,重写移动赋值运算符函数class Teacher
{
public:Teacher() = default;//=default  表示C++编译器给无参数的构造方法生成函数体//有参数的构造方法Teacher(int age,string sname,char* name,char *othername,char **stuname):_age(age),_sname(sname), _othername(othername),_stuname(stuname){cout << "Teacher 有参数的构造方法被调用" << endl;strcpy(_name, name);}//拷贝构造函数 正常的写法//Teacher(const Teacher& obj) {//	this->_age = obj._age;//	strcpy(_name, obj._name);//	this->_sname = obj._sname;//	//	//如果原先的othername不为nullptr,则先要将原先的othername delete掉。实际代码中//	if (this->_othername!= nullptr) {//		delete this->_othername;//		this->_othername = nullptr;//	}//	if (this->_stuname != nullptr) {//		for (int i = 0; i < 5; i++)//		{//			delete this->_stuname[i];//			this->_stuname[i] = nullptr;//		}//		delete this->_stuname;//		this->_stuname = nullptr;//		//delete [] this->_stuname;//	}//	if (obj._othername != nullptr) {//		int othernamelen = strlen(obj._othername) + 1;//		this->_othername = (char *)malloc(sizeof(char) * othernamelen);//		strcpy(this->_othername,obj._othername);//	}//	if (obj._stuname != nullptr) {//		this->_stuname = (char **)malloc(sizeof(char *) * 5);//		for (int i = 0; i < 5; i++)//		{//			if (obj._stuname[i] != nullptr) {//				int stunamelen = strlen(obj._stuname[i]) + 1;//				this->_stuname[i] = (char *)malloc(sizeof(char *) *stunamelen);//				strcpy(this->_stuname[i],obj._stuname[i]);//			}//		}//	}//}//拷贝构造函数Teacher(const Teacher& obj) {cout << "Teacher 拷贝构造函数被调用" << endl;*this = obj;}//operator = 函数 重写Teacher& operator=( const Teacher & obj) {cout << "Teacher 赋值运算符函数被调用" << endl;this->_age = obj._age;strcpy(_name, obj._name);this->_sname = obj._sname;//如果原先的othername不为nullptr,则先要将原先的othername delete掉。实际代码中if (this->_othername != nullptr) {free( this->_othername);this->_othername = nullptr;}if (this->_stuname != nullptr) {for (int i = 0; i < 5; i++){free( this->_stuname[i]);this->_stuname[i] = nullptr;}free( this->_stuname);this->_stuname = nullptr;//delete [] this->_stuname;}if (obj._othername != nullptr) {int othernamelen = strlen(obj._othername) + 1;this->_othername = (char *)malloc(sizeof(char) * othernamelen);strcpy(this->_othername, obj._othername);}if (obj._stuname != nullptr) {this->_stuname = (char **)malloc(sizeof(char *) * 5);for (int i = 0; i < 5; i++){if (obj._stuname[i] != nullptr) {int stunamelen = strlen(obj._stuname[i]) + 1;this->_stuname[i] = (char *)malloc(sizeof(char *) *stunamelen);strcpy(this->_stuname[i], obj._stuname[i]);}}}return *this;}//移动拷贝函数Teacher( Teacher &&obj) {cout << "Teacher 移动构造函数被调用" << endl;*this = move(obj);}//移动赋值运算符 重写Teacher& operator=(Teacher && obj) {cout << "Teacher 移动赋值运算符 被调用" << endl;this->_age = obj._age;strcpy(_name, obj._name);this->_sname = obj._sname;//如果原先的othername不为nullptr,则先要将原先的othername delete掉。实际代码中if (this->_othername != nullptr) {free( this->_othername);this->_othername = nullptr;}if (this->_stuname != nullptr) {for (int i = 0; i < 5; i++){free( this->_stuname[i]);this->_stuname[i] = nullptr;}free( this->_stuname);this->_stuname = nullptr;//delete [] this->_stuname;}if (obj._othername != nullptr) {int othernamelen = strlen(obj._othername) + 1;this->_othername = (char *)malloc(sizeof(char) * othernamelen);strcpy(this->_othername, obj._othername);}if (obj._stuname != nullptr) {this->_stuname = (char **)malloc(sizeof(char *) * 5);for (int i = 0; i < 5; i++){if (obj._stuname[i] != nullptr) {int stunamelen = strlen(obj._stuname[i]) + 1;this->_stuname[i] = (char *)malloc(sizeof(char *) *stunamelen);strcpy(this->_stuname[i], obj._stuname[i]);}}}//if (obj._othername != nullptr) {free(obj._othername);obj._othername = nullptr;}if (obj._stuname!=nullptr) {for (int i = 0; i < 5; i++){if (obj._stuname[i] !=nullptr) {free( obj._stuname[i]);obj._stuname[i] = nullptr;}}free( obj._stuname);obj._stuname = nullptr;}return *this;}// 写一个clone函数,内部调用 拷贝构造方法,Teacher* clone() {return new Teacher(*this);}~Teacher() {if (this->_othername != nullptr) {free(this->_othername);this->_othername = nullptr;}if (this->_stuname != nullptr) {for (int i = 0; i < 5; i++){free(this->_stuname[i]);this->_stuname[i] = nullptr;}free(this->_stuname);this->_stuname = nullptr;//delete [] this->_stuname;}}
public:int _age = 0 ;string _sname = "";char _name[128] = {0};char *_othername = nullptr;char **_stuname = nullptr;
};
void printTea(Teacher *tea) {if (tea == NULL) {return;}printf("------ printTea start ------\n");printf("tea->age = %d,tea->name = %s,tea->othername=%s\n",tea->_age,tea->_name,tea->_othername);for (size_t j = 0; j < 5; j++){printf("tea->stuname[%d] = %s,  ",j, tea->_stuname[j]);}printf("\n");printf("------ printTea end ------\n");}
int main()
{_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);//程序退出时检测内存泄漏并显示到“输出”窗口std::cout << "Hello World!\n";Teacher tea;tea._age = 10;strcpy(tea._name, "tea---name");tea._sname = "teasname";tea._othername = (char *)malloc(sizeof(char) * 128);memset(tea._othername,0,sizeof(char) * 128);strcpy(tea._othername, "teaothername");tea._stuname = (char**)malloc(sizeof(char *) * 5);memset(tea._stuname, 0, sizeof(char *) * 5);for (int i = 0; i < 5; i++){tea._stuname[i] = (char *)malloc(sizeof(char) * 128);memset(tea._stuname[i], 0, sizeof(char) * 128);sprintf(tea._stuname[i], "teastuname%d", i + 100);}printTea(&tea);cout << "---------" << endl;Teacher tea1 = tea;printTea(&tea1);cout << "----1111111111-----" << endl;Teacher tea2 ;tea2 = tea;printTea(&tea2);cout << "----2222222222-----" << endl;Teacher tea3 = move(tea);printTea(&tea3);cout << "----333333333-----" << endl;Teacher tea4;tea4 = move(tea3);printTea(&tea4);// 原型模式Teacher.clone方法的调用Teacher *tea5 = tea4.clone();printTea(tea5);delete tea5;//这里有些Teacher 的 析构函数,因此delete 会调用析构函数,
}

继承中的代码

namespace _nmsp2
{//怪物父类class Monster{public://构造函数Monster(int life, int magic, int attack) :m_life(life), m_magic(magic), m_attack(attack) {}virtual ~Monster() {} //做父类时虚构函数应该为虚函数public:virtual Monster* clone() = 0; //具体的实现在子类中进行//public://	void setlife(int tmplife)//	{//		m_life = tmplife;//	}protected: //可能被子类访问的成员,所以用protected修饰//public://怪物属性int m_life;  //生命值int m_magic; //魔法值int m_attack;  //攻击力};//亡灵类怪物class M_Undead :public Monster{public://构造函数M_Undead(int life, int magic, int attack) :Monster(life, magic, attack){cout << "一只亡灵类怪物来到了这个世界" << endl;}//拷贝构造函数//..........留给大家自己写virtual Monster* clone(){//return new M_Undead(300, 50, 80); //创建亡灵类怪物return new M_Undead(*this); //触发拷贝构造函数的调用来创建亡灵类怪物/*Monster * pmonster = new M_Undead(300, 50, 80); //创建亡灵类怪物//pmonster->m_life = m_life;pmonster->setlife(m_life);pmonster->m_magic = m_magic;pmonster->m_attack = m_attack;return pmonster;*/}//...其他代码略};//元素类怪物class M_Element :public Monster{public://构造函数M_Element(int life, int magic, int attack) :Monster(life, magic, attack){cout << "一只元素类怪物来到了这个世界" << endl;}//拷贝构造函数M_Element(const M_Element& tmpobj) :Monster(tmpobj) //初始化列表中注意对父类子对象的初始化{cout << "调用了M_Element::M_Element(const M_Element& tmpobj)拷贝构造函数创建了一只元素类怪物" << endl;}virtual Monster* clone(){//return new M_Element(200, 80, 100); //创建元素类怪物return new M_Element(*this);}//...其他代码略};//机械类怪物class M_Mechanic :public Monster{public://构造函数M_Mechanic(int life, int magic, int attack) :Monster(life, magic, attack){cout << "一只机械类怪物来到了这个世界" << endl;}//拷贝构造函数M_Mechanic(const M_Mechanic& tmpobj) :Monster(tmpobj) //初始化列表中注意对父类子对象的初始化{cout << "调用了M_Mechanic::M_Mechanic(const M_Mechanic& tmpobj)拷贝构造函数创建了一只机械类怪物" << endl;}virtual Monster* clone(){//return new M_Mechanic(400, 0, 110); //创建机械类怪物return new M_Mechanic(*this);}//...其他代码略};//全局的用于创建怪物对象的函数/*void Gbl_CreateMonster2(Monster* pMonster){Monster* ptmpobj = nullptr;if (dynamic_cast<M_Undead*>(pMonster) != nullptr){ptmpobj = new M_Undead(300, 50, 80); //创建亡灵类怪物}else if (dynamic_cast<M_Element*>(pMonster) != nullptr){ptmpobj = new M_Element(200,80, 100); //创建元素类怪物}else if (dynamic_cast<M_Mechanic*>(pMonster) != nullptr){ptmpobj = new M_Mechanic(400, 0, 110); //创建机械类怪物}if (ptmpobj != nullptr){//这里就可以针对ptmpobj对象实现各种业务逻辑//......//不要忘记释放资源delete ptmpobj;}}*/void Gbl_CreateMonster2(Monster* pMonster){Monster* ptmpobj = pMonster->clone(); //根据已有对象直接创建新对象,不需要知道已有对象所属的类型//这里就可以针对ptmpobj对象实现各种业务逻辑//......//不要忘记释放资源delete ptmpobj;}
}

调用

	_nmsp2::M_Mechanic myPropMecMonster(400, 0, 110); //创建一只机械类怪物对象作为原型对象以用于克隆目的_nmsp2::Monster* pmyPropEleMonster = new _nmsp2::M_Element(200, 80, 100); //创建一只元素类怪物对象作为原型对象以用于克隆目的,//这里可以直接new,也可以通过工厂模式创建原型对象,取决于程序员自己的洗好。//....._nmsp2::Monster* p_CloneObj1 = myPropMecMonster.clone(); //使用原型对象克隆出新的机械类怪物对象_nmsp2::Monster* p_CloneObj2 = pmyPropEleMonster->clone(); //使用原型对象克隆出新的元素类怪物对象_nmsp2::Monster* p_CloneObj3 = new _nmsp2::M_Mechanic(myPropMecMonster);//可以对p_CloneObj1、p_CloneObj2所指向的对象进行各种操作(实现具体的业务逻辑)//......//释放资源//释放克隆出来的怪物对象delete p_CloneObj1;delete p_CloneObj2;//释放原型对象(堆中的)delete pmyPropEleMonster;_nmsp2::Monster* pMonsterObj = new _nmsp2::M_Element(200, 80, 100);_nmsp2::Gbl_CreateMonster2(pMonsterObj);delete pMonsterObj;

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

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

相关文章

24.什么是跨域?解决方案有哪些?

为什么会出现跨域问题 存在浏览器同源策略&#xff0c;所以才会有跨域问题。那么浏览器是出于何种原因会有跨域的限制呢。其实不难想到&#xff0c;跨域限制主要的目的就是为了用户的上网安全。 同源策略导致的跨域是浏览器单方面拒绝响应数据&#xff0c;服务器端是处理完毕…

opencv基础篇 ——(十)非真实感渲染

非真实感渲染&#xff08;Non-Photorealistic Rendering, NPR&#xff09;是指通过一系列图像处理技术&#xff0c;将真实感图像转换为具有特定艺术风格或视觉效果的图像&#xff0c;模拟绘画、素描、卡通等非现实主义表现手法。OpenCV 提供了一些内置函数来实现非真实感渲染&a…

2024最新的,免费的 ChatGPT 网站AI(八个)

ChatGPT是美国人工智能研究实验室OpenAI在2022年11月推出的一款人工智能技术驱动的语言模型应用。它基于GPT-3.5架构&#xff08;后续还有GPT-4架构的升级版&#xff09;构建&#xff0c;拥有强大的自然语言处理能力和上下文理解能力&#xff0c;能够参与多轮对话&#xff0c;为…

Python_GUI工具包 PyQt 与 Pyside6的介绍

Python_GUI工具包 PyQt 与 Pyside6的介绍 一、简介 在Python的GUI&#xff08;图形用户界面&#xff09;开发领域&#xff0c;PyQt和PySide6是两个非常重要的工具包。它们都基于Qt库&#xff0c;为Python开发者提供了丰富的GUI组件和强大的功能。当然Python也有一些其他的GU…

Halcon 检测物体定位点

文章目录 get_domain 返回所有输入图像的定义域作为一个区域add_channels 给区域增加灰度值find_shape_model 发现匹配模板find_shape_models 发现最佳模板示例 get_domain 返回所有输入图像的定义域作为一个区域 Halcon 中的区域 get_domain(Image : Domain : : ) Image : …

SpringBoot之自定义注解参数校验

SpringBoot之自定义注解参数校验 为什么要自定义注解 我这里先引入一个例子&#xff0c;就比如我现在要写文章&#xff0c;文章也许写完正要发布&#xff0c;也可以是还没写完正要存草稿&#xff0c;前端往后端发送数据&#xff0c;如果前端的state不是草稿或者已发布状态&…

大模型应用开发极简入门

简单的归纳一下书的前序部分 目录 LLM&#xff08;Large Language Model&#xff09;的应用技术栈通常包括以下几个方面&#xff1a; 深度学习框架&#xff1a; 数据预处理工具&#xff1a; 训练资源&#xff1a; 模型优化和调参工具&#xff1a; 部署和应用集成&#xf…

智慧图书管理|基于SSM+vue的网上服装商城系统(源码+数据库+文档)

智慧图书管理目录 基于SSMvue的网上服装商城系统 一、前言 二、系统设计 三、系统功能设计 1.1 服装列表 1.2 公告信息管理 1.3 公告类型管理 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1…

linus下Anaconda创建虚拟环境pytorch

一、虚拟环境 1.创建 输入下面命令 conda create -n env_name python3.8 输入y 2.激活环境 输入 conda activate env_name 二、一些常用的命令 在Linux的控制平台 切换到当前的文件夹 cd /根目录/次目录 查看conda目录 conda list 查看pip目录 pip list查看历史命…

Python 爬虫如何配置代理 IP (Py 采集)

在Python中配置代理IP&#xff0c;可以通过设置requests库的proxies参数来实现。以下是一个示例&#xff1a; import requests# 则立可以获取稳定代理Ip&#xff1a;https://www.kuaidaili.com/?refrg3jlsko0ymg # 推荐使用私密动态 IP proxies {"http": "ht…

芒果超媒财报解读:科技加持下,如何蜕变为内容“全科生”?

在降本增效和内容为王的基调下&#xff0c;国内头部长视频平台正在拥抱增长。 爱奇艺率先公布2023年财务数据&#xff0c;实现归母净利润19.25亿元&#xff0c;与2022年亏损1.36亿元相比&#xff0c;扭亏为盈且增幅显著。 而近日&#xff0c;随着新一季《浪姐》播出&#xff…

JAVA面试题分享---多线程与线程池

多线程 什么是线程?线程和进程的区别?&#xff08;了解&#xff09; 线程&#xff1a;是进程的一个实体&#xff0c;是 cpu 调度和分派的基本单位&#xff0c;是比进程更小的 可以独立运行的基本单位。 进程&#xff1a;具有一定独立功能的程序关于某个数据集合上的一次运…

【漏洞复现】IP-guard WebServer 权限绕过漏洞

0x01 产品简介 IP-guard WebServer 是 IP-guard 网络安全管理系统的一部分,用于提供 Web 界面以进行用户权限管理、监控和审计。 0x02 漏洞概述 IP-guard WebServer的权限验证机制中存在设计缺陷,未授权的攻击者能够规避安全验证,通过后端接口执行文件的任意读取和删除操…

DaVinci Resolve Studio 19(达芬奇19调色剪辑)win/mac激活版

DaVinci Resolve Studio是一个结合专业的8k 编辑&#xff0c;颜色混合&#xff0c;视觉效果和音频后期制作的软件。只需点击一下&#xff0c;你就可以立即在编辑、混音、特效和音频流之间切换。此外&#xff0c;达芬奇解决(达芬奇)是一个多用户协作的解决方案&#xff0c;使编辑…

一单利润100+,不起眼的小生意,却能闷声发财!

今天&#xff0c;我想向大家介绍一个看似不太热门&#xff0c;但实际上需求很高的项目——酒店代订。这个项目其实很早以前就已经有人开始尝试了&#xff0c;但可能并没有被大众所熟知。简而言之&#xff0c;酒店代订就是帮助他人通过我们来预订他们想要入住的酒店。 当客户将…

管理能力学习笔记八:Will-Skill矩阵“盘“团队

如何把握带教中的“度”&#xff0c;才能在把事情做好的基础上&#xff0c;又能使员工获得成长呢&#xff1f; 需要做到 合理授权 & 适当辅导 如何做到&#xff1f; 通过使用 意愿-技能矩阵(Will-Skill Matrix) 辨别不同带教方法的适用情形&#xff0c;"盘"…

ElasticSearch总结1

目录 一、ElasticSearch介绍&#xff1a; 举例一&#xff1a; 举例二&#xff1a; 举例三&#xff1a; 二、ELK技术栈 三、Elasticsearch 的基本概念&#xff1a; 四、正向索引和倒排索引&#xff1a; 正向索引&#xff1a; 倒排索引&#xff1a; 五、Mysql和Elastics…

新版本Qt Creator安装配置

新版本Qt Creator安装配置 文章目录 新版本Qt Creator安装配置1、前言2、环境3、安装配置4、总结 更多精彩内容&#x1f449;个人内容分类汇总 &#x1f448;&#x1f449;Qt开发经验 &#x1f448; 1、前言 Qt是一个跨平台的C应用程序开发框架&#xff0c;而Qt Creator是专为Q…

免费的单片机物联网MQTT平台选择

目的是多设备接入中控&#xff0c;平台只做转发。 选择巴法云&#xff1a;巴法科技&巴法云-巴法设备云-巴法物联网云平台 clientId是私钥uid&#xff1a; 多设备 clientId 填同一个 uid 都是可以的。平台应该是加了后缀区分。 支持自定义topic&#xff0c;操作简单&#x…

Spring ai 快速入门及使用,构建你自己的ai

第一步&#xff1a;创建springboot项目 jdk必须是17及以上 1.8用不了 第二步 选择web和ai的依赖 选择openai 第三步 需要配置openai key 配置 分享个免费或的apikey的地方New API 会免费赠送1刀的token spring.application.namespringAI spring.ai.openai.base-urlhttps://ap…