jar\war\SpringBoot加载包内外资源的方式,告别FileNotFoundException吧
工作中常常会用到文件加载,然后又经常忘记,印象不深,没有系统性研究过,从最初的war包项目到现在的springboot项目,从加载外部文件到加载自身jar包内文件,也发生了许多变化,这里开一贴,作为自己的备忘录,也希望能给广大 java
coder 带来帮助。
一、目标
通过此文,能熟知普通war包项目目录内、jar包自身内文件的加载方式。
二、文件定位
2.1 WAR 包项目
为什么先说war包项目,war包项目部署到Web容器里后 ,会被解压,所以文件读取方式,和在ide里面读取是类似的。
读取文件,首先要定位文件,定位到文件之后才能读取。
定位文件,java常用的有两种,分别是
URL Class.getResource(String name)
URL ClassLoader.getResource(String name)
这里的参数 name
,就是咱们认为的路径,官方对这个参数名的描述是:
name of the desired resource
渴望得到的资源的名字
URL
则是资源的定位,可以得到资源所在路径。
URL
可以是不同的资源,通过其字段 protocol
来区分是哪种类型资源,取值有:
- ftp
- nntp
- http
- file
- jar
感兴趣的同学可以自行了解 URL
的定义
2.1.1 Class.getResource(String name)
通过class实例获得资源的定位,传入参数有如下查找方式:
- 以
/
开头,则从classPath
即运行的class
文件所在的项目的***/classes/
目录下找起 - 非以
/
开头的,则从当前class所在路径下找起
验证:
先上项目结构图
验证代码
public class ClassResource {
public static void main(String[] args) {
ClassResource classResource = new ClassResource();
classResource.resWithInstance("");
classResource.resWithInstance("/");
classResource.resWithInstance("ClassResource.class");
classResource.resWithInstance("/ClassResource.class");
classResource.resWithInstance("/1.txt");
}
public void resWithInstance(String path) {
URL resource = this.getClass().getResource(path);
print(resource, path);
}
private static void print(URL resource, String path) {
try {
System.out.println("ClassResource 根据目录[" + String.format("%-20s", path) + "] 获取路径为 " + resource);
} catch (Exception e) {
System.out.println("ClassResource 根据目录[" + path + "] 获取路径出错,错误原因:" + e.getMessage());
}
}
}
我们传入了5个参数,分别是
- 空字符串
/
- 当前类文件名
/
+ 当前类文件名/
+ 项目resources
目录下的 1.txt 文件
运行结果如下:
结果分析:
空字符串
定位为当前类路径
/
定位为
classPath
路径当前类文件名
定位为当前类文件所在路径,成功定位到文件
/
+ 当前类文件名定位不到文件
/
+ 项目resources
目录下的 1.txt 文件定位为
resoures/1.txt
,因为编译后resources
目录里的文件都移动到了classPath
路径下,所以也成功定位
4 的错误原因很明显,因为 classPath
路径下没有名叫 ClassResource.class
的文件,所以定位不到
总结:
使用 class 查找文件,以
/
开头的文件名,是从classPath
目录下找,否则从当前类文件目录下找
2.1.2 ClassLoader.getResource(String name)
通过 classLoader
实例获得资源的定位,传入参数仅有如下查找方式:
- 从
classPath
路径下找起
验证:
验证代码
public class ClassLoaderResource {
public static void main(String[] args) {
ClassLoaderResource classLoaderResource = new ClassLoaderResource();
classLoaderResource.resWithInstance("");
classLoaderResource.resWithInstance("/");
classLoaderResource.resWithInstance("ClassLoaderResource.class");
classLoaderResource.resWithInstance("/ClassLoaderResource.class");
classLoaderResource.resWithInstance("1.txt");
classLoaderResource.resWithInstance("/1.txt");
}
public void resWithInstance(String path) {
URL resource = this.getClass().getClassLoader().getResource(path);
print(resource, path);
}
private static void print(URL resource, String path) {
try {
System.out.println("ClassLoaderResource 根据目录[" + String.format("%-26s", path) + "] 获取路径为" + resource);
} catch (Exception e) {
System.out.println("ClassLoaderResource 根据目录[" + path + "]获取路径出错,错误原因:" + e.getMessage());
}
}
}
我们传入了6个参数,分别是
- 空字符串
/
- 当前类文件名
/
+ 当前类文件名1.txt
/1.txt
运行结果如下:
结果分析:
空字符串
定位为
classPath
路径/
定位不到
当前类文件名
定位不到
/
+ 当前类文件名定位不到
resources
目录下的 1.txt 文件名定位为
resoures/1.txt
,因为编译后resources
目录里的文件都移动到了classPath
路径下,成功定位/
+resources
目录下的 1.txt 文件名定位不到
3 的错误原因很明显,因为 classPath
路径下没有名叫 ClassResource.class
的文件,所以定位不到
2、4、6 的错误原因是因为以 /
开头,这里先记着:
以
/
开头的都会定位不到,但是参数中可以带有/
来表示下一级路径如:查找
Main.class
的参数此处应写为com/yx/jtest/Main.class
总结:
使用 classLoader 查找文件,总是从
classPath
目录下找起,且不能以/
开头
2.1.3 Class.getResource 与 ClassLoader.getResource 的异同原因
对于相同的开头字符 /
、空字符串
为什么两种方式的执行结果不一样呢
来分析下 class.getResource
源码
public class Class {
public java.net.URL getResource(String name) {
name = resolveName(name); // ①
ClassLoader cl = getClassLoader0();
if (cl == null) {
// A system class.
return ClassLoader.getSystemResource(name);
}
return cl.getResource(name);
}
}
可以看到在进行 ① 转换资源名称后,内部还是调用了 classLoader.getResource
方法。
那么异同的奥秘就都在这个第一行里的 resolveName(name)
方法里了
来看 resolveName(name)
public class Class {
/**
* Add a package name prefix if the name is not absolute Remove leading "/"
* if name is absolute
*/
private String resolveName(String name) {
if (name == null) {
return name;
}
if (!name.startsWith("/")) {
Class<?> c = this;
while (c.isArray()) {
c = c.getComponentType();
}
// 这里的baseName类似 com.foo.Bar 之类的形式
String baseName = c.getName();
int index = baseName.lastIndexOf('.');
if (index != -1) {
// 拼name,把包名称拼上,如 "com/foo/" + "/" + "Bar1"
// 就是获取当前类目录下的路径名
name = baseName.substring(0, index).replace('.', '/')
+ "/" + name;
}
} else {
name = name.substring(1);
}
return name;
}
}
可以看到:
- 如果不以
/
开头,就返回当前类所在目录
+资源名
- 否则返回
/
后面的字符串 - 总结就是该方法把相对路径转换为了基于
classPath
的绝对路径
在经过资源名称处理后,就跟 classLoader.getResource
的规则一样了。
这里处理 /
符号也间接说明了 classLoader.getResource
不再接受 /
开头的资源名称,因为它把 /
当成了路径分隔符,下面是官方的参数说明
The name of a resource is a '/'-separated path name that identifies the resource.
资源的名称是一个“/”分隔的路径名,用于标识资源。
所以两者的异同点在于:
class.getResource
先进行了/
符号开头的路径的预处理,使之转换为了基于classPath
的绝对路径,再调用classLoader.getResource
的方法而
classLoader.getResource
只接受基于classPath
的绝对路径,并不再接受以/
开头的路径,此时""
空字符串则代表classPath
路径,而非class.getResource
的/
2.2 JAR包项目
当项目为jar项目时,加载的方式变了,主要有
classPath
路径由file
目录变成了jar
文件,这影响到资源的定位方式,而且不再支持获取当前classPath
路径URLClassPath
加载资源时候由FileLoader
变成了JarLoader
,这影响到资源对特殊符号的处理方式- 定位内部文件的URL协议由
file
变成了jar
,这影响到资源文件的读取方式
先来看打包成jar后的运行情况,这次使用另外一个类去写测试,该类直接调用上面的演示方法
代码:
public class Main {
public static void main(String[] args) {
ClassResource classResource = new ClassResource();
classResource.resWithInstance("");
classResource.resWithInstance("/");
classResource.resWithInstance("ClassResource.class");
classResource.resWithInstance("/ClassResource.class");
classResource.resWithInstance("1.txt");
classResource.resWithInstance("/1.txt");
ClassLoaderResource classLoaderResource = new ClassLoaderResource();
classLoaderResource.resWithInstance("");
classLoaderResource.resWithInstance("/");
classLoaderResource.resWithInstance("ClassResource.class");
classLoaderResource.resWithInstance("/ClassResource.class");
classLoaderResource.resWithInstance("1.txt");
classLoaderResource.resWithInstance("/1.txt");
}
}
运行结果:
分析:
class.getResource()
- 空字符串仍代表当前类路径,以非
/
开头的资源名称都会从当前类路径下找起。记得没有打包时候的规则吗,没错,这里的空格又被转换成了当前类路径,然后调用的classLoader.getResource()
方法/
仍代表应用classPath
路径,以/
开头的资源名称都会从应用classPath
路径下找起,找资源名为/
后面的字符的资源。但是如果只传/
则定位不到,不能输出classPath
路径
classLoader.getResource()
- 还是不接受
/
开头的资源名称,所有/
开头的资源名称都会返回为null,定位不到- 空字符串原本代表
classPath
路径,这里不再支持- 非
/
开头的资源从应用classPath
路径下找起
其他
classPath
由文件夹变成了jar文件- URL协议由
file:
变成了jar:file:
三、文件加载
上一章节,我们已经知道了 class
与 classLoader
定位资源的异同,和在打成 jar 包之后的变化。
现在定位文件已经做到了,这里不再区分究竟是 class
定位的文件还是classLoader
定位的文件,本章节就使用 class
去定位文件如何加载我们定位到的文件呢?
3.1 WAR 包项目
因为 WAR
项目会被解压成为具体的文件(Tomcat),所以这里我们用传统的 File
描述一个对象,并读取即可。
public class ClassResource {
public static void main(String[] args) {
readFile("/1.txt");
}
public static void readFile(String path) {
//1.定位资源
URL resource = ClassResource.class.getResource(path);
System.out.println("[getResource ] 读取文件:" + resource);
if (null == resource) {
System.out.println("找不到资源文件");
return;
}
//2.映射资源
File file = new File(resource.getPath());
InputStream inputStream = null;
try {
inputStream = new FileInputStream(file);
//3.读取资源
read(inputStream);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private static void read(InputStream resource) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int i;
while ((i = resource.read()) != -1) {
baos.write(i);
}
System.out.println(new String(baos.toByteArray(), StandardCharsets.UTF_8));
}
}
执行结果:
可以看到能正常读取,这里不再叙述。
3.2 JAR 包项目
我们首先将上述方法打到 jar 包里面去运行,看一下效果
import com.yx.jtest.loadfile.ClassLoaderResource;
import com.yx.jtest.loadfile.ClassResource;
public class Main {
public static void main(String[] args) {
System.out.println("#############打包后ClassResource开始读取文件############");
ClassResource.readFile("/1.txt");
}
}
执行结果:
可以看到,读取失败了:FileNotFoundException
,到这里,大家可以思考下,为什么文件读取不到了?
3.2.1 为什么路径是 jar:
开头
注意看红框部分输出的文件 URL,这个 URL 不再是以 file:
开头的了。这里先标记下,我们来跟踪下 classLoader.getResource()
的方法,来找到为什么是 jar:
开头。不感兴趣的同学可以跳过这部分
public class ClassLoader {
//...
public URL getResource(String name) {
URL url;
if (parent != null) {
url = parent.getResource(name);
} else {
url = getBootstrapResource(name);
}
if (url == null) {
url = findResource(name);
}
return url;
}
}
熟悉的双亲委任模型,这里不多说,介绍下 ClassLoader
这个类和 Java 的类加载器
从Java虚拟机的角度来讲,只存在两种不同的类加载器:
一种是启动类加载器 (Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;
另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且 全都继承自抽象类java.lang.ClassLoader。
摘自:《深入理解Java虚拟机-JVM高级特性与最佳实践》
其中启动类加载器和其他类加载器的关系,如下图所示:
到这里,我们能知道上述代码的 ClassLoader
实例的 parent
变量都是谁了,这里揭示下:
jar 启动调用的类加载器为
AppClassLoader
,其parent
为ExtClassLoader
,而ExtClassLoader
的父加载器就是启动类加载器了
其中:
- 启动类加载器 默认加载 <JAVA_HOME>/lib 目录下的能被虚拟机正确识别的类库
- 扩展加载器 默认加载 <JAVA_HOME>/lib/ext 目录下的类库 可以看到,这两个都不是用来加载我们指定的文件的,加载
1.txt
只能是AppClassLoader
的工作了。
因为父类加载器得到的 url
均为null,所以方法执行到 findResource(name)
这一行
AppClassLoader
本身没有这个方法的实现类,这里追踪到其父类 URLClassLoader
的实现
public class URLClassLoader {
//...
public URL findResource(final String name) {
/*
* 忽略这个方法,可以看到是交个成员变量 ucp 去找资源了
*/
URL url = AccessController.doPrivileged(
new PrivilegedAction<URL>() {
public URL run() {
//交给 ucp 寻找
return ucp.findResource(name, true);
}
}, acc);
return url != null ? ucp.checkURL(url) : null;
}
}
这里的 ucp 变量,是个 URLClassPath
实例,继续往下追
public class URLClassPath {
//...
public URL findResource(String var1, boolean var2) {
int[] var4 = this.getLookupCache(var1);
URLClassPath.Loader var3; //找到对应的Loader
for (int var5 = 0; (var3 = this.getNextLoader(var4, var5)) != null; ++var5) {
//让Loader去找资源
URL var6 = var3.findResource(var1, var2);
if (var6 != null) {
//找到资源并返回
return var6;
}
}
return null;
}
}
通过注释可以看到最终是通过 URLClassPath
的内部类 Loader
去定位的资源
这里介绍下 Loader
的两个实现类
- JarLoader
- FileLoader
到这里就不再往下追踪了,需要知道的是,打成Jar包后,文件的定位靠 JarLoader
来了
private static class Loader implements Closeable {
private final URL base;
Loader(URL var1) {
this.base = var1;
}
}
static class JarLoader extends Loader {
private final URL csu;
JarLoader(URL var1, URLStreamHandler var2, HashMap<String, URLClassPath.Loader> var3, AccessControlContext var4) throws IOException {
//这里设置 base url 的协议为 jar:
super(new URL("jar", "", -1, var1 + "!/", var2));
//..
}
//1
URL findResource(String var1, boolean var2) {
//先获取resource, 找到 resource 获得其资源定位符 URL
Resource var3 = this.getResource(var1, var2);
//返回 文件 url 给我们写的代码
//返回 文件 url 给我们写的代码
//返回 文件 url 给我们写的代码
return var3 != null ? var3.getURL() : null;
}
//2
Resource getResource(String var1, boolean var2) {
//省略部分代码
//其他不看,看这里,checkResource后会返回resource
return this.checkResource(var1, var2, var3);
}
//3
Resource checkResource(final String var1, boolean var2, final JarEntry var3) {
final URL var4;
//..
//获取初始化时候设置的 base url ,其协议为 jar,并重新封装目标 url,然后赋值给下面的 Resource 实例
var4 = new URL(this.getBaseURL(), ParseUtil.encodePath(var1, false));
//..
//返回资源
return new Resource() {
public URL getURL() {
//上述的封装的目标 url
return var4;
}
// ..
};
}
}
所以我们获取到的资源定位就是以 jar:
开头的了
3.2.2 打 jar 包后,jar 包内资源为什么不能读取了
显而易见,对于 File
类来说,单个的 jar 文件,既是一个 File
, 那么,再通过一个 File
去描述一个文件内部的 File
是不太合适的。
这有点像压缩文件一样:你不能直接操作压缩包内的文件。
那么,该如何快速方便地读取 jar 包内我们想要操作的文件(证书、固定配置)呢?
3.2.3 打 jar 包后,jar 包内资源该怎么读取
答案是,用流的形式,只要稍微改写就可以了,请看如下demo
public class ClassResource{
//以流的形式读取文件
public static void readFileByStream(String path) {
System.out.println("[getResourceAsStream] 读取文件:" + path);
InputStream inputStream = null;
try {
//获得jar包内的文件的流
inputStream = ClassResource.class.getResourceAsStream(path);
read(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
//输出文件内容
private static void read(InputStream resource) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int i;
while ((i = resource.read()) != -1) {
baos.write(i);
}
System.out.println(new String(baos.toByteArray(), StandardCharsets.UTF_8));
}
}
//jar 启动类
public class Main {
public static void main(String[] args) {
System.out.println("#############打包后ClassResource开始读取文件############");
//注意这里的文件名,因为仍然是使用 Class.getResourceXxxx(),所以文件名解析路径方式仍然不变
//跟上述章节保持一致
ClassResource.readFileByStream("/1.txt");
}
}
输出结果:
可以看到,是能够正常读取 jar 内部文件的内容的
3.2.4 jar 包内资源的其他读取方法
也可以使用 JarFile
的形式去读取 jar 包内的资源,这种适合读取别的 jar 包内的资源,这里就不再介绍,感兴趣的同学可以自行百度。
3.3 SpringBoot JAR 包的文件加载方式
Spring boot 项目打包后不同于普通的 jar 包目录结构
执行原有jar读取方式代码
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
readFileByStream("/1.txt");
}
//以流的形式读取文件
public static void readFileByStream(String path) {
System.out.println("[getResourceAsStream] 读取文件:" + path);
InputStream inputStream = null;
try {
//获得jar包内的文件的流
inputStream = ClassResource.class.getResourceAsStream(path);
read(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
//输出文件内容
private static void read(InputStream resource) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int i;
while ((i = resource.read()) != -1) {
baos.write(i);
}
System.out.println(new String(baos.toByteArray(), StandardCharsets.UTF_8));
}
}
输出结果:
可以看到,即使目录结构变了,Springboot jar 包也能正常读取到文件内容,这是因为,Spring boot 把如下两个目录添加到了 classPath
当中
- BOOT-INF/classes
- BOOT-INF/lib
Spring boot 额外提供了一种新的 jar 包内部的资源读取方式,即 ClassPathResource
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
//使用SpringBoot的方式读取资源文件,这里不再以 ‘/’ 开头,类似ClassLoader加载资源的name写法
ClassPathResource classPathResource = new ClassPathResource("1.txt");
try {
read(classPathResource.getInputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
private static void read(InputStream resource) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int i;
while ((i = resource.read()) != -1) {
baos.write(i);
}
System.out.println(new String(baos.toByteArray(), StandardCharsets.UTF_8));
}
}
执行结果:
四、总结
现今微服务大行其道,读取项目内的资源文件也常常在 SpringBoot jar中出现问题,这里使用 ClassPathResource
和 class.getResourceAsStream()
均可。
但是在企业提供高质量服务的目标下,应当把这些额外读取资源的需求,迁移到可配置化的环境当中,这样就能避免因改动配置引起的服务启停和中断。
本人才疏学浅,人微技轻,如有不妥之处,请留下宝贵批评指正。
jar\war\SpringBoot加载包内外资源的方式,告别FileNotFoundException吧的更多相关文章
- SpringBoot加载配置文件的几种方式
首先回忆一下在没有使用SpringBoot之前也就是传统的spring项目中是如何读取配置文件,通过I/O流读取指定路径的配置文件,然后再去获取指定的配置信息. 传统项目读取配置方式 读取xml配置文 ...
- spring-boot 加载本地静态资源文件路径配置
1.spring boot默认加载文件的路径是 /META-INF/resources/ /resources/ /static/ /public/ 这些目录下面, 当然我们也可以从spring bo ...
- jvm加载包名和类名相同的类的规则,以及如何加载包名和类名相同的类(转)
jvm包括三种类加载器: 第一种:bootstrap classloader:加载java的核心类. 第二种:extension classloader:负责加载jre的扩展目录中的jar包. 第三种 ...
- Springboot 加载配置文件源码分析
Springboot 加载配置文件源码分析 本文的分析是基于springboot 2.2.0.RELEASE. 本篇文章的相关源码位置:https://github.com/wbo112/blogde ...
- Microsoft Visual Studio 2008 未能正确加载包“Visual Web Developer HTML Source Editor Package” | “Visual Studio HTM Editor Package”
在安装Microsoft Visual Studio 2008 后,如果Visual Studio 2008的语言版本与系统不一致时,比如:在Windows 7 English System 安装Vi ...
- Visual Studio 2008 Package Load Failure:未能正确加载包“Microsoft.VisualStudio.Xaml”
在安装好Visual Studio 2008后,启动Visual Studio 2008 发现如下提示: 包加载失败 未能正确加载包“Microsoft.VisualStudio.Xaml”( GUI ...
- 未能加载包“Microsoft SQL Server Data Tools”
直接在vs2013里的App_Data目录创建数据库,在服务器资源管理器中查看时报错: 未能加载包“Microsoft SQL Server Data Tools” 英文: The 'Microsof ...
- 使用NuGet加载包,发现加载的dll都是最新版,原来少加了参数[-Version]
使用NuGet获取AutoMapper 发现无法正确加载包,项目版本是3.5,获取的dll版本较高,查资料发现可以通过 “-Version” 指定加载包版本 http://www.mamicode.c ...
- 开启Microsoft SQL Management时,如果出现"未能加载包
Ms Sql server 2005在开启Microsoft SQL Management时,如果出现"未能加载包“Microsoft SQL Management Studio Packa ...
随机推荐
- flink Checkpoint优化
一.设置最小时间间隔 当flink应用开启Checkpoint功能,并配置Checkpoint时间间隔,应用中就会根据指定的时间间隔周期性地对应用进行Checkpoint操作.默认情况下Checkpo ...
- nologin用户执行命令
使用su su -s 是指定shell,这里www用户是nologin用户,是没有默认的shell的,这里指定使用/bin/bash, -c 后面接需要运行的命令, 后面www是用www用户来运行 s ...
- CUDA刷新器:CUDA编程模型
CUDA刷新器:CUDA编程模型 CUDA Refresher: The CUDA Programming Model CUDA,CUDA刷新器,并行编程 这是CUDA更新系列的第四篇文章,它的目标是 ...
- [NOIP1998 提高组] 拼数
题目描述 设有 n 个正整数 a1-an,将它们联接成一排,相邻数字首尾相接,组成一个最大的整数. 输入格式 第一行有一个整数,表示数字个数 n. 第二行有 n 个整数,表示给出的 n 个整数 a_ ...
- 教你在Kubernetes中快速部署ES集群
摘要:ES集群是进行大数据存储和分析,快速检索的利器,本文简述了ES的集群架构,并提供了在Kubernetes中快速部署ES集群的样例:对ES集群的监控运维工具进行了介绍,并提供了部分问题定位经验,最 ...
- sql 数据库使用注意事项
1.在对数据库表进行操作时,一定要注意当前操作的是哪一个数据库,否则很容易引起不必要的错误.对于master数据库中的数据文件,尽量不要去对其操作. 2.可通过图形方式对数据库进行备份操作,可通过数据 ...
- Java网络编程实践
网络编程的目的 无线电台......传播交流信息,数据交换.通信 想要达到这个效果需要什么: 1. 如何准确的定位网络上的第一台主机 192.168.16.124:端口,定位到这个计算机上的某个资源. ...
- linux安装后配置
1.1 系统设置(自测用,公司不需要) 1.1.1 Selinux系统安全保护 Security-Enhanced Linux – 美国NSA国家安全局主导开发,一套增强Linux系统安 全的强制访问 ...
- 2、java基础语法(上):变量与运算符
关键字与保留字 关键字 定义:被Java语言赋予了特殊含义,用做专门用途的字符串(单词) 特点:关键字中所有字母都为小写 官方地址:https://docs.oracle.com/javase/tut ...
- Reactive Spring实战 -- 响应式Kafka交互
本文分享如何使用KRaft部署Kafka集群,以及Spring中如何实现Kafka响应式交互. KRaft 我们知道,Kafka使用Zookeeper负责为kafka存储broker,Consumer ...