【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

设计并实现超媒体 API

  • 2013-08-06
  • 本文字数:10482 字

    阅读完需:约 34 分钟

本文(这一系列的第二篇文章)的主题是超媒体服务器的实现。本文用到的消息设计和问题领域描述已经在之前的文章中介绍过。在本文中,我们首先会简要介绍超媒体服务器的角色(路由、评估和执行),然后会讨论它的基础实现,包括组件层、展现层和连接层。最后,我们将简要介绍一个超媒体API 的客户端浏览器;特别是常见的Web 浏览器和命令行工具的限制以及“媒体类型浏览器”的优点。

本文详细介绍了使用Node.js 构建一个完整功能的服务器所需的高层细节信息。为了让事情相对简单一些,本文中的示例实现并没有使用其他自定义Node 模块或者框架,甚至连简单的磁盘文件的存储处理都没有使用。因为本文的实现只有骨架和基础实现,所以它并不具有真实的产品级服务器所具有的所有特性和安全,但是你依然能够从中了解实现的主要技术点。同时,虽然这个服务器是使用Node.js 构建的,但是将本文所介绍的思想转换成你喜欢的其他编程语言、框架和/ 或者平台也没有问题。

注意:

可以从 GitHub 上获取这个服务器实现的所有源代码。

超媒体 API 服务器

超媒体 API 服务器在基本 Web 服务器的基础上添加了很多功能。和常见的 Web 服务器一样,超媒体 API 服务器会接收请求、处理请求并返回响应。但是,超媒体 API 服务器还会执行一些其他的工作。它们就像翻译家,按照预定义的消息格式发送每一个请求,将其翻译成服务器组件(存储、数据库和业务逻辑)能够理解的内容进行处理,然后重新翻译回预定义的格式,翻译后的格式中可能包含发送请求的客户端做下一步操作所需的信息。典型的 RPC 类型的 API 服务器并不会包含这些信息。

这些额外的信息可能会包含这个客户端是否能够看到相关的资源,是否能够执行一个搜索操作,是否能够修改存储在服务器上的数据等内容。所有这一切都是基于发送请求的客户端所能理解的媒体类型设计,通过增加超媒体控制(链接和表单)通信的。客户端请求可能会和用户身份绑定到一起;该身份可能会(也可能不会)包含一些额外的特权,同时也会影响下一个请求进行时哪些内容是有效的。对响应做出的修正是上下文驱动的,这也是对超媒体类型的实现所添加的关键价值增值元素之一。

路由

在 Web 环境中,一个 API 服务器的首要任务是接收、解析并路由传入的请求。在设计 Web API 服务器时,通常会将 URI 作为首选方式路由到达服务器的请求。这个过程是这样的,首先将 URI 解析成路径片段和查询字符串等内容,然后使用这些信息将请求的详细内容(包括可能的请求体数据)发送到正确的内部组件进行处理。

例如下面的 HTTP 请求:

复制代码
GET/users/?search=pending
Host: http://www.example.org
Accept: application/vnd.collection+json
Authorization: q1w2e3r4t5=

这个请求的第一部分告诉服务器一个客户端想要执行一个“读”操作,它要搜索 www.example.org 服务器上被挂起的用户。客户端请求的第二部分表明响应的数据应该是 collection+json 格式的(一种已注册的超媒体类型)。请求的最后一部分表示该请求是由一个经过授权的用户发出的,可以通过授权头中的加密值鉴别该用户。

服务器通常会将 URI 分解为不同的部分:

  • users
  • search=pending

然后会生成一个有效的请求发送给对应的内部组件处理:

复制代码
results = Users.Search('pending');

处理结果会被转换成被请求的格式并发送回客户端:

复制代码
http.Response = Representation(results, 'collection+json');

上面的例子使用的是伪代码,但是你能从中体会到基础思路。API 服务器接收、路由并处理请求,然后创建符合格式的结果返回给客户端。

评估和执行

请求处理包括评估请求(不仅仅是 URI,还包括协议的详细内容,例如方法、额外的头以及任何有效负载)和决定为了完成该请求需要执行哪一个内部程序。在之前的示例中,服务器“明确了”/users/?search=pending 表示服务器应当将“pending“查询字符串参数传递给 User 模块的 Search 函数。同时服务器还根据客户端请求中的 Accept 头信息明确了要将响应格式转化成 collection+json 格式。

服务器实际上是外部世界(也就是 HTTP)和自身内部组件(也就是任何源码或者正在使用的本地网络语言)之间沟通的桥梁。服务器的角色是评估请求,将其转换成“组件能懂的语言”,然后将响应转化成合适的格式。这让服务器扮演了外部世界和内部组件之间 “连接器”的角色。

注意:

Roy Fielding 在描述 REST 风格的 Web 架构时引用了这个“组件——连接器”模型。

在很多实现中,组件(内部的)和连接器(外部的)的任务会混合在一起。长期来看,这种关注点的混合会让服务器难以维护和演化。因此,本文所展示的实现模式将会强调组件和连接器之间的区别。另外,你还将看到另一个被识别的责任作为单独的关注点,那就是为响应生成内部数据的展现。

组件层

组件层就是解决领域问题的地方,以前并没有人按照同样的方式做过这些事情。这里的工作通常也与 HTTP 或者 Web 没有关系。例如,读取并将数据写入存储,计算与业务相关的公式,强制执行业务规则等等。这些都是组件的基本活动。

领域特定的独立实现

这一系列的前一篇文章所处理的领域问题是排课系统。该系统管理学生、老师和课程,同时将这三者联合起来形成排课表。这些都是存在于组件层的领域特定细节。为了实现相关功能,我们实现了一个模块(称为 component.js)处理这些工作。同时,为了处理相关的读写操作,我们还使用了一个简单的基于文件的存储模块(称为 storage.js)。

这两个模块(storage.js 和 component.js)的实现中并不会有任何有关于连接器(例如 HTTP、WebSockets 等)细节的内容。我们的例子虽小,但即使在大系统中也应该是组件层包含目标领域(本示例中是课程表领域)特定的细节。该层通常是你的实现中的增值点,确实没有人会以完全相同的方式实现这一部分。

在组件层和系统剩余部分之间实现关注点分离(SoC)还能够提升将来以最低的成本添加新连接器(FTP、SMTP 等)的机会。此外,这样做还能够在不影响组件的情况下实现连接器层(缓存、服务器数量的横向扩展能力等)的优化。

Storage.js

在本示例中,数据存储是以一个简单文件系统的方式实现的。而在产品实现时则很有可能会使用结构化存储实现,例如文档型数据库(MongoDB、CouchDB 等),关系型数据库(MySQL、Oracle、SQL Server 等),或者一些其他的存储系统。甚至有可能会使用 HTTP 连接器通过一个远程的存储模型实现。

下面是一个代码片段,它展示了在我们的示例应用中存储是如何实现的:

复制代码
var fs = require('fs');
var folder = process.cwd()+'/data/';
module.exports = main;
function main(object, action, arg1, arg2) {
var rtn;
switch (action) {
case 'list':
rtn = getList(object);
break;
case 'filter':
rtn = getList(object, arg1);
break;
case 'item':
rtn = getItem(object, arg1);
break;
case 'add':
rtn = addItem(object, arg1);
break;
case 'update':
rtn = updateItem(object, arg1, arg2);
break;
case 'remove':
rtn = removeItem(object, arg1);
break;
default:
rtn = null;
break;
}
return rtn;
}

注意:

可以从 GitHub 上获取这个服务器实现的完整源码。

下面是 addItem 方法的详细内容:

复制代码
function addItem(object, item) {
item.id = makeId();
item.dateCreated = new Date();
fs.writeFileSync(folder+object+'/'+item.id, JSON.stringify(item));
return getItem(object, item.id);
}

下面是一个将一条真实的 student 记录存储到磁盘上的示例:

复制代码
{
studentName: "Mark Bunce",
standing: "sophomore",
id: "121drdhk3xh",
dateCreated: "2013-01-26T01:47:01.057Z"
}

总的来说,JSON 对象会使用由 makeId() 程序创建的唯一名称被存储到磁盘上。接下来我们将回顾 component.js 模块,并将介绍它如何调用存储模块。

Component.js

在这个示例应用中,component.js 模块会处理所有的领域层细节。该模块清楚自己如何与存储沟通,如何转换“Add Student”和“Assign Student to a class”这样的服务请求。在一个较大的系统中,组件层可能会包含一些模块,但是它们依然会做同样的基础类型的工作。

除了存储处理之外,组件层还负责解决方案的业务逻辑处理。在我们的示例中,源代码位于一个单独的模块(component.js),但是在更大更完整的系统中,你很有可能会有多个组件,每一个组件处理业务逻辑的不同方面。

下面是一段实现排课信息处理业务逻辑的高层代码:

复制代码
exports.schedule = function(action, args1, args2) {
var object, rtn;
object = 'schedule';
rtn = null;
switch(action) {
case 'list':
rtn = loadList(storage(object, 'list'), object);
rtn = addEditing(rtn, object, args1);
break;
case 'read':
rtn = loadList(storage(object, 'item', args1), object);
rtn = addEditing(rtn, object, args1);
break;
case 'add':
rtn = loadList(storage(object, 'add', args1), object);
rtn = addEditing(rtn, object, args1);
break;
case 'update':
rtn = loadList(storage(object, 'update', args1, args2), object);
rtn = addEditing(rtn, object, args1);
break;
case 'remove':
rtn = loadList(storage(object, 'remove', args1), object);
rtn = addEditing(rtn, object, args1);
break;
case 'assign':
appendStudent(args1, args2);
rtn = loadList(storage(object, 'item', args1), object);
rtn = addEditing(rtn, object, args1);
break;
case 'unassign':
dropStudent(args1, args2);
rtn = loadList(storage(object, 'item', args1), object);
rtn = addediting(rtn, object, args1);
break;
default:
rtn = null;
}
return rtn;
}

注意:

可以从 GitHub 上获取该服务器实现的完整源码。

下面是 appendStudent 函数的实现:

复制代码
function appendStudent(skid, stid) {
var schedule, student, coll, i, x, flg;
schedule = storage('schedule', 'item', skid);
student = storage('student', 'item', stid);
flg = false;
// make sure there's a collection
if(!schedule.students) {
schedule.students = [];
}
{1}
// see if this student already exists
coll = schedule.students;
for(i=0, x=coll.length; i<x; i++) {
if(coll[i].id===student.id) {
flg = true;
}
}
{1}
// add it if needed
if(flg===false) {
coll.push(student);
schedule.students = coll;
}
{1}
// save results
storage('schedule', 'update', skid, schedule);
}
{1}

最后,下面这段程序能够将来自于存储的一条或者多条记录的列表处理成一个内部对象图。该系统中所有组件级别的程序都能够理解这种格式。

复制代码
function loadList(elm, name) {
var coll, list, data, item, i, x;
if(Array.isArray(elm)===false) {
coll = [];
coll.push(elm);
}
else {
coll = elm;
}
item = [];
data = [];
for(i=0, x=coll.length; i<x; i++) {
for(prop in coll[i]) {
d = {};
d.name = prop;
d.value = coll[i][prop];
d.prompt = prop;
data.push(d);
}
item[i] = {};
item[i].name = name;
item[i].display = {};
item[i].display.data = data;
data = [];
}
list = {};
list.name = name;
list.item = item;
return list;
}

注意,组件层并没有“谈到”HTTP 或者 XML,因为它们是被分开处理的。组件层仅需要能够实现内部的业务需求,并且能够与存储服务(本地的或者远程的)沟通即可。但是,在合适的时候组件层也可能会包含一些链接。在我们的实现中,它们将如何渲染的工作交给了下一个元素:表现层。

表现层

HTTP 是一个与众不同的协议,因为它被设计为能够允许同样的数据按照不同的格式进行展现,称为媒体类型。这些媒体类型意义明确,并且(通常)会在一个标准机构(IANA,互联网数字分配机构)中注册。客户端和服务器都清楚数据和事务详细信息是如何表示的,因此对客户端(例如一个感知 HTML 的 Web 浏览器)而言,它能成功地与新发现的服务进行通信。

聚焦媒体类型

客户端和服务器之间共享的不仅仅是协议语义,还有消息语义。例如,HTML 中的 A、LINK、FORM 和 INPUT 元素都表明了转换细节。在本系列的前一篇文章中,我们设计了一个自定义的超媒体类型(application/TK)。在那个设计中有 LINK、ACTION 和 DATA 元素需要转换。展现层的责任就是将来自于内部存储的信息和私有组件层的操作转换成公共展现,客户端和服务器都理解的展现。

这需要聚焦于消息本身——在客户端和服务器之间使用媒体类型作为主要“共识”是超媒体系统的重要特性之一。借助于媒体类型,客户端和服务器之间进行交流时不需要知道编程语言(Ruby、Python、PHP、Node 等),编码风格(面向对象的、函数式的、过程式的),甚至不需要知道各部分使用的操作系统。

领域特定信息的转换

展现层的工作非常重要。它们接收来自于公共连接器的请求(相关内容将会在下一部分介绍),然后将其分配给私有组件层,它们是这两个世界之间的翻译官。

Representation.js

在这个示例实现中,我们在一个单独的模块中封装了展现层,称为 representation.js。该模块能够“讲”application/TK。

下面是一段高层代码,该代码会“处理”由组件层提供的内部对象模型,将其转换成公共排课超媒体类型。

复制代码
function processCSDoc(object) {
var doc, i, x, tmp, coll;
doc += '<root>';
// handle action element
if(object && object.action) {
doc += actionElement(object.action);
}
// handle lists
if(object && object.list) {
for(i=0,x=object.list.length; i<x;i++) {
doc += listElement(object.list[i]);
}
}
// handle error
if(object && object.error) {
doc += '<error>';
coll = object.error.data;
for(i=0, x=coll.length; i<x; i++) {
doc += dataElement(coll[i]);
}
doc += '</error>';
}
doc += '</root>';
return doc;
}

注意:

可以从 GitHub 上获取该服务器实现的完整源码。

该程序“知道”object 参数所表示的对象的数据结构,它还“清楚”一个有效排课消息的结构。这段程序会将一个私有的对象图转换成一个公有的超媒体消息。

下面是 dataElement 程序;该程序会将私有图中的所有数据点转换成为消息中的有效数据元素。

复制代码
function dataElement(data) {
var rtn;
rtn = '<data ';
if(data.name) {
rtn += 'name="'+data.name+'" ';
}
if(data.prompt) {
rtn += 'prompt="'+data.prompt+'" ';
}
if(data.value) {
rtn += 'value="'+data.value+'" ';
}
if(data.embed) {
rtn += 'embed="'+data.embed+"' ";
}
rtn += '/>';
return rtn;
}

最后,下面是一个遵循如下超媒体格式

复制代码
application/vnd.apiacademy-scheduling+xml

的内部对象图的示例。

复制代码
// internal object graph
{
"action":
{
"link": [
{
"name":"home",
"href":"http://localhost:1337/",
"action":"read",
"prompt":"Home"
},
{
"name":"student",
"href":"http://localhost:1337/student/",
"action":"list",
"prompt":"Students"
},
{
"name":"teacher",
"href":"http://localhost:1337/teacher/",
"action":"list",
"prompt":"Teachers"
},
{
"name":"course",
"href":"http://localhost:1337/course/",
"action":"list",
"prompt":"Courses"
},
{
"name":"schedule",
"href":"http://localhost:1337/schedule/",
"action":"list",
"prompt":"Schedules"
}
]
}
}
}
// public hypermedia message
<root>
<actions>
<link name="home"
href="http://localhost:1337/"
action="read"
prompt="Home" />
<link name="student"
href="http://localhost:1337/student/"
action="list"
prompt="Students" />
<link name="teacher"
href="http://localhost:1337/teacher/"
action="list"
prompt="Teachers" />
<link name="course"
href="http://localhost:1337/course/"
action="list"
prompt="Courses" />
<link name="schedule"
href="http://localhost:1337/schedule/"
action="list"
prompt="Schedules" />
</actions>
</root>

你可能已经注意到了,在示例中内部数据和公共数据的结构非常相似。虽然私有对象图和公共媒体类型之间的转换并不是必须的,但这样做有时确实能够让事情变得容易。无论如何,这并不是一个通用的案例;特别是在支持多种公共消息格式的系统中。此处这样做的目的是为了简化转换过程,在比较时更加容易查看和分析。

现在,展现层已经就位了,最后一步便是实现连接器层,将进入的协议请求(本例中是 HTTP)转换成组件能够理解的内容,然后将展现层的工作结果返回给调用者。

连接器层

连接器层是暴露在公网上的层。它在与外界通信时使用 HTTP 和 DNS 等协议或系统,连接器作为网关,请求流和响应返回流都需要经过它。Web 服务器引擎(Apache、IIS、Nginx 等)是最著名的连接器类型。它们中的大部分所具有的功能不仅仅是一味地接收请求并返回响应。它们还支持一些级别的路由和脚本。这使得用户能够编写代码检查进入的请求,将其传送给合适的组件处理,并在组件处理完成后提供合适的响应。

协议层交互

连接器关注于协议层的交互。一个 HTTP 连接器能够理解 HTTP 协议的细节信息,能够让外部程序检查并操作这些信息。连接器的工作是检查进入请求的 URL,验证请求头信息从而决定响应所应采用的格式,并将请求(以及所有参数)路由给合适的组件进行处理。

对于这一系列文章而言,连接器是 Node.js。使用 Node.js 启动一个 HTTP 连接器是非常简单的,同时使用它提供路由,操作 HTTP 消息也是非常容易的。

内部世界和外部世界之间的中介

因为面对外部世界的是连接器,所以连接器的脚本处理意味着决定要接收哪些请求,哪些 URL 是有效的,每一个请求执行后要返回什么。这也是将问题领域(排课系统)中的内部操作映射到外部的 HTTP 限制。相关的大部分内容已经在该系列之前的文章中介绍过。媒体类型文档的协议映射部分将领域操作映射到 HTTP 方法。问题域文档设置了 HTTP 请求产生时应该提供哪些数据元素。这些材料提供了为我们的服务器实现连接器脚本所需的基础知识。

App.js

在本示例中,连接器代码存在于 app.js 模块中。这是接收 HTTP 请求并产生 HTTP 响应的地方。为了容易阅读,该示例并没有安装使用外部模块或框架。这也导致了有些代码有点“啰嗦”,但是这也意味着没有“隐藏”的特性,要理解这些示例你并不需要了解太多的 Node 外部模块。

该模块使用简单的正则表达式识别请求路由规则:

复制代码
// routing rules
var reHome = new RegExp('^\/$','i');
var reCourse = new RegExp('^\/course\/.*','i');
var reSchedule = new RegExp('^\/schedule\/.*','i');
var reStudent = new RegExp('^\/student\/.*','i');
var reTeacher = new RegExp('^\/teacher\/.*','i');

下面的代码使用了一个上面的规则评估进入的请求,并将请求路由到合适的连接器组件。

复制代码
// schedule
if(flg===false && reSchedule.test(req.url)) {
flg = true;
doc = schedule(req, res, parts, root)
}

注意: 可以从 GitHub 上获取该服务器实现的完整源码。

每一个路由表达式在 app.js 模块中都有相似的代码。

下面是排课连接器内的一段代码,它检查请求的详细信息,将其转换成一个内部的表示,然后传递给合适的组件。

复制代码
function schedule(req, res, parts, base) {
var code, doc;
root = base;
switch(req.method) {
case 'GET':
if(parts[1]) {
doc = {code:200, doc:sendItem(req, res, parts[1])};
}
else {
doc = {code:200, doc:sendList(req, res)};
}
break;
case 'POST':
if(parts[1]) {
doc = errorDoc(req, res, 'Method Not Allowed', 405);
}
else {
switch(parts[0].toLowerCase()) {
case 'assign' :
doc = {code:200, doc:assignStudent(req, res)};
break;
case 'unassign' :
doc = {code:200, doc:dropStudent(req, res)};
break;
case 'schedule' :
doc = {code:200, doc:addItem(req, res)};
break;
default :
doc = errorDoc(req, res, 'Method Not Allowed', 405);
break;
}
}
break;
case 'PUT':
if(parts[1]) {
doc = {code:200,doc:updateItem(req, res, parts[1])};
}
else {
doc = utils.errorDoc(req, res, 'Method Not Allowed',405);
}
break;
case 'DELETE':
if(parts[1]) {
doc = {code:204,doc:removeItem(req, res, parts[1])};
}
else {
doc = utils.errorDoc(req, res, 'Method Not Allowed', 405);
}
default:
doc = utils.errorDoc(req, res, 'Method Not Allowed', 405);
}
return doc;
}

你能够看到,连接器检查 URL,检查 HTTP 方法,然后将工作转交给一个能够处理传入请求的本地程序,再将结果传入组件层。

下面是一段连接器代码,它调用组件模块将一个学生分配到一个现有的班级:

复制代码
function assignStudent(req, res) {
var body, doc, msg, item;
body = '';
req.on('data', function(chunk) {
body += chunk;
});
req.on('end', function() {
try {
msg = qs.parse(body);
item = component.schedule('assign', msg.scheduleId, msg.studentId);
doc = sendItem(req, res, msg.scheduleId);
}
catch(ex) {
doc = utils.errorDoc(req, res, 'Server Error', 500);
}
});
return doc;
}

最后,一旦组件完成了相关的工作便会为调用者返回一个内部的图对象,该对象必须以公共排课超媒体类型表示。

下面的代码同样来自于 app.js,用于调用表示层并将结果通过 HTTP 返回给调用者:

复制代码
// send out response
if(doc!==null) {
rtn = representation(doc.doc);
sendResponse(req, res, rtn, doc.code, doc.headers);
}
else {
sendResponse(req, res, '<root></root>', 500);
}

这就是连接器的全部代码。连接器层路由并解析请求,将其传递给合适的组件,在内部响应返回时把响应传递给展现层,最后再返回给调用者。

浏览 API

在服务器启动并运行之后,你会想浏览这些 API,验证各种操作,做一点探索。对于典型的 Web 应用程序而言,这可以通过一个常见的 Web 浏览器实现。之所以可以这样做是因为几乎所有的 Web 应用程序都将自己限定为一种单一的超媒体类型(HTML)和一些其他的标准媒体格式(CSS、JavaScript、二进制图像等)。

常见的 Web 浏览器是令人难以置信的精密应用。通过紧密遵循一些标准,浏览器能够成功地与任何遵循同样标准的 Web 服务器连接和交互。这使得发现和互操作能够同时工作。

常见 Web 浏览器的限制

如果服务器使用了不常见的注册媒体类型(例如 Atom、HAL、Collection+JSON、Siren 等),那么不能保证常见的 Web 浏览器能够理解它们并与服务器成功地交互。浏览器之间不会共享它们对消息中出现的超媒体控制(转换)的理解。浏览器可能“不知道”不同的转换(GET、POST、PUT、DELETE 等)应该采用哪个 HTTP 方法。同时浏览器可能也不清楚哪种数据元素应该在本地呈现(例如图片),哪种元素应该当作导航(例如链接)。

对于我们这个仅为本系列文章而创建的基于 XML 的超媒体类型而言,浏览器实际上能够部分理解我们的意图。因为采用的是 xml 格式,所有常见的浏览器都能清晰地显示响应。甚至有些 Web 浏览器插件不仅能够很好地呈现 XML,还能够解析它们,并且允许用户单击链接导航 API。下面是一个 Google Chrome 浏览器的截图,在查看来自于排课服务器的响应时它就加载并运行了这样一个插件。

(单击放大图片)

这个插件非常有用,因为我们能够通过单击响应中的链接从一个状态移动到下一个状态。但是,这并不能执行支持数据参数的转换。简单点说,就是插件不清楚如何识别和处理我们自定义媒体类型中的

2013-08-06 11:125289
用户头像

发布了 321 篇内容, 共 115.9 次阅读, 收获喜欢 18 次。

关注

评论

发布
暂无评论
发现更多内容

Java 多线程:并发编程的三大特性

Java快了!

Java多线程

担心今年的金九银十收不到满意的offer?这份18位阿里架构师耗时60天整合的面试总结太香了!

收到请回复

Java 云计算 开源 架构 编程语言

Plato Labs推出的SeedX,公测15天570万美金净利润

EOSdreamer111

NtyCo纯C协程的原理分析

C++后台开发

后台开发 协程 后端开发 异步IO C++开发

【蓝桥杯Web】2022年第十三届蓝桥杯Web大学组省赛真题解析(完整版)

海底烧烤店ai

算法 前端 JavaScrip 9月月更

Github最新霸榜!号称架构师修炼之路的“葵花宝典”限时开源

了不起的程序猿

阿里巴巴 编程 程序员 架构 架构师

超详细:这份全网首发的Kafka技术手册,从基础到实战一应俱全!

收到请回复

Java 云计算 开源 架构 编程语言

Cryptocell-712安全引擎概述

Java-fenn

Java

继GitHub的Copilot收费后,亚马逊推出了 CodeWhisperer,感觉不错哟!

Python猫

Python

【Vue3】 评论列表(简易)-- 思路与实现分析

Sam9029

JavaScript Vue Vue3 9月月更

DPDK源码分析之DPDK基础概览

于顾而言

DPDK DPDK开发

Java程序员:为了跳槽刷完1000道真题,没想到老板直接给我升职了!

收到请回复

Java 云计算 开源 架构 编程语言

Sentinel哨兵机制

急需上岸的小谢

9月月更

只要32天就能拿下Offer?框架+性能优化+微服务+分布式,Java程序员必备!

收到请回复

Java 云计算 开源 架构 编程语言

MyBatis-Plus(三、增删改查)

MySQL MyBatisPlus 9月月更

Elasticsearch聚合学习之四:结果排序

程序员欣宸

elasticsearch 9月月更

户外LED广告屏如何才能保养好?

Dylan

LED显示屏 led显示屏厂家

MFC与Qt多个控件响应统一响应消息处理

中国好公民st

c++ qt 9月月更

阿里前端常见面试题总结

loveX001

JavaScript 前端

Plato Labs推出的SeedX,公测15天570万美金净利润

鳄鱼视界

手把手教你如何使用 Timestream 实现物联网时序数据存储和分析

亚马逊云科技 (Amazon Web Services)

数据分析 物联网 数据存储

2022年第十三届蓝桥杯Web国赛真题解析

海底烧烤店ai

前端 JavaScrip 9月月更

5 个 Promise 要避免的常见用法~

掘金安东尼

前端 9月月更

Redis命令传播的心跳检测

急需上岸的小谢

9月月更

mysql实数类型和字符串类型

急需上岸的小谢

9月月更

白天建筑师,晚上CG艺术家,他将建筑的华丽发挥极致

Renderbus瑞云渲染农场

云渲染 云渲染农场 渲染农场

羊了个羊”通关修改思路

Java-fenn

Java

跟着卷卷龙一起学Camera--CameraService

卷卷龙

ISP 9月月更

SpringBoot数据库管理 - 用Liquibase对数据库管理和迁移?

Java快了!

数据库 spring-boot

C++学习---cstdio的源码学习分析02-文件删除函数remove

桑榆

c++ 源码阅读 9月月更

【蓝桥杯Web】2022年第十三届蓝桥杯Web大学组省赛真题解析(精华版)

海底烧烤店ai

算法 前端 JavaScrip 9月月更

设计并实现超媒体API_REST_Mike Amundsen_InfoQ精选文章