绝对勇士
【HTTP协议基础知识分享1】HTTP“请求-响应”模式及常见的HTTP方法
一、 HTTP简介HTTP的定义和作用HTTP(Hypertext Transfer Protocol)是一种用于传输超文本的应用层协议。它是在Web上进行数据通信的基础,允许将超文本文档从Web服务器传输到本地浏览器。HTTP是一个无状态的协议,意味着每个请求都是独立的,服务器不会保留关于之前请求的任何信息。HTTP协议的作用包括但不限于:•在客户端和服务器之间传输网页、图片、视频等资源•支持各种不同类型的内容,例如HTML、CSS、JavaScript等•实现网页的超链接功能,使得用户可以通过点击链接访问其他页面•支持Web表单,用于向服务器提交数据HTTP的发展历史HTTP协议最初由蒂姆·伯纳斯-李(Tim Berners-Lee)在1989年创建,是为了支持他设计的全球信息系统(World Wide Web)而开发的。最早的版本是0.9,之后发展到1.0和1.1版本。HTTP/1.1是目前使用最广泛的版本,它引入了许多新特性,例如持久连接、管道化、虚拟主机等,以提高性能和减少延迟。在今天的互联网世界中,HTTP已经成为万维网的基础协议之一,为人们浏览网页、传输数据提供了重要支持。随着互联网的不断发展,HTTP协议也在不断演进,以满足更加复杂和多样化的网络应用需求。二、 HTTP请求和响应请求-响应模式HTTP协议采用了请求-响应模式,客户端向服务器发送一个HTTP请求,服务器接收并处理该请求后,返回一个HTTP响应。这种模式是HTTP通信的基础,通过它实现了客户端和服务器之间的数据交换。请求和响应的组成结构请求结构•请求行:包括请求方法(GET、POST等)、请求的URL和协议版本•头部:包含了一些关于客户端和请求的信息,比如User-Agent(用户代理)、Host(主机)、Accept(可接受的媒体类型)等•请求体:可选的部分,包含了客户端传输给服务器的数据,比如表单数据或上传的文件响应结构•状态行:包括协议版本、状态码和相应状态消息•头部:包含了一些关于服务器和响应的信息,比如Server(服务器软件信息)、Content-Type(内容类型)、Content-Length(内容长度)等•消息体:包含了服务器返回给客户端的数据,比如HTML页面、图片、视频等让我们概括这种模式就是HTTP请求和响应的结构都很清晰明了,这个模式里面每个部分都有特定的作用,正是因为这种结构化的设计使得HTTP协议具有较好的灵活性和扩展性,能够很好地适应不同类型的内容和多样化的网络应用需求。三、 HTTP方法常见的HTTP方法HTTP定义了多种不同的请求方法,其中常见的包括:•GET:从服务器获取资源,通常用于请求页面或图片等静态内容•POST:向服务器提交数据,通常用于提交表单或上传文件•PUT:将资源上传到指定URI,如果已存在则进行更新,如果不存在则创建•DELETE:删除指定URI的资源•HEAD:与GET类似,但只返回响应头部,不包含实际内容•OPTIONS:用于获取目标资源支持的通信选项•PATCH:对资源进行部分修改HTTP方法的作用不同的HTTP方法具有不同的作用,它们使得HTTP协议能够支持各种不同类型的操作和数据交互。其中,GET用于获取资源,POST用于提交数据,PUT用于更新资源,DELETE用于删除资源,而HEAD则常用于获取资源的元信息而不获取实际内容。这些方法的存在丰富了HTTP协议的功能,使得它可以满足各种不同场景下的需求。RESTful架构中的HTTP方法在RESTful架构中,HTTP方法被赋予了更加具体的语义,比如GET用于获取资源,POST用于新建资源,PUT用于更新资源,DELETE用于删除资源。这种基于HTTP方法的RESTful设计风格使得API的设计更加直观和符合标准化,也更容易被理解和使用。综上所述,HTTP方法是HTTP协议的重要组成部分,不同方法的存在丰富了协议的功能,并且在RESTful架构中扮演着关键的角色,帮助实现了资源的操作和管理。总结到这里我们今天关于HTTP协议基础部分知识分享就结束啦~在下一节我会为大家继续介绍另一部分:HTTP的状态码以及URL基础知识点
绝对勇士
【HTTP协议基础知识分享4】深入解析HTTP中的Cookie、Session和缓存基础知识
引言在Web开发和网络通信中,HTTP协议是一种基础而重要的通信协议。在上一章节我们讲了HTTP的连接管理和安全认证。为了实现用户状态的维护、安全性的保障以及性能的优化,开发者们引入了Cookie、Session和缓存等概念。在今天这篇文章中我将和大家一同深入探讨这些概念的基础知识,帮助我们能更好地理解它们在Web开发中的作用。一、Cookie1. 什么是Cookie?Cookie是由服务器发送到用户浏览器并保存在本地的小型文本文件,用于存储特定网站的用户信息。每次用户访问该网站时,浏览器都会将相应的Cookie信息发送给服务器,以便实现用户状态的跟踪和维护。2. Cookie的结构一个Cookie通常包含以下信息:· 名称(Name): 用于标识Cookie的唯一性。· 值(Value): 包含与名称相关联的信息。· 过期时间(Expiration Time): 指定Cookie的有效期限,可以是会话级别或固定的日期时间。· 域(Domain): 指定Cookie可见的域名。· 路径(Path): 指定Cookie的可见路径。· 安全标志(Secure Flag): 表示Cookie只能通过HTTPS连接传输。· HttpOnly标志: 防止通过JavaScript访问Cookie,提高安全性。3. Cookie的工作流程· 服务器端创建Cookie: 当用户访问网站时,服务器在响应头中添加Set-Cookie字段,告诉浏览器需要存储的Cookie信息。· 浏览器保存Cookie: 浏览器接收到Cookie信息后,将其保存在本地。· 后续请求发送Cookie: 用户再次访问网站时,浏览器会在请求头中附上之前保存的Cookie信息,发送给服务器。· 服务器读取Cookie: 服务器收到请求后,可以读取Cookie信息,实现用户状态的跟踪和维护。4. Cookie的应用场景· 用户认证: 记录用户登录状态,实现持久登录。· 购物车管理: 存储用户选购商品的信息。· 个性化设置: 记录用户的个性化偏好。二、Session1. 什么是Session?Session是一种在服务器端存储用户状态的机制。与Cookie不同,Session数据保存在服务器上,而仅有一个用于标识用户的Session ID 存储在Cookie中。2. Session的工作流程· 生成Session ID: 用户第一次访问服务器时,服务器会生成一个唯一的Session ID,并将其存储在Cookie中,返回给浏览器。· 保存Session数据: 服务器将用户的状态信息存储在与Session ID 相关联的数据存储中,比如内存、数据库等。· 后续请求使用Session ID: 用户每次请求时,浏览器都会将之前存储的Session ID 发送给服务器。· 服务器读取Session数据: 服务器通过Session ID 取出相应的用户状态信息,实现状态的跟踪和维护。3. Session的优势· 安全性: 用户状态信息存储在服务器端,相较于存储在客户端的Cookie更加安全。· 存储容量: 服务器端的存储容量比客户端更大,适合存储大量数据。· 隐私保护: 实际上,Cookie中只包含了一个标识符,用户的隐私得到更好的保护。4. Session的应用场景· 用户登录状态管理: 存储用户登录后的相关信息。· 购物车管理: 存储用户选购商品的信息。· 权限控制: 存储用户的权限信息。三、缓存1. 什么是缓存?缓存是一种将计算结果或数据存储起来,以便在后续请求中能够更快地获取结果的技术。在HTTP中,缓存可以存在于客户端和服务器之间,通过各种机制实现。2. 缓存的工作原理· 客户端缓存: 浏览器在首次请求资源时,服务器返回的响应头中可能包含缓存控制信息,如Cache-Control、Expires等。浏览器根据这些信息决定是否缓存资源。· 服务器端缓存: 服务器可以通过设置响应头中的缓存控制信息,告知客户端该资源在一段时间内可被缓存。3. 缓存的优势· 降低网络负载: 缓存可以减少重复传输相同的资源,降低网络流量。· 提升性能: 缓存可以减少资源加载时间,提升网页加载速度。· 节省带宽: 通过减少网络请求,可以节省带宽成本。4. 缓存的应用场景· 静态资源缓存: 如图片、样式表、脚本文件等。· 页面缓存: 缓存整个页面的HTML内容。· API响应缓存: 对于不经常变化的API响应,可以进行缓存以提高访问速度。结论深入理解HTTP中的Cookie、Session和缓存对Web开发至关重要。Cookie用于客户端存储用户信息,广泛应用于认证和购物车管理;Session在服务器端安全存储用户状态。缓存通过存储数据提高网页加载速度,降低网络负载。这三者协同工作,例如,Cookie记录用户身份,而Session存储具体状态信息。在应用时,开发者需注意安全性和隐私保护,合理设置Cookie标志和使用加密手段。总体而言,深入理解HTTP中的Cookie、Session和缓存是Web开发中不可或缺的基础知识。通过合理运用这些机制,开发者能够更好地平衡用户体验和系统性能,为用户提供高效、安全的网络服务。
绝对勇士
【HTTP协议基础知识分享3】HTTP“连接管理”及HTTP的"安全性和认证"
序言在Web应用程序开发中,HTTP连接管理是至关重要的一环,它直接影响着网络通信的效率和性能。通过持久连接、管道化连接、连接复用等技术手段的应用,以及与安全性的结合,HTTP连接管理不断得到优化和改进,为用户带来更快、更安全的网络体验。在网络通信中,安全性和认证也是至关重要的方面。在今天这篇文章我们将一起深入HTTP连接管理和HTTP安全性认证,让大家更全面地理解这些关键概念。HTTP连接管理连接预热(Connection Prefetching)连接预热是一种优化技术,它通过在客户端请求之前就建立TCP连接,将连接的握手过程隐藏在后台,以减少请求响应时的延迟。这样可以在实际需要使用连接时,已经建立好了TCP连接,从而加速数据传输,提高用户体验。连接池(Connection Pooling)连接池是一种常见的数据库连接管理技术,但在HTTP连接管理中同样非常有用。连接池维护着已经建立的TCP连接,当需要发送HTTP请求时,可以从连接池中获取可用的连接,而不需要重新进行TCP的三次握手,从而减少了连接建立的开销,提高了性能。HTTP/2的新特性HTTP/2引入了多路复用(Multiplexing)的机制,允许在单个TCP连接上同时进行多个请求和响应的交互,避免了HTTP/1.x中串行发送请求和等待响应的低效率问题。此外,HTTP/2还引入了头部压缩(Header Compression)等技术,减少了数据传输时的开销,进一步提高了网络通信的效率。安全性考虑:HTTPS协议随着网络安全意识的提高,越来越多的网站开始采用HTTPS协议来保障通信的机密性和完整性。HTTPS通过在HTTP和TCP之间加入一层加密传输层,确保了通信的安全性,但同时也带来了一些额外的握手开销和复杂性。然而,为了保护用户的隐私和数据安全,采用HTTPS协议仍然是非常值得的。通过对HTTP连接管理的优化,我们可以更好地利用这些特性,为Web应用程序提供更快、更安全的网络体验。连接预热、连接池、HTTP/2的新特性以及HTTPS协议的使用,都是优化HTTP连接管理的有效手段,帮助我们提升网络通信的效率和性能,为用户带来更好的上网体验。HTTP安全和认证安全性和认证1. 加密通信HTTPS通过SSL/TLS协议对HTTP通信进行加密,确保数据在传输过程中不被窃取或篡改,提高了通信的安全性。加密通信在今天的互联网通信中扮演着至关重要的角色。2. 认证机制基于密码的认证是最常见的认证机制,用户需要提供用户名和密码来验证身份。另外,还有诸如数字证书、令牌、双因素认证等更强大的认证方式,以提高系统的安全性。3. 访问控制通过访问控制列表(ACL)、角色基础访问控制(RBAC)等方式,系统管理员可以限制用户对资源的访问权限,保护敏感数据不被未授权的用户获取。总结总的来说就是,HTTP连接管理和安全性与认证是构建安全、高效网络通信的重要组成部分。了解这些概念有助于我们更好地设计和管理网络系统,确保数据的安全传输和系统的稳定运行。到这里我们今天关于HTTP协议基础部分知识分享就结束啦~在下一节我会为大家继续介绍另一部分:HTTP的Cookie和Session以及缓存基础知识点
绝对勇士
从 xhr 到$.ajax、fetch 轻松开启异步通信的新境界
引言在前端Web开发里,AJAX(异步JavaScript和XML)就像一支法宝,让你的网页可以在不刷新的情况下与服务器悄悄聊天,无需重新刷新页面,也可以实现网页与服务器之间进行无缝通信。而这三招:XMLHttpRequest、jQuery中的$.ajax,以及现代的Fetch API,它们就像是三种神奇的武功,各自有各自的绝学。今天让我们探讨这三种不同的AJAX实现方式,一起来领略AJAX的魅力!一、XMLHttpRequestXMLHttpRequest对象是AJAX的基础,自其诞生以来就一直是不可或缺的组成部分。它就像是AJAX的老祖宗,像是用一把古老的键盘敲出来的代码,悠久而又经典。const btn = document.getElementById('btn');
btn.addEventListener('click', () => {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/api/data', true);
xhr.onreadystatechange = () => {
if (xhr.readyState == 4 && xhr.status == 200) {
let result = JSON.parse(xhr.responseText);
console.log(result);
}
};
xhr.send();
});我们上面这段代码的功能简单来说就是通过XHR(XMLHttpRequest)方法实现向后端提供的URL拿取数据,当我们点击按钮时,就会发出请求实现AJAX从服务器异步获取歌曲信息并更新页面内容。让我们来对这些代码解释一下:解释:1.按钮触发事件: 通过JavaScript获取按钮元素,添加点击事件监听器。2.AJAX请求配置: 当我们用老式的XMLHttpRequest对象配置GET请求,指定服务器URL为 http://192.168.31.45:3000/top/song?type=7 ,感觉就像是在用古董手机进行日常通讯。3.异步处理: 设置回调函数,onreadystatechange监听XMLHttpRequest对象状态的变化,就像是你此刻守在手机旁等待消息的回复,确保在请求完成且成功时进行处理。状态码4表示通信完毕,200是服务器说“好的,拿走不谢”。4.处理服务器响应: 最后解析从服务器返回的JSON数据,然后遍历歌曲信息,创建列表项并将获取到的信息内容添加到页面中。注意: 由于可能涉及跨域请求,所以我们需要确保服务器正确配置CORS,以允许跨域资源的访问。二、jQuery的$.ajax虽然我们上面用XHR完成了要求,但是这些代码写起来也太痛苦了。欸,这个时候我们就需要提到jQuery的的$.ajax了,它就像是轻量化的XHR,大大改善了代码的可读性。const btn = document.getElementById('btn');
btn.addEventListener('click', () => {
$.ajax({
url: 'http://example.com/api/data',
method: 'GET',
dataType: 'json',
success: function (res) {
console.log(res);
},
error: function (xhr, status, error) {
console.error('Error:', xhr, status, error);
}
});
});解释: 我们使用$.ajax时,需要配置相应的参数:url: 设置请求的URL,这里我们要访问的URL是example.com/api/datamethod: 指定HTTP请求方法,GET ——表示发送一个获取数据的请求。dataType: 指定期望从服务器接收的数据类型,我们这里是json,希望服务器返回JSON格式的数据。success: 定义请求成功时的回调函数。当请求成功完成时,服务器的响应将传递给这个函数,并且我们在函数内部可以处理这个响应。error: 定义请求失败时的回调函数。如果请求无法完成,或者服务器返回一个错误状态码,这个函数将被调用,你可以在函数内部处理错误信息。我们可以看到当我们使用了jQuery库大大简化了AJAX请求的过程,但是在此之前不要忘记引入我们jQuery库哦~三、Fetch API讲了两种以实现AJAX的方式,那么现代的Fetch API就像是AJAX的新晋小生,简单直接,不需要太多花里胡哨,请看下列代码。const btn = document.getElementById('btn');
btn.addEventListener('click', () => {
fetch('http://example.com/api/data')
.then(data => data.json())
.then(res => {
console.log(res);
})
.catch(error => {
console.error('Error:', error);
});
});解释:在fetch api这个方法里面我们同样需要配置参数,但不多,简单粗暴。fetch(url): 使用Fetch API发起GET请求到指定的URL。 Fetch API就是一群新潮的年轻人,你不是异步吗,那我就用Promise搞定一切,拒绝冗杂和花里胡哨,像是在拍独立电影一样。.then(data => data.json()): 使用.then()处理Promise,将响应体解析为JSON格式。Fetch API返回的是一个Promise,其中data是表示响应的Response对象。.then(res => { console.log(res); }): 处理JSON解析后的数据。在这个例子中,将解析后的数据打印到控制台。这些.then()的链式调用就像是一场流畅的对白,如果期间出现了小插曲,不行就.catch()。.catch(error => { console.error('Error:', error); }): 处理任何可能的错误。如果请求失败或JSON解析出错,将捕获到的错误打印到控制台。看完这段代码,不知你是否有种赏心悦目,神清气爽地感觉。相较于传统的XHR方式,我们通过使用现代的Promise和Fetch API,更换成了更简洁和方便的方式来处理异步网络请求。结语掌握这三招AJAX神功,你就像是一位不羁的武林少年,可以在网页的江湖里畅快自如地舞动。不同的招式适合不同的场景,所以,别拘泥于一招,放开心扉,开始你的AJAX江湖之旅吧!少年,愿你的代码逍遥自在,BUG永远躲不过你的剑!
绝对勇士
【HTTP协议基础知识分享2】HTTP“状态码”及"URL"的组成和作用
一、 HTTP状态码状态码的作用在HTTP协议中,状态码用于表示服务器对请求的处理结果。它们提供了一种标准化的方式来指示请求的成功、失败以及其他各种情况,使得客户端能够根据状态码来采取适当的行动。常见的状态码类别HTTP状态码分为五个类别,每个类别用一个数字的第一个数字表示:•1xx:信息性状态码,表示服务器正在处理请求,请求进行中•2xx:成功状态码,表示请求被成功接收、理解和处理•3xx:重定向状态码,跳转状态,表示需要进一步的操作以完成请求•4xx:客户端错误状态码,表示请求包含错误或无法完成•5xx:服务器错误状态码,表示服务器在处理请求时发生了错误常见的状态码•200 OK:表示请求成功,服务器已成功处理了请求•301 Moved Permanently:永久重定向,请求的资源已被永久移动到新位置•400 Bad Request:客户端发送的请求有错误,服务器无法处理•404 Not Found:所请求的资源不存在•500 Internal Server Error:服务器遇到了意料不到的情况,导致无法完成请求 比如你的代码中可能写错了等等RESTful架构中的状态码在RESTful架构中,状态码被赋予了更具体的语义,比如200 OK表示成功,201 Created表示资源创建成功,404 Not Found表示资源未找到,500 Internal Server Error表示服务器内部错误。这种明确的状态码语义有助于客户端能够更好地理解请求的结果,并且更好地采取后续的操作。二、 URL(Uniform Resource Locator)当我们在浏览器中输入网址或者点击链接时,实际上是在使用URL(Uniform Resource Locator)来指定要访问的资源。下面我会为大家展开关于URL的详细介绍:URL的组成部分:协议(Protocol):URL的开头部分通常包含协议名称,例如“http://”、“https://”等。主机名(Host Name):指定了资源所在的主机(服务器)的域名或IP地址。端口号(Port Number):可选部分,用于指定服务器上接收请求的端口号。如果未指定,默认使用协议的默认端口(如80或443)。路径(Path):指定了服务器上资源的位置,表示资源在服务器文件系统中的路径。查询参数(Query Parameters):可选部分,用于向服务器传递额外的参数,通常以键值对的形式出现,例如“?key1=value1&key2=value2”。锚点(Fragment):可选部分,用于指定资源内的特定位置(如页面内的锚点)。URL的编码和解码:•由于URL中允许包含一些特殊字符(如空格、斜杠、问号等),为了确保这些字符能够被正确解析,需要进行URL编码。编码后的URL使用百分号编码(%XX)表示特殊字符。•浏览器在发送请求时会自动对URL中的特殊字符进行编码,服务器在接收到请求后会对编码后的URL进行解码,还原成原始的字符。URL的作用:•定位资源:URL主要用于定位互联网上的资源,可以是网页、图片、视频、API等各种类型的数据。•传递参数:通过查询参数,可以向服务器传递各种参数,例如搜索关键字、用户身份信息、分页信息等。URL的安全性:•在设计Web应用程序时,需要注意对URL进行合理的设计,避免泄露敏感信息,防止跨站脚本(XSS)攻击和其他安全问题。总之,URL是Web中非常重要的概念,它定义了我们如何访问和定位互联网上的各种资源,同时也承载着在客户端和服务器之间传递信息的功能。深入理解URL的结构和作用有助于我们更好地理解Web应用程序的工作原理和设计思路。总结到这里我们今天关于HTTP协议基础部分知识分享就结束啦~在下一节我会为大家继续介绍另一部分:HTTP连接管理和HTTP安全性认证
绝对勇士
HTTP文本协议——从入门到熟练
为什么你的女神回消息那么慢?搞懂HTTP请求-响应模式,还怕已读不回?
绝对勇士
Reactor 第三篇 flatMap vs map
1 作用不同1.2 映射?展平?map 只执行映射flatMap 既执行映射,也执行展平什么叫只能执行映射?我理解是把一个数据执行一个方法,转换成另外一个数据。举个例子:mapper 函数把输入的字符串转换成大写。map()方法执行这个 mapper 函数。Function<String, String > mapper = String::toUpperCase;
Flux<String> inFlux = Flux.just("hello", ".", "com");
Flux<String> outFlux = inFlux.map(mapper);
// reactor 测试包提供的测试方法
StepVerifier.create(outFlux)
.expectNext("HELLO", ".", "COM")
.expectComplete()
.verify();什么叫展平?mapper 函数把字符串转成大写,然后分割成一个一个字符。Function<String, Publisher<String>> mapper = s -> Flux.just(s.toUpperCase().split(""));
Flux<String> inFlux = Flux.just("hello", ".", "com");
// 这里只能使用 flatMap,因为参数是 Function<T, Publisher<V>> 形式
Flux<String> outFlux = inFlux.flatMap(mapper);
List<String> output = new ArrayList<>();
outFlux.subscribe(output::add);
// 输出 [H, E, L, L, O, ., C, O, M]
System.out.println(output); 请注意,由于来自不同来源的数据项交错,它们在输出中的顺序可能与我们在输入中看到的不同。1.2 同步?异步?map 是同步的,非阻塞的,1-1(1个输入对应1个输出) 对象转换的;flatMap 是异步的,非阻塞的,1-N(1个输入对应任意个输出) 对象转换的;当流被订阅(subscribe)之后,映射器对输入流中的元素执行必要的转换(执行上述 mapper 操作)。这些元素中的每一个都可以转换为多个数据项,然后用于创建新的流。一旦一个由 Publisher 实例表示的新流准备就绪,flatMap 就会急切地订阅。operator 不会等待发布者完成,会继续下一个流的处理,这意味着订阅是非阻塞的。同时也说明 flatMap() 是异步的。由于管道同时处理所有派生流,因此它们的数据项可能随时进入。结果就是原有的顺序丢失。如果项目的顺序很重要,请考虑改用 flatMapSequential 运算符。2 方法签名的区别很明显2.1 方法签名map 参数是 Function<T, U> ,返回是 Flux<U>flatMap 参数是 Function<T, Publisher<V>> 返回是 Flux<V>举例:这里只能使用 flatMap,因为参数是 Function<T, Publisher<V>> 形式Function<String, Publisher<String>> mapper = s -> Flux.just(s.toUpperCase().split(""));
Flux<String> inFlux = Flux.just("hello", ".", "com");
// 这里只能使用 flatMap,因为参数是 Function<T, Publisher<V>> 形式
Flux<String> outFlux = inFlux.flatMap(mapper);这里只能使用 map,因为参数是 Function<String, String >Function<String, String > mapper = String::toUpperCase;
Flux<String> inFlux = Flux.just("hello", ".", "com");
// 这里只能使用 map,因为参数是 Function<String, String >
Flux<String> outFlux = inFlux.map(mapper);此外,看方法签名,可以看出,可以给 map() 传参 Function<T, Publisher<V>>,按照方法签名,它会返回Flux<Publisher<V>>,但它不知道如何处理 Publishers。比如下面的代码:编译不会报错,但是不知道后续怎么处理。Function<String, Publisher<String>> mapper = s -> Flux.just(s.toUpperCase().split(""));
Flux<String> inFlux = Flux.just("hello", ".", "com");
Flux<Publisher<String>> map = inFlux.map(mapper);下面的例子来源于 stackoverflow:使用 map 方法会产生 Mono<Mono<T>>,而使用 flatMap 会产生 Mono<T>。使用 map() 就是给 map 传参了Function<T, Publisher<V>>,它返回的也是 Mono<Publisher<V>>。// Signature of the HttpClient.get method
Mono<JsonObject> get(String url);
// The two urls to call
String firstUserUrl = "my-api/first-user";
String userDetailsUrl = "my-api/users/details/"; // needs the id at the end
// Example with map
Mono<Mono<JsonObject>> result = HttpClient.get(firstUserUrl).
map(user -> HttpClient.get(userDetailsUrl + user.getId()));
// This results with a Mono<Mono<...>> because HttpClient.get(...)
// returns a Mono
// Same example with flatMap
Mono<JsonObject> bestResult = HttpClient.get(firstUserUrl).
flatMap(user -> HttpClient.get(userDetailsUrl + user.getId()));
// Now the result has the type we expected2.3 返回map() 返回一个值的流flatMap() 返回一个流值的流Flux<String> stringFlux = Flux.just("hello word!");
Function<String, Publisher<String>> mapper = s -> Flux.just(s.toUpperCase().split(""));
// 使用 flatMap() 返回的是 FluxFlatMap.
Flux<String> flatMapFlux = stringFlux.flatMap(mapper);
// 使用 map() 返回的是 FluxMapFuseable
Flux<String> mapFlux = stringFlux.map(s -> s);flatMapFlux 类型是 FluxFlatMap;也就是说,使用 flatMap() 返回的是 FluxFlatMap.mapFlux 类型是 FluxMapFuseable。也就是说,使用 map() 返回的是 FluxMapFuseableFluxMapFuseable 是什么?FluxFlatMap 是什么?FluxFlatMap 和 FluxMapFuseable 是什么区别?各位看官可以一起讨论!参考链接:baeldung: Project Reactor: map() vs flatMap()csdn: map VS flatmapgeeksforgeeks: Difference Between map() And flatMap() In Java StreamstackOverFlow: map vs flatMap in reactor
绝对勇士
Java开发者的Python快速进修指南:自定义模块及常用模块
好的,按照我们平常的惯例,我先来讲一下今天这节课的内容,以及Java和Python在某些方面的相似之处。Python使用import语句来导入包,而Java也是如此。然而,两者之间的区别在于Python没有类路径的概念,它直接使用.py文件的文件名作为导入路径,并将其余的工作交给Python解释器来扫描和处理。另外,你可能经常看到有人使用from..import语句,这种语法是为了从.py文件中只导入部分函数或变量而设计的。也可能是导致不同包目录的情况自定义模块我来举一个在Java开发中常用的开发方式作为例子。在我们进行项目开发时,通常会在项目的结构中创建一个util包,用于存放一些工具类。同样,Python也可以采用类似的方式来组织代码结构,让大家更容易理解。在同目录下如果你想在在同目录下创建一个nameUtil.py文件,并不想另外创建一个util包的话,也可以,我们可以演示一下。以下是nameUtil.py的简单内容:name = "xiaoyu"
def getName(name):
print(f"我的名字是{name}")
在执行的Python文件(main.py)中,可以直接使用import语句来引入其他模块。例如:import nameUtil
name = "xiaoyu"
nameUtil.getName(nameUtil.name) # 可将nameUtil.name替换成name
可以使用from..import语句优化一下上面的内容:from nameUtil import getName
getName("xiaoyu")
这种情况是最简单的。当你想要导入某个包的时候,你可能会想,为什么我可以直接导入,而不需要在同目录下引入各种第三方包呢?实际上,这和Java是一样的。Python也有固定的包扫描路径。比如,当我们导入第三方包时,Python会搜索第三方库的安装路径。这些路径通常是通过包管理工具(如pip)安装的。不在同一目录下如果我们将一些Python工具类单独放在一个名为"util"的目录中,这样可以更好地组织代码。这种做法符合规范,并且让我们的代码更易读、易维护。下面我将演示一下如何使用这种目录结构,唯一的区别是,你需要使用"from"语句来导入工具类,而不能简单地使用"import"语句。目录结构如下:main.py内容如下:from utils import nameUtil
name = "xiaoyu"
nameUtil.getName(nameUtil.name) # 可将nameUtil.name替换成name
这就很像我们正常开发的模式了,因为在开发过程中,我们通常需要将模块按照不同的功能进行分级,并创建相应的包目录结构。这样,我们就可以像上面那样编写代码。如果有多层包目录,我们可以继续按照相同的方式导入模块,例如:from java.utils import nameUtil。感觉就像是回到了熟悉的开发环境,我们可以开始进行正常的开发工作了。常用模块我们将举一些在开发Java工作中常用的工具类,这些工具类同样适用于Python编程语言。timetime模块:提供了与时间相关的函数和类,可以用来获取当前时间、格式化时间、计时等操作。在加密接口中,经常需要使用时间戳场景来确保数据的安全性。获取当前时间戳:current_time = time.time()将时间戳转换为可读时间:readable_time = time.ctime(current_time)格式化时间:formatted_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(current_time))程序休眠一定时间:time.sleep(2) # 程序暂停2秒datetimedatetime模块是Python中提供的一个功能强大的模块,它包含了许多与日期和时间相关的函数和类,可以方便地进行日期和时间的计算、格式化等操作。在很多场景下,比如列表查询等,我们经常会用到日期模块的功能。获取当前日期时间:current_datetime = datetime.datetime.now()格式化日期时间:formatted_datetime = current_datetime.strftime("%Y-%m-%d %H:%M:%S")计算两个日期之间的差值:time_difference = datetime.datetime(2022, 1, 1) - datetime.datetime(2021, 1, 1)jsonjson模块在Python中提供了处理JSON数据的函数和类,它可以被广泛地应用于解析和生成JSON数据。在工作中,我们经常会遇到需要处理JSON数据的情况,所以我想详细解释一下它的用法。然而,需要注意的是,在某些特定的情况下,比如与微信开放者平台进行接口对接时,数据的传输形式可能会是XML格式。在我之前的工作经历中,就遇到过这样的情况,在与微信开放者平台对接时我也踩过一些坑,需要特别注意。解析JSON字符串:json_data = '{"name": "xiaoyu", "age": 30}'
parsed_data = json.loads(json_data)
# 获取key的value值
name = parsed_data["name"]
age = parsed_data["age"]
print(name) # 输出:xiaoyu
print(age) # 输出:30
# 添加新的键值对
parsed_data["city"] = "China"
print(parsed_data)
生成JSON字符串:data = {"name": "John", "age": 30} json_data = json.dumps(data)这里的data是对象,变量json_data就是一个包含了"name"和"age"键值对的JSON格式字符串。osos模块:提供了与操作系统相关的函数和类,可以用来进行文件和目录操作、进程管理等操作。如果你经常需要进行文件操作,不管是在学习还是工作中,了解和掌握os模块都是非常重要的。获取当前工作目录:current_directory = os.getcwd()创建目录:os.mkdir("new_directory")判断文件或目录是否存在:exists = os.path.exists("file.txt")syssys模块:提供了与Python解释器和系统相关的函数和变量,可以用来获取命令行参数、退出程序等操作。这在开发中非常有用,尤其是当我们需要与系统进行交互时。获取命令行参数:arguments = sys.argv退出程序:sys.exit()总结在导入包方面,两者都使用import语句,但是Python没有类路径的概念,直接使用文件名来导入模块。我们还讨论了自定义模块的创建和使用,以及在不同目录下如何组织代码结构。此外,我们介绍了一些常用的Python模块,包括time、datetime、json、os和sys,它们在开发中非常实用。希望今天的课程对大家有所帮助!
绝对勇士
Reactor 第四篇 subcribeOn vs publishOn
我们使用 subscribeOn 和 publishOn 操作符在响应链中切换执行上下文(Reactor 中叫 Scheduler)。上一篇文章中,我们说到 Reactor 默认行为是执行订阅的同一线程将用于整个管道执行。如果要切换执行线程怎么办?可以使用 publishOn 和 SubscribeOn让我们看个简单的例子:class ReactiveJavaTutorial {
public static void main(String[] args) {
Flux<String> cities = Flux.just("New York", "London", "Paris", "Amsterdam")
.map(String::toUpperCase)
.filter(cityName -> cityName.length() <= 8)
.map(cityName -> cityName.concat(" City"))
.log();
cities.subscribe();
}
输出:17:39:41.693 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
17:39:41.712 [main] INFO reactor.Flux.MapFuseable.1 - | onSubscribe([Fuseable] FluxMapFuseable.MapFuseableSubscriber)
17:39:41.714 [main] INFO reactor.Flux.MapFuseable.1 - | request(unbounded)
17:39:41.715 [main] INFO reactor.Flux.MapFuseable.1 - | onNext(NEW YORK City)
17:39:41.715 [main] INFO reactor.Flux.MapFuseable.1 - | onNext(LONDON City)
17:39:41.715 [main] INFO reactor.Flux.MapFuseable.1 - | onNext(PARIS City)
17:39:41.716 [main] INFO reactor.Flux.MapFuseable.1 - | onComplete()中括号中的就是线程名称,在这个例子中,都是 main。可以看到整个管道执行器中都是使用的 main 线程。有的时候,我们可能想告诉 Reactor 别在整个管道中使用同一个线程。我可以使用 subscribeOn() 和 publishOn() 方法达到效果。subscribeOn() 方法subscribeOn() 方法适用于订阅过程。我们可以把它放在响应链条中的任意位置。它接收 Scheduler 参数,且在提供的线程池中选择线程执行。在下面的例子中,我们使用有界弹性线程池(Schedulers.boundElastic())。@Test
public void testSubscribeThread() {
Flux<String> cities = Flux.just("New York", "London", "Paris", "Amsterdam")
.subscribeOn(Schedulers.boundedElastic())
.map(String::toUpperCase)
.filter(cityName -> cityName.length() <= 8)
.map(cityName -> cityName.concat(" City"))
.map(TestCase::concat)
.map(TestCase::stringToUpperCase)
.log();
// cities.subscribe();
System.out.println(cities.blockFirst());
}PS: 原文提供的case,没有输出,简单修改了一下。输出:20:07:53.517 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
20:07:53.558 [main] INFO reactor.Flux.Map.1 - onSubscribe(FluxMap.MapSubscriber)
20:07:53.560 [main] INFO reactor.Flux.Map.1 - request(unbounded)
concat: boundedElastic-1
stringToUpperCase: boundedElastic-1
20:07:53.564 [boundedElastic-1] INFO reactor.Flux.Map.1 - onNext(NEW YORK CITY CITY)
20:07:53.565 [boundedElastic-1] INFO reactor.Flux.Map.1 - cancel()
NEW YORK CITY CITY可以看到 main 线程开始订阅,但是被切换成 boundedElastic-1 线程。我们提供了一个 Scheduler (Schedulers.boundedElastic()),然后这个线程池中的一个线程被选中来替换 main 线程。publishOn() 方法publishOn() 方法跟 subscribeOn() 很类似,但是有一个主要区别。来看个例子:class ReactiveJavaTutorial {
public static void main(String[] args) {
Flux.just("New York", "London", "Paris", "Amsterdam")
.map(ReactiveJavaTutorial::stringToUpperCase)
.publishOn(Schedulers.boundedElastic())
.map(ReactiveJavaTutorial::concat)
.subscribe();
}
private static String stringToUpperCase(String name) {
System.out.println("stringToUpperCase: " + Thread.currentThread().getName());
return name.toUpperCase();
}
private static String concat(String name) {
System.out.println("concat: " + Thread.currentThread().getName());
return name.concat(" City");
}
}这里,我们在两个 map 操作中放一个 publishOn()。我们来看输出:stringToUpperCase: main
stringToUpperCase: main
stringToUpperCase: main
concat: boundedElastic-1
concat: boundedElastic-1
concat: boundedElastic-1可以看到,所有在 publishOn 操作之前的都是 main 线程执行,所有 publishOn 之后的都是 boundedElastic-1 执行。这是因为 publishOn 充当任何其他操作符。它从上游接收信号,并在关联的 Scheduler 上对一个 worker 执行回调时向下游重播。这就是 publishOn 和 subscribeOn() 的主要区别。无论我们把 subscribeOn() 放在哪里,它提供的 Scheduler 都会应用到整条响应链。subscribeOn and publishOn operators in Project Reactor
绝对勇士
Reactor 第十一篇 WebFlux集成Redis
引言在现代的分布式系统中,缓存是提高性能和扩展性的重要组成部分之一。Redis 是一个开源、内存中的数据结构存储系统,可以用作数据库、缓存和消息中间件。而 WebFlux 是 Spring 框架提供的响应式编程模型,在处理高并发和大数据量的情况下具有很好的性能和扩展性。本文将介绍如何使用 Reactor 和 WebFlux 集成 Redis,利用其响应式特性来处理缓存操作。1. 环境准备首先,我们需要在项目的 pom.xml 文件中添加对 Spring WebFlux 和 Spring Data Redis 的依赖:<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
...
</dependencies>2. 配置Redis连接信息在 application.properties 文件中添加Redis连接的配置信息:spring.redis.host=127.0.0.1
spring.redis.port=63793. 创建缓存管理器在项目的配置类中创建一个 RedisCacheManager 来管理缓存:@Configuration
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()));
return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory)
.cacheDefaults(cacheConfiguration)
.build();
}
}在上述代码中,我们使用 RedisCacheConfiguration 配置了缓存的默认过期时间、键和值的序列化方式。4. 编写缓存逻辑定义一个Service类来处理缓存操作:@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private ReactiveRedisOperations<String, User> redisOperations;
@Cacheable(cacheNames = "users", key = "#id")
public Mono<User> getUserById(String id) {
return userRepository.findById(id)
.flatMap(user -> redisOperations.opsForValue().set(id, user)
.then(Mono.just(user)));
}
@CachePut(cacheNames = "users", key = "#user.id")
public Mono<User> saveUser(User user) {
return userRepository.save(user)
.flatMap(savedUser -> redisOperations.opsForValue().set(savedUser.getId(), savedUser)
.then(Mono.just(savedUser)));
}
@CacheEvict(cacheNames = "users", key = "#id")
public Mono<Void> deleteUserById(String id) {
return userRepository.deleteById(id)
.then(redisOperations.opsForValue().delete(id));
}
}在上述代码中,我们使用 Spring 框架的缓存注解来定义缓存的逻辑。@Cacheable 用于读取缓存,@CachePut 用于更新缓存,@CacheEvict 用于清除缓存。同时,我们使用 ReactiveRedisOperations 来执行Redis的操作。5. 创建WebFlux控制器编写一个WebFlux控制器来处理请求:@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public Mono<User> getUserById(@PathVariable String id) {
return userService.getUserById(id);
}
@PostMapping("/users")
public Mono<User> saveUser(@RequestBody User user) {
return userService.saveUser(user);
}
@DeleteMapping("/users/{id}")
public Mono<Void> deleteUserById(@PathVariable String id) {
return userService.deleteUserById(id);
}
}在上述代码中,我们使用 @GetMapping、@PostMapping 和 @DeleteMapping 来映射 URL,并调用 UserService 中的相应方法来处理具体的业务逻辑。总结本文介绍了如何使用 Reactor 和 WebFlux 集成 Redis 来处理缓存操作。通过使用 ReactiveRedisOperations 和 Spring 框架的缓存注解,我们可以方便地实现响应式的缓存逻辑。这种方式可以提升系统的性能和扩展性,特别适用于高并发和大数据量的场景。希望本文对您在使用 Reactor 和 WebFlux 集成 Redis 方面有所帮助。
绝对勇士
Reactor 第六篇 响应式编程 之 简介
1 reactor 出现的背景、初衷和要达到什么样的目标Reactor 项目始于 2012 年。 经过长时间的内部孵化,于 2013 年发布 Reactor 1.x 版本。 Reactor 1 在各种架构下都能成功部署,包括开源的(如 Meltdown)和商业的(如 Pivotal RTI)。2014年,通过与一些新兴的响应式数据流规范合作,重新设计并于 2015 年 4 月发布 Reactor 2.0 版本。1.1 阻塞浪费资源互联网企业基本上都有着大量的用户,即使当代硬件的性能已经提升了很多,但是性能问题一直是互联网企业不能忽略的一个问题。通常有两种方式来提升应用的性能:使用更多的线程和硬件资源达到并行化。这也是很多企业采用的方式;在当前使用的资源上寻求更高效的处理。这在全球经济下行的背景下,是一种成本更低的方式;1.2 异步能拯救一切嘛?通过编写异步非阻塞的代码,可以将执行切换到使用了相同底层资源的另一活动任务上,然后在异步完成之后返回到当前任务。提升资源利用率。java 提供了两种编写异步(异步不一定非阻塞)代码的方式。Callbacks:不立即返回对象,但是提供了一个 callback 参数,当结果可返回时调用。Future:这也是现在大部分程序员在使用的方式。异步方法会立即返回一个 Future。Future 对象对获取该值进行了包装,这个对象可以一直轮询知道返回(除非设置了超时时间)。例如,ExecutorService 使用 Future 对象执行 Callable 任务。这些技术都有自己的问题:callback 不好组合,编写有难度,且很容易导致代码难以阅读和维护。Future 比callback好很多,但是也有自己的问题。调用 get() 方法会阻塞;缺乏对多值和高级错误处理的支持。1.3 从命令式到响应式作为响应式编程方向上的第一步,Microsoft在.NET生态中创建了响应式(Rx)扩展库。然后RxJava实现了JVM上的响应式编程。随着时间的推移,通过Reactive Streams的努力,一套基于JVM为响应式库定义接口与交互规则的标准规范Reactive Streams 出现了。其接口已经集成到了Java9中的 Flow 类下。响应式旨在解决上述 JVM 提供的异步方式的缺点,同时关注了其他一些方面:组合型和易读性数据作为 流 操作,有着丰富的操作符在订阅之前什么都不会发生(有什么优点?)背压,消费者可以向生产者发送信号表示发布速率太快与并发无关的高阶抽象reactor 是响应式编程的一种实现。现代应用程序需要处理大量并发请求并处理大量数据。标准的阻塞代码不再足以满足这些要求。反应式设计模式是一种基于事件的架构方法,用于异步处理来自单个或多个服务处理程序的大量并发服务请求。Project Reactor 基于这种模式,并有一个明确而雄心勃勃的目标,即在 JVM 上构建非阻塞、反应式应用程序。2 reactor 优势和劣势分别是什么优势异步非阻塞代码可读性高背压 解决消息的消费可能比生产慢。劣势对于非响应式 java 开发者来说,学习曲线陡峭。debug 难度高3 reactor 的适用场景创建事件驱动程序;亚马逊等大型在线购物平台的通知服务为银行业提供庞大的交易处理服务股票价格同时变动的股票交易业务4 reactor 组成部分和关键节点4.1 Mono一种生成数据流的方式。包含0-1个结果的异步序列。Mono.just(1);4.2 Flux另一种生成数据流的方式。包含0-N个结果的异步序列。Flux.just(1, 2, 3, 4)5 底层原理与关键实现生产者-消费者模式?迭代模式?6 其他竞品技术lxdd.gitbook.io/spring-webf…Spring WebfluxRxJavaSpring WebFlux (project-reactor) 和 RxJava2+ 都是响应式流的实现。Spring 正在使用 project-reactor,因此它得到了更多的支持、广告和更大的社区,所以用它的人比较多。6.1 Spring WebfluxSpring Webflux 是一个使用响应式库创建 web 服务的框架。它的主要目标是确保低资源使用(即线程数量少)的高可伸缩性。在底层,它使用 Project Reactor,但是,你也可以将它与 RxJava (或任何其他的响应流实现)一起使用,它甚至可以与 Kotlin 协程一起工作。换句话说, Reactor 是一个基础响应式包,Spring WebFlux 是一个框架,这个框架默认使用 Reactor,但是可以使用 RxJava,也可以使用 Kotlin 等其他响应式包。Spring Framework 中包含的原始 Web 框架 Spring Web MVC 是专门为 Servlet API 和 Servlet 容器构建的。反应式堆栈 Web 框架 Spring WebFlux 是在 5.0 版中添加的。它是完全非阻塞的,支持 Reactive Streams 背压,并且可以在 Netty、Undertow 和 Servlet 3.1+ 容器等服务器上运行。Hello World 级示例:blog.csdn.net/get_set/art…6.2 RxJava2ReactiveX 结合了观察者模式、迭代器模式和函数式编程的最佳思想。它扩展了观察器模式,以支持数据序列和/或事件,并添加了操作符,允许您以声明的方式将序列组合在一起,同时抽象出诸如低级线程、同步、线程安全、并发数据结构和非阻塞I/O等问题。一般来说,RxJava 支持基于 JDK8- 的项目,project Reactor 支持 JDK8 +。但是对于初学者来说,你可以先学习 RxJava。Project Reactor 可以弥补 RxJava 的缺点,更适合后端开发。RxJava 有太多的问题,如果你不能很好地使用它,可能会导致内存溢出。但最后,如果你想很好地使用 Spring 5.2+,你需要学习 RxJava->Reactor->NIO->Netty->Reactor Netty。6.3 Reactor VS RxJavaRxJava 和 Reactor 是一些非常著名的库,用于与任何应用程序的后端相关的一些开发。Rxjava 支持的项目大多与 JDK8 相关,而 Reactor 则与所有与 JDK8 + 相关的项目相关。RxJava产生了许多可能导致内存相关问题的问题,但是当与 spring 5.2+ 一起使用时,它会变得非常好。reactor 通常被称为反应式编程范式,它主要涉及用于操作的反应式流 API,并使整个 API 流活动。www.educba.com/rxjava-vs-r…1、github地址:github.com/reactor/rea…2、官方文档:easywheelsoft.github.io/reactor-cor…3、segmentfault.com/a/119000001…4、www.infoq.com/articles/re…5、Spring Webflux : www.baeldung.com/spring-webf…6、Java Spring WebFlux vs RxJava:stackoverflow.com/questions/5…
绝对勇士
Java开发者的Python快速进修指南:迭代器(Iterator)与生成器
这一篇内容可能相对较少,但是迭代器在Java中是有用处的。因此,我想介绍一下Python中迭代器的使用方法。除了写法简单之外,Python的迭代器还有一个最大的不同之处,就是无法直接判断是否还有下一个元素。我们只能通过捕获异常或使用for循环来退出迭代,这点让我感到十分惊讶。可迭代对象可迭代对象是指那些可以通过for循环进行遍历的对象。在Python中,可迭代对象通常是容器类型,例如列表、元组、字典和集合,同时也包括字符串和文件对象等。要获取一个迭代器,我们可以使用内置函数iter()。你可能会问,如何判断一个变量是否是可迭代对象呢?不用担心,不需要死记硬背。只要这个变量具有__iter__()方法,那么它就是可迭代对象。这与Java中的情况相似,Java也是通过实现一个接口来拥有迭代器的能力。所以,不用担心,你只需要记住这个简单的规则,就可以轻松判断一个变量是否是可迭代对象了。以下示例参考:my_list = [1, 2, 3, 4, 5]
for item in my_list:
print(item)
迭代器(Iterator)代器是用于遍历可迭代对象的工具。它有两个基本方法,即__iter__()和__next__()。__iter__()方法返回迭代器对象本身,而__next__()方法用于返回容器中的下一个元素。然而,当迭代器没有更多的元素可供遍历时,__next__()方法会引发StopIteration异常。下面是一个示例,演示了如何使用迭代器遍历可迭代对象:my_dict = {"name": "xiaoyu", "age": 18, "country": "China"}
for key in iter(my_dict):
print(key)
正常情况下,我们通常会使用for循环来遍历可迭代对象。但是如果你选择使用while循环,需要注意处理StopIteration异常,这就很尴尬。以下是一个示例代码供参考:my_dict = {"name": "xiaoyu", "age": 18, "country": "China"}
iterator = iter(my_dict)
while True:
try:
key = next(iterator)
print(key)
except StopIteration:
break
生成器(Generator)我认为这个与Java不同,有着独特的特点。生成器是一种特殊的迭代器,它利用函数和yield语句逐步生成数据。生成器可以使用yield语句逐个生成元素,而无需一次性生成所有元素。这种方式既可以节省内存空间,又可以延迟计算。为了更好地理解,以下是一个示例:def my_generator():
yield 1
# todo 业务
yield 2
# todo 业务
yield 3
gen = my_generator()
print(next(gen)) # 输出1
print(next(gen)) # 输出2
return语句的优势在于它可以一次性返回一个值,而且立即执行。而相比之下,它的对手——yield关键字更加强大,它不仅可以多次返回值,还能够延迟计算其中的业务逻辑。总结通过本文,我们了解了Python中迭代器的使用方法。Python的迭代器与Java有些不同,无法直接判断是否还有下一个元素,需要通过捕获异常或使用for循环来退出迭代。可迭代对象是指可以通过for循环进行遍历的对象,可以使用内置函数iter()获取迭代器。判断一个变量是否是可迭代对象,只需要判断是否具有__iter__()方法。迭代器是用于遍历可迭代对象的工具,具有__iter__()和__next__()方法,当没有更多元素可供遍历时,__next__()方法会引发StopIteration异常。生成器是一种特殊的迭代器,利用函数和yield语句逐步生成数据,可以节省内存空间,并且延迟计算。
绝对勇士
Java开发者的Python快速进修指南:函数进阶
在上一篇文章中,我们讲解了函数最基础常见的用法,今天我想在这里简单地谈一下函数的其他用法。尽管这些用法可能不是非常常见,但我认为它们仍然值得介绍。因此,我将单独为它们开设一个章节,并探讨匿名函数和装饰器函数这两种特殊的用法。匿名函数在Python中,匿名函数也被称为lambda函数,它是一种没有名称的函数。但是与Java的lambda表达式相比,它们有一些区别。匿名函数通常用于在代码中定义简单的功能,并且可以在不需要额外定义函数的情况下使用。主要就是省事~~匿名函数的语法如下:lambda arguments: expression
其中,arguments是函数的参数,expression是函数的返回值。如果在expression中没有使用print这样的打印函数,通常情况下函数会返回expression的值,这意味着函数中包含了return语句。# 写法一
print((lambda x, y: x + y)(2, 3))
# 写法二,但是这个赋值了一个函数名字,没啥意义了就
add = lambda x, y: x + y
print(add(2, 3))
在Java中,并没有直接对应的匿名函数的概念,但可以通过定义接口或使用Lambda表达式来实现类似的功能。装饰器函数在Python中,装饰器是一种特殊的函数,它可以接受一个函数作为参数,并返回一个新的函数。装饰器函数通常用于在不改变原函数代码的情况下,对函数进行扩展或修改。而在Java中,装饰器函数的概念可以通过使用注解来实现。通过在方法前添加特定的注解,可以实现对方法的装饰。简单来说,装饰器是一种用于修改其他函数行为的函数。它们允许在不修改原始函数定义的情况下,对其进行扩展、修改或包装。装饰器函数的语法如下:def log_decorator(original_function):
def wrapper_function(*args, **kwargs):
print(f"Calling {original_function.__name__} function")
result = original_function(*args, **kwargs)
print(f"{original_function.__name__} function finished")
return result
return wrapper_function
@log_decorator
def add(x, y):
return x + y
print(add(2, 3))
#输出结果如下
#Calling add function
#add function finished
#5
这里我们定义了一个装饰器函数log_decorator,它在调用原始函数之前和之后打印了一些信息。通过在add函数上使用@log_decorator,我们将add函数传递给log_decorator函数进行装饰。当调用add函数时,实际上是调用了被装饰后的wrapper_function函数。总结在这篇文章中,我们介绍了函数的两种不常用的特殊用法:匿名函数和装饰器函数。匿名函数是一种没有名称的函数,通常用于定义简单的功能。我们可以使用lambda关键字来创建匿名函数,并在需要时直接调用它们。装饰器函数是一种特殊的函数,可以接受一个函数作为参数,并返回一个新的函数。装饰器函数通常用于在不改变原函数代码的情况下,对函数进行扩展或修改。通过使用装饰器,我们可以在函数调用前后执行额外的操作。这些特殊用法可以帮助我们更灵活地使用函数,并使代码更加简洁和可读。
绝对勇士
Java开发者的Python快速进修指南:面向对象基础
当我深入学习了面向对象编程之后,我首先感受到的是代码编写的自由度大幅提升。不同于Java中严格的结构和约束,Python在面向对象的实现中展现出更加灵活和自由的特性。它使用了一些独特的关键字,如self和cls,这些不仅增强了代码的可读性,还提供了对类和实例的明确引用。正如Java,Python也依赖于对象和类的概念,允许我们通过定义类来创建和操作对象。尽管在表面上Python和Java在面向对象的实现上看似相似,但实际上,它们在细节处理上存在一些显著的差异。接下来,我们将探索这些差异,并深入了解它们在实际应用中的具体表现,以便更好地理解面向对象编程在不同语言中的独特风格和优势。Python中的类声明首先,你需要声明一个类。在Python中,这通常是通过使用class关键字来完成的。下面是一个简单的类声明的示例:class MyClass:
myAttr = "类的属性"
def __init__(self, attribute):
self.attribute = attribute
def my_method(self):
return f"Value of attribute is {self.attribute}"
关于上面的类声明你可能发现了attribute和myAttr属性不一样,不报错吗?这就是Python的特点:动态属性赋值。在Python中,不仅可以在类的初始化方法__init__中直接定义新的属性,还可以在对象创建之后的任何时刻动态地添加属性,这种做法在Java中会引发错误,但在Python中却是完全合法的,反映了其动态类型的本质。下面再详细说下。在Java中,this关键字是隐式的,用于指代当前对象的实例,而在Python中,self必须显式声明并作为方法的第一个参数传递。返回值里的f在这里表示格式化,它使得在字符串中直接嵌入表达式成为可能。Python会自动进行求值并将结果转换为字符串。创建对象一旦定义了类,就可以使用该类来创建对象。这是通过简单地调用类名并传递必要的参数来完成的。例如:my_object = MyClass("Hello")
my_object.subAttr = "是子类的"
print(my_object.subAttr) #输出:是子类的
print(my_object.my_method()) # 输出:Value of attribute is Hello
虽然在Python中,self关键字需要显式地在方法定义中指出,但其实它的作用与Java中的this关键字相似,代表着方法所属的对象实例。在调用实例方法时,Python会自动将对象实例作为第一个参数传递给self,因此在正常使用实例方法时,我们无需显式地传递这个参数。例如,在调用my_object.my_method()时,my_object实例会自动作为self参数传递给my_method。这种机制确保了方法能够访问和操作所属对象实例的数据。如果尝试直接通过类名来调用实例方法,如MyClass.my_method(),将会引发错误。这是因为没有提供必要的实例参数,导致self没有被正确初始化。要想通过类名调用方法,方法必须是类方法或静态方法。来看下类方法和静态方法在Python中,@classmethod和@staticmethod是两种常用的方法装饰器,它们分别用于定义类方法和静态方法。其特点是第一个参数通常是cls,代表着类本身。这与实例方法中的self参数相似,但有一个重要的区别:cls参数指向类,而不是类的某个特定实例。类方法的一个限制是它们无法访问特定实例的属性,因为它们不与任何实例绑定。class MyClass:
@classmethod
def my_class_method(cls):
# 可以访问类属性,如cls.some_class_attribute
return "这是一个类方法"
静态方法实际上是独立于类的实例和类本身的。静态方法不接收传统意义上的self或cls参数,这意味着它们既不能访问类的实例属性(即对象级别的数据),也不能访问类属性(即与类本身相关联的数据)。静态方法的这种特性使得它们更像是普通函数,但为了逻辑上的整洁和组织性,它们被放置在类的定义中。class MyClass:
@staticmethod
def my_static_method():
return "这是一个静态方法"
总结作为一名有着Java背景的开发者,你无疑已经习惯了Java那严格的类型系统和细致的访问控制机制。转向Python,你会发现一个截然不同的编程世界。Python的面向对象编程(OOP)方式为代码组织提供了更高的自由度和灵活性,这种变化可能会给你带来新鲜感,同时也是一个挑战。需要注意的是,Python的这种灵活性可能会导致更少的编译时错误检查。由于Python是一种解释型语言,很多错误只有在运行时才会被捕捉到。
绝对勇士
wasm+pygbag让你在网页上也能运行Python代码:【贪吃蛇游戏】
引言最近小伙伴告诉我一种新的方法,可以使用wasm来使浏览器网页能够运行Python代码。这一下子激起了我的兴趣,因为这意味着用户无需安装Python环境就能直接运行我的demo,这真是太方便了。所以,我们的主要目标今天就是让网页能够直接运行我的贪吃蛇游戏。贪吃蛇游戏其实很简单,因为Python有一个很棒的pygame库可以供我们使用。所以编写起来也不会太复杂。废话不多说,让我们开始吧。何为wasm全称为WebAssembly ,简称为Wasm,是一种能够在web上加载非常快速的一种格式。它可以被视为HTML、JS等其他表达形式的一种补充。对于我来说,这就是它的简单定义。然而,我们今天的任务并不是去介绍Wasm,而是探讨如何实现Python在web中直接运行的方法。emscripten我们现在已经了解到,WebAssembly可以在web上运行,但我想知道如何将我的Python代码转换成WebAssembly。当然,肯定有相应的工具可供使用。在网上,最常被提及的工具就是emscripten,可以说是WebAssembly的核心工具。它的主要功能是将其他高级语言编译成WebAssembly。然而,我在本地尝试过后发现,emscripten无法直接将Python代码转换成WebAssembly格式。你需要使用其他工具先将Python转换成其他高级语言如C语言,然后再使用emscripten将其转换成WebAssembly。如果你知道其他更好的方法,可以在下方提出来,非常感谢。我就不实验了,毕竟后台报错,如果你有兴趣可以看看emscripten官方网站:emscripten.org/在网站的顶部导航栏中,找到并点击 "Get Started"(开始使用)。pyodide如果你尝试过在Web上运行Python代码,那你肯定了解到pyodide方案,它确实是一个功能强大的工具。然而,它也存在明显的缺点,例如它所支持的第三方库非常有限,而且加载速度也很慢。尽管如此,它仍然是最便捷的选择,因为你可以直接在HTML中编写代码,而不需要额外的工具。唯一需要注意的是需要引用它的JavaScript文件。test.html代码示例如下:<script src="https://cdn.jsdelivr.net/pyodide/v0.18.1/full/pyodide.js"></script>
<script type="text/javascript">
loadPyodide({ indexURL : "https://cdn.jsdelivr.net/pyodide/v0.18.1/full/" }).then((pyodide) => {
pyodide.runPython(`
def hello_world():
return "Hello, World!"
print(hello_world())
`);
});
</script>
对于我们来说,使用pyodide是相对简单的。只需点击文件后,浏览器就能正常运行其中的Python代码。但是要直接使用Python的pygame库是不可能的。不过,一些简单的代码还是可以运行的。那么,是否还有其他解决方案呢?答案是肯定的。pygbag开发人员在寻找解决方案时,最好的资源就是Github了。几乎所有开源的代码仓库都可以在那里找到,只要你能想到的,几乎都能找到。我也是通过搜索找到了一个解决方案,真的有一个开源的第三方库叫做pygbag。虽然这个库很新,但它是由Python官方支持的。只是要依靠官方文档和浏览器去寻找示例代码。连gpt这样的人工智能模型都不知道有这么一个东西存在。此外,pygbag专门集成了pygame,可以直接将Python代码编译成wasm,在浏览器中运行。它还有官方开发人员制作的游戏可供参考,当然如果你也制作了游戏,也可以上传到这里。官方Github地址:github.com/pygame-web/…官方游戏首页:itch.io/games/tag-r…他的宗旨也很简单:Python WebAssembly for everyone ( packager + test server ),但是他也有一些编码要求,我们先来实现一个贪吃蛇游戏吧。贪吃蛇游戏在开始使用pygbag三方库之前,我们需要确保已经在本地实现了贪吃蛇游戏。现在,请跟着我一起按照以下步骤进行操作。安装 Pygame命令如下:pip install pygamePygame是一套专门用于编写游戏的Python模组,它在SDL库的基础上添加了各种游戏功能的实现。借助Python语言的灵活性,你可以快速、轻松地创建各种类型的游戏。正如之前所提到的,Python拥有丰富的库和模组,我们只需直接使用它们,而不必重复造轮子。我已经为你写好了贪吃蛇游戏的代码,你可以直接使用。这是一个大家都很熟悉的游戏,所以没有太多需要解释的。import pygame
import random
pygame.init()
screen = pygame.display.set_mode((1280, 720))
clock = pygame.time.Clock()
running = True
dt = 0
player_pos = [pygame.Vector2(screen.get_width() / 2, screen.get_height() / 2)]
player_speed = 300
player_radius = 30
food_pos = pygame.Vector2(random.randint(0, screen.get_width()), random.randint(0, screen.get_height()))
food_radius = 15
direction = pygame.Vector2(0, 0)
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen.fill("black")
keys = pygame.key.get_pressed()
if keys[pygame.K_w] or keys[pygame.K_UP]:
direction = pygame.Vector2(0, -1)
if keys[pygame.K_s] or keys[pygame.K_DOWN]:
direction = pygame.Vector2(0, 1)
if keys[pygame.K_a] or keys[pygame.K_LEFT]:
direction = pygame.Vector2(-1, 0)
if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
direction = pygame.Vector2(1, 0)
# Move the player's head
player_pos[0] += direction * player_speed * dt
# Check if player eats the food
if player_pos[0].distance_to(food_pos) < player_radius + food_radius:
player_pos.append(player_pos[-1].copy()) # Add new segment to the player
food_pos = pygame.Vector2(random.randint(0, screen.get_width()), random.randint(0, screen.get_height())) # Generate new food
# Move the player's body
for i in range(len(player_pos)-1, 0, -1):
player_pos[i] = player_pos[i-1].copy()
pygame.draw.circle(screen, "white", food_pos, food_radius) # Draw the food
for pos in player_pos:
pygame.draw.circle(screen, "red", pos, player_radius) # Draw the player
pygame.display.flip()
dt = clock.tick(60) / 1000
pygame.quit()
启动命令就是python main.py。运行效果如下:我想要说明的是,我只是简单地实现了一下,并没有进行太多的校验。另外,值得注意的是,尽管我吃完食物后,并没有在身体上感受到太大的变化,但当我吃了很多食物之后,你就可以看到明显的变化了。pygbag改造使用 pygbag 将 pygame 制作的游戏打包,使游戏可在浏览器中直接运行。 pygbag 的使用可参考 Pygbag Wiki。官方文档:pygame-web.github.io/使用 pip 安装 pygbag:pip install pygbag使用 pygbag 打包游戏前,待打包的目录下的游戏代码文件须名为 main.py。 然后仅需对游戏代码做简易修改,修改后代码如下:import pygame
import random
import asyncio
pygame.init()
screen = pygame.display.set_mode((1280, 720))
clock = pygame.time.Clock()
running = True
dt = 0
player_pos = [pygame.Vector2(screen.get_width() / 2, screen.get_height() / 2)]
player_speed = 300
player_radius = 30
food_pos = pygame.Vector2(random.randint(0, screen.get_width()), random.randint(0, screen.get_height()))
food_radius = 15
direction = pygame.Vector2(0, 0)
async def main():
global screen, clock, running, dt ,player_pos ,player_speed,player_radius,food_pos,food_radius,direction
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen.fill("black")
keys = pygame.key.get_pressed()
if keys[pygame.K_w] or keys[pygame.K_UP]:
direction = pygame.Vector2(0, -1)
if keys[pygame.K_s] or keys[pygame.K_DOWN]:
direction = pygame.Vector2(0, 1)
if keys[pygame.K_a] or keys[pygame.K_LEFT]:
direction = pygame.Vector2(-1, 0)
if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
direction = pygame.Vector2(1, 0)
# Move the player's head
player_pos[0] += direction * player_speed * dt
# Check if player eats the food
if player_pos[0].distance_to(food_pos) < player_radius + food_radius:
player_pos.append(player_pos[-1].copy()) # Add new segment to the player
food_pos = pygame.Vector2(random.randint(0, screen.get_width()), random.randint(0, screen.get_height())) # Generate new food
# Move the player's body
for i in range(len(player_pos)-1, 0, -1):
player_pos[i] = player_pos[i-1].copy()
pygame.draw.circle(screen, "white", food_pos, food_radius) # Draw the food
for pos in player_pos:
pygame.draw.circle(screen, "red", pos, player_radius) # Draw the player
pygame.display.flip()
dt = clock.tick(60) / 1000
await asyncio.sleep(0)
pygame.quit()
if __name__ == '__main__':
# 使用 asyncio.run() 调用主函数 main()
asyncio.run(main())
可以看到,基本上必须引入另一个包:导入 asyncio。剩下的就没啥了。我们再来启动下。启动命令需要修改下:python -m pygbag pyGame,这里注意下,pygbag打包的是整个目录,所以不能像Python那样启动,所以我基本上都是回退到父级目录后,在进行终端控制台打包。命令会直接在本地启动游戏服务。你稍等片刻。然后直接字啊浏览器查看URL地址:http://localhost:8000/,仍然是稍等片刻,让他加载一下。这时候,你就可以看到浏览器的游戏界面了,如下:总结经过努力,我成功完成了任务。如果你有兴趣,也可以将你的游戏上传到官方网站,但作为示例,我并不打算上传。不过,我已经提供了源代码给你,所以你可以直接复制粘贴并运行它。虽然Python现在可以直接在web端使用,但我个人不太喜欢这种方式。
绝对勇士
Java开发者的Python快速进修指南:控制之if-else和循环技巧
简单介绍在我们今天的学习中,让我们简要了解一下Python的控制流程。考虑到我们作为有着丰富Java开发经验的程序员,我们将跳过一些基础概念,如变量和数据类型。如果遇到不熟悉的内容,可以随时查阅文档。但在编写程序或逻辑时,if-else判断和循环操作无疑是我们经常使用的基本结构。毕竟,我们初步编写的代码很多时候都是在if嵌套中度过的。随着经验的积累,我们才逐渐开始考虑如何将设计模式融入代码中进行优化。循环也是同样如此,我们的取值逻辑涉及到分页批量处理,遍历是不可或缺的。基础首先,让我们深入了解一下Python的写法。在Python中,缩进是至关重要的,这与我们编写YAML文件时的逻辑非常相似。在Python中,同一层逻辑保持相同的缩进,子逻辑则进一步缩进,而换逻辑则从新的一行从头开始。在编程中,我们经常使用打印语句来进行调试和输出信息。不同于Java中的println和print,Python中只有一个print函数。然而,它具有一个默认的第二个参数end,你可以使用类似这样的方式进行打印:print('test', end='\t')。这对于控制输出的结尾非常有用。在函数调用方面,Python允许传递默认值,这使得函数调用时不像Java那样必须提供所有参数。这些是Python中一些基础的语法和习惯,接下来我们将深入探讨更多关于if判断和循环的内容,以及如何在Python中灵活运用这些概念。if判断在Python中,if判断的灵活性给我们带来了很多便利。特别是在缩进写法上,省略了冗长的括号,使得代码更加清晰。不仅如此,Python还提供了elif来简化多个条件的判断,让你的键盘少敲两次,看起来更简洁。下面,让我们通过一个例子来深入了解:# 例子:判断一个数字的正负和奇偶性
num = int(input("请输入一个整数:"))
if num > 0:
print("这是一个正数。")
elif num < 0:
print("这是一个负数。")
else:
print("这是零。")
if num % 2 == 0:
print("这是一个偶数。")
else:
print("这是一个奇数。")
了解了Python中的if判断写法后,让我们研究一下在业务逻辑中常用的and和or操作符。这两个操作符在Python中的灵活运用能够使代码更加简洁,提高可读性。在业务逻辑中,有些开发者习惯在if判断中直接使用and和or,而不愿意提前声明变量记录判断值。这样的写法虽然直接,但也可能让代码变得冗长。让我们通过一个实例来展示如何巧妙地运用and和or:# 例子:验证用户名和密码是否同时符合要求
username = input("请输入用户名:")
password = input("请输入密码:")
# 判断用户名和密码是否同时满足条件
if len(username) > 5 and len(password) >= 8:
print("用户名和密码符合要求,验证通过。")
else:
print("用户名或密码不符合要求,请重新输入。")
在这个例子中,我们通过and操作符将两个条件同时纳入判断,使得验证逻辑更加清晰。接下来,让我们看一下取反操作,Python中使用的是not。这与Java中使用感叹号!的逻辑相似,但更贴近自然语言,提高了可读性。下面是一个简单的取反操作的例子:# 例子:判断一个数字是否不在指定范围内
number = 25
if not (10 <= number <= 20):
print("这个数字不在10到20之间。")
else:
print("这个数字在指定范围内。")
通过这个例子,你可以更好地理解在Python中如何使用not进行取反操作。while循环在Python中,while循环不仅与Java一样有强行退出的break和继续执行的continue关键字,而且有一个独特的特性,即在循环正常结束后可以使用else块,前提是循环没有被break中断。这个特性使得Python中的while循环更加灵活,可以在循环结束后执行特定的业务逻辑。让我们通过一个例子来深入了解:# 例子:利用while循环计算数字的阶乘,并在循环结束后输出结果
num = int(input("请输入一个正整数:"))
factorial = 1
count = 1
while count <= num:
factorial *= count
count += 1
else:
print(f"{num}的阶乘是:{factorial}")
print("循环正常结束,执行了else块中的业务逻辑。")
在这个例子中,循环通过while count <= num条件进行控制,当循环正常结束时,执行else块中的代码。这种结构在Java中是不常见的,但在Python中却是一种很有用的模式。for循环与while循环类似,for循环在Python中也有强行退出的break和继续执行的continue关键字。下面是一个使用for循环的例子:# 例子:遍历列表并输出元素的平方,并在循环结束后输出提示信息
numbers = [1, 2, 3, 4, 5]
for num in numbers:
square = num ** 2
print(f"{num}的平方是:{square}")
else:
print("for循环正常结束,执行了else块中的业务逻辑。")
在这个例子中,我们使用for循环遍历列表numbers,并计算每个元素的平方并输出。同样地,循环正常结束后,执行了else块中的代码。总结在今天的学习中,我们简要了解了Python的控制流程,特别是if-else判断和循环操作。作为有着Java开发经验的程序员,我们跳过了一些基础概念,着重探讨if判断和循环的灵活运用。Python的缩进写法和与Java的一些语法区别都是需要注意的地方。在编写程序时,if嵌套和循环是基础结构,而设计模式的融入和循环的灵活运用则是经验积累后的优化方向。在学习中,我发现Python的语法简洁,控制流程更为灵活,使得编写清晰可读的代码变得更加容易。
绝对勇士
【Reactor第八篇】WebFlux 服务编排
WebFlux 服务编排是指使用 WebFlux 框架来编排多个异步服务的执行顺序和数据流动,从而构建出一个完整的、基于事件驱动的响应式应用程序。WebFlux服务编排的优势如下:高性能:WebFlux基于响应式编程模型,可以使用少量的线程处理大量的请求,从而提高系统的并发能力和吞吐量。异步处理:WebFlux可以异步处理请求和响应,避免线程的阻塞和等待,提高系统的并发能力和性能。高可靠性:WebFlux基于事件驱动的编程模型,可以更好地处理错误和异常,从而提高系统的可靠性和稳定性。简洁清晰:WebFlux的代码简洁清晰,可以使用函数式编程风格来编写业务逻辑,提高代码的可读性和可维护性。可扩展性:WebFlux可以轻松地集成其他的响应式组件和服务,例如Reactive Streams、Spring Cloud、RSocket等,从而提高系统的可扩展性和灵活性。综上所述,WebFlux服务编排可以帮助我们构建高性能、高可靠性、可扩展性强的响应式应用程序,提高系统的并发能力和性能,从而更好地满足现代应用程序的需求。一个示例public Mono> getOrderDetails(String orderId) {
return Mono.fromCallable(() -> {
// 查询订单基本信息
return "order info";
})
.flatMap(orderInfo -> {
// 查询订单商品信息
return Mono.fromCallable(() -> {
return "order item info";
});
})
.flatMap(orderItemInfo -> {
// 查询订单配送信息
return Mono.fromCallable(() -> {
return "order delivery info";
});
})
.flatMap(orderDeliveryInfo -> {
// 查询订单支付信息
return Mono.fromCallable(() -> {
return "order payment info";
});
});
}为什么使用 fromCallable,就是上面说的,WebFlux 编排的是异步服务,而不是同步服务。但是实际线上不要使用 fromCallable,会导致创建很多个线程,高并发场景下会导致资源竞争激烈,从而服务性能急剧下降。1 串行1.1 不需要 invoker1 的结果long start = System.currentTimeMillis();
Mono<String> invoke1 = Invoker1.invoke1();
Mono<String> result = invoke1.flatMap(p -> Invoker2.invoke2())
.map(s -> {
return s.toString();
});
// result: invoker2, 耗时:3592(串行)
System.out.println("result: " + result.block() + ", 耗时:" + (System.currentTimeMillis() - start));1.2 需要返回 invoker1 的结果long start = System.currentTimeMillis();
Mono<String> invoke1 = Invoker1.invoke1();
Mono<String> result = invoke1.flatMap(p -> {
return Invoker2.invoke2().map(s -> {
return p + s;
});
});
// result: invoker1invoker2, 耗时:3554(串行)
System.out.println("result: " + result.block() + ", 耗时:" + (System.currentTimeMillis() - start));2 并行2.1 zip 方法zip() 方法可以一次组装任意个Mono,适用于有多个Mono的情况long start = System.currentTimeMillis();
Mono<String> invoke1 = Invoker1.invoke1();
Mono<String> invoker2 = Invoker2.invoke2();
Mono<String> result = Mono.zip(invoke1, invoker2)
.map(s-> {
String t1 = s.getT1();
String t2 = s.getT2();
return String.format("invoke1:%s, invoke2: %s", t1, t2);
});
// invoker1invoker2耗时:2650 (并行)
System.out.println("result: " + result.block() + ",耗时:" + (System.currentTimeMillis() - start));2.2 zipWith 方法zipWith() 每次组装一个Mono对象,使用于组装Mono个数比较少的情况。long start = System.currentTimeMillis();
Mono<String> invoke1 = Invoker1.invoke1();
Mono<String> invoker2 = Invoker2.invoke2();
Mono<String> result = invoke1.zipWith(invoker2)
.map(s -> {
return String.format("invoke1:%s, invoke2: %s", s.getT1(), s.getT2());
});
// invoker1invoker2耗时:2469 (并行)
System.out.println(result.block() + ",耗时:" + (System.currentTimeMillis() - start));3 前提这里的 invoker 就是第三方系统调用。保证 invoker 是在独立的线程中执行,这样 invoker 不会影响业务处理。public class Invoker1 {
public static Mono<String> invoke1() {
return Mono.
fromSupplier(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "invoker1";
})
.subscribeOn(Schedulers.parallel())
.doOnError(e -> {
System.out.println("error invoker1");
});
}
}public class Invoker2 {
public static Mono<String> invoke2() {
return Mono.fromSupplier(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "invoker2";
})
.subscribeOn(Schedulers.parallel())
.doOnError(e -> {
System.out.println("error invoker2");
});
}
}
绝对勇士
Java开发者的Python快速进修指南:文件操作
Python提供的文件操作相对于Java来说,确实简单方便许多。不仅操作简单,代码可读性也相对较高。然而,我们需要注意的不仅仅是文件操作的简单性,还有文件操作的各种模式。在Java中,我们并不经常使用像Python中那样的操作模式。另外,我们还需要注意文件指针的移动。无论是Java还是Python,文件都可以看作是IO流,流到哪里就算是哪里。除非重新对文件进行操作,否则想要回到文件开头,只能通过移动指针来实现。因此,在进行文件操作时,我们需要谨慎考虑文件指针的位置。基本语法和模式首先,我们需要使用open()函数来打开文件,并指定文件名和打开模式。常用的打开模式有多种选项,对于我们有经验的来说,r、w、a基本都能猜到他们所代表的英文意思。r:只读模式,从文件中读取数据(默认模式)。w:写入模式,首先清空文件内容,然后写入数据。a:追加模式,将数据写入文件末尾。b:二进制模式,用于处理二进制数据,也就是图片和视频文件了。你可以将"b"理解为"binary"的缩写t:文本模式(默认模式),用于处理文本文件。你可以将"t"理解为"text"的缩写# 打开文件 也可以是file = open("filename.txt", "r")
file = open("example.txt", "rt")
# 一次性读取文件内容
content = file.read()
print(content)
# 关闭文件
file.close()
除了示例中使用的rt模式,还有其他常用的模式,就是r、w和b、t的字母组合了:wt:以文本模式写入文件。如果文件不存在,则创建一个新文件;如果文件已存在,则清空文件内容。rb:以二进制模式读取文件。wb:以二进制模式写入文件。我们上面的写法是最基础的,为了保证文件操作的正确性和资源的释放,我们需要手动关闭文件。在Java中,可以使用try-with-resource语法来自动关闭流,而在Python中,我们也可以使用with语句来实现类似的功能,自动关闭文件,如下所示:with open("filename.txt", "r") as file:
content = file.read()
print(content)
当你在写入文件后,想要回到文件开头以便读取文件内容时,可以使用seek(0)将指针移动到文件的开头位置。以下是一个示例:with open("file.txt", "a+") as file:
file.write("This is a new line.")
file.seek(0)
content = file.read()
print(content)
使用seek(0)将指针移动到文件的开头位置。最后,我们使用read()函数读取整个文件的内容,并将其打印出来。指令后面的+号可以表示以读写方式打开文件。简单案例使用with open()语句可以更简洁地管理文件的打开和关闭,下面是使用with open()语句进行文件交换、删除源文件和重命名临时文件的示例代码:import os
# 源文件路径
source_file = "path/to/source_file.txt"
# 临时文件路径
temp_file = "path/to/temp_file.txt"
# 文件交换
with open(source_file, "rt") as file, open(temp_file, "wt") as temp:
content = file.read()
temp.write(content)
# 删除源文件
os.remove(source_file)
# 重命名临时文件为源文件
os.rename(temp_file, source_file)
这次我们第一次使用了import语句,这个语句的作用是导入包。通过导入包,我们可以直接使用写好的逻辑,而不需要自己去编写。Python之所以能够如此简洁,离不开各种强大的包的支持。实际上,文件交换部分的代码也可以利用包来实现,因为已经有其他人写好了相关的功能,就像我们需要实现列表功能时可以直接使用ArrayList一样。市面上已经有很多优秀的轮子可供使用,只需要直接拿来用,千万不要重复造轮子~~总结Python提供的文件操作相对于Java来说,更简单方便。不仅操作简单,代码可读性也更高。不过,我们还需要注意文件操作的各种模式和文件指针的移动。虽然文件操作只有几种方式,但我不会给出示例,避免浪费大家的时间和精力。
绝对勇士
Reactor 第五篇 多任务并发执行,结果按顺序返
1 场景调用多个平级服务,按照服务优先级返回第一个有效数据。具体case:一个页面可能有很多的弹窗,弹窗之间又有优先级。每次只需要返回第一个有数据的弹窗。但是又希望所有弹窗之间的数据获取是异步的。这种场景使用 Reactor 怎么实现呢?2 创建 service2.1 创建基本接口和实体类public interface TestServiceI {
Mono request();
}提供一个 request 方法,返回一个 Mono 对象。@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class TestUser {
private String name;
}2.2 创建 service 实现@Slf4j
public class TestServiceImpl1 implements TestServiceI {
@Override
public Mono request() {
log.info("execute.test.service1");
return Mono.fromSupplier(() -> {
try {
System.out.println("service1.threadName=" + Thread.currentThread().getName());
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "";
})
.map(name -> {
return new TestUser(name);
});
}
}第一个 service 执行耗时 500ms。返回空对象;创建第二个 service 执行耗时 1000ms。返回空对象;代码如上,改一下sleep时间即可。继续创建第三个 service 执行耗时 1000ms。返回 name3。代码如上,改一下 sleep 时间,以及返回为 name3。3 主体方法public static void main(String[] args) {
long startTime = System.currentTimeMillis();
TestServiceI testServiceImpl4 = new TestServiceImpl4();
TestServiceI testServiceImpl5 = new TestServiceImpl5();
TestServiceI testServiceImpl6 = new TestServiceImpl6();
List<TestServiceI> serviceIList = new ArrayList<>();
serviceIList.add(testServiceImpl4);
serviceIList.add(testServiceImpl5);
serviceIList.add(testServiceImpl6);
// 执行 service 列表,这样有多少个 service 都可以
Flux<Mono<TestUser>> monoFlux = Flux.fromIterable(serviceIList)
.map(service -> {
return service.request();
});
// flatMap(或者flatMapSequential) + map 实现异常继续下一个执行
Flux flux = monoFlux.flatMapSequential(mono -> {
return mono.map(user -> {
TestUser testUser = JsonUtil.parseJson(JsonUtil.toJson(user), TestUser.class);
if (Objects.nonNull(testUser) && StringUtils.isNotBlank(testUser.getName())) {
return testUser;
}
// null 在 reactor 中是异常数据。
return null;
})
.onErrorContinue((err, i) -> {
log.info("onErrorContinue={}", i);
});
});
Mono mono = flux.elementAt(0, Mono.just(""));
Object block = mono.block();
System.out.println(block + "blockFirst 执行耗时ms:" + (System.currentTimeMillis() - startTime));
}1、Flux.fromIterable 执行 service 列表,可以随意增删 service 服务。2、flatMap(或者flatMapSequential) + map + onErrorContinue 实现异常继续下一个执行。具体参考:Reactor 之 onErrorContinue 和 onErrorResume3、Mono mono = flux.elementAt(0, Mono.just("")); 返回第一个正常数据。执行输出:20:54:26.512 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
20:54:26.553 [main] INFO com.geniu.reactor.TestServiceImpl1 - execute.test.service1
service1.threadName=main
20:54:27.237 [main] INFO com.geniu.reactor.TestReactorOrderV2 - onErrorContinue=TestUser(name=)
20:54:27.237 [main] INFO com.geniu.reactor.TestServiceImpl2 - execute.test.service2
service5.threadName=main
20:54:28.246 [main] INFO com.geniu.reactor.TestReactorOrderV2 - onErrorContinue=TestUser(name=)
20:54:28.246 [main] INFO com.geniu.reactor.TestServiceImpl3 - execute.test.service3
service6.threadName=main
TestUser(name=name3)blockFirst 执行耗时ms:28951、service1 和 service2 因为返回空,所以继续下一个,最终返回 name3。2、查看总耗时:2895ms。service1 耗时 500,service2 耗时1000,service3 耗时 1000。发现耗时基本上等于 service1 + service2 + service3 。这是怎么回事呢?查看返回执行的线程,都是 main。总结:这样实现按照顺序返回第一个正常数据。但是执行并没有异步。下一步:如何实现异步呢?4 实现异步4.1 subcribeOn 实现异步修改 service 实现。增加 .subscribeOn(Schedulers.boundedElastic())如下:@Slf4j
public class TestServiceImpl1 implements TestServiceI {
@Override
public Mono request() {
log.info("execute.test.service1");
return Mono.fromSupplier(() -> {
try {
System.out.println("service1.threadName=" + Thread.currentThread().getName());
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "";
})
//增加subscribeOn
.subscribeOn(Schedulers.boundedElastic())
.map(name -> {
return new TestUser(name);
});
}
}再次执行输出如下:21:02:04.213 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
21:02:04.265 [main] INFO com.geniu.reactor.TestServiceImpl1 - execute.test.service1
service4.threadName=boundedElastic-1
21:02:04.300 [main] INFO com.geniu.reactor.TestServiceImpl2 - execute.test.service2
21:02:04.302 [main] INFO com.geniu.reactor.TestServiceImpl3 - execute.test.service3
service2.threadName=boundedElastic-2
service3.threadName=boundedElastic-3
21:02:04.987 [boundedElastic-1] INFO com.geniu.reactor.TestReactorOrderV2 - onErrorContinue=TestUser(name=)
21:02:05.307 [boundedElastic-2] INFO com.geniu.reactor.TestReactorOrderV2 - onErrorContinue=TestUser(name=)
TestUser(name=name6)blockFirst 执行耗时ms:12421、发现具体实现 sleep 的线程都不是 main 线程,而是 boundedElastic;2、最终执行耗时 1242ms,只比执行时间最长的 service2 和 service3 耗时 1000ms,多一些。证明是异步了。4.2 CompletableFuture 实现异步修改 service 实现,使用 CompletableFuture 执行耗时操作(这里是sleep,具体到项目中可能是外部接口调用,DB 操作等);然后使用 Mono.fromFuture 返回 Mono 对象。@Slf4j
public class TestServiceImpl1 implements TestServiceI{
@Override
public Mono request() {
log.info("execute.test.service1");
CompletableFuture<String> uCompletableFuture = CompletableFuture.supplyAsync(() -> {
try {
System.out.println("service1.threadName=" + Thread.currentThread().getName());
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "testname1";
});
return Mono.fromFuture(uCompletableFuture).map(name -> {
return new TestUser(name);
});
}
}执行返回如下:21:09:59.465 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
21:09:59.510 [main] INFO com.geniu.reactor.TestServiceImpl2 - execute.test.service2
service2.threadName=ForkJoinPool.commonPool-worker-1
21:09:59.526 [main] INFO com.geniu.reactor.TestServiceImpl3 - execute.test.service3
service3.threadName=ForkJoinPool.commonPool-worker-2
21:09:59.526 [main] INFO com.geniu.reactor.TestServiceImpl1 - execute.test.service1
service1.threadName=ForkJoinPool.commonPool-worker-3
21:10:00.526 [ForkJoinPool.commonPool-worker-1] INFO com.geniu.reactor.TestReactorOrder - onErrorContinue=TestUser(name=)
21:10:00.538 [ForkJoinPool.commonPool-worker-2] INFO com.geniu.reactor.TestReactorOrder - onErrorContinue=TestUser(name=)
TestUser(name=testname1)blockFirst 执行耗时ms:12381、耗时操作都是使用 ForkJoinPool 线程池中的线程执行。2、最终耗时和方法1基本差不多。大家都去试试吧~相关链接:Reactor 之 onErrorContinue 和 onErrorResumeReactor 之 flatMap vs map 详解
绝对勇士
Reactor 第一篇 Spring Boot 整合 Reactor
Reactor 是一个完全非阻塞的 JVM 响应式编程基础,有着高效的需求管理(背压的形式)。它直接整合 Java8 的函数式 API,尤其是 CompletableFuture, Stream,还有 Duration 。提供了可组合的异步化序列 API — Flux (对于 [N] 个元素) and Mono (对于 [0|1] 元素) — 并广泛实现 响应式Stream 规范。这次带大家从零开始,使用 Spring Boot 框架建立一个 Reactor 响应式项目。1 创建项目使用 start.spring.io/ 创建项目。添加依赖项:H2、Lombok、Spring Web、JPA、JDBC然后导入 Reactor 包<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>2 集成 H2 数据库application.properties 文件中添加 H2 数据连接信息。此外,端口使用 8081(随意,本地未被使用的端口即可)。server.port=8081
################ H2 数据库 基础配置 ##############
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.url=jdbc:h2:~/user
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database=h2
spring.jpa.hibernate.ddl-auto=update
spring.h2.console.path=/h2-console
spring.h2.console.enable=true3 创建测试类3.1 user 实体建立简单数据操作实体 User。import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
/**
* @Author: prepared
* @Date: 2022/8/29 21:40
*/
@Data
@NoArgsConstructor
@Table(name = "t_user")
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String userName;
private int age;
private String sex;
public User(String userName, int age, String sex) {
this.userName = userName;
this.age = age;
this.sex = sex;
}
}
3.2 UserRepository数据模型层使用 JPA 框架。import com.prepared.user.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* @Author: prepared
* @Date: 2022/8/29 21:45
*/
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
3.3 UserServiceservice 增加两个方法,add 方法,用来添加数据;list 方法,用来查询所有数据。所有接口返回 Mono/Flux 对象。最佳实践:所有的第三方接口、IO 耗时比较长的操作都可以放在 Mono 对象中。doOnError 监控异常情况;doFinally 监控整体执行情况,如:耗时、调用量监控等。import com.prepared.user.dao.UserRepository;
import com.prepared.user.domain.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.util.List;
/**
* @Author: prepared
* @Date: 2022/8/29 21:45
*/
@Service
public class UserService {
private Logger logger = LoggerFactory.getLogger(UserService.class);
@Resource
private UserRepository userRepository;
public Mono<Boolean> save(User user) {
long startTime = System.currentTimeMillis();
return Mono.fromSupplier(() -> {
return userRepository.save(user) != null;
})
.doOnError(e -> {
// 打印异常日志&增加监控(自行处理)
logger.error("save.user.error, user={}, e", user, e);
})
.doFinally(e -> {
// 耗时 & 整体健康
logger.info("save.user.time={}, user={}", user, System.currentTimeMillis() - startTime);
});
}
public Mono<User> findById(Long id) {
long startTime = System.currentTimeMillis();
return Mono.fromSupplier(() -> {
return userRepository.getReferenceById(id);
}).doOnError(e -> {
// 打印异常日志&增加监控(自行处理)
logger.error("findById.user.error, id={}, e", id, e);
})
.doFinally(e -> {
// 耗时 & 整体健康
logger.info("findById.user.time={}, id={}", id, System.currentTimeMillis() - startTime);
});
}
public Mono<List<User>> list() {
long startTime = System.currentTimeMillis();
return Mono.fromSupplier(() -> {
return userRepository.findAll();
}).doOnError(e -> {
// 打印异常日志&增加监控(自行处理)
logger.error("list.user.error, e", e);
})
.doFinally(e -> {
// 耗时 & 整体健康
logger.info("list.user.time={}, ", System.currentTimeMillis() - startTime);
});
}
public Flux<User> listFlux() {
long startTime = System.currentTimeMillis();
return Flux.fromIterable(userRepository.findAll())
.doOnError(e -> {
// 打印异常日志&增加监控(自行处理)
logger.error("list.user.error, e", e);
})
.doFinally(e -> {
// 耗时 & 整体健康
logger.info("list.user.time={}, ", System.currentTimeMillis() - startTime);
});
}
}
3.4 UserControllercontroller 增加两个方法,add 方法,用来添加数据;list 方法,用来查询所有数据。list 方法还有另外一种写法,这就涉及到 Mono 和 Flux 的不同了。返回List可以使用Mono<List<User>> ,也可以使用 Flux<User>。Mono<T> 是一个特定的 Publisher<T>,最多可以发出一个元素Flux<T> 是一个标准的 Publisher<T>,表示为发出 0 到 N 个元素的异步序列import com.prepared.user.domain.User;
import com.prepared.user.service.UserService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: prepared
* @Date: 2022/8/29 21:47
*/
@RestController
public class UserController {
@Resource
private UserService userService;
@RequestMapping("/add")
public Mono<Boolean> add() {
User user = new User("xiaoming", 10, "F");
return userService.save(user) ;
}
@RequestMapping("/list")
public Mono<List<User>> list() {
return userService.list();
}
}
@RequestMapping("/listFlux")
public Flux<User> listFlux() {
return userService.listFlux();
}3.5 SpringReactorApplication 添加注解支持Application 启动类添加注解 @EnableJpaRepositoriesimport org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
/**
* Hello world!
*/
@SpringBootApplication
@EnableJpaRepositories
public class SpringReactorApplication {
public static void main(String[] args) {
SpringApplication.run(SpringReactorApplication.class, args);
}
}测试启动项目,访问 localhost:8081/add,正常返回 true。查询所有数据,访问localhost:8081/list,可以看到插入的数据,已经查询出来了。PS:我这里执行了多次 add,所以有多条记录。后台日志:2022-09-05 20:13:17.385 INFO 15696 --- [nio-8082-exec-2] com.prepared.user.service.UserService : list.user.time=181,
执行了 UserService list() 方法的 doFinnally 代码块,打印耗时日志。总结响应式编程的优势是不会阻塞。那么正常我们的代码中有哪些阻塞的操作呢?Future 的 get() 方法;Reactor 中的 block() 方法,subcribe() 方法,所以在使用 Reactor 的时候,除非编写测试代码,否则不要直接调用以上两个方法;同步方法调用,所以高并发情况下,会使用异步调用(如Future)来提升响应速度。下一篇,讲解如何将熔断、限流框架 resilience4j 整合到项目中,敬请期待。
绝对勇士
Java开发者的Python快速进修指南:面向对象--高级篇
首先,让我来介绍一下今天的主题。今天我们将讨论封装、反射以及单例模式。除此之外,我们不再深入其他内容。关于封装功能,Python与Java大致相同,但写法略有不同,因为Python没有修饰符。而对于反射来说,我认为它比Java简单得多,不需要频繁地获取方法和属性,而是有专门的方法来实现。最后,我们将简单地实现一下单例模式,这样面向对象章节就告一段落了。封装(Encapsulation)封装是指将数据和方法封装在一个类中。在Python中,我们可以通过属性和方法来实现封装。属性可以通过getter和setter方法来访问和修改,而方法可以在类的内部进行访问和使用。然而,与Java不同的是,虽然方法在Python中是可以调用的,但Java不允许。另外,属性也有一些区别,如果属性以双下划线开头,并且没有声明属性,将无法直接访问。除非你动态赋值,那么将失去封装的作用。使用双下划线开头的属性是私有属性,下面是一个简单的示例代码:class Person:
def __init__(self, name, age):
self.__name = name #
def get_name(self):
return self.__name
def set_name(self, name):
self.__name = name
person = Person("xiaoyu")
print(person.get_name()) # 输出:xiaoyu
我们都是学习Java的,所以对于getter和setter方法的使用应该是基本常识了。记住在Python中,我们使用双下划线来定义私有属性,但实际上这只是一种约定,Python并没有真正的私有属性概念。我们可以通过一些特殊的方式来访问和修改私有属性,但这违背了封装的原则,不建议直接这样做。反射(Reflection)反射是一种强大的编程技术,它使得在运行时可以动态地获取和修改对象的属性和方法。在Python中,我们可以利用内置的getattr()、setattr()和hasattr()等函数来实现反射的功能。通过反射,我们可以在运行时根据需要获取或修改对象的属性和方法,从而实现更灵活和动态的编程。不过,我还是有原则的,毕竟Java作为一种商业生态体系成熟的编程语言,在各个领域都有着强大的应用和支持,这是其他语言所无法比拟的。下面是一个简单的示例代码:class MyClass:
def __init__(self, name):
self.name = name
def hello(self):
print("Hello, {}!".format(self.name))
def dance(self):
print("dance, {}!".format(self.name))
def cmd(self):
method_name = input("====>")
if hasattr(obj, method_name):
method = getattr(obj, method_name)
method()
obj = MyClass("xiaoyu")
obj.cmd()
这样就可以获取到方法然后去实现反射了,我就不演示setattr了,自行演示吧。单例模式(Singleton)单例模式是一种常用的设计模式,它可以确保一个类只有一个实例,并且提供一个全局访问点,方便其他对象对该实例进行调用。在Python中,我们可以通过使用模块级别的变量来实现单例模式,这种方式非常简洁和高效。下面是一个简单的示例代码,展示了如何在Python中实现单例模式:class Singleton:
_instance = None
@classmethod
def get_instance(cls):
if not cls._instance:
cls._instance = cls()
return cls._instance
s1 = Singleton.get_instance()
s2 = Singleton.get_instance()
print(s1 is s2) # 输出: True
与Java相似,Python中也可以使用classmethod装饰器来实现方法,只是在Python中我们称之为装饰器而非注解。另外,Python中也有一种类似于Java中常用的stream流处理for循环的高级用法,只不过在Python中这种写法是倒着的。所以人们称之为字典推导或列表推导。为了方便记忆,我一直称之为推倒。student = {
"name": "xiaoyu",
"age": 18
}
[print(key + ": " + str(value)) for key, value in student.items() if key == "name"]
# 输出 name: xiaoyu
总结在今天的课上,我们深入讨论了封装、反射和单例模式这几个重要的概念。我不想过多地赘述它们的细节,但是请大家务必记住它们的基本语法规则,因为这也是面向对象章节的结束。我希望大家能够牢牢掌握这些知识点,为未来的学习打下坚实的基础。
绝对勇士
Java开发者的Python快速实战指南:实用工具之PDF转DOCX文档(可视化界面)
首先,大家对Python语法的了解已经基本完成,现在我们需要开始进行各种练习。我为大家准备了一些练习题目,比如之前的向量数据库等,这些题目可以参考第三方的SDK来进行操作,文档也是比较完善的。这个过程有点像我们之前使用Java对接第三方接口的方式,所以今天我想开发一个很实用的工具类,用于将PDF转换为DOCX文档。我觉得这个工具非常实用,所以通过这个项目,我想带领那些在Python基础上还比较薄弱的同学们从零开始,一起完成这个项目。首先,我也刚开始接触这个项目,所以我并不知道如何实现。我的第一反应是去搜索引擎上查找是否有其他人已经实现了类似的功能,因为现在有很多优秀的开源项目可供参考。毕竟,站在巨人的肩膀上进行开发并不可耻,而是一种聪明的做法。幸运的是,我找到了一个名为"pdf2docx"的第三方包,它提供了非常优秀的功能。令人惊讶的是,仅仅几行代码就可以完成PDF转换为DOCX的工作。而且,转换结果也非常出色。让我们来看一下具体的实现过程。希望大家可以去仓库中查看源码,学习如何使用这个工具包,也欢迎大家在仓库中留言,提出任何问题或建议。一起进步,共同学习!仓库地址为:github.com/StudiousXia…PDF转DOCX文档第三方包:pdf2docxfrom pdf2docx import Converter
def convert_pdf_to_docx(pdf_path, docx_path):
# 创建一个转换器对象
converter = Converter(pdf_path)
# 将PDF转换为DOCX
converter.convert(docx_path, start=0, end=None)
# 关闭转换器
converter.close()
# 调用函数进行转换
pdf_path = "input.pdf"
docx_path = "output.docx"
convert_pdf_to_docx(pdf_path, docx_path)
他很容易理解,只需要你定义好文件路径即可完成转换操作。此外,我也不多解释了,因为start参数用于指定转换的起始页码,而end参数用于指定转换的结束页码。你可以根据需要设置这两个参数的值,如果不需要指定起始页码,可以将start参数设置为0;如果不需要指定结束页码,则可以将end参数设置为None。官方可视化界面代码很简单,但是如果是自己使用的话,每次都要写一次路径可能会很麻烦。不过你可以使用一个可视化交互界面来简化这个过程,这样会更方便一些。幸运的是,pdf2docx提供了一个简易版的界面,你可以在控制台中直接输入"pdf2docx gui"来启动。在界面中,你只需要选择要转换的PDF文件和一个文件夹作为保存路径,就可以完成转换操作了。这样的话,你就不需要每次都手动输入路径了。非常方便。简易版可交互界面但是,如果你对pdf2docx提供的界面不满意,并且觉得界面不够好看,那么可以考虑使用另一个第三方界面库,叫做gradio。我记得你之前在向量数据库中使用过这个库,对后端非常友好。你可以先写一个简单的界面,然后逐步优化它,以满足你的需求。gradio提供了很多功能和自定义选项,你可以根据自己的喜好来设计界面的外观和交互方式。然后慢慢优化吧。import gradio as gr
from pdf2docx import Converter
def convert_pdf_to_docx_with_display(pdf_file):
tmp_file = "./output.docx"
# Convert PDF to DOCX
cv = Converter(pdf_file)
cv.convert(tmp_file)
cv.close()
return tmp_file
def convert_and_display_pdf_to_docx(pdf_file):
docx_file = convert_pdf_to_docx_with_display(pdf_file)
return docx_file
iface = gr.Interface(
fn=convert_and_display_pdf_to_docx,
inputs=["file"],
outputs=["file"],
title="[努力的小雨] PDF to DOCX Converter",
description="上传pdf文件,并将其转化为docx文件",
)
iface.launch()
恩恩,我看着是相当不错的,这个小工具已经可以满足用户的需求了。效果图,你可以看看:优化版界面好的,目前可交互的资源还相对较少。然而,如果我们能够提前预览解析后的文字内容,有时就能避免不必要的下载。比如,在查看PDF文件时,我们只需要复制粘贴其中的文字,而无需下载整个文件。为了实现这一功能,我们可以考虑在文件底部添加一个额外的窗口,用于显示解析后的文字内容。通过提供复制粘贴功能,用户可以轻松地获取所需的文字信息。import gradio as gr
from pdf2docx import Converter
import docx2txt
def convert_pdf_to_docx_with_display(pdf_file):
tmp_file = "./output.docx"
# Convert PDF to DOCX
cv = Converter(pdf_file)
cv.convert(tmp_file)
cv.close()
# Extract text from DOCX
docx_text = docx2txt.process(tmp_file)
return tmp_file, docx_text
def convert_and_display_pdf_to_docx(pdf_file):
docx_file, docx_text = convert_pdf_to_docx_with_display(pdf_file)
return docx_file, docx_text
iface = gr.Interface(
fn=convert_and_display_pdf_to_docx,
inputs=["file"],
outputs=["file", "text"],
title="[努力的小雨] PDF to DOCX Converter",
description="上传pdf文件,并将其转化为docx文件且在界面单独显示文件的文字",
)
iface.launch()
当我们完成代码的修改后,运行一下,我发现效果与我预期的是一致的。至强版界面如果我们已经能够显示文字,那么是否还需要显示图片呢?考虑到PDF中常常包含图片,为了满足用户复制粘贴图片的需求,我认为单独开发一个窗口来保存图片是合理的。然而,在这个过程中,我遇到了一些困难,几乎是我的噩梦。我一直遇到报错,而且这些错误几乎是我之前从未遇到过的。就像当初学习Java的时候,总是需要上网搜索解决方法一样。在使用gradio时,我创建了一个画廊窗口,但是错误地以为它可以直接返回图像的二进制内容,所以没有进行保存,结果一直报错。后来,我保存了图像,问题得以解决。现在我们来修改代码,因为有很多重复的代码,我就不再一直复制粘贴了。# 此处省略部分代码
# Extract images from DOCX
images = []
image_dir = os.path.join(tmp_dir, "images")
os.makedirs(image_dir, exist_ok=True)
for embed, related_part in document.part.related_parts.items():
if isinstance(related_part, ImagePart):
image_path = os.path.join(image_dir, f'image_{embed}.png')
with open(image_path, 'wb') as f:
f.write(related_part.image.blob)
images.append(image_path)
return tmp_file, docx_text, images
# 此处省略部分代码
我将图片保存到一个文件夹中,并返回一个包含图片实体的列表。现在让我们来看一下效果:可以看到图片已经显示出来了,但我觉得交互性还不够,如果用户不想要前几页的PDF怎么办呢?为了解决这个问题,我将再添加一个输入框,让用户可以输入相关信息。让我们继续优化一下。inputs=["text","file"],
为了实现传参,我们可以修改输入参数的类型。这个过程非常简单。除了我之前演示的简单样式外,Gradio还有很多其他样式可供选择。我只是提供了一个最简单的示例,剩下的优化工作就交给你了。你可以根据需要选择适合的样式进行优化。这里我就不演示了,因为只要我们能够获取参数,我们就可以实现各种功能。就pdf转docx的可视化界面而言,我已经基本完成了它,它符合我的要求并且基本上令我满意。毕竟,我不需要去优化界面。总结pdf转docx文档是一个非常实用的功能,我只是简单地实现了一个可视化界面供用户操作。我这么做的目的之一是想更多地掌握gradio的使用方法,同时也加强对Python流行第三方包的熟悉程度,因为这些第三方包是快速开发的关键。我也希望你能从中有所收获,我已经公布了本期的源码地址,如果你觉得还不错,或者在自己编写的过程中遇到问题,可以简单地参考一下。不过,我仍然希望你能自己解决bug问题,这样一旦熟悉了,就知道如何处理,不用总是上网寻找解决方案。
绝对勇士
Java开发者的Python快速进修指南:函数基础
话不多说,今天我们要介绍的是函数。本系列文章追求短而精,今天我们将重点讨论函数以及与Java方法的区别。与Java方法不同,函数不需要像Java方法一样讲究修饰符等其他特性,它只需要使用"def"关键字进行声明。另外,函数的参数也与Java方法有所不同,Java方法中不存在默认参数的概念,而在Python中,函数参数是可以有默认值的,并且可以通过传递关键字参数的方式来指定参数顺序。此外,Python函数还具有可变参数的特性,不同于Java中的实现方式,Python使用星号符号(*)来实现可变参数。请注意这种写法的使用方式。更为强大的是,Python还提供了双星号符号(**)的写法,下面我们将详细讨论这一特性。最后,我们来谈谈返回值。与Java不同的是,Python函数可以返回多个值,而Java中需要将这些值封装成对象。Python的这种设计让我们能够更加方便地处理返回值。另外,Python还提供了一些内置函数,但如果你想使用Java的内置方法,很抱歉,你需要直接使用对象来调用这些方法。好了,接下来让我们简单了解一下函数的一些其他特性吧。函数声明在Python中,可以使用关键字def来声明函数。函数声明的基本语法如下:def 函数名(参数1, 参数2, ...):
# 函数体
# 执行的代码块
return 返回值
def关键字用于定义函数。函数名是你给函数起的名字,应该具有描述性。参数是可选的,你可以在括号内指定函数需要接收的输入参数。如果没有参数,括号仍然是必须的,但可以留空。函数体是函数的具体实现,包含一系列的语句和逻辑。return语句用于指定函数的返回值。可以选择省略return语句,这样函数将不会返回任何值。以下是一个简单的示例:def greet(name):
print("Hello, " + name + "!")
greet("xiaoyu") # 调用函数,输出 "Hello, xiaoyu!"
参数默认参数默认参数(Default arguments):函数定义时可以为参数指定默认值,这样在函数调用时如果没有传递对应参数的值,将使用默认值。def power(x, n=2):
return x ** n
result1 = power(2) # 调用函数,n使用默认值2
result2 = power(2, 3) # 调用函数,指定n为3
print(result1) # 输出 4
print(result2) # 输出 8
可变参数可变参数(Variable arguments):。与Java的...使用类似,有时候我们无法确定调用函数时会传递多少个参数,这时可以使用可变参数来接收不定数量的参数。在函数定义时,在参数前面加上一个星号*,这样传递的参数将被打包成一个元组def add(*numbers):
result = 0
for num in numbers:
result += num
return result
sum1 = add(1, 2, 3) # 调用函数,传递3个参数
sum2 = add(1, 2, 3, 4, 5) # 调用函数,传递5个参数
print(sum1) # 输出 6
print(sum2) # 输出 15
关键字参数关键字参数(Keyword arguments):当函数调用时,可以使用关键字参数来指定参数的名称和对应的值,这样参数的顺序可以任意。在函数定义时,在参数前面加上两个星号**,这样传递的参数将被打包成一个字典。def person_info(**info):
for key, value in info.items():
print(key + ": " + value)
person_info(name="Alice", age="25", city="New York") # 调用函数,传递关键字参数
以上我们之讲解了在Java中不常见的,常规用法就不讲解了,浪费时间。返回值有时候,Python中我们还可以在函数中返回多个值。实际上,Python中的多个返回值是以元组的形式返回的。我们可以通过解包操作将返回的元组拆分为多个变量。而Java中需要将这些值封装成对象下面是一个示例,演示了函数如何返回多个值:def calculate(a, b):
sum = a + b
difference = a - b
return sum, difference
result1, result2 = calculate(8, 3)
print(result1) # 输出 11
print(result2) # 输出 5
除了这一个我还没看到有啥别的大区别,Java同学注意一下!内置函数我举一些不好理解的例子吧,像min、max、sum这种数值操作我就不列举了,我们看下range、zip、all、any吧。这些你遇见了直接百度就可以明白的,无所谓记住什么的,写多了就记住了。range函数range(start, stop, step):range函数用于生成一个整数序列,可以用来遍历数字范围。它接受三个参数:起始值(可选,默认为0),结束值(必选),步长(可选,默认为1)。返回的对象是一个可迭代的序列。for i in range(1, 10, 2):
print(i)
# 输出:1 3 5 7 9
zip函数zip(*iterables):zip函数用于将多个可迭代对象进行配对。它接受任意个可迭代对象作为参数,并返回一个元组的迭代器,其中每个元组由输入迭代器中对应位置的元素组成。当输入的可迭代对象长度不一致时,zip函数会以最短的长度为准,超出部分将被忽略。names = ["Alice", "Bob", "xiaoyu"]
ages = [25, 30, 35]
for name, age in zip(names, ages):
print(name, age)
# 输出:
# Alice 25
# Bob 30
# xiaoyu 35
all函数all(iterable):all函数用于检查可迭代对象中的所有元素是否为真。如果可迭代对象中所有元素都为真,返回True;否则返回False。如果可迭代对象为空,则返回True。numbers = [1, 2, 3, 4, 5]
print(all(numbers)) # 输出:True
numbers = [0, 1, 2, 3, 4, 5]
print(all(numbers)) # 输出:False
numbers = []
print(all(numbers)) # 输出:True
any函数any(iterable):any函数用于检查可迭代对象中的任何一个元素是否为真。如果可迭代对象中任何一个元素为真,返回True;否则返回False。如果可迭代对象为空,则返回False。numbers = [0, 0, 0, 1]
print(any(numbers)) # 输出:True
numbers = [0, 0, 0]
print(any(numbers)) # 输出:False
numbers = []
print(any(numbers)) # 输出:False
总结本文介绍了函数的基本概念和与Java方法的区别。在Python中,函数使用"def"关键字进行声明,不需要像Java方法一样讲究修饰符等其他特性。函数的参数可以有默认值,并且可以通过传递关键字参数的方式来指定参数顺序。Python函数还具有可变参数和关键字参数的特性,可以接收不定数量的参数,并且参数的顺序可以任意。与Java不同的是,Python函数可以返回多个值,而Java需要将多个值封装成对象。此外,Python还提供了一些内置函数,如range、zip、all、any等。
绝对勇士
Java开发者的Python快速进修指南:实战之简单跳表
前言之前我已经将Python的基本语法与Java进行了比较,相信大家对Python也有了一定的了解。我不会选择去写一些无用的业务逻辑来加强对Python的理解。相反,我更喜欢通过编写一些数据结构和算法来加深自己对Python编程的理解。学习任何语言都一样。通过编写数据结构和算法,不仅可以加强我自己的思维能力,还能提高对Python编程语言的熟练程度。在这个过程中,我会不断地优化我的代码,以提高算法的效率和性能。我相信通过这种方式,我能够更好地掌握Python编程,并且在解决实际问题时能够更加灵活地运用Python的特性和语法。跳表今天我们来使用Python实现一个简易版本的跳表。所谓跳表就是一种跳跃式的数据结构。假设你是一位图书馆管理员,你需要在图书馆的书架上找到一本特定的书。如果图书馆只是一个普通的书架,你需要逐本书进行查找,这样会花费很多时间和精力。然而,如果图书馆采用了跳表这种数据结构,书架上的书被分成了几个层次,每一层都有一个索引,上面标注了每本书的位置信息。当你需要找到一本书时,你可以先查看最高层的索引,快速定位到可能包含该书的区域,然后再在该区域内根据索引逐步查找,直到找到目标书籍。这样,跳表的索引层就相当于图书馆的书籍分类系统,它提供了一个快速查找的方法。通过索引层,你可以迅速定位到书籍所在的区域,减少了查找的次数和时间。跳表主要的思想是利用索引的概念。因此,每个节点除了保存下一个链表节点的地址之外,还需要额外存储索引地址,用于指示下一步要跳转的地址。它在有序链表的基础上增加了多层索引,以提高查找效率。而且这适合于读多写少的场景。在实现过程中,无论是在插入数据完毕后重新建立索引,还是在插入数据的同时重新建立索引,都会导致之前建立的索引丢弃,浪费了大量时间。而且,如果考虑多线程的情况,情况会更糟糕。写这种东西时,通常先实现一个简单版,然后根据各个环节进行优化,逐步改进算法。因此,我们今天先实现一个简单版的跳表。具体实现我们先来实现一个简单版的跳表,不动态规定步长。我们可以先定义一个固定的步长,比如2。为了实现跳表,我们需要定义一个节点的数据结构。这个节点包含以下信息:当前节点的值(value),指向前一个节点的指针(before_node),指向后一个节点的指针(next_node),以及指向索引节点的指针(index_node)。class SkipNode:
def __init__(self,value,before_node=None,next_node=None,index_node=None):
self.value = value
self.before_node = before_node
self.next_node = next_node
self.index_node = index_node
head = SkipNode(-1)
tail = SkipNode(-1)
为了方便操作,我先生成了两个特殊节点,一个是头节点,另一个是尾节点。头节点作为跳表的起始点,尾节点作为跳表的结束点。数据插入在跳表中插入节点时,我们按照从小到大的升序进行排序。插入节点时,无需维护索引节点。一旦完成插入操作,我们需要重新规划索引节点,以确保跳表的性能优化。def insert_node(node):
if head.next_node is None:
head.next_node = node
node.next_node = tail
node.before_node = head
tail.before_node = node
return
temp = head.next_node
# 当遍历到尾节点时,需要直接插入
while temp.next_node is not None or temp == tail:
if temp.value > node.value or temp == tail:
before = temp.before_node
before.next_node = node
temp.before_node = node
node.before_node = before
node.next_node = temp
break
temp = temp.next_node
re_index()
重建索引为了重新规划索引,我们可以先将之前已经规划好的索引全部删除。然后,我们可以使用步长为2的方式重新规划索引。def re_index():
step = 2
# 用来建立索引的节点
index_temp = head.next_node
# 用来遍历的节点
temp = head.next_node
while temp.next_node is not None:
temp.index_node = None
if step == 0:
step = 2
index_temp.index_node = temp
index_temp = temp
temp = temp.next_node
step -= 1
查询节点查询:从头节点开始查询,根据节点的值与目标值进行比较。如果节点的值小于目标值,则向右移动到下一个节点或者索引节点继续比较。如果节点的值等于目标值,则找到了目标节点,返回结果。如果节点的值大于目标值,则则说明目标节点不存在。def search_node(value):
temp = head.next_node
step = 0
while temp.next_node is not None:
step += 1
if value == temp.value:
print(f"该值已找到,经历了{step}次查询")
return
elif value < temp.value:
print(f"该值在列表不存在,经历了{step}次查询")
return
if temp.index_node is not None and value > temp.index_node.value:
temp = temp.index_node
else:
temp = temp.next_node
print(f"该值在列表不存在,经历了{step}次查询")
遍历为了方便查看,我特意编写了一个用于遍历和查看当前数据的功能,以便更清楚地了解数据的结构和内容。def print_node():
my_list = []
temp = head.next_node
while temp.next_node is not None:
if temp.index_node is not None:
my_dict = {"current_value": temp.value, "index_value": temp.index_node.value}
else:
my_dict = {"current_value": temp.value, "index_value": None} # 设置一个默认值为None
my_list.append(my_dict)
temp = temp.next_node
for item in my_list:
print(item)
查看结果所有代码已经准备完毕,现在我们可以在另一个文件中运行并查看跳表的内容和数据。让我们快速进行操作一下。import skipList
import random
for i in range(0,10):
random_number = random.randint(1, 100)
temp = skipList.SkipNode(random_number)
skipList.insert_node(temp)
skipList.print_node()
skipList.search_node(89)
以下是程序的运行结果。为了方便查看,我特意打印了索引节点的值,以告诉你要跳到哪一个节点。总结通过实现一个简易版本的跳表,可以加深了对Python编程的理解。跳表是一种跳跃式的数据结构,通过索引层提供快速查找的能力,提高了查找的效率。在实现跳表的过程中,会更加熟悉了Python的语法和特性,并且可以更加灵活地运用它来解决实际问题。
绝对勇士
Reactor第十篇 定制一个生产的WebClient
1 为什么要用 WebClient刚开始尝试使用 Spring WebFlux 的时候,很多人都会使用 Mono.fromFuture() 将异步请求转成 Mono 对象,或者 Mono.fromSupplier() 将请求转成 MOno 对象,这两种方式在响应式编程中都是不建议的,都会阻塞当前线程。1.1 Mono.fromFuture() VS WebClientMono.fromFuture()方法和使用 WebClient 调用第三方接口之间存在以下区别:异步 vs. 非阻塞Mono.fromFuture()方法适用于接收一个 java.util.concurrent.Future 对象,并将其转换为响应式的 Mono。这是一个阻塞操作,因为它会等待 Future 对象完成。而使用 WebClient 调用第三方接口是异步和非阻塞的,它不会直接阻塞应用程序的执行,而是使用事件驱动的方式处理响应。可扩展性和灵活性:使用 WebClient 可以更灵活地进行配置和处理,例如设置超时时间、请求头、重试机制等。WebClient 还可以与许多其他 Spring WebFlux 组件集成,如 WebSockets、Server-Sent Events 等。而 Mono.fromFuture() 是适用于单个 Future 对象转化为 Mono 的情况,可扩展性较差。错误处理WebClient 提供了更丰富的错误处理机制,可以通过 onStatus、onError 等方法来处理不同的 HTTP 状态码或异常。同时,WebClient 还提供了更灵活的重试和回退策略。Mono.fromFuture() 方法只能将 Future 对象的结果包装在 Mono 中,不提供特定的错误处理机制。阻塞操作Mono.fromFuture() 会阻塞。当调用 Mono.fromFuture() 方法将 Future 转换为 Mono 时,它会等待 Future 对象的结果返回。在这个等待的过程中,Mono.fromFuture()方法会阻塞当前的线程。这意味着,如果 Future 的结果在运行过程中没有返回,则当前线程会一直阻塞,直到 Future 对象返回结果或者超时。因此,在使用 Mono.fromFuture() 时需要注意潜在的阻塞风险。另外,需要确保F uture 的任务在后台线程中执行,以免阻塞应用程序的主线程。1.2 Mono.fromFuture VS Mono.fromSupplierMono.fromSupplier() 和 Mono.fromFuture() 都是用于将异步执行的操作转换为响应式的 Mono 对象,但它们的区别在于:Mono.fromSupplier() 适用于一个提供者/生产者,可以用来表示某个操作的结果,该操作是一些纯计算并且没有阻塞的方法。也就是说,Mono.fromSupplier() 将其参数 (Supplier) 所提供的操作异步执行,并将其结果打包成一个 Mono 对象。Mono.fromFuture() 适用于一个 java.util.concurrent.Future 对象,将其封装成 Mono 对象。这意味着调用 Mono.fromFuture() 方法将阻塞当前线程,直到异步操作完成返回一个 Future 对象。因此,Mono.fromSupplier() 与 Mono.fromFuture() 的主要区别在于:Mono.fromSupplier() 是一个非阻塞的操作,不会阻塞当前线程。这个方法用于执行计算型的任务,返回一个封装了计算结果的 Mono 对象。Mono.fromFuture() 是阻塞操作,会阻塞当前线程,直到异步操作完毕并返回看,它适用于处理 java.util.concurrent.Future 对象。需要注意的是,如果 Supplier 提供的操作是阻塞的,则 Mono.fromSupplier() 方法本身也会阻塞线程。但通常情况下,Supplier 提供的操作是纯计算型的,不会阻塞线程。因此,可以使用 Mono.fromSupplier() 方法将一个纯计算型的操作转换为 Mono 对象,而将一个异步返回结果的操作转换为 Mono 对象时,可以使用 Mono.fromFuture() 方法。2 定制化自己的 WebClient2.1 初始化 WebClientWebClient 支持建造者模式,使用 WebClient 建造者模式支持开发自己的个性化 WebClient,比如支持设置接口调用统一耗时、自定义底层 Http 客户端、调用链路、打印接口返回日志、监控接口耗时等等。WebClient builder 支持以下方法interface Builder {
/**
* 配置请求基础的url,如:baseUrl = "https://abc.go.com/v1";和 uriBuilderFactory 冲突,如果有 uriBuilderFactory ,则忽略 baseUrl
*/
Builder baseUrl(String baseUrl);
/**
* URI 请求的默认变量。也和 uriBuilderFactory 冲突,如果有 uriBuilderFactory ,则忽略 defaultUriVariables
*/
Builder defaultUriVariables(Map<String, ?> defaultUriVariables);
/**
* 提供一个预配置的UriBuilderFactory实例
*/
Builder uriBuilderFactory(UriBuilderFactory uriBuilderFactory);
/**
* 默认 header
*/
Builder defaultHeader(String header, String... values);
/**
* 默认cookie
*/
Builder defaultCookie(String cookie, String... values);
/**
* 提供一个 consumer 来定制每个请求
*/
Builder defaultRequest(Consumer<RequestHeadersSpec<?>> defaultRequest);
/**
* 添加一个filter,可以添加多个
*/
Builder filter(ExchangeFilterFunction filter);
/**
* 配置要使用的 ClientHttpConnector。这对于插入或自定义底层HTTP 客户端库(例如SSL)的选项非常有用。
*/
Builder clientConnector(ClientHttpConnector connector);
/**
* Configure the codecs for the {@code WebClient} in the
* {@link #exchangeStrategies(ExchangeStrategies) underlying}
* {@code ExchangeStrategies}.
* @param configurer the configurer to apply
* @since 5.1.13
*/
Builder codecs(Consumer<ClientCodecConfigurer> configurer);
/**
* 提供一个预先配置了ClientHttpConnector和ExchangeStrategies的ExchangeFunction。
这是对 clientConnector 的一种替代,并且有效地覆盖了它们。
*/
Builder exchangeFunction(ExchangeFunction exchangeFunction);
/**
* Builder the {@link WebClient} instance.
*/
WebClient build();
// 其他方法
}2.2 日志打印及监控打印参数、url、返回参数和返回需要转成json需要打印正常返回日志和异常正常监控、异常监控、总监控以及响应时间.doOnSuccess(response-> {
log.info("get.success, url={}, response={}, param={}", url, response);
})
.doOnError(error-> {
log.info("get.error, url={}", url, error);
// 监控
})
.doFinally(res-> {
//监控
})2.3 返回处理retrieve() // 声明如何提取响应。例如,提取一个ResponseEntity的状态,头部和身体:.bodyToMono(clazz) 将返回body内容转成clazz对象,clazz 对象可以自己指定类型。如果碰到有问题的无法转化的,也可以先转成String,然后自己实现一个工具类,将String转成 class 对象。2.3.1 getpublic <T> Mono<T> get(String url, Class<T> clazz, T defaultClass) {
long start = System.currentTimeMillis();
return webClient.get()
.uri(url)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(clazz)
.doOnSuccess(response-> {
log.info("get.success, url={}, response={}, param={}", url, response);
})
.doOnError(error-> {
log.info("get.param.error, url={}", url, error);
})
.onErrorReturn(defaultClass)
.doFinally(res-> {
})
.publishOn(customScheduler);
}2.3.2 get param 请求public <T> Mono<T> getParam(String url, MultiValueMap<String, String> param, Class<T> clazz, T defaultClass) {
long start = System.currentTimeMillis();
URI uri = UriComponentsBuilder.fromUriString(url)
.queryParams(param)
.build()
.toUri();
return webClient.get()
.uri(uri)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(clazz)
.doOnSuccess(response-> {
log.info("get.param.success, url={}, response={}, param={}", url, response, JsonUtil.toJson(param));
})
.doOnError(error-> {
log.error("get.param.error, url={}, param={}", url, JsonUtil.toJson(param), error);
})
.onErrorReturn(defaultClass)
.doFinally(res-> {
// 监控 or 打印日志 or 耗时
})
.publishOn(customScheduler);
}2.3.3 post json 请求public <T> Mono<T> postJson(String url, final HttpParameter4Json parameter, Class<T> clazz, T defaultClass) {
final long start = System.currentTimeMillis();
return webClient.post()
.uri(url)
.contentType(MediaType.APPLICATION_JSON)
.cookies(cookies -> cookies.setAll(parameter.getCookies()))
.body(Mono.just(parameter.getJsonBody()), ParameterizedTypeReference.forType(parameter.getBodyType()))
.headers(headers -> headers.setAll(parameter.getHeaders()))
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(clazz)
.doOnSuccess(response-> {
log.info("post.json.success, url={}, response={}, param={}", url, response, parameter.getJsonBody());
})
.doOnError(error-> {
log.error("get.param.error, url={}, param={}", url, parameter.getJsonBody(), error);
})
.onErrorReturn(defaultClass)
.doFinally(res-> {
})
.publishOn(customScheduler);
}2.3.4 post form Data 请求public <T> Mono<T> postFormData(String url, HttpParameter parameter, Class<T> clazz, T defaultClass) {
final long start = System.currentTimeMillis();
return webClient.post()
.uri(url)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.cookies(cookies -> cookies.setAll(parameter.getCookies()))
.body(BodyInserters.fromFormData(parameter.getMultiValueMapParam()))
.headers(headers -> headers.setAll(parameter.getMapHeaders()))
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(clazz)
.doOnSuccess(response-> {
log.info("post.fromData.success, url={}, response={}, param={}", url, response, JsonUtil.toJson(parameter));
})
.doOnError(error-> {
log.info("get.param.error, url={}, param={}", url, JsonUtil.toJson(parameter), error);
})
.onErrorReturn(defaultClass)
.doFinally(res-> {
})
.publishOn(customScheduler);
}2.4 异常处理2.4.1 异常返回兜底onErrorReturn 发现异常返回兜底数据2.4.2 异常处理状态码转成异常抛出.onStatus(HttpStatus::isError, response -> Mono.error(new RuntimeException("Request failed with status code: " + response.statusCode())))
监控异常.doOnError(error -> {
// log and monitor
})3 完整的 WebClient
package com.geniu.reactor.webclient;
import com.geniu.utils.JsonUtil;
import io.netty.channel.ChannelOption;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;
import reactor.netty.resources.LoopResources;
import reactor.netty.tcp.SslProvider;
import reactor.netty.tcp.TcpClient;
import java.net.URI;
import java.time.Duration;
import java.util.function.Function;
/**
* @Author: prepared
* @Date: 2023/8/15 11:05
*/
@Slf4j
public class CustomerWebClient {
public static final CustomerWebClient instance = new CustomerWebClient();
/**
* 限制并发数 100
*/
Scheduler customScheduler = Schedulers.newParallel("CustomScheduler", 100);
private final WebClient webClient;
private CustomerWebClient() {
final SslContextBuilder sslBuilder = SslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE);
final SslProvider ssl = SslProvider.builder().sslContext(sslBuilder)
.defaultConfiguration(SslProvider.DefaultConfigurationType.TCP).build();
final int cpuCores = Runtime.getRuntime().availableProcessors();
final int selectorCount = Math.max(cpuCores / 2, 4);
final int workerCount = Math.max(cpuCores * 2, 8);
final LoopResources pool = LoopResources.create("HCofSWC", selectorCount, workerCount, true);
final Function<? super TcpClient, ? extends TcpClient> tcpMapper = tcp -> tcp
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
.option(ChannelOption.SO_TIMEOUT, 10000)
.secure(ssl)
.runOn(pool);
ConnectionProvider.Builder httpClientOfSWC = ConnectionProvider
.builder("HttpClientOfSWC")
.maxConnections(100_000)
.pendingAcquireTimeout(Duration.ofSeconds(6));
final ConnectionProvider connectionProvider = httpClientOfSWC.build();
final HttpClient hc = HttpClient.create(connectionProvider)
.tcpConfiguration(tcpMapper);
final Function<HttpClient, HttpClient> hcMapper = rhc -> rhc
.compress(true);
final WebClient.Builder wcb = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(hcMapper.apply(hc)));
// .filter(new TraceRequestFilter()); 可以通过Filter 增加trace追踪
this.webClient = wcb.build();
}
public <T> Mono<T> get(String url, Class<T> clazz, T defaultClass) {
long start = System.currentTimeMillis();
return webClient.get()
.uri(url)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::isError, response -> Mono.error(new RuntimeException("Request failed with status code: " + response.statusCode())))
.bodyToMono(clazz)
.doOnSuccess(response-> {
log.info("get.success, url={}, response={}, param={}", url, response);
})
.doOnError(error-> {
log.info("get.param.error, url={}", url, error);
})
.onErrorReturn(defaultClass)
.doFinally(res-> {
})
.publishOn(customScheduler);
}
public <T> Mono<T> getParam(String url, MultiValueMap<String, String> param, Class<T> clazz, T defaultClass) {
long start = System.currentTimeMillis();
URI uri = UriComponentsBuilder.fromUriString(url)
.queryParams(param)
.build()
.toUri();
return webClient.get()
.uri(uri)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(clazz)
.doOnSuccess(response-> {
log.info("get.param.success, url={}, response={}, param={}", url, response, JsonUtil.toJson(param));
})
.doOnError(error-> {
log.error("get.param.error, url={}, param={}", url, JsonUtil.toJson(param), error);
})
.onErrorReturn(defaultClass)
.doFinally(res-> {
})
.publishOn(customScheduler);
}
public <T> Mono<T> postJson(String url, final HttpParameter4Json parameter, Class<T> clazz, T defaultClass) {
final long start = System.currentTimeMillis();
return webClient.post()
.uri(url)
.contentType(MediaType.APPLICATION_JSON)
.cookies(cookies -> cookies.setAll(parameter.getCookies()))
.body(Mono.just(parameter.getJsonBody()), ParameterizedTypeReference.forType(parameter.getBodyType()))
.headers(headers -> headers.setAll(parameter.getHeaders()))
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(clazz)
.doOnSuccess(response-> {
log.info("post.json.success, url={}, response={}, param={}", url, response, parameter.getJsonBody());
})
.doOnError(error-> {
log.error("get.param.error, url={}, param={}", url, parameter.getJsonBody(), error);
})
.onErrorReturn(defaultClass)
.doFinally(res-> {
})
.publishOn(customScheduler);
}
public <T> Mono<T> postFormData(String url, HttpParameter parameter, Class<T> clazz, T defaultClass) {
final long start = System.currentTimeMillis();
return webClient.post()
.uri(url)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.cookies(cookies -> cookies.setAll(parameter.getCookies()))
.body(BodyInserters.fromFormData(parameter.getMultiValueMapParam()))
.headers(headers -> headers.setAll(parameter.getMapHeaders()))
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(clazz)
.doOnSuccess(response-> {
log.info("post.fromData.success, url={}, response={}, param={}", url, response, JsonUtil.toJson(parameter));
})
.doOnError(error-> {
log.info("get.param.error, url={}, param={}", url, JsonUtil.toJson(parameter), error);
})
.onErrorReturn(defaultClass)
.doFinally(res-> {
})
.publishOn(customScheduler);
}
}
绝对勇士
Java开发者的Python快速进修指南:异常捕获
在之前的学习中,我们已经讲解了函数和控制流等基本概念。然而,在接触实际业务时,你会发现异常捕获也是必不可少的一部分,因为在Java编程中,异常处理是不可或缺的。Python的异常捕获与Java的异常捕获原理是相同的,只是在写法上有一些区别。它们的目的都是为了处理程序在执行过程中出现错误的机制。通过捕获异常,我们可以在遇到错误时进行适当的处理,而不是直接终止程序的执行。在接下来的内容中,我将介绍一些常见的异常情况,以及万能异常捕获(在工作中常常使用,即无论什么错误都直接抛出一个通用异常),还有为了处理业务逻辑而自定义的异常类。基本语法需要注意的是,在Python中,else块和finally块是可选的。你可以选择将它们完全写在try语句块里,就像在Java中一样。关于这一点,我就不再详细解释了。try:
# 可能引发异常的代码块
except ExceptionType1:
# 处理ExceptionType1类型的异常
except ExceptionType2:
# 处理ExceptionType2类型的异常
else:
# 如果没有发生任何异常,执行该块的代码
finally:
# 无论是否发生异常,都会执行该块的代码
常见异常就举一个异常例子吧,不多说占用精力了,自己有时间现查询百度都行。举例来说,当我们尝试将一个非整数的字符串转换为整数时,会触发ValueError异常。下面是一个处理ValueError异常的示例代码:try:
num = int(input("请输入一个整数: "))
print("你输入的整数是:", num)
except ValueError:
print("无效的输入,请输入一个整数")
其他常见异常:TypeError:类型错误,当一个操作或函数应用于不适当类型的对象时抛出。IndexError:索引错误,当尝试访问一个不存在的索引时抛出。KeyError:键错误,当尝试访问字典中不存在的键时抛出。FileNotFoundError:文件未找到错误,当试图打开一个不存在的文件时抛出。ZeroDivisionError:零除错误,当尝试除以零时抛出。万能异常捕获我觉得使用万能异常捕获也是一种优化语句的方法。就像在Java中一样,直接捕获Exception异常可以处理所有可能的异常情况,这种做法也很容易记住。不过需要注意的是,虽然这种方式可以简化代码,但有时候会隐藏潜在的问题,因此在实际使用时还是需要谨慎考虑。try:
# 可能引发异常的代码块
except Exception as e:
# 处理异常的代码块
自定义异常写自定义异常时,你会发现跟Java一样的思路,这就是为什么从Java转向Python的过程非常简单。你已经具备了各种能力,只需要用另一种语法重新实现一次即可。事实上,所有的编程语言都有相似之处,包括前端的Vue、React等框架也是如此。这意味着你可以在不同的语言中迅速适应和转换,因为它们之间存在共通的原理和概念。所以,只要你理解了一种编程语言,学习和掌握其他语言就会变得更加容易。class MyException(Exception):
def __init__(self, message):
self.message = message
def __str__(self):
return self.message
try:
# 可能引发自定义异常的代码块
raise MyException("This is a custom exception.")
except MyException as e:
# 处理自定义异常的代码块
print(e)
直接抛出Exception确实是一种简洁的写法。如果时间紧迫或者只是临时测试代码,这样做可以省去定义自定义异常的步骤。不过,需要注意的是,直接抛出Exception会导致代码的可读性和可维护性降低。定义自定义异常可以更好地表达代码的意图,并且提供了更好的错误信息和异常处理方式。所以,在实际项目中,我建议还是尽可能使用自定义异常来提高代码的可读性和可维护性。总结在本篇文章中,我们总结了Python中的异常捕获的重要性以及如何进行优化。异常捕获是一种处理程序在执行过程中出现错误的机制,对于程序的稳定性和可靠性至关重要。我们详细学习了Python中的基本异常捕获语法,包括try、except、else和finally块,并举例了常见的异常类型,总之,阅读本文只需5分钟,你就可以轻松掌握Python异常捕获的技巧,为自己的编程之路增添一份宝贵的经验。
绝对勇士
Java开发者的Python快速进修指南:实战之跳表pro版本
之前我们讲解了简易版的跳表,我希望你能亲自动手实现一个更完善的跳表,同时也可以尝试实现其他数据结构,例如动态数组或哈希表等。通过实践,我们能够发现自己在哪些方面还有所欠缺。这些方法只有在熟练掌握之后才会真正理解,就像我在编写代码的过程中,难免会忘记一些方法或如何声明属性等等。我不太愿意写一些业务逻辑,例如典型的购物车逻辑,因为这对个人的成长没有太大帮助,反而可能使我们陷入业务误区。但是,数据结构与算法则不同。好了,言归正传,现在我们来看看如何对之前的简易版跳表进行优化。关于跳表的解释我就不再赘述了。在上一篇中,我们只定义了一个固定步长为2的跳表,使节点可以进行跳跃查询,而不是遍历节点查询。然而,真正的跳表有许多跳跃步长的选择,并不仅限于单一的步长。因此,今天我们将实现多个跳跃步长的功能,先从简单的开始练习,例如增加一个固定的跳跃步长4。如果一个节点具有多个跳跃步长,我们就不能直接用单独的索引节点来表示了,而是需要使用列表来存储。否则,我们将不得不为每个步长定义一个索引节点。因此,我修改了节点的数据结构如下:class SkipNode:
def __init__(self,value,before_node=None,next_node=None,index_node=None):
self.value = value
self.before_node = before_node
self.next_node = next_node
# 这是一个三元表达式
self.index_node = index_node if index_node is not None else []
在这个优化过程中,我们使用了一个三元表达式。在Python中,没有像Java语言中的三元运算符(?:)那样的写法。不过,我们可以换一种写法:[值1] if [条件] else [值2],这与 [条件] ? [值1] : [值2] 是等价的。我们不需要对插入数据的逻辑实现进行修改。唯一的区别在于我们将重新建立索引的方法名更改为re_index_pro。为了节省大家查阅历史文章的时间,我也直接将方法贴在下面。def insert_node(node):
if head.next_node is None:
head.next_node = node
node.next_node = tail
node.before_node = head
tail.before_node = node
return
temp = head.next_node
# 当遍历到尾节点时,需要直接插入
while temp.next_node is not None or temp == tail:
if temp.value > node.value or temp == tail:
before = temp.before_node
before.next_node = node
temp.before_node = node
node.before_node = before
node.next_node = temp
break
temp = temp.next_node
re_index_pro()
为了提高性能,我们需要对索引进行升级和重新规划。具体操作包括删除之前已规划的索引,并新增索引步长为2和4。def re_index_pro():
step = 2
second_step = 4
# 用来建立步长为2的索引的节点
index_temp_for_one = head.next_node
# 用来建立步长为4的索引的节点
index_temp_for_second = head.next_node
# 用来遍历的节点
temp = head.next_node
while temp.next_node is not None:
temp.index_node = []
if step == 0:
step = 2
index_temp_for_one.index_node.append(temp)
index_temp_for_one = temp
if second_step == 0:
second_step = 4
index_temp_for_second.index_node.append(temp)
index_temp_for_second = temp
temp = temp.next_node
step -= 1
second_step -= 1
我们需要对查询方法进行优化,虽然不需要做大的改动,但由于我们的索引节点已更改为列表存储,因此需要从列表中获取值,而不仅仅是从节点获取。在从列表中获取值的过程中,你会发现列表可能有多个节点,但我们肯定先要获取最大步长的节点。如果确定步长太大,我们可以缩小步长,如果仍然无法满足要求,则需要遍历节点。def search_node(value):
temp = head.next_node
# 由于我们有了多个索引节点,所以我们需要知道跨步是否长了,如果长了需要缩短步长,也就是寻找低索引的节点。index_node[1] --> index_node[0]
step = 0
while temp.next_node is not None:
step += 1
if value == temp.value:
print(f"该值已找到,经历了{step}次查询")
return
elif value < temp.value:
print(f"该值在列表不存在,经历了{step}次查询")
return
if temp.index_node:
for index in range(len(temp.index_node) - 1, -1, -1):
if value > temp.index_node[index].value:
temp = temp.index_node[index]
break
else:
temp = temp.next_node
else:
temp = temp.next_node
print(f"该值在列表不存在,经历了{step}次查询")
为了使大家更容易查看数据和索引的情况,我对节点遍历的方法进行了修改,具体如下所示:def print_node():
my_list = []
temp = head.next_node
while temp.next_node is not None:
if temp.index_node:
my_dict = {"current_value": temp.value, "index_value": [node.value for node in temp.index_node]}
else:
my_dict = {"current_value": temp.value, "index_value": temp.index_node} # 设置一个默认值为None
my_list.append(my_dict)
temp = temp.next_node
for item in my_list:
print(item)
为了进一步优化查询结果,我们可以简单地运行一下,通过图片来观察优化的效果。从结果可以看出,我们确实减少了两次查询的结果,这是一个很好的进展。然而,实际的跳表结构肯定比我简化的要复杂得多。例如,步长可能不是固定的,因此我们需要进一步优化。由于我们已经将索引节点改为列表存储,所以我们能够进行一些较大的修改的地方就是重建索引的方法。为了实现动态设置步长,我需要获取当前列表的长度。为此,我在文件中定义了一个名为total_size的变量,并将其初始值设置为0。在插入操作时,我会相应地对total_size进行修改。由于多余的代码较多,我不会在此粘贴。def insert_node(node):
global total_size
total_size += 1
if head.next_node is None:
# 此处省略重复代码。
在这个方法中,我们使用了一个global total_size,这样定义的原因是因为如果我们想要在函数内部修改全局变量,就必须这样写。希望你能记住这个规则,不需要太多的解释。Python没有像Java那样的限定符。def re_index_fin():
# 使用字典模式保存住step与前一个索引的关系。
temp_size = total_size
dict = {}
dict_list = []
# 这里最主要的是要将字典的key值与节点做绑定,要不然当设置索引值时,每个源节点都不一样。
while int((temp_size / 2)) > 1:
temp_size = int((temp_size / 2))
key_str = f"step_{temp_size}"
# 我是通过key_str绑定了temp_size步长,这样当这个步长被减到0时,步长恢复到旧值时,我能找到之前的元素即可。
dict[key_str] = head.next_node
dict_list.append(temp_size)
# 备份一下,因为在步长减到0时需要恢复到旧值
backup = list(dict_list)
# 用来遍历的节点
temp = head.next_node
while temp.next_node is not None:
temp.index_node = []
# 直接遍历有几个步长
for i in range(len(dict_list)):
dict_list[i] -= 1 # 每个元素减一
if dict_list[i] == 0:
dict_list[i] = backup[i] # 恢复旧值
# 找到之前的源节点,我要进行设置索引节点了
temp_index = f"step_{backup[i]}"
temp_index_node = dict[temp_index]
temp_index_node.index_node.append(temp)
dict[temp_index] = temp # 更换要设置的源节点
temp = temp.next_node
这里有很多循环,其实我想将步长和节点绑定到一起,以优化性能。如果你愿意,可以尝试优化一下,毕竟这只是跳表的最初版本。让我们来演示一下,看看优化的效果如何。最终结果如下,其实还是可以的。我大概试了一下,如果数据分布不太好的话,很可能需要进行多达6次的查询才能找到结果。总结我们实现的跳表有许多优化的方面需要考虑。例如,我们可以避免每次都重新规划索引,因为这是不必要的。另外,我们也可以探索不同的步长绑定方法,不一定要按照我目前的方式进行。今天先说到这里,因为我认为跳表的实现逻辑相当复杂。我们可以在跳表这个领域暂时告一段落。
绝对勇士
Java开发者的Python快速进修指南:面向对象进阶
在上一期中,我们对Python中的对象声明进行了初步介绍。这一期,我们将深入探讨对象继承、组合以及多态这三个核心概念。不过,这里不打算赘述太多理论,因为我们都知道,Python与Java在这些方面的主要区别主要体现在语法上。例如,Python支持多重继承,这意味着一个类可以同时继承多个父类的属性和方法,而Java则只支持单一继承。另一个值得注意的差异是对super关键字的使用。在Java中,我们经常需要显式地使用super来调用父类的构造器,而在Python中,这一步骤是可选的。如果没有指定,Python会自动调用父类的构造器,这让代码看起来更加简洁。当然,我们这里不会举出太多复杂的例子来让大家头疼。毕竟我们的目标是理解和应用这些概念,而不是准备考试。我们将通过一些简单直观的例子,帮助大家清晰地把握Python在继承、组合和多态方面的特点和优势。对象的继承Python中的继承是一种用于创建新类的机制,新类可以继承一个或多个父类的特性。在面向对象编程中,和Java一样继承提供了代码复用的强大工具。class Animal:
def __init__(self, name):
self.name = name
def speak(self):
pass
class Dog(Animal):
# def __init__(self, name):
# super().__init__(name)
def speak(self):
return "Woof!"
# 使用
dog = Dog("Buddy")
print(dog.speak()) # 输出: Woof!
print(dog.name) # 输出: Buddy
不声明__init__默认使用super调用父类的构造器,如果你一旦声明了__init__那么记得显式的调用super,否则将无效在Python中,多继承是一个强大的特性,允许一个类同时继承多个父类。下面是一个多继承的例子,来帮助你更好地理解这一概念:class Father:
def __init__(self):
self.father_name = "Father's name"
def gardening(self):
return "I enjoy gardening"
class Mother:
def __init__(self):
self.mother_name = "Mother's name"
def cooking(self):
return "I love cooking"
class Child(Father, Mother):
def __init__(self):
Father.__init__(self)
Mother.__init__(self)
self.child_name = "Child's name"
def activities(self):
return f"{self.father_name} likes {self.gardening()} and {self.mother_name} likes {self.cooking()}."
# 使用
child = Child()
child.father_name = "father"
child.mother_name = "mom"
print(child.activities())
对象的组合在Python中,我们可以在程序运行过程中根据需要向对象动态地添加新的行为或数据,这种方式为处理各种复杂和不可预见的编程情况提供了极大的便利。相比之下,Java由于其静态类型的特性,对于运行时的动态变化就显得有些束手束脚。在Java中,所有的属性和方法都必须在编译时明确定义。class Printer:
def print_document(self, content):
return f"Printing: {content}"
class Scanner:
def scan_document(self):
return "Scanning a document"
class MultifunctionMachine:
pass
# 动态添加功能
mfm = MultifunctionMachine()
mfm.printer = Printer()
mfm.scanner = Scanner()
# 使用新功能
print(mfm.printer.print_document("Hello World")) # 输出: Printing: Hello World
print(mfm.scanner.scan_document()) # 输出: Scanning a document
pass 这个看似简单却不可忽视的关键字,我不太确定之前是否有提及。在Python的世界里,pass 是一种特别有趣的存在。想象一下,在编写代码时,我们常常会遇到一种场景:逻辑上需要一个语句,但此刻我们可能还没有准备好具体的实现,或者我们故意想留下一个空的代码块,以备将来填充。这时,pass 就像是一个灵巧的小助手,默默地站在那里,告诉Python解释器:“嘿,我在这里,但你可以暂时忽略我。”多态为了让大家更直观地理解多态,我准备了一个例子,相信通过这个实例,多态的概念会变得生动而明晰。在Python中,多态表现得非常直观,而且它与Java在这方面的处理几乎如出一辙。class Animal:
def speak(self):
raise NotImplementedError("Subclass must implement abstract method")
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
def animal_speak(animal):
print(animal.speak())
# 使用
my_dog = Dog()
my_cat = Cat()
animal_speak(my_dog) # 输出: Woof!
animal_speak(my_cat) # 输出: Meow!
总结在本期文章中,我们深入探讨了Python的对象继承、组合以及多态这三个核心概念。从继承的灵活性,如Python的多重继承和super关键字的使用,到组合中的动态属性添加,我们逐一解析了Python与Java在这些方面的相似之处和差异。通过具体的例子,我们展示了Python中多态的直观表现,强调了它与Java的相似性。这些讨论不仅帮助理解Python的对象模型,而且对比了Java和Python在面向对象编程方面的不同处理方式
绝对勇士
【翻译】Reactor 第七篇 Spring WebFlux 怎么进行异常处理
1 概览在本教程中,我们将通过一个实际示例了解Spring WebFlux项目中处理错误的各种策略。我们还将指出使用一种策略比另一种策略更有利的地方,并在最后提供完整源代码的链接。2 开始示例代码maven 设置和之前介绍 Spring WebFlux 的文章一样,对于我们的示例,我们将使用一个 RESTful 端点,它将用户名作为查询参数并返回“Hello username”作为结果。首先,让我们创建一个路由函数,这个路由函数将 “/hello” 请求路由到处理程序中名为 handleRequest 的方法,代码如下:@Bean
public RouterFunction<ServerResponse> routeRequest(Handler handler) {
return RouterFunctions.route(RequestPredicates.GET("/hello")
.and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
handler::handleRequest);
}然后,我们定义个 handleRequest() 方法,这个方法调用 sayHello() 方法,并找到一个在 ServerResponse 中包含或返回其(sayHello方法的返回)结果的方法。public Mono<ServerResponse> handleRequest(ServerRequest request) {
return
//...
sayHello(request)
//...
}最后,实现 sayHello 方法,实现很简单,直接拼接 hello 和参数 username 即可。private Mono<String> sayHello(ServerRequest request) {
try {
// 应该是 username
return Mono.just("Hello, " + request.queryParam("username").get());
} catch (Exception e) {
return Mono.error(e);
}
}因此,只要我们的请求中带了 username 参数,我们的请求就能正常返回。举个例子:我们请求“/hello?username=Tonni”,类似请求,我们总是能正常返回。然而,如果我们的请求不带 username 参数,我们的请求就会抛出异常了。下面,我们来看看 Spring WebFlux 在哪里以及怎么重组代码来处理我们的异常。3 方法级别处理异常Mono 和 Flux API 中内置了两个关键运算符来处理方法级别的错误。我们简要探讨一下它们及其用法。3.1 onErrorReturn 处理异常当我们碰到异常的时候,我们可以用 onErrorReturn 来直接返回静态结果:public Mono<ServerResponse> handleRequest(ServerRequest request) {
return sayHello(request)
.onErrorReturn("Hello Stranger")
.flatMap(s -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(s));
}这里,每当有问题的连接函数抛出异常的时候,我们直接返回一个静态结果:“Hello Stranger”。3.2 onErrorResume 处理异常有三种使用 onErrorResume 处理异常的方式:计算动态回调值通过回调函数执行其他分支捕获、包装并重新抛出错误,例如,作为自定义业务异常让我们看看怎么计算值:public Mono<ServerResponse> handleRequest(ServerRequest request) {
return sayHello(request)
.flatMap(s -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(s))
.onErrorResume(e -> Mono.just("Error " + e.getMessage())
.flatMap(s -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(s)));
}这里,每当 sayHello 抛出异常的时候,我们返回一个 “Error + 异常信息(e.getMessage())”。接下来,我们看看当异常发生调用回调函数:public Mono<ServerResponse> handleRequest(ServerRequest request) {
return sayHello(request)
.flatMap(s -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(s))
.onErrorResume(e -> sayHelloFallback()
.flatMap(s -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(s)));
}这里,每当 sayHello 抛出异常的时候,我们执行一个其他函数 sayHelloFallback 。最后,使用 onErrorResume 来捕获、包装并重新抛出错误,举例如:NameRequiredExceptionpublic Mono<ServerResponse> handleRequest(ServerRequest request) {
return ServerResponse.ok()
.body(sayHello(request)
.onErrorResume(e -> Mono.error(new NameRequiredException(
HttpStatus.BAD_REQUEST,
"username is required", e))), String.class);
}这里,当 sayHello 抛出异常的时候,我们抛出一个定制异常 NameRequiredException,message是 “username is required”。全局处理异常目前为止,我们提供的所有示例都在方法级别上处理了错误处理。但是我们可以选择在全局层面处理异常。为此,我们只需要两步:自定义一个全局错误响应属性实现全局错误处理 handler这样我们程序抛出的异常将会自动转换成 HTTP 状态和 JSON 错误体。我们只需要继承 DefaultErrorAttributes 类然后重写 getErrorAttributes 方法就可以自定义这些。public class GlobalErrorAttributes extends DefaultErrorAttributes{
@Override
public Map<String, Object> getErrorAttributes(ServerRequest request,
ErrorAttributeOptions options) {
Map<String, Object> map = super.getErrorAttributes(
request, options);
map.put("status", HttpStatus.BAD_REQUEST);
map.put("message", "username is required");
return map;
}
}这里,当异常抛出是,我们想要返回 BAD_REQUEST 状态码和“username is required”错误信息作为错误属性的一部分。然后,我们来实现全局错误处理 handler。为此,Spring 提供了一个方便的 AbstractErrorWebExceptionHandler 类,供我们在处理全局错误时进行扩展和实现:@Component
@Order(-2)
public class GlobalErrorWebExceptionHandler extends
AbstractErrorWebExceptionHandler {
// constructors
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(
ErrorAttributes errorAttributes) {
return RouterFunctions.route(
RequestPredicates.all(), this::renderErrorResponse);
}
private Mono<ServerResponse> renderErrorResponse(
ServerRequest request) {
Map<String, Object> errorPropertiesMap = getErrorAttributes(request,
ErrorAttributeOptions.defaults());
return ServerResponse.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(errorPropertiesMap));
}
}在这个例子中,我们设置 handler 的 order 为 -2。这是为了给它一个比默认 handler,也就是 DefaultErrorWebExceptionHandler 一个更高的优先级,它设置的 order 为 -1。errorAttributes 对象将是我们在 Web 异常处理程序的构造函数中传递的对象的精确副本。理想情况下,这应该是我们自定义的错误属性类。然后我们明确生命了,我们希望将所有的异常处理路由到 renderErrorResponse() 中。最后,我们获取了错误属性并插入到服务端响应体中。然后这会生成一个 JSON 响应,其中包含了错误的详细信息,HTTP 状态、机器端的异常信息等。对于浏览器端,它有一个 “white-label”错误处理程序,可以以 HTML 形式呈现相同的数据,当然这个页面可以定制。总结在本文中,我们研究了在 Spring WebFlux 项目中处理异常的集中策略,并指出使用一个策略优于其他策略的地方。源码在 github 上原文:www.baeldung.com/spring-webf…
绝对勇士
Reactor 第二篇 SpringBoot 整合熔断限流 Resilience4j
Reactor 整合 Resilence4j1 引入 pom 包<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-all</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
</dependency>2 配置说明2.1 限流 ratelimiter两个限流配置:backendA 1s 中最多允许 10 次请求;backendB 每 500ms 最多允许 6 次请求。resilience4j.ratelimiter:
instances:
backendA:
limitForPeriod: 10
limitRefreshPeriod: 1s
timeoutDuration: 10ms
registerHealthIndicator: true
eventConsumerBufferSize: 100
backendB:
limitForPeriod: 6
limitRefreshPeriod: 500ms
timeoutDuration: 3s配置属性默认值描述timeoutDuration5【s】一个线程等待许可的默认等待时间limitRefreshPeriod500【ns】限制刷新的周期。在每个周期之后,速率限制器将其权限计数设置回 limitForPeriod 值limitForPeriod50一个 limitRefreshPeriod (周期)允许访问的数量(许可数量)2.2 重试 retry注意指定需要重试的异常,不是所有的异常重试都有效。比如 DB 相关校验异常,如唯一约束等,重试也不会成功的。重试配置:resilience4j.retry:
instances:
backendA:
maxAttempts: 3
waitDuration: 10s
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
retryExceptions:
- org.springframework.web.client.HttpServerErrorException
- java.io.IOException
backendB:
maxAttempts: 3
waitDuration: 10s
retryExceptions:
- org.springframework.web.client.HttpServerErrorException
- java.io.IOException配置属性默认值描述maxAttempts3最大重试次数(包括第一次)waitDuration500【ms】两次重试之间的等待间隔intervalFunctionnumOfAttempts -> waitDuration修改失败后等待间隔的函数。默认情况下,等待时间是个常量。retryOnResultPredicateresult->false配置一个判断结果是否应该重试的 predicate 函数。如果结果应该重试,Predicate 必须返回 true,否则它必须返回 false。retryExceptionPredicatethrowable -> true和 retryOnResultPredicate 类似,如果要重试,Predicate 必须返回true,否则返回 false。retryExceptions空需要重试的异常类型列表ignoreExceptions空不需要重试的异常类型列表failAfterMaxAttemptsfalse当重试达到配置的 maxAttempts 并且结果仍未通过 retryOnResultPredicate 时启用或禁用抛出 MaxRetriesExceededException 的布尔值intervalBiFunction(numOfAttempts, Either<throwable, result>) -> waitDuration根据 maxAttempts 和结果或异常修改失败后等待间隔时间的函数。与 intervalFunction 一起使用时会抛出 IllegalStateException。2.3 超时 TimeLimiter超时配置:resilience4j.timelimiter:
instances:
backendA:
timeoutDuration: 2s
cancelRunningFuture: true
backendB:
timeoutDuration: 1s
cancelRunningFuture: false超时配置比较简单,主要是配置 timeoutDuration 也就是超时的时间。cancelRunningFuture 的意思是:是否应该在运行的 Future 调用 cancel 去掉调用。2.4 断路器 circuitbreaker断路器有几种状态:关闭、打开、半开。注意:打开,意味着不能访问,会迅速失败。CircuitBreaker 使用滑动窗口来存储和汇总调用结果。您可以在基于计数的滑动窗口和基于时间的滑动窗口之间进行选择。基于计数的滑动窗口聚合最后 N 次调用的结果。基于时间的滑动窗口聚合了最后 N 秒的调用结果。断路器配置:resilience4j.circuitbreaker:
instances:
backendA:
// 健康指标参数,非断路器属性
registerHealthIndicator: true
slidingWindowSize: 100
配置属性默认值描述slidingWindowSize100记录断路器关闭状态下(可以访问的情况下)的调用的滑动窗口大小failureRateThreshold50(百分比)当失败比例超过 failureRateThreshold 的时候,断路器会打开,并开始短路呼叫slowCallDurationThreshold60000【ms】请求被定义为慢请求的阈值slowCallRateThreshold100(百分比)慢请求百分比大于等于该值时,打开断路器开关permittedNumberOfCalls10半开状态下允许通过的请求数maxWaitDurationInHalfOpenState0配置最大等待持续时间,该持续时间控制断路器在切换到打开之前可以保持在半开状态的最长时间。值 0 表示断路器将在 HalfOpen 状态下无限等待,直到所有允许的调用都已完成。2.5 壁仓 bulkheadresilience4j 提供了两种实现壁仓的方法:SemaphoreBulkhead 使用 Semaphore 实现FixedThreadPoolBulkhead 使用有界队列和固定线程池实现resilience4j.bulkhead:
instances:
backendA:
maxConcurrentCalls: 10
backendB:
maxWaitDuration: 10ms
maxConcurrentCalls: 20
resilience4j.thread-pool-bulkhead:
instances:
backendC:
maxThreadPoolSize: 1
coreThreadPoolSize: 1
queueCapacity: 12.5.1 SemaphoreBulkhead配置属性默认值描述maxConcurrentCalls25允许的并发执行的数量maxWaitDuration0尝试进入饱和隔板时线程应被阻止的最长时间2.5.2 FixedThreadPoolBulkhead配置属性默认值描述maxThreadPoolSizeRuntime.getRuntime().availableProcessors()线程池最大线程个数coreThreadPoolSizeRuntime.getRuntime().availableProcessors()-1线程池核心线程个数queueCapacity100线程池队列容量keepAliveDuration20【ms】线程数超过核心线程数之后,空余线程在终止之前等待的最长时间3 使用3.1 配置在 application.yml 文件中添加以下 resilience4j 配置:resilience4j.circuitbreaker:
instances:
backendA:
registerHealthIndicator: true
slidingWindowSize: 100
resilience4j.retry:
instances:
backendA:
maxAttempts: 3
waitDuration: 10s
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
retryExceptions:
- org.springframework.web.client.HttpServerErrorException
- java.io.IOException
backendB:
maxAttempts: 3
waitDuration: 10s
retryExceptions:
- org.springframework.web.client.HttpServerErrorException
- java.io.IOException
resilience4j.bulkhead:
instances:
backendA:
maxConcurrentCalls: 10
backendB:
maxWaitDuration: 10ms
maxConcurrentCalls: 20
resilience4j.thread-pool-bulkhead:
instances:
backendC:
maxThreadPoolSize: 1
coreThreadPoolSize: 1
queueCapacity: 1
resilience4j.ratelimiter:
instances:
backendA:
limitForPeriod: 10
limitRefreshPeriod: 1s
timeoutDuration: 10ms
registerHealthIndicator: true
eventConsumerBufferSize: 100
backendB:
limitForPeriod: 6
limitRefreshPeriod: 500ms
timeoutDuration: 3s
resilience4j.timelimiter:
instances:
backendA:
timeoutDuration: 2s
cancelRunningFuture: true
backendB:
timeoutDuration: 1s
cancelRunningFuture: false3.2 使用注解实现直接在需要限流的方法上增加注解@RateLimiter 实现限流;增加注解@Retry 实现重试;增加注解 @CircuitBreaker 熔断;增加注解 @Bulkhead 实现壁仓。name 属性中分别填写限流器、重试、熔断、壁仓组件的名字。@Bulkhead(name = "backendA")
@CircuitBreaker(name = "backendA")
@Retry(name = "backendA")
@RateLimiter(name = "backendA")
public Mono<List<User>> list() {
long startTime = System.currentTimeMillis();
return Mono.fromSupplier(() -> {
return userRepository.findAll();
}).doOnError(e -> {
// 打印异常日志&增加监控(自行处理)
logger.error("list.user.error, e", e);
})
.doFinally(e -> {
// 耗时 & 整体健康
logger.info("list.user.time={}, ", System.currentTimeMillis() - startTime);
});
}
@Bulkhead(name = "backendA")
@CircuitBreaker(name = "backendA")//最多支持10个并发量
@Retry(name = "backendA")//使用 backendA 重试器,如果抛出 IOException 会重试三次。
@RateLimiter(name = "backendA")// 限流 10 Qps
public Mono<Boolean> save(User user) {
long startTime = System.currentTimeMillis();
return Mono.fromSupplier(() -> {
return userRepository.save(user) != null;
})
.doOnError(e -> {
// 打印异常日志&增加监控(自行处理)
logger.error("save.user.error, user={}, e", user, e);
})
.doFinally(e -> {
// 耗时 & 整体健康
logger.info("save.user.time={}, user={}", user, System.currentTimeMillis() - startTime);
});
}
注意:以上所有组件,都支持自定义。
绝对勇士
Reactor 第九篇 WebFlux重构个人中心,效果显著
1 重构背景原有的开发人员早已离职,代码细节没人知道,经过了一段时间的维护,发现有以下问题:个人中心系统的特征就是组装各个业务的接口,输出个人中心业务需要的数据,整个系统调用了几十个第三方业务线的接口,如果编排不合理,可能会导致响应时间急剧上涨,尤其是弹窗业务,新的弹窗会不断接入,整个接口可能会不可用。2 整体架构service:是最小的业务编排单元,request方法对infrastructure第三方接口进行编排调用;apply 方法对第三方接口调用的结果进行组装,结果是service的业务返回;infrastructure:是对第三方的异步非阻塞调用,不包含业务逻辑。一个service内部根据实际业务可以编排0个或者多个infrastructure服务。在实际优化过程中我们抽象了30多个infrastructure第三方调用,40多个service。他们都是小而且独立的类,减轻了开发同学尤其是新同学熟悉的成本。边界也比较清晰,逻辑内聚。3 编排举例每个 service 内部都是由一个或者多个 infrastructure 第三方调用组装编排的业务单元,内部处理能异步处理的全是使用异步处理,实在不能异步处理的使用串行+并行的方式。3.1 串行需要串行的可以使用 flatMap 方法,可以参考以下格式。这种方式会执行S1,然后S2。伪代码如下:Mono.from(service1.func())
.flatMap(service1Res-> {
return service2.func();
})3.2 并行zip 和 zipWith,zipWith一次组装一个Mono,zip 一次可以组装多个Mono。示例代码如下:service1.zipWith(service2)
Mono.zip(service1, service2, service3)一个使用 zip 组装多个service的示例代码,并行执行service1, service2, ......, service6,使用doOnError处理错误,onErrorReturn 处理异常返回,doOnFinally 监控整个接口调用量、耗时情况。Mono.zip(service1, service2, service3, service4, service5, service6)
.map(t -> {
String service1Ret = t.getT1();
String service2Ret = t.getT2();
// ....
return "组合结果";
})
// 异常返回
.onErrorReturn(new DTO())
.doOnError(e -> {
// 异常详情日志;异常请求量监控
})
.doFinally(e -> {
// 请求量、耗时监控
});3.3 并行-但只取第一个有数据的结果弹窗类业务与一般service不通,它需要调用很多的业务的数据出不同的弹窗,但是每次都只能给用户展示确定的一个。但是如果串行的话,随着上线的弹窗越来越多,整个弹窗接口的耗时会越来越长。但是如果改成异步的话,又无法控制弹窗之间的优先级,优先级对于公司整体业务来说是必要的,把重要的业务放在高优的位置上,做到资源最大利用,才能实现利润的最大化,从而做到基业长青。Flux 有个flatMapSequential方法,它能完美解决这个问题,看看它的注释:Transform the elements emitted by this Flux asynchronously into Publishers, then flatten these inner publishers into a single Flux, but merge them in the order of their source element.将此Flux发出的元素异步地转换为 publisher,然后将这些内部 publisher 扁平化为单个Flux,但按照源元素的顺序合并它们。如上图所示,总共有S1、S2、S3、S4按顺序的四个弹窗,会并行执行S1到S4,如果S1和S2没有数据,S3有数据,则会返回S3。伪代码如下:Flux<Map<String, Object>> monoFlux = Flux.fromIterable(serviceList)
.flatMapSequential(serviceName -> {
})
.onErrorContinue((err, i) -> {
// 某个service异常或者无数据,继续执行
});
})
.onErrorContinue((err, i) -> {
// 服务异常,继续执行
});
Mono<Map<String, Object>> mono = monoFlux.elementAt(0, Maps.newHashMap()); 这里就是异步执行所有弹窗service,运行过程中某个弹窗异常或者无数据返回,则继续下一个。通过monoFlux.elementAt(0, Maps.newHashMap())获取第一个有数据的弹窗。4 重构效果4.1 后端指标相比于原来的后端系统,所有接口耗时都有大幅度降低,:头部身份信息接口响应速度提升:26%。卡片各业务线入口接口响应速度提升:87%。弹窗和浮标接口响应速度提升:146%。经过 flatMapSequential 编排弹窗之后,耗时从220ms,降到160ms,绝对值下降了60ms,下降了 28%;4.2 新需求开发和维护新需求开发更快,QA 测试更快。原来开发一个弹窗,需要考虑的事情很多:开发的时候需要考虑代码放在哪个层级上,是否与其他弹窗有耦合,弹窗优先级需要通过if-else实现,很容易出错;弹窗自测很麻烦,需要注释调其他弹窗;QA 需要测试所有弹窗的优先级是否有问题;现在开发一个弹窗,只需要增加一个service类,然后把service配置再优先级列表中即可。4.3 其他框架使用了响应式框架 Spring WebFlux,也支持本地启动,编写了service层和基础设施层的单测case,提升开发效率。删除了原来的业务网关层,使用公司层面的网关系统,配置即生效;删除了原来业务网关中的业务逻辑代码,把相关逻辑移动到业务层中,解除了原来的多层之间的耦合关系。现在各个service之前相互独立,异常不会相互影响。
绝对勇士
Java开发者的Python快速实战指南:探索向量数据库之文本搜索
前言如果说Python是跟随我的步伐学习的话,我觉得我在日常开发方面已经没有太大的问题了。然而,由于我没有Python开发经验,我思考着应该写些什么内容。我回想起学习Java时的学习路线,直接操作数据库是其中一项重要内容,无论使用哪种编程语言,与数据库的交互都是不可避免的。然而,直接操作MySQL数据库似乎缺乏趣味性,毕竟每天都在写SQL语句。突然我想到了我之前写过的一系列私人知识库文章,于是我想到了向量数据库,毕竟这是当前非常热门的技术之一。如果AI离开了向量数据库,就好像失去了灵魂一样。市面上有很多向量数据库产品,我选择了最近腾讯推出的向量数据库,并且我还有一张免费试用卡,趁着还没过期,我决定写一些相关文章。而且我看了一下,这个数据库对于新手来说非常友好,因为它有可视化界面。对于一个新手来说,能够看到实际效果是最客观的。就像当初学习SQL时,如果没有Navicat这个可视化工具,就会感觉力不从心一样。向量数据库向量数据库具有将复杂的非结构化数据转化为多维逻辑坐标值的能力,简而言之,它可以将我们所了解的所有事物转化为可计算的数字。一旦数据进入数学领域,我们就能够对其进行计算。此外,向量数据库还可以作为一个外部知识库,为大型模型提供最新、最全面的信息,以应对需要及时回答的问题。同时,它也能够赋予大型语言模型长期记忆的能力,避免在对话过程中产生"断片"的情况。可以说,向量数据库是大型语言模型的最佳合作伙伴。如果你对任何内容有任何疑问,请点击以下官方文档链接查看更多信息:img-bss.csdnimg.cn/1113tusoutu…虽然这是官方的文档,里面存在许多错误,我已经积极提供了反馈,但可惜没有得到有效处理。尽管如此,这并不会妨碍我们的阅读。文档最后还有一个官方的案例代码仓库,对于有兴趣的同学可以直接滑动到最后进行查阅。不过,对于新手而言,可能并不太友好,原因在于代码量较大,很难一下子消化。就好比刚学习Java的时候,要看别人的业务逻辑一样,即使有大量注释,也会感到吃力。好的,废话不多说,我们直接进入正题吧。如果你还有未领取的,可以免费领取一下。腾讯官方体验地址:cloud.tencent.com/product/vdb建立数据库连接领取完毕后,你需要创建一个新的免费示例,这个过程不难,大家都会。成功之后,你需要开启外网访问,否则无法进行本地的测试和联调。在开启外网访问时,需要将外网白名单ip设置为0.0.0.0/0,这将接受所有IP的请求。好的,接下来我们需要获取数据库的登录名和密码。这些信息将用于连接和管理数据库。创建数据库import tcvectordb
from tcvectordb.model.enum import FieldType, IndexType, MetricType, ReadConsistency
#create a database client object
client = tcvectordb.VectorDBClient(url='http://*******', username='root', key='1*******', read_consistency=ReadConsistency.EVENTUAL_CONSISTENCY, timeout=30)
# create a database
db = client.create_database(database_name='db-xiaoyu')
print(db.database_name)
# list databases
db_list = client.list_databases()
for db in db_list:
print(db.database_name)
好的,我们现在开始替换所需的内容,完成数据库的创建。一旦数据库创建完成,我们还需要创建集合,而不是传统的表,因为在向量数据库中,它们被称为集合。因此,我们接下来要创建集合。创建集合创建集合和创建表的过程类似,但前提是集合需要存储向量,而表用于存储数据。在这里,我们选择使用集成了embedding的集合。如果不使用集成的embedding,你需要使用其他embedding模型来输出向量,然后将其输入到集合中进行存储。除非你想手动输入向量值,否则这是必要的。设计索引(不是设计 Collection 的结构)在使用向量对应的文本字段时,不建议建立索引。这样做会占用大量内存资源,而且没有实际作用。除了向量对应的文本字段外,如果需要进行业务过滤,也就是在查询时需要使用where条件,那么必须单独为这个条件字段定义一个索引。也就是说,你需要用哪个字段进行过滤,就必须为该字段定义一个索引。向量数据库支持动态模式(Schema),在写入数据时可以写入任意字段,无需提前定义,类似于MongoDB。目前,主键id和向量字段vector是固定且必需的,字段名称也必须一致,否则会报错。在之前讲解私人知识库的时候,我会单独引入其他embedding模型,因为向量数据库没有继承这些模型。不过,腾讯已经将embedding模型集成在了他们的系统中,这样就不需要来回寻找模型了。需要注意的是,为了确保一致性,你选择的embedding模型后面的vector字段要设置为768维。db = client.database('db-xiaoyu')
# -- index config
index = Index(
FilterIndex(name='id', field_type=FieldType.String, index_type=IndexType.PRIMARY_KEY),
VectorIndex(name='vector', dimension=768, index_type=IndexType.HNSW,
metric_type=MetricType.COSINE, params=HNSWParams(m=16, efconstruction=200)),
FilterIndex(name='author', field_type=FieldType.String, index_type=IndexType.FILTER),
FilterIndex(name='bookName', field_type=FieldType.String, index_type=IndexType.FILTER)
)
# Embedding config
ebd = Embedding(vector_field='vector', field='text', model=EmbeddingModel.BGE_BASE_ZH)
# create a collection
coll = db.create_collection(
name='book-xiaoyu',
shard=1,
replicas=0,
description='this is a collection of test embedding',
embedding=ebd,
index=index
)
print(vars(coll))
我们已经成功创建了数据库和集合,并且现在让我们来看一下它们的结构。实际上,它们的原理与MySQL和其他数据库相似,只是存储的内容和术语发生了变化。我们可以将其视为数据库操作。插入/替换数据当插入数据时,如果集合中已经存在具有相同ID的文档,则会删除原始文档并插入新的文档数据。需要注意的是,很多字段我们都没有指定,例如page、text等。你可以继续添加这些字段,因为它们类似于MongoDB。但请注意,text字段必须与你在配置embedding时指定的字段相同,否则无法将其转换为向量。coll = db.collection('book-emb')
# 写入数据。
# 参数 build_index 为 True,指写入数据同时重新创建索引。
res = coll.upsert(
documents=[
Document(id='0001', text="话说天下大势,分久必合,合久必分。", author='罗贯中', bookName='三国演义', page=21),
Document(id='0002', text="混沌未分天地乱,茫茫渺渺无人间。", author='吴承恩', bookName='西游记', page=22),
Document(id='0003', text="甄士隐梦幻识通灵,贾雨村风尘怀闺秀。", author='曹雪芹', bookName='红楼梦', page=23)
],
build_index=True
)
当我们完成数据插入后,我们可以立即执行查询操作。但请注意,如果你将 "build_index" 字段设置为 "false",即使插入成功,查询时也无法检索到数据。因此,如果要立即生效并能查询到数据,你必须将其设置为 "true"。这个是重建索引的过程查询数据这里的查询可以分为精确查询和相似度查询两种。精确查询是指除了向量字段外的其他字段查询条件都是精确匹配的。由于我们在建立索引时已经对作者(author)和书名(bookName)建立了索引,所以我们可以直接对它们进行数据过滤,但是我不会在这里演示。现在我将演示一下模糊查询,即对向量字段匹配后的结果进行查询,并同时加上过滤条件。doc_lists = coll.searchByText(
embeddingItems=['天下大势,分久必合,合久必分'],
filter=Filter(Filter.In("bookName", ["三国演义", "西游记"])),
params=SearchParams(ef=200),
limit=3,
retrieve_vector=False, # 不返回向量
output_fields=['bookName','author']
)
# printf
for i, docs in enumerate(doc_lists.get("documents")):
for doc in docs:
print(doc)
除了上面提到的Python的写法,我们还可以通过界面来进行精确查询。只需要在界面中填写where后的条件即可。要进行模糊查询,可以直接使用text文字进行查询,或者定义过滤字段来进行查询优化。总结剩下的删除数据这部分我就不演示了。今天先跟向量数据库熟悉一下界面操作,感觉就像在使用Kibana查询ES数据一样。不知道你们有没有类似的感觉。好了,今天我们先只关注文本操作,下一期我会尝试处理图像或者视频数据。总的来说,相比Java,Python的SDK使用起来更加舒适。如果你曾经使用过Java SDK与平台接口对接,就会发现Python SDK上手更快。
绝对勇士
手把手教你用python做一个年会抽奖系统
引言马上就要举行年会抽奖了,我们都不知道是否有人能够中奖。我觉得无聊的时候可以尝试自己写一个抽奖系统,主要是为了娱乐。现在人工智能这么方便,写一个简单的代码不是一件困难的事情。今天我想和大家一起构建一个简易的抽奖系统,这样也能够巩固一下我自己对Python语法和框架的理解。今天我们将继续使用Python语言进行开发,并且使用最简单的HTML、JS、CSS来配置样式和界面。在Python中,我们将使用一个名为fastapi的第三方框架,虽然这是我第一次接触它,但我发现它真的非常方便使用,简直就像是把飞机开在马路上一样。与使用Spring框架相比,fastapi让搭建过程变得轻松愉快。这个抽奖系统的业务逻辑其实非常简单。首先,我们需要一个9宫格的页面,用户可以在页面上添加参与人员。虽然我们可以使用数据库来存储参与人员的信息,但为了方便演示,我选择了简单地使用内存存储。在这个系统中,除了保证每个人只有一个参与机会外,其他的校验要求都很少。然后,用户可以通过点击开始按钮,页面会随机停下来,然后将停下来的奖项传给后台并保存,最后在前端页面上显示。虽然逻辑简单,但是通过这个抽奖系统的开发,我们可以巩固自己对Python语法和框架的理解,同时也能够体验到人工智能带来的便利。让我们一起动手搭建这个简易版的抽奖系统吧!前端界面尽管前端界面写得不够出色,但这并非我今天的重点。实际上,我想回顾一下Python的编写方式和框架的理解。我创建了一个简单的九宫格,每个格子都设有不同的奖项,而且用户还可以手动进行设置和修改,从而保证了灵活性。前端代码:<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>抽奖系统</title>
<link rel="stylesheet" href="/static/css/styles.css">
<script src="/static/js/main.js"></script>
</head>
<body>
<h1>欢迎来到小雨抽奖系统</h1>
<form id="participant-form">
<label for="participant-name">参与者姓名:</label>
<input type="text" id="participant-name" name="participant-name" required>
<button type="submit">添加参与者</button>
</form>
<div id="grid">
<div class="grid-item" data-prize="奖项1">奖项1</div>
<div class="grid-item" data-prize="奖项2">奖项2</div>
<div class="grid-item" data-prize="奖项3">奖项3</div>
<div class="grid-item" data-prize="奖项4">奖项4</div>
<div class="grid-item" data-prize="奖项5">奖项5</div>
<div class="grid-item" data-prize="奖项6">奖项6</div>
<div class="grid-item" data-prize="奖项7">奖项7</div>
<div class="grid-item" data-prize="奖项8">奖项8</div>
<div class="grid-item" data-prize="奖项9">奖项9</div>
</div>
<button id="draw-button">抽奖</button>
<h2>获奖名单</h2>
<ul id="prize-list"></ul>
<script>
document.getElementById('participant-form').addEventListener('submit', function(event) {
event.preventDefault();
var participantName = document.getElementById('participant-name').value;
fetch('/participant', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({name: participantName}),
})
.then(response => response.json())
.then(data => {
console.log(data);
document.getElementById('participant-name').value = '';
})
.catch((error) => {
console.error('Error:', error);
});
});
document.getElementById('draw-button').addEventListener('click', function() {
var items = document.getElementsByClassName('grid-item');
var index = 0;
var interval = setInterval(function() {
items[index].classList.remove('active');
index = (index + 1) % items.length;
items[index].classList.add('active');
}, 100);
setTimeout(function() {
clearInterval(interval);
var prize = items[index].getAttribute('data-prize');
fetch('/draw', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({prize: prize}),
})
.then(response => response.json())
.then(data => {
console.log(data);
if (data.code !== 1) {
alert(data.message);
} else {
var li = document.createElement("li");
li.appendChild(document.createTextNode(data.message));
document.getElementById('prize-list').appendChild(li);
}
})
.catch((error) => {
console.error('Error:', error);
});
}, Math.floor(Math.random() * (10000 - 3000 + 1)) + 3000);
});
</script>
</body>
</html>
</h2></button></title>
CSS样式主要用于配置9个宫格的显示位置和实现动态动画高亮效果。除此之外,并没有对其他效果进行配置。如果你有兴趣,可以在抽奖后自行添加一些炫彩烟花等效果,完全取决于你的发挥。代码如下:body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
}
h1, h2 {
color: #333;
}
form {
margin-bottom: 20px;
}
#participant-form {
display: flex;
justify-content: center;
margin-top: 20px;
}
#participant-form label {
margin-right: 10px;
}
#participant-form input {
margin-right: 10px;
}
#participant-form button {
background-color: #4CAF50;
color: white;
border: none;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
}
#draw-button {
display: block;
width: 200px;
height: 50px;
margin: 20px auto;
background-color: #f44336;
color: white;
border: none;
text-align: center;
line-height: 50px;
font-size: 20px;
cursor: pointer;
}
#grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 10px;
width: 300px;
height: 300px;
margin: 0 auto; /* This will center the grid horizontally */
}
.grid-item {
width: 100%;
height: 100%;
border: 1px solid black;
display: flex;
justify-content: center;
align-items: center;
}
.grid-item.active {
background-color: yellow;
}
#prize-list {
list-style-type: none;
padding: 0;
width: 80%;
margin: 20px auto;
}
#prize-list li {
padding: 10px;
border-bottom: 1px solid #ccc;
}
Python后台在我们的Python后端中,我们选择使用了fastapi作为框架来接收请求。这个框架有很多优点,其中最重要的是它的速度快、简单易懂。但唯一需要注意的是,在前端向后端传递请求参数时,请求头必须包含一个json的标识。如果没有这个标识,后端将无法正确接收参数,并可能报错。为了更好地优化我们的后端,如果你有足够的时间,可以考虑集成数据库等一些重量级的操作。这样可以更好地处理数据,并提供更多功能。主要的Python代码如下:from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
# from models import Participant, Prize
# from database import SessionLocal, engine
from pydantic import BaseModel
from random import choice
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
prizes = []
participants = []
class Participant(BaseModel):
name: str
class Prize(BaseModel):
winner: str
prize: str
class DatePrize(BaseModel):
prize: str
@app.get("/")
async def root(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@app.post("/participant")
async def add_participant(participant: Participant):
participants.append(participant)
return {"message": "Participant added successfully"}
@app.post("/draw")
async def draw_prize(date_prize: DatePrize):
if not participants:
return {"message": "No participants available","code":0}
winner = choice(participants)
prize = Prize(winner=winner.name,prize=date_prize.prize)
prizes.append(prize)
participants.remove(winner)
return {"message": f"Congratulations {winner.name}, you have won a prize : {date_prize.prize}!","code":1}
@app.get("/prizes")
async def get_prizes():
return {"prizes": [prize.winner for prize in prizes]}
@app.get("/participants")
async def get_participants():
return {"participants": [participant.name for participant in participants]}
由于我使用的是poetry作为项目的运行工具,因此在使用之前,你需要进行一些配置工作。[tool.poetry]
name = "python-lottery"
version = "0.1.0"
description = "python 抽奖"
authors = ["努力的小雨"]
[tool.poetry.dependencies]
python = "^3.10"
fastapi = "^0.105.0"
jinja2 = "^3.1.2"
[[tool.poetry.source]]
name = "aliyun"
url = "https://mirrors.aliyun.com/pypi/simple/"
default = true
secondary = false
启动项目命令:poetry run uvicorn main:app --reload --port 8081效果图总结在本文中,我们使用Python语言和fastapi框架构建了一个简易的抽奖系统。系统的前端界面使用了HTML、JS和CSS来配置样式和实现交互效果。后端使用了fastapi框架接收前端的请求,并处理抽奖逻辑。说实话,虽然我们有能力开发一个简易的抽奖系统,但既然我们都是程序员,为何要费力去搞一个抽奖系统呢?我们可以采用更简单的方式,将每个人的序号写在纸条上,放进一个纸箱子里,然后让领导亲自用手抓取。这样做不仅更可靠,还能增加年会的活跃氛围。
绝对勇士
Java开发者的Python快速进修指南:网络编程及并发编程
今天我们将对网络编程和多线程技术进行讲解,这两者的原理大家都已经了解了,因此我们主要关注的是它们的写法区别。虽然这些区别并不是非常明显,但我们之所以将网络编程和多线程一起讲解,是因为在学习Java的socket知识时,我们通常会将它们结合使用,以实现服务器对多个客户端连接的阻塞IO的处理。虽然我是这样解释的,但是Python在控制连接数方面更加友好,相对于Java来说更加便捷。好了,废话不多说,让我们开始今天的讲解吧。socket及线程这里我将给大家举一个例子,同时也会指出一些需要注意的问题,以帮助Java同学们避免再次遇到这些坑。import socket
import multiprocessing
import time
# 创建互斥锁
lock = multiprocessing.Lock()
# 处理客户端请求的函数
def handle_client(conn, addr):
print(f"Connected to {addr}")
time.sleep(100)
# 创建服务器
def create_server():
# 创建socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定地址和端口
server_addr = ("localhost", 8000)
server_socket.bind(server_addr)
# 监听连接
server_socket.listen(1)
print("Server started. Listening for connections...")
while True:
# 接受客户端连接
conn, addr = server_socket.accept()
handle_client(conn, addr)
# 创建进程处理客户端请求
process = multiprocessing.Process(target=handle_client, args=(conn, addr))
process.start()
if __name__ == '__main__':
# 启动服务器
print("启动服务器")
create_server()
以下是客户端的代码:import socket
import time
client = socket.socket() #创建socket对象
host = '127.0.0.1' #服务端ip
port = 8000 #服务端ip端口
client.connect((host, port)) #根据服务端地址,建立连接
print('client对象:', client) #查看socket对象属性
time.sleep(100)
#client.close() #关闭与服务端的连接
上面的例子已经涵盖了我今天要讲的内容,所以没有太多需要补充的了。不过,我可以谈一下与Java的一些区别。首先,Python使用multiprocessing来创建多线程,当然还有其他的包可以实现相同的功能,这里就不一一赘述了。另外,还有一个需要注意的地方是,在Python中使用if __name__ == '__main__':语句时,你必须将其写在主函数中,而不要写在你定义的子函数中。为什么要这样写呢?原因是,当调用process.start()后,Python会重新执行当前文件,也就是说如果你将if __name__ == '__main__':这句话写在非主函数中,create_server()它将会被再次调用。而使用if __name__ == '__main__':语句可以判断是否是主函数执行,如果不是,则会过滤掉这部分代码。虽然这个机制可能有些令人困惑,但是去深入了解其执行原理并不是必要的,所以大家要记住这个要点。这张图显示的是再次被调用是的名字:第二:在Python官网中指出,通过调用server_socket.listen(1)可以启动一个服务器,用于接受连接并将未接受的客户端连接放入等待队列中。需要注意的是,等待队列的大小由listen(n)中的参数n+1指定,并不代表实际监听到的客户端连接。如果超过队列大小的连接尝试进入,服务器将直接报错。总结今天我们学习了网络编程和多线程技术的写法区别。我们主要关注了在Java中使用socket和多线程结合实现服务器处理多个客户端连接的阻塞IO的方法,以及在Python中使用multiprocessing模块创建多线程的方式。通过一个实例来说明了这些概念,并指出了需要注意的问题。其实了解了这些基本用法后,我们还能够自己实现许多其他功能,例如了解了线程之后,就知道会有队列的概念,然后可以尝试自己实现一个生产者消费者队列。这与学习Java的路线非常相似,并且我们拥有丰富的开发经验,因此我们只需要关注语法方面的学习即可~~
绝对勇士
Java开发者的Python快速实战指南:探索向量数据库之图像相似搜索-文字版
首先,我要向大家道个歉。原本我计划今天向大家展示如何将图片和视频等形式转换为向量并存储在向量数据库中,但是当我查看文档时才发现,腾讯的向量数据库尚未完全开发完成。因此,今天我将用文本形式来演示相似图片搜索。如果您对腾讯的产品动态不太了解,可以查看官方网址:cloud.tencent.com/document/pr…在开始讲解之前,我想给大家介绍一个很有用的第三方包,它就是gradio。如果你想与他人共享你的机器学习模型、API或数据科学工作流的最佳方式之一,可以创建一个交互式应用,让用户或同事可以在浏览器中试用你的演示。而gradio正是可以帮助你在Python中构建这样的演示,并且只需要几行代码即可完成!作为一个后端开发者,我了解如果要我开发前端代码来进行演示,可能需要花费很长时间,甚至可能需要以月为单位计算。所幸,我发现了gradio这个工具的好处,它可以帮助我解决这个问题。使用gradio,我只需要专注于实现我的方法,而不需要关心如何实现界面部分,这对于像我这样不擅长前端开发的人来说非常合适。gradio为我提供了一个简单而有效的解决方案。源码仓库地址:github.com/StudiousXia…Gradio关于gradio的环境配置和官方文档,我就不再赘述了,有兴趣的同学可以去官方文档地址www.gradio.app/guides/quic… 查看。对于后端开发者来说,上手使用gradio非常容易。接下来,我们将搭建一个最简单的图片展示应用。由于我要实现的功能是图片展示,所以我将直接上代码。数据准备首先,我们需要准备数据。我已经从官方获取了训练数据,并将图片的信息和路径保存到了我的向量数据库中。幸运的是,这些数据已经被整理成了一个CSV文件。现在,我想要将这些数据插入到数据库中。这是一个很好的机会来练习一下我们的Python语法,比如读取文件、引用第三方包以及使用循环。让我们来看一下具体的实现方法。我的csv文件是这样的:id,path,label
0,./train/brain_coral/n01917289_1783.JPEG,brain_coral
1,./train/brain_coral/n01917289_4317.JPEG,brain_coral
2,./train/brain_coral/n01917289_765.JPEG,brain_coral
3,./train/brain_coral/n01917289_1079.JPEG,brain_coral
4,./train/brain_coral/n01917289_2484.JPEG,brain_coral
5,./train/brain_coral/n01917289_1082.JPEG,brain_coral
6,./train/brain_coral/n01917289_1538.JPEG,brain_coral
在这个文件中,第一行是列名,从第二行开始,我可以开始解析数据了。之前已经完成了数据库的创建,所以我就不再演示了。现在,我们将直接开始设计集合,并将数据插入到我们的集合中。import gradio as gr
import numpy as np
import tcvectordb
from tcvectordb.model.collection import Embedding
from tcvectordb.model.document import Document, Filter, SearchParams
from tcvectordb.model.enum import FieldType, IndexType, MetricType, ReadConsistency,EmbeddingModel
from tcvectordb.model.index import Index, VectorIndex, FilterIndex, HNSWParams
client = tcvectordb.VectorDBClient(url='http://*****',
username='root', key='1tWQ*****',
read_consistency=ReadConsistency.EVENTUAL_CONSISTENCY, timeout=30)
db = client.database('db-xiaoyu')
上面提到的这些流程是基本的,我就不再详细解释了。我们可以直接开始连接,但是在此之前,我们需要先创建一个专门用于图片搜索的集合。之前我们创建的是用于文本搜索的集合,现在我们需要创建一个新的集合来区分。以下是相应的代码:# -- index config
index = Index(
FilterIndex(name='id', field_type=FieldType.String, index_type=IndexType.PRIMARY_KEY),
VectorIndex(name='vector', dimension=768, index_type=IndexType.HNSW,
metric_type=MetricType.COSINE, params=HNSWParams(m=16, efconstruction=200))
)
# Embedding config
ebd = Embedding(vector_field='vector', field='image_info', model=EmbeddingModel.BGE_BASE_ZH)
# create a collection
coll = db.create_collection(
name='image-xiaoyu',
shard=1,
replicas=0,
description='this is a collection of test embedding',
embedding=ebd,
index=index
)
由于目前向量数据库尚未完全支持图像文件转换为向量的功能,因此我们决定将其改为存储图像描述信息,并将图像路径直接存储为普通字段。由于我们对路径没有过滤要求,因此将其作为普通字段进行存储。所有信息已经成功存储在CSV文件中,因此我们只需直接读取该文件内容并将其存入向量数据库中即可。以下是相关代码示例:data = np.genfromtxt('./reverse_image_search/reverse_image_search.csv', delimiter=',', skip_header=1, usecols=[0, 1, 2], dtype=None)
doc_list = []
for row in data:
id_row = str(row[0])
image_url = row[1].decode()
image_info = row[2].decode()
doc_list.append(Document(id=id_row,image_url=image_url,image_info=image_info))
res = coll.upsert(
documents=doc_list,
build_index=True
)
在这段代码中,我使用了 import numpy as np 语句来导入 numpy 库。为什么我使用它呢?因为我在搜索中发现它可以处理 CSV 文件。毕竟,在Python编程中总是喜欢使用现成的工具。最后,我将 Document 封装成一个列表,并将其全部插入到集合中。构建Gradio交互界面数据准备工作已经完成,接下来我们需要考虑如何建立一个交互界面。我知道Python有很多优秀的库,其中有一个可以一键构建交互界面的库,这真的很厉害。与Java的自定义界面相比,它们是完全不同的东西,因为他俩没得比。为了实现交互界面的功能,我们需要在一个新的py文件中编写以下代码:import gradio as gr
import tcvectordb
from tcvectordb.model.document import SearchParams
from tcvectordb.model.enum import ReadConsistency
client = tcvectordb.VectorDBClient(url='http://lb-m*****',
username='root', key='1tWQ*****',
read_consistency=ReadConsistency.EVENTUAL_CONSISTENCY, timeout=30)
db = client.database('db-xiaoyu')
coll = db.collection('image-xiaoyu')
def similar_image_text(text):
doc_lists = coll.searchByText(
embeddingItems=[text],
params=SearchParams(ef=200),
limit=3,
retrieve_vector=False,
output_fields=['image_url', 'image_info']
)
img_list = []
for i,docs in enumerate(doc_lists.get("documents")):
for my_doc in docs:
print(type(my_doc["image_url"]))
img_list.append(str(my_doc["image_url"]))
return img_list
def similar_image(x):
pass
with gr.Blocks() as demo:
gr.Markdown("使用此演示通过文本/图像文件来找到相似图片。")
with gr.Tab("文本搜索"):
with gr.Row():
text_input = gr.Textbox()
image_text_output = gr.Gallery(label="最终的结果图片").style(height='auto', columns=3)
text_button = gr.Button("开始搜索")
with gr.Tab("图像搜索"):
with gr.Row():
image_input = gr.Image()
image_output = gr.Gallery(label="最终的结果图片").style(height='auto', columns=3)
image_button = gr.Button("开始搜索")
with gr.Accordion("努力的小雨探索AI世界!"):
gr.Markdown("先将图片或者路径存储到向量数据库中。然后通过文本/图像文件来找到相似图片。")
text_button.click(similar_image_text, inputs=text_input, outputs=image_text_output)
image_button.click(similar_image, inputs=image_input, outputs=image_output)
demo.launch()
我创建了一个带有两个标签页的界面。由于本次项目不需要使用图像相似搜索功能,所以等到该功能推出后,我会再次进行图像方面的相似搜索演示。目前,我们只能通过图片描述来查找并显示图片。这部分没有太多值得讲的,我只是对 Gardio 官方示例进行了一些修改。如果你还不清楚的话,我建议你查看官方示例和介绍。现在,让我们来看一下我的运行界面吧。当我输入"gold"后,根据我所存储的图片描述是"gold fish",所以可以找到对应的匹配项。当我看到三种金鱼的图片时,就说明我们的运行是正常的。我已经为图片相似搜索留出来了,以便及时更新。总结今天我们写代码时,基本上已经熟练掌握了Python的语法。剩下的就是学习如何使用第三方包,以及在编写过程中遇到不熟悉的包时,可以通过百度搜索来获取答案。虽然并没有太大难度,但是对于使用gradio来说,可能需要花费一些时间上手。有时会遇到一些错误,不像Java那样能够一眼识别出问题所在,需要上网搜索来解决。
绝对勇士
Reactor 第十二篇 WebFlux集成PostgreSQL
1 引言在现代的应用开发中,数据库是存储和管理数据的关键组件。PostgreSQL 是一种强大的开源关系型数据库,而 WebFlux 是 Spring 框架提供的响应式编程模型。本文将介绍如何使用 Reactor 和 WebFlux 集成 PostgreSQL,实现响应式的数据库访问。1. 环境准备首先,我们需要在项目的 pom.xml 文件中添加 Spring Data R2DBC 和 PostgreSQL 的依赖:<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-postgresql</artifactId>
</dependency>
...
</dependencies>2. 配置PostgreSQL连接信息在 application.properties 文件中添加 PostgreSQL 连接的配置信息:spring.r2dbc.url=r2dbc:postgresql://localhost:5432/mydatabase
spring.r2dbc.username=postgres
spring.r2dbc.password=1234563. 创建实体类和数据访问接口在 Java 包中创建一个实体类和一个数据访问接口,用于定义数据库表和相应的 CRUD 操作:@Table("users")
public class User {
@Id
private Long id;
private String name;
// 省略其他属性和方法
}
interface UserRepository extends ReactiveCrudRepository<User, Long> {
// 省略其他CRUD操作方法
}在上述代码中,我们使用 Spring Data R2DBC 提供的注解和接口来定义实体类和数据访问接口。@Table 用于指定表名,@Id 用于定义主键。编写数据库访问逻辑定义一个 Service 类来处理数据库访问操作:@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public Mono<User> getUserById(Long id) {
return userRepository.findById(id);
}
public Flux<User> getAllUsers() {
return userRepository.findAll();
}
public Mono<User> saveUser(User user) {
return userRepository.save(user);
}
public Mono<Void> deleteUserById(Long id) {
return userRepository.deleteById(id);
}
}在上述代码中,我们使用 Spring Data R2DBC 提供的方法来实现数据库的增删改查操作。通过使用 Mono 和 Flux 来处理响应式流,使得数据访问操作变得高效和灵活。创建WebFlux控制器编写一个 WebFlux 控制器来处理请求:@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/users/{id}")
public Mono<User> getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
@GetMapping("/users")
public Flux<User> getAllUsers() {
return userService.getAllUsers();
}
@PostMapping("/users")
public Mono<User> saveUser(@RequestBody User user) {
return userService.saveUser(user);
}
@DeleteMapping("/users/{id}")
public Mono<Void> deleteUserById(@PathVariable Long id) {
return userService.deleteUserById(id);
}
}在上述代码中,我们使用 @GetMapping、@PostMapping 和 @DeleteMapping 来映射 URL,并调用 UserService 中的相应方法来处理具体的数据库访问逻辑。2 总结本文介绍了如何使用 Reactor 和 WebFlux 集成 PostgreSQL,实现响应式的数据库访问。通过使用 Spring Data R2DBC 和响应式的流处理,我们可以方便地进行数据库的增删改查操作。这种方式可以提升系统的性能和扩展性,特别适用于高并发和大数据量的场景。希望本文对您在使用 Reactor 和 WebFlux 集成 PostgreSQL 方面有所帮助。无论是使用 PostgreSQL 还是其他数据库,使用响应式方式来进行数据库访问都能带来很多好处。不断地学习和探索新的技术,可以让我们的应用更加高效和可维护。参考链接:Spring Data R2DBC: spring.io/projects/sp…R2DBC: r2dbc.io
绝对勇士
Java开发者的Python快速进修指南
穿越Java与Python的编程边界,Java开发者的Python快速进修指南引领您迅速掌握Python的关键知识。
绝对勇士
WebFlux进行响应式编程的理论以及最佳实践
介绍使用WebFlux进行响应式编程的理论以及最佳实践