开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

JWT:你真的了解它吗? 1、初识JWT 2、JWT的结构

发表于 2024-04-07
大家好,我是石头~


在数字化时代,网络安全和用户隐私保护成为了我们无法忽视的关键议题,也是我们作为一个后端开发的必修课。


而在这个领域中,JWT(JSON Web Token)作为一种现代、安全且高效的会话管理机制,在各类Web服务及API接口中得到了广泛应用。


那么,什么是JWT?

下载 (3).jfif

1、初识JWT

JWT,全称为JSON Web Token,是一种开放标准(RFC 7519),用于在网络应用环境间安全地传输信息。


它本质上是一个经过数字签名的JSON对象,能够携带并传递状态信息(如用户身份验证、授权等)。


了解了JWT之后,那么它的组成结构又是怎样的?

2、JWT的结构

u=2288314449,1048843062&fm=253&fmt=auto&app=138&f=JPEG.webp

如上图,JWT由三部分组成,通过点号(.)连接,这三部分分别是头部(Header)、载荷(Payload)和签名(Signature)。
  • 头部(Header):声明了JWT的类型(通常是JWT)以及所使用的加密算法(例如HMAC SHA256或RSA)
  • 载荷(Payload):承载实际数据的部分,可以包含预定义的声明(如iss(签发者)、exp(过期时间)、sub(主题)等)以及其它自定义的数据。这些信息都是铭文的,但不建议存放敏感信息。
  • 签名(Signature):通过对前两部分进行编码后的信息,使用指定的密钥通过头部(Header)中声明的加密算法生成,拥有验证数据完整性和防止篡改。

3、JWT的常规认证流程

2020040121153580.png

JWT的认证流程如上图。当用户登录时,服务器通过验证用户名和密码后,会生成一个JWT,并将其发送给客户端。这个JWT中可能包含用户的认证信息、权限信息以及其它必要的业务数据。


客户端在接收到JWT后,通常将其保存在本地(如Cookie、LocalStorage或者SessionStorage)。


客户端在后续的请求中,携带此JWT(通常是附在HTTP请求头中),无需再次提交用户名和密码。服务器只需对收到的JWT进行解码并验证签名,即可完成用户身份的确认和权限验证。

4、JWT的完整认证流程

在上面的JWT常规认证流程中,我们可以正常完成登陆、鉴权等认证,但是你会发现在这个流程中,我们无法实现退出登陆。


当服务端将JWT发放给客户端后,服务端就失去了对JWT的控制权,只能等待这些发放出去的JWT超过有效期,自然失效。


为了解决这个问题,我们引入了缓存,如下图。

2020040121022176.png

当服务端生成JWT之后,在返回给客户端之前,先将JWT存入缓存中。要鉴权的时候,需要检验缓存中是否存在这个JWT。


这样的话,如果用户退出登陆,我们只需要将缓存中的JWT删除,即可保证发放出去的JWT无法再通过鉴权。

5、JWT的优势与挑战

JWT的主要优点在于无状态性,服务器无需存储会话状态,减轻了服务器压力,同时提高了系统的可扩展性和性能。


此外,由于JWT的有效期限制,增强了安全性。


然而,JWT也面临一些挑战,比如密钥的安全保管、JWT过期策略的设计以及如何处理丢失或被盗用的情况。


因此,在实际应用中,需要综合考虑业务场景和技术特性来合理运用JWT。

6、JWT示例

概念讲完了,我们最后来看个实例吧。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
typescript复制代码// Java代码示例
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

// 假设有一个User类,其中包含用户ID
public class User {
private String id;
// 其他属性和方法...
}

// 创建JWT
public String generateJWT(User user) {
// 设置秘钥(在此处使用的是HMAC SHA-256算法)
String secret = "your-secret-key"; // 在实际场景中应当从安全的地方获取秘钥
long ttlMillis = 60 * 60 * 1000; // JWT的有效期为1小时

// 构建载荷,包含用户ID和其他相关信息
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("exp", System.currentTimeMillis() + ttlMillis); // 设置过期时间

// 生成JWT
    String jwt = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS256, secret.getBytes(StandardCharsets.UTF_8))
.compact();
    // TODO JWT写入缓存
    return jwt;
}

// 验证JWT
public boolean validateJWT(String jwtToken, String secretKey) {
boolean flag = false;
try {
Jwts.parser().setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)).parseClaimsJws(jwtToken);
// 如果没有抛出异常,则JWT验证通过
flag = true;
} catch (ExpiredJwtException e) {
// 如果Token过期
System.out.println("JWT已过期");
} catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
// 其他非法情况,如格式错误、签名无效等
System.out.println("JWT验证失败:" + e.getMessage());
}
if (flag) {
        // TODO 校验缓存中是否有该JWT
}
return false;
}

// 使用示例
public static void main(String[] args) {
User user = new User("123"); // 假设这是合法用户的ID
String token = generateJWT(user); // 生成JWT
System.out.println("生成的JWT Token: " + token);

// 验证生成的JWT
boolean isValid = validateJWT(token, "your-secret-key");
if (isValid) {
System.out.println("JWT验证通过!");
} else {
System.out.println("JWT验证未通过!");
}
}

**MORE | 更多精彩文章**

  • H5推送,为什么都用WebSocket?
  • 别再这么写POST请求了~
  • 揭秘布谷鸟过滤器:一场数据过滤中的“鸠占鹊巢”大戏!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

用零代码的思维设计文档编辑引擎

发表于 2024-04-07

嗨,大家好,我是徐小夕。之前和大家分享了很多零代码,可视化相关的最佳实践,最近也花了很多实践研究文档引擎,接下来就和大家分享一下最近的研究成果。

大家看到的就是我做的文档编辑引擎,类似于飞书文档和钉钉文档,我们可以使用它轻松编写文档,并能随意调整文档元素的顺序。

这一方案实现依赖于
block 的设计思想,我们可以把文档想象成一个个由块组成的内容,每一个块都能共享编辑器对应的能力,并且可以分治管理。

体验地址:文档案例入口

功能介绍

由于我之前研究了并开发了很多零代码产品,所以基于零代码的设计思想,将文档编写进行了进一步的升级,支持如下功能:

  • 支持插入互动组件(如点赞,关注,赞赏,弹幕等)
  • 支持嵌入丰富组件物料(图表,思维导图,网页,视频等)
  • 支持自定义文档元素 / 组件
  • 支持文档大纲自动生成

接下来我演示一下部分功能:

doc.gif

插入表格

插入图表

编辑图表数据:

插入网页

我们铜过输入链接可以自动生成网页的信息, 这块采用类似爬虫的技术来实现对网址的解析。

插入视频

我们可以输入视频网址来一键渲染视频:

插入第三方网页

我们可以在自己的文档里内嵌一个第三方网址,并能提供相关的服务。

插入数学公式

我们可以通过可视化的方式来编写高级数学公式,比如微积分,三角函数,矩阵变换等,而无需懂 Latex 语法。

当然还有很多功能,大家也可以体验反馈。

目前我会在10号晚上上线内测,大家也可以随时提出优化建议。

后期规划

  • 思维导图
  • 批注
  • 多人协同
  • AI能力
  • 文档组件库

体验地址:文档案例入口

更多推荐

可视化表单&试卷搭建平台技术详解

爆肝1000小时, Dooring零代码搭建平台3.5正式上线

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

揭秘布谷鸟过滤器:一场数据过滤中的“鸠占鹊巢”大戏! 1、布

发表于 2024-04-05
大家好,我是石头~


前面我发了一篇关于缓存系统三大痛点的文章《[一文读懂缓存击穿、穿透与雪崩,破局之道何在?](https://dev.newban.cn/7353483477553152041)》,里面有提到用布谷鸟过滤器来解决缓存穿透的问题。今日,我将继续深化探讨,为大家揭开布谷鸟过滤器的神秘面纱。

u=2823600658,684681512&fm=253&fmt=auto&app=138&f=JPEG.webp

1、布谷鸟过滤器的诞生背景

布谷鸟过滤器,这个名称听起来似乎与数据过滤关联不大,实则源于它的工作机制类似于布谷鸟将蛋下在其他鸟类巢穴中的行为。


在计算机科学领域,布谷鸟过滤器作为一种空间效率极高的概率型数据结构,专为大规模数据去重和查找问题设计,是布隆过滤器的优秀替代者。

1f9ada3b2e754eada37b3eb92bc18070.png

2、布谷鸟过滤器的组成

在了解布谷鸟过滤器的实现原理之前,我们先了解下布谷鸟过滤器的组成。

image.png

如上图所示,布谷鸟过滤器由三部分组成:哈希表、存储在哈希表中的指纹和两个并不独立的哈希函数(h1和h2)。

哈希表

布谷鸟过滤器的布谷鸟哈希表的基本单位称为条目, 每个条目存储一个指纹。


哈希表由一个桶数组组成,其中一个桶可以有多个条目(比如上图中有四个条目)。而每个桶中有四个指纹位置,意味着一次哈希计算后布谷鸟有四个“巢“可用,而且四个巢是连续位置,可以更好的利用cpu高速缓存。也就是说每个桶的大小是4\*8bits。

指纹

我们在使用布谷鸟过滤器的时候,并不需要存储具体的信息,因为整个过滤器的作用只是证明当前元素是否可能存在,因此我们需要把可以证明这个元素的关键信息放进去就可以了,这个关键信息我们称之为指纹。


指纹是使用一个哈希函数生成的n位比特位,n的具体大小由所能接受的误判率来设置。

哈希函数

这两个哈希函数,是布谷鸟过滤器用来计算元素存储的桶位置的。每个待插入的元素,都会通过这两个哈希元素计算出2个候选桶位置。

3、布谷鸟过滤器实现原理

了解了布谷鸟过滤器的组成之后,我们就可以接着了解下它的实现原理。

1617168-20210727195625561-1153589445.jpg
如上图,(a)和(b)是布谷鸟过滤器的插入过程。

插入

image.png
上图是一布谷鸟过滤器计算桶位置过程,当一个元素x要插入到布谷鸟过滤器时,布谷鸟过滤器会先使用fingerprint函数计算出指纹fp,接着用hash算法对计算式第一个桶位置p1,并用p1和指纹通过第3个公式计算出第2个桶位置p2。

接下来,我们要开始执行插入动作,这里会遇到三种情况:
  • p1桶和p2桶都有空余空间:将指纹fp随机插入一个桶
  • p1桶和p2桶只有一个桶有空余空间:将指纹fp插入有空余空间的桶中
  • p1桶和p2桶都没有空余空间:此时将触发布谷鸟过滤器的“踢巢”操作(将现有桶内一个元素的指纹移出,并根据此时桶位置和该元素指纹计算出该指纹的另外一个桶位置。若此时这个桶的位置也满了,则继续“踢巢”,直到找到一个可供新元素插入的空桶为止。)

查找

布谷鸟过滤器的查找过程很简单,给定一个项x,算法首先根据上述插入公式,计算x的指纹和两个候选桶。然后读取这两个桶:如果两个桶中的任何现有指纹匹配,则布谷鸟过滤器返回true,否则过滤器返回false。此时,只要不发生桶溢出,就可以确保没有假阴性。

删除

删除的过程也很简单,检查给定项的两个候选桶;如果任何桶中的指纹匹配,则从该桶中删除匹配指纹的一份副本。

4、布谷鸟过滤器的不足

存在误删的可能行

若是不同元素根据函数计算出来的指纹和桶位置相同,则可能出现误删。

插入复杂度比较高

随着插入元素的增多,触发“踢巢”的可能性越高,复杂度会越来越高。

存储空间的大小必须为2的指数

这个限制让空间效率打了折扣。

同一个元素最多插入kb次

k指哈希函数的个数,b指的桶中能装指纹的个数也可以说是桶的尺寸大小。如果布谷鸟过滤器支持删除,则必须存储同一项的多个副本。 插入同一项kb+1次将导致插入失败。 这类似于计数布隆过滤器,其中重复插入会导致计数器溢出。

5、总结

以上为本次分析的内容,欢迎大家评论区留言讨论~

**MORE | 更多精彩文章**

  • H5推送,为什么都用WebSocket?
  • 别再这么写POST请求了~
  • Redis不再“开源”~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

字节二面:为什么SpringBoot的 jar 可以直接运行

发表于 2024-04-04

引言

在传统的Java应用程序开发和部署场景中,开发者往往需要经历一系列复杂的步骤才能将应用成功部署到生产环境。例如,对于基于Servlet规范的Java Web应用,开发完成后通常会被打包成WAR格式,然后部署到像Apache Tomcat、Jetty这样的Web容器中。这一过程中,不仅要管理应用本身的编译产物,还需要处理各种第三方依赖库的版本和加载顺序,同时在服务器端进行相应的配置以确保应用正常运行。

随着Spring Boot产生,它以其开箱即用、约定优于配置的理念彻底改变了Java应用的开发体验。其中一个标志性特征便是Spring Boot应用可以被打包成一个可直接运行的jar文件,无需外部容器的支持。

当提及“Spring Boot的jar可以直接运行”,我们不禁好奇:这背后究竟是怎样的机制让一个简单的命令行操作就能启动一个完整的Web服务或任何类型的Java应用呢?本文将深入剖析Spring Boot的打包过程和运行原理,揭示其jar包是如何巧妙地集成了依赖、嵌入了Web容器、实现了自动配置等功能,从而使得开发人员能够迅速地将应用部署到任何支持Java的环境中。

springboot的jar包为什么可以直接运行.png

SpringBoot JAR包基础概念

Fat JAR(也称作Uber JAR,也被戏称为胖Jar)是一种特殊的Java归档(JAR)文件,它将应用程序所需的全部依赖库与应用程序自身的类文件合并到了同一个JAR文件中。在Spring Boot上下文中,Fat JAR被用于构建一种完全自包含且可独立运行的应用程序包。这样的jar文件不仅仅包含项目的主代码,还包括了所有必要的第三方库、资源文件等一切运行时所需要的组件。

Fat JAR的核心特点是“自包含”,意味着只需分发这一个文件即可部署应用,无需再额外处理众多的依赖库。这种形式极大地方便了应用的快速部署与迁移,尤其适合于云端部署或者无网络环境下的安装。

而对于普通jar包来说,它通常仅包含一个模块或应用程序的一部分,主要用来封装和组织Java类及相关资源。在Java生态系统中,一个普通的jar包可能仅是一个库,或者一组相关功能的集合,但它不会包含其他依赖的jar包,因此在运行时需要与之相关的其他库一起存在于类路径中。

相比之下,Fat JAR则解决了依赖管理的问题,通过将所有的依赖都纳入其中,避免了由于类路径设置不正确导致的“缺失类”或“找不到类”的问题。在Spring Boot项目中,通过Maven或Gradle插件可以轻易地构建出这样的Fat JAR,使得最终生成的jar文件成为一个真正的“一站式”解决方案,只需使用java -jar命令就可以启动整个应用程序,无需预先配置复杂的类路径环境。

Spring Boot应用打包机制

Spring Boot应用打包机制充分利用了Maven或Gradle构建工具的强大功能,旨在简化传统Java应用的构建与部署流程。其核心在于创建一个可执行的Fat JAR,使得开发者能够轻松地将整个Spring Boot应用及其依赖项打包成单个文件,从而实现一键启动和便捷部署。

我们以Maven打包为例:

对于使用Maven构建的Spring Boot应用,spring-boot-maven-plugin是关键插件,负责处理Fat JAR的构建。在pom.xml文件中,通常会看到如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>${spring-boot.version}</version>
            <configuration>
                <!-- 可选配置项,如mainClass属性指定入口类 -->
                <mainClass>${start-class}</mainClass>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

通过mvn package命令,Maven首先会按照标准流程构建项目,随后spring-boot-maven-plugin会执行repackage目标,该目标会重新包装已生成的标准JAR文件,将其转换为包含所有依赖项和适当的启动器信息的Fat JAR。这样生成的JAR可以直接通过java -jar命令启动。

Spring Boot应用打包机制均确保了生成的包不仅包含了项目本身的类,还包含了运行时所必需的所有依赖库,以及一些特定的元数据(如MANIFEST.MF中的启动类信息)。这一特性大大简化了部署过程,并有助于提升应用的可移植性和维护性。Fat jar中的内容:

image.png

  • META-INF/: 包含MANIFEST.MF文件和其他元数据信息,其中Main-Class属性指向Spring Boot的启动类加载器。
  • BOOT-INF/classes/: 存放项目自身的类文件和资源文件。
  • BOOT-INF/lib/: 放置所有依赖的jar包,包括Spring Boot starter依赖以及其他第三方库。(如果项目中有静态资源文件,也会在BOOT-INF下有对应的static、templates等目录)

Spring Boot启动器与Loader机制

Spring Boot应用的jar包可以直接运行主要依赖于它的启动器以及Loader机制,而对于Loader机制主要利用MANIFEST.MF文件以及其内部类加载逻辑。

MANIFEST.MF文件是什么?

MANIFEST.MF是JAR文件内的一个标准元数据文件,它包含了关于JAR包的基本信息和运行指令。在Spring Boot应用的jar包中,MANIFEST.MF尤为重要,因为它设置了Main-Class属性,指示了用于启动整个应用程序的类,这个类通常是org.springframework.boot.loader.JarLauncher或其他由Spring Boot提供的启动器类。

image.png

image.png

Main-Class属性指向的JarLauncher类是Spring Boot自定义的类加载器体系的一部分。JarLauncher继承自org.springframework.boot.loader.Launcher,专门用于启动以Fat JAR形式发布的Spring Boot应用。JarLauncher负责创建一个类加载器LaunchedURLClassLoader。

image.png

image.png

image.png

当通过java -jar命令执行Spring Boot jar包时,JVM会依据MANIFEST.MF中的Main-Class启动指定的启动器。

JarLauncher获取MainClass源码.png

JarLauncher获取MainClass源码.png

Spring Boot的启动器类加载器LaunchedURLClassLoader首先会读取MANIFEST.MF中的附加属性,如Start-Class(标识应用的实际主类)和Spring-Boot-Lib(指向内部依赖库的位置)。

image.png

image.png

启动类加载器工作流程如下:

  1. 当启动器类加载器启动时,它会根据MANIFEST.MF中的信息来组织类路径,保证所有内部的依赖库都能正确地被加载。
  2. 加载器会区分出 BOOT-INF/classes中的应用程序类和 BOOT-INF/lib 下的依赖库,分别处理并加入到类加载器的搜索路径中。
  3. 加载器加载并执行实际的Start-Class,即应用的主类,触发Spring Boot框架的初始化和应用的启动流程。比如示例中的应用主类:com.springboot.base.SpringBootBaseApplication

Spring Boot的启动器和加载器机制有效地实现了对自包含jar包的管理和执行,我们无需关心复杂的类路径配置和依赖加载,只需通过一个简单的命令即可启动一个完整、独立运行的应用程序。

内嵌Web容器

Spring Boot的一大特色就是能够无缝整合并内嵌多种轻量级Web容器,比如:Apache Tomcat、Jetty、Undertow以及Reactor Netty(对于响应式编程模型)。内嵌Web容器的引入极大地简化了Web应用的部署流程,我们不再需要在本地或服务器上独立安装和配置Web服务器(比如以前还要在本地安装tomcat)。

当Spring Boot应用引入了spring-boot-starter-web依赖时,默认情况下会自动配置并启动一个内嵌的Web容器。在Spring Boot启动的过程中,内嵌容器作为应用的一部分被初始化并绑定到特定端口上,以便对外提供HTTP服务。

Spring Boot内嵌web容器的优点在于简化部署,通过将Web容器内置于应用中,只需分发单一的JAR文件,就能在干净的环境中运行应用,避免了与现有Web服务器版本冲突或配置不当等问题;同时加快了启动速度,尤其在开发和测试阶段,实现近乎即时的热重启;提高了应用的稳定性,因为开发环境和生产环境使用相同的Web容器,降低了因环境差异导致的问题;此外,虽然容器是内嵌的,但仍然可以进行全面的配置调整,如端口、连接数、SSL设置等,以满足不同场景的需求。通过内嵌Web容器,Spring Boot真正实现了“开箱即用”的理念。

自动配置与类路径扫描

Spring Boot的核心特性之一就是其强大的自动配置能力,它允许应用在几乎零配置的情况下快速启动并运行。

当应用启动时,Spring Boot会读取resource/META-INF/spring.factories文件,该文件列出了所有可用的自动配置类。当它检测到应用环境中对应的自动配置类就会生效,通过@Configuration注解的类创建并注册Bean到Spring容器中,从而实现Bean的自动装配。

这里说明下,在springboot3.x以后,就不在从resource/META-INF/spring.factories读取自动配置类了,而是从org.springframework.boot.autoconfigure.AutoConfiguration.imports中读取,这一点请参考文章:华为二面:SpringBoot如何自定义Starter?

并且Spring Boot还采用条件注解(如@ConditionalOnClass、@ConditionalOnMissingBean等)来智能判断何时应用特定的配置。这些注解可以根据类路径中是否存在特定类、系统属性或环境变量的值等因素,决定是否应该激活某个自动配置类。这意味着只有当满足特定条件时,相应的Bean才会被创建和注入。

而对于应用主类则是用@SpringBootApplication注解标识。@SpringBootApplication是一个复合注解,包含了@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan三个注解的功能。其中

  • @SpringBootConfiguration是一个Spring配置类,可以替代@Configuration注解,声明当前类是Spring配置类,里面包含了一系列@Bean方法或@ConfigurationProperties等配置。
  • @EnableAutoConfiguration启用自动配置特性,告诉Spring Boot根据应用类路径中的依赖来自动配置Bean。Spring Boot会根据类路径扫描的结果,智能地决定哪些自动配置类应当生效。
  • @ComponentScan会自动扫描和管理Spring组件,包括@Service、@Repository、@Controller和@Component等注解标注的类。通过该注解,Spring Boot能自动发现和管理应用中的各个组件,并将其注册为Spring容器中的Bean。

通过上述机制,Spring Boot能够智能识别项目依赖、自动配置Bean,并结合类路径扫描确保所有相关的组件和服务都被正确地初始化和管理,我们就可以专注于业务逻辑的开发,而不必过多考虑基础设施层面的配置问题。

总结

Spring Boot 应用程序被打包成的jar包之所以可以直接通过 java -jar 命令运行,是因为Spring Boot在构建过程中做了一些特殊的设计和配置。具体原因:

  1. Fat/Uber JAR: Spring Boot使用maven插件spring-boot-maven-plugin(或Gradle对应的插件)将项目及其所有依赖项打包成一个单一的、自包含的jar文件,通常称为“Fat JAR”或“Uber JAR”。这意味着不仅包含了自己的类文件,还包含了运行应用所需的所有第三方库。
  2. Manifest.MF: 在打包过程中,此插件会修改MANIFEST.MF文件,这是jar包中的一个元数据文件。在MANIFEST.MF中,特别指定了Main-Class属性,该属性指向Spring Boot的一个内置的启动类(如org.springframework.boot.loader.JarLauncher),这个启动器类知道如何正确启动Spring Boot应用程序。
  3. 嵌入式Servlet容器:Spring Boot默认集成了诸如Tomcat、Jetty或Undertow等嵌入式Web容器,使得无需外部服务器环境也能运行Web应用。
  4. 启动器类加载器:当通过java -jar运行Spring Boot应用时,JVM会根据MANIFEST.MF中的Main-Class找到并运行指定的启动器类。这个启动器类加载器能够解压并加载内部的依赖库,并定位到实际的应用主类(在spring-boot-starter-parent或@SpringBootApplication注解标记的类),进而执行其main方法。
  5. 类路径扫描和自动配置:Spring Boot应用通过特定的类路径扫描机制和自动配置功能,能够在启动时识别出应用所依赖的服务和组件,并自动配置它们,大大简化了传统Java应用的配置和部署过程。

Spring Boot通过精心设计的打包流程和启动器类,使得生成的jar包可以直接作为一个独立的应用程序运行,极大地简化了部署和运维复杂度。

本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Web Component 组件库有什么优势

发表于 2024-04-04

前言

前端目前比较主流的框架有 react,vuejs,angular 等。 我们通常去搭建组件库的时候都是基于某一种框架去搭建,比如 ant-design 是基于 react 搭建的UI组件库,而 element-plus 则是基于 vuejs 搭建的组件库。

可能你有这种体会,在开发 vue 项目需要用到一个库插件,去搜索一遍发现只有 react 开发的库插件功能比较符合自己的需求,这时候就比较纠结了。虽然目前社区有相关工具 像 veaury,提供框架之间的转化服务,比如 vuejs 组件转化为 react 组件。但是毕竟是不同的框架,有不同的标准。如果有问题,就不知道从何入手调试解决问题了。

并且框架升级之后,比如从 2.x 升级到 3.x,总是带来一定的破坏性,升级和更改框架的成本很大,项目业务稳定后几乎是不可能升级。

所以在寻找有没有哪一种 UI 库可以跨越框架限制使用在项目中,在了解 web-component 技术后,使用 Stencil.js 框架根据 Element-plus 设计造轮子搭建了一个 stencil-component-ui 示例组件库,发现这是一种不错的技术和方向。

没错,上面看到的是 web-component 开发的 button 组件,感觉和 element-plus button 按钮长的一样,不过开发起来更简单代码量更少,而且能用在 React/Vue/Angular 不同框架项目上,甚至能用在 纯JS和服务端渲染 静态网站

注意:只是简单示例项目,功能不完善

什么是 web-component

“Web Components” 是一组现代的 Web 开发标准和技术,它的一系列 HTML 和 DOM 的特性加入了 w3c 标准,允许开发者创建封装好的、可复用的自定义元素。这些自定义元素可以在 HTML 中像原生的 HTML 元素一样的使用

由于 Web Components 是由w3c组织去推动的,因此它很有可能在不久的将来成为浏览器的一个标配。

在我了解到京东 micro-app 微前端框架、taro-ui UI 库,语雀的富文本编辑器都使用到 web components 技术

web-component 特性

Web Components 由 3 个核心部分组成

Custom Elements

允许开发者定义自己的 HTML 元素。可以创建新的 HTML 标签,并定义它们的行为。一旦一个 Custom Element 被定义和注册,可以在页面上像使用其他 HTML 元素一样使用它。

1
html复制代码<my-element></my-element>

Shadow DOM

Shadow DOM 允许开发者封装一个 DOM 子树和样式,让它们与主文档的 DOM 保持独立。这意味着在组件内部定义的样式不会影响到外部的页面,反之亦然。

例如,在 Shadow DOM 中的样式只影响 Shadow DOM:

1
2
3
4
5
6
7
html复制代码<my-element>
#shadow-root
<style>
p { color: red; }
</style>
<p>This is in shadow DOM and styled red.</p>
</my-element>

HTML Templates

HTML Templates 通过 <template> 和 <slot> 元素,允许定义一个可以在 JavaScript 中使用的 HTML 片段。它在不被引用的情况下不会被渲染,且不会影响页面的加载。
例如:

1
2
3
html复制代码<template id="my-template">
<p>My Template</p>
</template>

应用场景

Web Components 提供了一种创建可重用、封装和模块化的自定义 HTML 元素的方式。由于它们是基于 Web 平台的标准技术构建的,这意味着构建的 Web Components 可以在任何使用了现代 Web 技术的应用中使用,无论这些应用是使用哪种框架(如 React、Angular、Vue 等)或者不使用框架构建的。

在 caniuse 查看它的兼容性,不兼容 IE11,Vue3 都不兼容 IE11了,总体还不错

使用 Web Component 可以做的事情

1、开发 UI 组件库:

  • 开发一个通用的、可在多个项目中重用的 UI 组件库。
  • 由于 Web Components 本质上是框架无关的,它们可以在不同的前端框架中使用,增强其可移植性。

2、第三方组件:

  • 创建可以轻松集成到其他应用的第三方组件(例如,社交分享按钮、聊天窗口、支付窗口等)。
  • 避免样式和脚本干扰,提供更加干净、无冲突的集成。

3、微前端:

  • 使用 Web Components 构建微前端,允许将大型应用拆分成独立部署的较小应用,这些较小的应用可以由不同的团队独立开发和部署。
  • 由于 Web Components 的封装性,不同的微前端应用不会互相干扰。

4、嵌入内容和小部件:

  • 创建可嵌入到其他应用中的内容和小部件,例如:评论系统、天气小部件等。

5、多平台应用:

  • 构建能在多个平台(桌面、移动、Web)上运行的应用。使用 Web Components 可以保证在不同平台之间重用 UI 组件。

所以 Web Components 天然的和我们开发的组件思想相吻合,支持原子性、可复用、可重组,并且移植性强,支持跨平台跨框架

入门示例

接下来介绍 如何创建一个简单 Web Components 组件

1、定义组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
js复制代码/**
* 使用 customElements.define() 方法用来注册一个 custom element
* 参数如下:
* - 元素名称,符合 DOMString 规范,名称不能是单个单词,且必须用短横线隔开
* - 元素行为,必须是一个类
* - 继承元素,可选配置,一个包含 extends 属性的配置对象,指定创建的元素继承自哪个内置元素,可以继承任何内置元素。
*/

class MyCustomElement extends HTMLElement {
constructor(){
super();
this.render();
}
render(){
const shadow = this.attachShadow({mode: 'open'});
const text = document.createElement("span");
text.textContent = 'Hi Custom Element!';
text.style = 'color: red';
shadow.append(text);
}
}

customElements.define('my-custom-element', MyCustomElement)

2、使用组件

1
2
3
4
5
6
7
8
9
10
html复制代码<!DOCTYPE html>
<html lang="en">
<head>
<script src="./index.js" defer></script>
</head>
<body>
<h1>my-custom-element</h1>
<my-custom-element></my-custom-element>
</body>
</html>

上面的代码实现 MyCustomElement 类来定义组件,通过 customElements.define 方法定义注册组件,导入组件后,跟使用普通 HTML 标签一样直接使用自定义组件 <my-custom-element></my-custom-element>

Web Component 缺点

从上面的开发示例来看,虽然开发一个组件看起来很简单,但是在我们使用习惯框架之后,发现使用它开发似乎要回到 jquery 时代

  • 没有响应式
  • 没有虚拟 dom
  • 没有简洁的api
  • 没有脚手架
  • HTMLElement 不支持SSR
  • ……

所以开发一个复杂的组件库,这些远远不够,好在现在也有一些非常好用的开源库

Web Component 开源库

Stencil

GitHub 仓库: Stencil

Stencil:2019 年6月正式发布第一版,官方定义是一个Web Component编译器,lonic 团队开发,把现在流行的虚拟 dom、异步渲染、响应式、JSX 等概念都做了支持,并且自己只是一个构建时工具。用 Stencil 开发的框架可以独立运行、也可以运行在主流框架。

它的设计思想和 React 框架很相似,拥有虚拟 dom、异步渲染、响应式、JSX,也有 Vue 框架 watch 监听数据的影子,总的来说集成了 Angular、React、Vue 框架的诸多优点,这也正是选择它开发组件库的原因。

Lit

GitHub 仓库: Lit

Lit 是由 Google 制作的一个简单的库,用于创建 Web Components。Lit 提供了一个基础类(LitElement)来帮助开发者创建 Web Components,并使用一个叫做 lit-html 的模板引擎来定义组件的 HTML 结构。

Lit-html 基于 ES 的模板自变量和 template 标签,用注释节点去动态填充,没
有 JSX 转换虚拟 dom的过程,把大部分模板创建渲染的事都交给浏览器去做,提供了轻量的 api 让我们可以在JS中写 HTML-Templates。 Lit- Element的 Reactive properties、Scoped styles 等功能也很实用。

Fast

GitHub 仓库: Fast

FAST 微软2020 年发布的标准化解决方案,可以用来创建组件和设计系统。组件核心是基于 Web Components 做到框架无关,帮助开发者快速构建高性能的 Web 用户界面。

Vaadin Components

GitHub 仓库: Vaadin Components

Vaadin Components 是一套用于构建 Web 应用的高质量的 Polymer(一种 Google 提出的创建 Web Components 的库)元素集。

Material Web Components

GitHub 仓库: Material Web Components

Material Web Components 提供了一套实现了 Google 的 Material Design 设计理念的 Web Components。

Shoelace

GitHub 仓库: Shoelace

Shoelace 提供了一套完整的、用原生 Web Components 制作的、可自定义的 UI 组件。

Open-WC

GitHub 仓库: Open-WC

Open-WC 提供了一套建议和工具集,用于帮助开发者创建 Web Components 和 Web 应用。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一文读懂缓存击穿、穿透与雪崩,破局之道何在? 1、正常查询流

发表于 2024-04-03
大家好,我是石头~


在当今的高并发互联网环境中,缓存已经成为提升系统性能的重要手段之一。


然而,就像任何利器都有其双刃性一样,我们在享受缓存带来高效的同时,也必须面对“缓存击穿”、“缓存穿透”以及“缓存雪崩”这三大挑战。


那么,这些现象究竟是怎么回事?又该如何应对呢?今天,我们就一起深入探讨这些问题。

1、正常查询流程

在研究上面三个问题之前,我们先来看下,一个正常的数据查询流程是怎样的。

image.png

如上图,在客户端发送查询请求时,服务端会先从Redis查询是否有数据,如果有,则直接返回;如果没有,则从数据库查询。数据库查询后,将结果返回给客户端,并写入到Redis中。


了解完这个基本的数据流程之后,我们就可以继续来了解下面的内容了.

2、缓存穿透

image.png

缓存穿透是指查询的数据既不存在于缓存中,也不存在于数据库中,但恶意用户或爬虫等高频访问不存在的数据,导致大量请求直接打到数据库上,形成穿透效应。

解决方案:

  • 布谷鸟过滤器预判:对所有可能存在的数据哈希,存储在布谷鸟过滤器中,当查询请求到来时,先经过布谷鸟过滤器判断,如果确定数据一定不存在,则直接返回,避免无效请求穿透到数据库。

1.布谷鸟过滤器,由于其挤兑特性,如果数组较小的话,会发生循环挤兑的情况,这个时候可以设置最大挤兑次数,如果超过该次数,进行扩容,重新计算每个指纹的位置。

2.布谷鸟过滤器存在误判可能,布谷鸟过滤器校验通过的,可能是存在,也是不存在的。而布谷鸟过滤器校验不通过的,肯定是不存在的。因此,此处在进行存在校验的时候,要校验不存在。

  • 空值缓存:即使查询结果为空,也将空值进行缓存,并设置较短的有效期,防止恶意攻击。

3、缓存击穿

image.png
缓存击穿是指热点数据过期后,在高并发场景下,大量请求同时查询数据库,导致数据库压力剧增的现象。

形象地说,就像是众多请求一同击穿了原本由缓存构筑起来的防护墙,直指后端数据库。

解决方案:

  • 互斥锁或分布式锁:在缓存失效时,通过锁,确保同时只有一个请求去查询数据库并回填缓存。

采用互斥锁虽然能有效防止缓存击穿,但在高并发场景下可能会导致请求排队等待,从而影响接口响应速度,降低系统吞吐量。

因此,对于读多写少的场景,可以使用可重入读写锁。当缓存失效时,持有写锁的线程负责从数据库加载数据并回填缓存,期间其他读请求仍可以通过读锁获取缓存中的旧数据,直至新数据加载完毕。

  • 设置热点数据永不过期,通过后台定时更新策略来保证数据新鲜度。
  • 异步更新缓存:当检测到缓存失效时,不立即阻塞请求,而是异步地将数据库加载任务放入任务队列,主线程快速返回临时默认值或旧缓存值,后续请求在数据更新完成后获取最新数据。

4、缓存雪崩

image.png

缓存雪崩是指缓存服务集体宕机或者大面积缓存同一时间失效,所有请求都涌向数据库,造成数据库瞬间压力过大甚至崩溃的情况。

解决方案:

  • 缓存预热:在缓存重启或大规模更新前,提前将数据加载到缓存中,减少瞬时流量冲击。
  • 分布式缓存集群:使用多节点部署,增加缓存系统的可用性和容错性。
  • 限流降级:在系统入口处设置合理的流量控制,如熔断机制、令牌桶算法等,当请求量超过阈值时,可以暂时拒绝部分请求或者调用降级逻辑。

5、总结

总的来说,应对缓存击穿、穿透和雪崩的问题需要综合考虑业务特性和系统负载情况,灵活应用各种策略。


这些解决方案不仅有助于维护数据库安全,也有利于提升系统的稳定性和用户体验。期待你在评论区分享你的实战经验和观点!

**MORE | 更多精彩文章**

  • H5推送,为什么都用WebSocket?
  • 别再这么写POST请求了~
  • Redis不再“开源”~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

靠维护老项目度中年危机

发表于 2024-04-03

最近靠维护老项目度过中年危机的话题挺火,刚好最近也在维护一个PHP开发的CRM的老项目,项目由于数据量比较大, 导致查询速度很慢, 经常出现超时的情况, 下面记录一下具体的优化过程。

优化老项目,老生常淡的几点:

1
2
3
4
5
markdown复制代码1. 数据库优化
2. 代码结构优化
3. 缓存优化
4. 资源优化
...

数据库优化

众所周知, MySQL 优化第一步,就是建索引, 看了一下整个系统的表, 发现有大量的表都没有索引, 建了索引的表,索引名称有点花里胡哨, 如下:

1
2
3
4
5
6
7
8
css复制代码contractId	`contacts_id`	NORMAL	BTREE	27599	A		0		
customer_id `customer_id` NORMAL BTREE 27599 A 0

--

index_group `role_id`, `callDate` NORMAL BTREE 4359069 A 0
business_id `business_id` NORMAL BTREE 518 A 0
status_id `status_id` NORMAL BTREE 43 A 0

于是,优化第一步,规范一下索引的命名,MySQL索引的命名虽然没有硬性的规范,但是修改一下自己看着舒服, 个人理解:

1
2
3
复制代码普通索引:idx_字段1_字段2
唯一索引:uk_字段1_字段2
主键索引:pk_字段1_字段2

于是 上面的索引改成了:

1
2
3
4
5
6
7
8
css复制代码idx_contacts_id	`contacts_id`	NORMAL	BTREE	27599	A		0		
idx_customer_id `customer_id` NORMAL BTREE 27599 A 0

--

idx_role_id_callDate `role_id`, `callDate` NORMAL BTREE 4359069 A 0
idx_business_id `business_id` NORMAL BTREE 518 A 0
idx_status_id `status_id` NORMAL BTREE 43 A 0

一下看起来舒服多了, 于是, 优化第二步, 就是给没有索引的表加上索引, 这个工作量比较大, 先把几个 常用功能模块的 表给加上索引, 于是 吭哧吭哧的 分析了 2天的 慢日志, 给需要加索引的表加上索引,本以为 加完索引后, 查询速度会快很多,结果发现, 并没有什么卵用. 一个页面 虽然快了点, 但是 不是太明显.

本着能加 配置 绝不改代码的原则,先去问了一下运维 Mysql 运行的机器内存是多大 64G. 这么大,那好办,先分析一下 数据库中的表引擎. 上了一段代码:

1
php复制代码<?php/** * Author: PFinal南丞 * Date: 2023/12/28 * Email: <lampxiezi@163.com> *//** 确保这个函数只能运行在 shell 中 **/if (!str_starts_with(php_sapi_name(), "cli")) {    die("此脚本只能在cli模式下运行.\n");}/** 关闭最大执行时间限制 */set_time_limit(0);error_reporting(E_ALL);ini_set('display_errors', 1);const MAX_SLEEP_TIME = 10;$hostname   = '';$username   = '';$password   = '';$connection = mysqli_connect($hostname, $username, $password);if (!$connection) {    die('Could not connect: ' . mysqli_error($connection));}$query  = "SELECT table_name,engine FROM information_schema.tables WHERE table_schema = 'smm';";$result = mysqli_query($connection, $query);if (!$result) {    die("Query failed: " . mysqli_error($connection));}$InnoDB_num = 0;$MyISAM_num = 0;while ($process = mysqli_fetch_assoc($result)) {    echo $process['table_name'] . " " . $process['engine'] . PHP_EOL;    if ($process['engine'] == 'InnoDB') {        $InnoDB_num++;    }    if ($process['engine'] == 'MyISAM') {        $MyISAM_num++;    }}echo "InnoDB " . $InnoDB_num . " MyISAM " . $MyISAM_num . PHP_EOL;mysqli_close($connection);

得出结果:

表引擎 MyISAM 的表 176 张 InnoDB的表引擎 88张. 要了一份 线上MySql 的配置发现:

1
2
3
4
5
6
ini复制代码...

key_buffer_size = 512M
innodb_buffer_pool_size = 2048M

...

都知道 innodb_buffer_pool_size 针对的 是 InnoDB的表引擎,key_buffer_size 针对的 是 MyISAM的表引擎. 这配置不得修改一下. 果断打申请, 申请修改线上配置.

1
2
3
4
5
6
ini复制代码...

key_buffer_size = 2048M
innodb_buffer_pool_size = 2048M

...

重启服务后,果然比原来快了好多.能撑到 同事不在群里 打报告了.

艰巨的长征路迈出了第一步,接下来,本着 死道友不死贫道的原则, 厚着脸皮,让运维帮忙整了一台mysql 的机器,来做了个主从分离。 速度一下,不影响业务的正常使用了.

接着 开启漫长的 优化之路.

缓存优化

  1. 项目没有开启数据缓存, 只有 代码编译的缓存

所以这一块是一个大的工程, 所以先不动, 只是 给 几个 常用的功能加了一个 数据 的 缓存。后续的思路是:

1
2
3
4
css复制代码  a. 加一个 redis, 使用 把代码中的统计数据 缓存到 redis 中

b. 把客户信息,客户的关联信息,组合到一起, 然后缓存到 redis中.
....

代码结构优化

开始挖开代码, 看看 查询慢的 功能 代码是咋写的,不看不知道,一看直接上头:

  1. 几乎全是 foreach 中 的 SQL 查询:
1
scss复制代码foreach($customer_list as $key=>$value){        # ......        $customer_list[$key]['customer_name'] = $this->customer_model->get_customer_name($value['customer_id']);        $customer_list[$key]['customer_phone'] = $this->customer_model->get_customer_phone($value['customer_id']);        $customer_list[$key]['customer_address'] = $this->customer_model->get_customer_address($value['customer_id']);                # ......    }
  1. 由于 ORM 的方便复用, 大量的 表关联模型 复用,导致查询的 废字段特别多.比如:
1
ruby复制代码<?php    class CustomerViewModel extends ViewModel {        protected $viewFields;  public function _initialize(){   $main_must_field = array('customer_id','owner_role_id','is_locked','creator_role_id','contacts_id','delete_role_id','create_time','delete_time','update_time','last_relation_time','get_time','is_deleted','business_license');   $main_list = array_unique(array_merge(M('Fields')->where(array('model'=>'customer','is_main'=>1,'warehouse_id'=>0))->getField('field', true),$main_must_field));   $data_list = M('Fields')->where(array('model'=>'customer','is_main'=>0,'warehouse_id'=>0))->getField('field', true);   $data_list['_on'] = 'customer.customer_id = customer_data.customer_id';   $data_list['_type'] = "LEFT";   //置顶逻辑   $data_top = array('set_top','top_time');   $data_top['_on'] = "customer.customer_id = top.module_id and top.module = 'customer' and top.create_role_id = ".session('role_id');   $data_top['_type'] = "LEFT";   //首要联系人(姓名、电话)   $data_contacts = array('name'=>'contacts_name', 'telephone'=>'contacts_telephone');   $data_contacts['_on'] = "customer.contacts_id = contacts.contacts_id";   // 检查是否存在部门库字段            $warehouse_id = I('warehouse_id', '', 'intval');            if ($warehouse_id) {                $warehouse_id = D('Fields')->isExistsWarehouseTable(1, $warehouse_id);                if ($warehouse_id) {                    $customer_warehouse_data_table = customer_warehouse_table($warehouse_id);                    $warehouse_data_list = M('Fields')->where(array('model'=>'customer','is_main'=>0,'warehouse_id'=>$warehouse_id))->getField('field', true);                    $warehouse_data_list['_on'] = 'customer.customer_id = ' . $customer_warehouse_data_table .'.customer_id';                    $warehouse_data_list['_type'] = "LEFT";                    $this->viewFields = array('customer'=>$main_list,'customer_data'=>$data_list,$customer_warehouse_data_table=>$warehouse_data_list,'top'=>$data_top,'contacts'=>$data_contacts);                } else {                    $this->viewFields = array('customer'=>$main_list,'customer_data'=>$data_list,'top'=>$data_top,'contacts'=>$data_contacts);                }            } else {                $this->viewFields = array('customer'=>$main_list,'customer_data'=>$data_list,'top'=>$data_top,'contacts'=>$data_contacts);            }  }    ?>
  1. 代码中的业务逻辑一直再叠加,导致废代码量特别的大需要重新梳理逻辑

针对以上的代码做修改:

1
2
3
sql复制代码a. 第一点, 把所有foreach 中的 sql拆出来,先去查询到内存中,然后组合减少sql语句 

b. 第二点, 简化 ORM的乱用,比如只需要查询一个字段的 就直接用原生sql或者新的一个不关联的orm 来处理

资源优化

  1. 由于录音文件过大, 找运维 做了一个专门的文件服务器,移到了文件服务器上

最后

最后,给加了个定时任务告警的功能, 方便及时发现异常, 优化的 第一步 勉强交活。剩下的 优化 需要再花点时间了,慢慢来了.

🌟 更多精彩内容等你发现!关注【PFinalClub】,成为我们的一员,让我们一起在编程的海洋中探索、学习、成长!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

在Jetpack Compose中管理网络请求竟然如此简单!

发表于 2024-04-03

写在前面

本文中提及的use开头的函数,都出自与我的 ComposeHooks 项目,它提供了一系列 React Hooks 风格的状态封装函数,可以帮你更好的使用 Compose,无需关心复杂的状态管理,专注于业务与UI组件。

这是系列文章的第7篇,前文:

  • 在Compose中使用useRequest轻松管理网络请求
  • 在Compose中使用状态提升?我提升个P…Provider
  • 在Compose中父组件如何调用子组件的函数?
  • 在Compose中方便的使用MVI思想?试试useReducer!
  • 在Compose中像使用redux一样轻松管理全局状态
  • 在Compose中轻松使用异步dispatch管理全局状态

在上篇文章中我提到如果你项目中使用的是retrofit,并且已经做了协程改造,那么你可以轻松的将你的网络请求改造到 Compose 中,使用状态驱动你的UI。

上一个例子比较粗糙,可能有小伙伴不理解,同时考虑到减少模板代码,我升级了 useRedux 系列钩子,下面我将演示如何在项目中轻松的使用网络请求,并且不用再担心重组导致请求状态消失!amazing!!!!

Compose 下网络请求的痛点

众所周知,Compose的组件是有状态驱动的,并且作为函数式组件,它会不断地重组。

当我们的组件不可见时,状态从状态树移除,如果想要保留状态就需要使用 ViewModel 来进行一些状态保存,但是 viewModel 本身也会因为跨页面导航丢失状态,每次再进入页面都要重新发起请求,不能保存之前的请求状态无疑是非常制杖的!

那么怎么才能丝滑的使用网络请求呢?如何避免网络请求因为重组再次发起?

答案就是上两篇文章,我们通过 ReduxProvider 将状态提升到最根部,那么全局范围内,同一个网络请求在全局使用相同的状态,就不会出现各种场景下的状态丢失了。

  • 在Compose中像使用redux一样轻松管理全局状态
  • 在Compose中轻松使用异步dispatch管理全局状态

show time !!

1. 创建状态存储 store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
kotlin复制代码// 请求结果状态封装,同时这也是 Action
sealed interface NetFetchResult {
// 成功状态
data class Success<T>(val data: T, val code: Int) : NetFetchResult
// 错误状态
data class Error(val msg: Throwable) : NetFetchResult
// 初始空闲状态
data object Idle : NetFetchResult
// 请求loading状态
data object Loading : NetFetchResult
}

// reducer
val fetchReducer: Reducer<NetFetchResult, NetFetchResult> = { _, action ->
action
}

// 创建存储对象
val store = createStore {
arrayOf("fetch1","fetch2").forEach {
named(it) {
fetchReducer with NetFetchResult.Idle
}
}
}

上篇文章介绍了,在createStore函数的闭包作用域内,你可以使用中缀函数 with,来创建一条存储,并且将 reducer 函数与初始状态传递给store;

同样的你可以使用 named(alias){} 这个作用域函数,来创建一个带别名的状态存储,这里的fetch1、fetch2是请求状态的别名,你应该使用有实际意义的名称。

所有的网络请求都是相同的逻辑,所以我们可以直接使用 forEach 来批量创建具有别名的状态存储;

这里的reducer函数逻辑非常简单,因为我直接使用 Action 类型区分网络请求状态,所以Action 也同时是我们的 State,如果你使用的状态不同于我的封装你应该使用自己的 reducer 函数逻辑;

2. 通过 ReduxProvider 暴露状态存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeHooksTheme {
// 在这里将 store 对象传递给 ReduxProvider 组件,向全部子组件暴露状态
ReduxProvider(store = store) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
useRoutes(routes = routes)
}
}
}
}
}
}

将 ReduxProvider 置于根组件,全局共享状态

3. 按需使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
kotlin复制代码@Composable
fun UseReduxFetch() {
val fetchResult: NetFetchResult = useSelector("fetch1")
val dispatchAsync = useDispatchAsync<NetFetchResult>("fetch1")
Column {
Text(text = "result: $fetchResult")
TButton(text = "fetch") {
dispatchAsync {
it(NetFetchResult.Loading)
delay(2.seconds)
//网络请求结果
NetFetchResult.Success("success", 200)
}
}
}
}

@Composable
fun UseReduxFetch2() {
val fetchResult: NetFetchResult = useSelector("fetch2")
val dispatchAsync = useDispatchAsync<NetFetchResult>("fetch2")
Column {
Text(text = "result: $fetchResult")
// 使用 when 可以方便的判断请求结果状态
when(fetchResult) {
is NetFetchResult.Success<*> -> {
// 对成功结果进行转型
val succ= fetchResult as NetFetchResult.Success<SimpleData>
Text(text = succ.toString())
}
else->{}
}
TButton(text = "fetch2") {
dispatchAsync {
// 请求开始
it(NetFetchResult.Loading)
// 这里假装在进行异步操作
delay(2.seconds)
// 包装网络请求结果
NetFetchResult.Success(SimpleData("Tony Stark", 53), 200)
}
}
}
}

useSelector<NetFetchResult>("fetch1") 即可拿到对应别名的状态,需要传递的泛型就是我们的网络请求结果的 sealed interface 类型。我们只需要对这个类型进 行when 判断,即可在组件中动态处理不同状态的显示逻辑。

useDispatchAsync<NetFetchResult>("fetch1") 则可以拿到对应的 异步dispatch函数,我们可以在这个函数中执行异步操作,最终返回结果将作为 Action 传递给 reducer 函数。

dispatchAsync 的签名是 :

1
2
3
> kotlin复制代码typealias DispatchAsync<A> = (block: suspend CoroutineScope.(Dispatch<A>) -> A) -> Unit
>
>

现在你无需对你过去的网络请求做任何改动,不需要 ViewModel,不需要LaunchedEffect,直接在 dispatchAsync 中使用 retrofit 发起请求!

1
2
3
4
5
kotlin复制代码dispatchAsync { it->
it(NetFetchResult.Loading)
delay(2.seconds) //假装在进行携程上的耗时操作
NetFetchResult.Success(SimpleData("Tony Stark", 53), 200)
}

这里的 it 是 dispatch 函数,你可以在闭包内发起状态变更,对你的网络请求进行 try-catch,然后将结果或者异常使用 NetFetchResult.Success 或 NetFetchResult.Error 包装即可!

进一步封装,自动处理Loading、Error

上边的代码,还是存在一些模板代码,例如: lodaing 状态、请求结果缺乏泛型、需要对请求手动try-catch,就如我之前评论所言,只需要简单的进行一个封装即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码// 定义函数一个高级函数ReduxFetch,它接收一个挂起函数作为参数
typealias ReduxFetch<T> = (block: suspend CoroutineScope.() -> T) -> Unit

@Composable
inline fun <reified T> useFetch(alias: String): ReduxFetch<T> {
// 在函数调用时首先 dispatch Loading 状态
val dispatchAsync= useDispatchAsync<NetFetchResult>(alias, onBefore = { it(NetFetchResult.Loading) })
return { block ->
dispatchAsync{
try {
// 这里的block 就是上面定义的ReduxFetch的参数,是一个retrofit挂起函数
NetFetchResult.Success(block())
} catch (t: Throwable) {
NetFetchResult.Error(t)
}
}
}
}

这个封装非常简单,请求发出前首先 dispatch Loading状态,对网络请求的挂起函数执行try-catch,分别将结果|错误 dispatch出去即可;

将组件中原来使用 useDispatchAsync 函数的位置进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码interface WebService {
@GET("users/{user}")
suspend fun userInfo(@Path("user") user: String): UserInfo
}

@Composable
fun UseReduxFetch2() {
val fetchResult: NetFetchResult = useSelector("fetch2")
val dispatchFetch = useFetch<UserInfo>("fetch2")
Column {
Text(text = "result: $fetchResult")
TButton(text = "fetch2") {
dispatchFetch {
// 这里是你的 retrofit suspend 请求,返回值是上面声明的泛型
NetApi.SERVICE.userInfo("junerver")
}
}
}
}

可以看到简化后的 dispatchFetch 函数只需要在闭包内执行 retrofit 请求即可,状态的切换已经都由 useFetch 处理了,基本没有模板代码了。

现在我们迎来了在Compose下的终极网络请求状态管理,非常简单易用,你可以几乎没有额外成本的将你的 retrofit 网络请求迁移到 compose 下,无需 ViewModel、
无需操心重组导致的状态丢失,非常的鹅妹子嘤!!!!

最后请说声:⌈ 多谢提升哥!⌋

状态管理三剑客

到此为止我们已经介绍了三位用于在 Compose 中进行状态管理的钩子函数:

  • useReducer:用于实践MVI,只需要传递 reducer 函数与初始状态,返回给我们状态、dispatch函数
  • useContext:用于状态提升,解耦组件之间的状态传递,底层实现是:ProvidableCompositionLocal 与 CompositionLocalProvider
  • useSelector/useDispatch:基于 useContext 实现的的全局版本的 useReducer

题外话

这里我要补充一句:useSelector/useDispatch主要还是用来进行全局 MVI式状态管理,本文展示的其实只是 全局状态管理 的一个小小的应用场景,如果你想在实际生产中使用,可能需要根据我的例子进行改造。

在 ComposeHooks 中提供的 useRequest 才是真正用于服务端状态管理的工具,详情参考:在Compose中使用useRequest轻松管理网络请求

它的功能更加全面,例如:错误重试、依赖刷新、ready、自动|手动请求、轮询、防抖、节流等等

探索更多

好了以上就是 hooks 1.0.10 版本带来的一点小小改动,现在你可以自信在在Compose中使用网络请求了!

示例源码地址:UseReduxExample

项目开源地址:junerver/ComposeHooks

MavenCentral:hooks

1
kotlin复制代码implementation("xyz.junerver.compose:hooks:1.0.10")

欢迎使用、勘误、pr、star。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

凌晨重磅!ChatGPT 今天起免登录使用

发表于 2024-04-02

GPT 3.5 免登录使用

今日凌晨,OpenAI 宣布,ChatGPT 无需登录就能使用。

一直以来,GPT 3.5 都是注册账号,就能免费使用,只有 GPT 4 才是需要花钱订阅。

但即使只是注册账号,对于中国大陆用户来说,也是不少的麻烦。

需要科学上网不说,注册 OpenAI 还需要海外手机收短信验证码,这一度让「直接售卖 GPT 账号」和「短信打码服务二房东」成为了一门生意。

如今,OpenAI 取消了登录使用限制,让国内使用 GPT 的难度下降到访问 Google 的级别。

大家只要打开浏览器访问 chat.openai.com 即可使用 ChatGPT 3.5。

红色输入框里,填入你想问的问题,回车

需要注意的是:由于是非登录下使用 GPT,因此所有上下文会在刷新浏览器后消失。

因此这种免登录使用 GPT 的模式,更适合偶尔临时使用。

另外不知道是不是错觉,虽然和登录状态下的 GPT 数据截止日期一致,但总感觉非登录状态下的 GPT 更”笨”一些:

非登录状态

登录状态

但无论如何,降低使用门槛总是好的,你觉得呢?

…

回归主线。

来做一道和「美团」二面相关的算法原题。

题目描述

平台:LeetCode

题号:108

给你一个整数数组 nums,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。

高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 111 」的二叉树。

示例 1:

1
2
3
4
5
ini复制代码输入:nums = [-10,-3,0,5,9]

输出:[0,-3,9,-10,null,5]

解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:

示例 2:

1
2
3
4
5
ini复制代码输入:nums = [1,3]

输出:[3,1]

解释:[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。

提示:

  • 1<=nums.length<=1041 <= nums.length <= 10^41<=nums.length<=104
  • −104<=nums[i]<=104-10^4 <= nums[i] <= 10^4−104<=nums[i]<=104
  • nums 按严格递增顺序排列

递归分治

题目给定的 nums 严格有序,为满足构造出来的 BST 整体平衡,我们需要保证每个子树的构造也是平衡的。

一个容易想到的思路:使用 nums 中最靠近中心的位置作为整棵 BST 的根节点,例如下标 mid=⌊l+r2⌋mid = \left \lfloor \frac{l + r}{2} \right \rfloormid=⌊2l+r⌋ 的位置,确保左右子树节点数量平衡。随后递归构造 nums 中下标范围为 [0,mid−1][0, mid - 1][0,mid−1] 作为左子树,递归构造 nums 中下标范围为 [mid+1,n−1][mid + 1, n - 1][mid+1,n−1] 作为右子树。

Java 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
Java复制代码class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return build(nums, 0, nums.length - 1);
}
TreeNode build(int[] nums, int l, int r) {
if (l > r) return null;
int mid = l + r >> 1;
TreeNode ans = new TreeNode(nums[mid]);
ans.left = build(nums, l, mid - 1);
ans.right = build(nums, mid + 1, r);
return ans;
}
}

Python 代码:

1
2
3
4
5
6
7
8
9
10
11
12
Python复制代码class Solution:
def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
return self.build(nums, 0, len(nums) - 1)

def build(self, nums, l, r):
if l > r:
return None
mid = l + r >> 1
ans = TreeNode(nums[mid])
ans.left = self.build(nums, l, mid - 1)
ans.right = self.build(nums, mid + 1, r)
return ans

C++ 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
C++复制代码class Solution {
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
return build(nums, 0, nums.size() - 1);
}
TreeNode* build(vector<int>& nums, int l, int r) {
if (l > r) return nullptr;
int mid = l + r >> 1;
TreeNode* ans = new TreeNode(nums[mid]);
ans->left = build(nums, l, mid - 1);
ans->right = build(nums, mid + 1, r);
return ans;
}
};

TypeScript 代码:

1
2
3
4
5
6
7
8
9
10
11
TypeScript复制代码function sortedArrayToBST(nums: number[]): TreeNode | null {
const build = function (nums: number[], l: number, r: number): TreeNode | null {
if (l > r) return null;
const mid = l + r >> 1;
const ans = new TreeNode(nums[mid]);
ans.left = build(nums, l, mid - 1);
ans.right = build(nums, mid + 1, r);
return ans;
}
return build(nums, 0, nums.length - 1);
};
  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(n)O(n)O(n)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

在Jetpack Compose中轻松使用异步dispatc

发表于 2024-04-02

写在前面

本文中提及的use开头的函数,都出自与我的 ComposeHooks 项目,它提供了一系列 React Hooks 风格的状态封装函数,可以帮你更好的使用 Compose,无需关系复杂的状态管理,专心于业务与UI组件。

这是系列文章的第6篇,前文:

  • 在Compose中使用useRequest轻松管理网络请求
  • 在Compose中使用状态提升?我提升个P…Provider
  • 在Compose中父组件如何调用子组件的函数?
  • 在Compose中方便的使用MVI思想?试试useReducer!
  • 在Compose中像使用redux一样轻松管理全局状态

useSelector、useDispatch 足够好用么?

在上一次更新中,为了解决全局状态的管理问题,我们引入了新的钩子:useSelector、useDispatch

它们的源码非常简单

1
2
3
4
5
6
kotlin复制代码@Composable
inline fun <reified T> useSelector(): T = useContext(context = ReduxContext).first[T::class] as T

@Composable
inline fun <reified A> useDispatch(): Dispatch<A> =
useContext(context = ReduxContext).second[A::class] as Dispatch<A>

得力于 kotlin 的 inline、reified 关键字,我们可以轻松的从store中取出我们的状态、以及dispatch函数。

但是有时我们并不需要整个状态对象,我们可能只需要其中部分成员属性,亦或者需要对状态中的某个属性进行变形映射。

说到这里不知道你有没有想到什么?还记得么你可能一直在kt文件中写Java代码?没错就是 run 映射

更好用的 useSelector

我们只需要简单的构建一个重载函数,就可以让 useSelector 变得更好用:

1
2
kotlin复制代码@Composable
inline fun <reified T, R> useSelector(block: T.() -> R) = useSelector<T>().run(block)

现在我们继续对之前例子的代码进行改造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码@Composable
private fun SubSimpleDataStateText() {
/**
* 使用[useSelector]的另一个重载,你可以轻松的对状态进行变形,或者只取状态对象的部分属性作为你要关注的状态;
*/
val name = useSelector<SimpleData, String> { name }
Text(text = "User Name: $name")
}

@Composable
private fun SubSimpleDataStateText2() {
// age属性类型是Int,我们可以轻松的进行数据变形
val age = useSelector<SimpleData, String> { "age : $age" }
Text(text = "User $age")
}

更好用的 useDispatch

dispatch 函数非常非常简单:typealias Dispatch<A> = (A) -> Unit

1
kotlin复制代码{ action: Any -> setState(reducer(state, action)) }

但是在异步场景,使用它有一点点麻烦,例如一个网络请求的场景:

1
2
3
4
5
6
7
8
9
kotlin复制代码val scope = rememberCoroutineScope() //获取协程作用域
TButton(text = "changeName") {
scope.launch {
// 这里在异步任务
delay(1.seconds)
val result = //....
dispatch(SimpleAction.ChangeName(result)) //dispatch
}
}

多多少少我们要写一点模板代码;

我们继续对 useDispatch 进行改造,增加一个异步版本的:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码typealias DispatchAsync<A> = (block: suspend CoroutineScope.() -> A) -> Unit

@Composable
inline fun <reified A> useDispatchAsync(): DispatchAsync<A> {
val dispatch: Dispatch<A> = useDispatch()
val asyncRun = useAsync() //等同于 rememberCoroutineScope().launch {}
return { block ->
asyncRun {
dispatch(block())
}
}
}

改造后的 useDispatchAsync 函数将会返回一个异步版本的 dispatch 函数,函数闭包的返回值将会作为 Action 进行 dispatch 操作。

那么上边的模板代码将会变成:

1
2
3
4
5
6
7
8
kotlin复制代码val asyncDispatch = useDispatchAsync<SimpleAction>()
TButton(text = "Async changeName") {
asyncDispatch {
delay(1.seconds)
val result = //....
SimpleAction.ChangeName(result) //闭包的最后一行是返回值Action
}
}

使用新的 hook 改造你的 retrofit 请求获得全局状态

如果你使用 retrofit ,并且已经使用协程改造了网络请求,你甚至可以将请求结果作为Action,那么这里将会进一步简化

一个极简的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
kotlin复制代码//网络状态的封装
sealed interface NetFetchResult {
data class Success(val data: String, val code: Int) : NetFetchResult
data class Error(val msg: Throwable) : NetFetchResult
data object Idle : NetFetchResult
data object Loading : NetFetchResult
}

// 极简的reducer
val fetchReducer: Reducer<NetFetchResult, NetFetchResult> = { _, action ->
action
}

// 注册到store
val store = createStore {
fetchReducer with NetFetchResult.Idle
}

// 组件中使用
@Composable
fun UseReduxFetch() {
val fetchResult: NetFetchResult = useSelector()
val dispatchAsync = useDispatchAsync<NetFetchResult>()
Column {
Text(text = "result: $fetchResult")
TButton(text = "fetch") {
dispatchAsync {
delay(2.seconds)
//网络请求结果
NetFetchResult.Success("success", 200) //这里替换成你的retrofit请求即可
}
}
}
}

探索更多

好了以上就是 hooks 1.0.9 版本带来的一点小小改动,现在你的全局状态可以更加轻松的管理与使用了!

项目开源地址:junerver/ComposeHooks

MavenCentral:hooks

1
kotlin复制代码implementation("xyz.junerver.compose:hooks:1.0.9")

欢迎使用、勘误、pr、star。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…424344…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%