Spring WebFlux 响应式编程深度解析:从架构选型到核心抽象

前言

在构建即时通讯(IM)软件的技术选型过程中,开发者常常面临编程范式的抉择。通过前三轮的问答讨论,我们已经深入探讨了基于 Spring WebFlux 和 JavaFX 构建 IM 系统的可行性、前端替代方案以及开源项目参考。本文将从更宏观的角度,剖析响应式编程与传统编程的本质差异,并深入解析 Spring WebFlux 中的核心数据抽象 ------ DataBuffer 及其工厂模式的设计哲学。


一、响应式编程与传统编程的范式对决

1.1 编程模型的根本差异

传统编程模型遵循命令式风格,代码按照编写顺序逐行执行,每个操作都会阻塞当前线程直到任务完成。在 IM 系统中,这意味着当服务端处理一个客户端的消息时,处理线程将被占用,无法响应其他客户端的请求。这种模型在面对海量长连接时,不得不依赖线程池扩容来应对并发,而线程本身是昂贵的操作系统资源,每个线程默认占用 1MB 以上的栈空间。

响应式编程则基于声明式事件驱动的理念。它不等待操作完成,而是注册回调或使用数据流操作符来定义"当数据到达时该做什么"。在 WebFlux 中,请求处理全程非阻塞:线程不会等待数据库查询结果或网络响应,而是立即返回并处理其他任务,当数据就绪时由事件循环通知继续执行。这种模型下,少量线程即可处理数千个并发连接。

1.2 背压机制:流量控制的关键差异

传统编程中,生产者和消费者之间的速度不匹配常导致内存溢出或数据丢失。例如,当消息生产者(如网络接收)速度远快于消费者(如消息持久化)时,中间缓冲区会无限制增长。

响应式编程引入了**背压(Backpressure)**机制,允许消费者主动告知生产者"我当前的处理能力有限,请降低发送速度"。这通过 Reactive Streams 规范中的 Subscription.request(n) 方法实现。Spring WebFlux 完全遵循该规范,使得数据流能够在上下游之间达成动态平衡,这是传统阻塞式编程中难以优雅实现的能力。

1.3 线程模型的对比

传统 Servlet 容器(如 Tomcat)采用一请求一线程模型,每个 HTTP 请求分配一个专属线程,线程在数据库查询或远程调用期间持续阻塞。虽然后续版本引入了非阻塞 I/O,但编程模型本质上仍是阻塞的。

WebFlux 底层基于 Netty 的**事件循环(Event Loop)**模型。Netty 使用少量(通常等于 CPU 核心数)的 I/O 线程,这些线程永不阻塞,它们在处理完一个任务的非阻塞阶段后立刻转向下一个任务。当需要执行 CPU 密集型操作或阻塞调用时,WebFlux 允许将其调度到专门的线程池,而不影响主事件循环。


二、架构选型中的响应式考量

2.1 WebFlux 在 IM 系统中的优势

即时通讯软件天然适合响应式架构。IM 的核心挑战在于维护大量长连接(WebSocket 或 SSE)以及实时消息路由。长连接本身不产生持续的数据传输,但每个连接都占用内存资源。WebFlux 的非阻塞特性使得在有限内存下支持数万并发连接成为可能。

同时,IM 中的消息处理链条通常涉及多个 I/O 操作:消息持久化(数据库)、未读计数更新(缓存)、离线消息存储(文件系统)、推送通知(第三方服务)。这些操作在传统模型中会串行阻塞,而响应式流可以将它们组合为异步管道,多个操作并行执行,显著降低端到端延迟。

2.2 前端选型中的响应式无关性

虽然 WebFlux 是响应式服务端框架,但它并不强制要求前端采用特定技术。前两轮讨论中提到的 Electron、Flutter、Swing 等方案,都可以通过 WebSocket 协议与 WebFlux 后端通信。这意味着响应式编程的收益主要体现在服务端吞吐量和资源利用率上,前端只需按照标准协议交互即可。

2.3 开源项目经验与教训

调研发现,目前尚无可直接使用的 "WebFlux + JavaFX" 完整 IM 开源项目。但相关项目展示了两种技术整合的模式:一种是以 WebFlux 作为纯粹的后端服务,前端通过 HTTP 或 WebSocket 调用;另一种是在同一进程中同时启动 WebFlux 和 JavaFX,通过内存共享或内部事件总线通信。前者更适合分布式部署,后者适合单机桌面应用。


三、DataBuffer:响应式数据流的基石

3.1 DataBuffer 接口的设计哲学

在 Spring WebFlux 的源码中,DataBuffer 接口是处理二进制数据最核心的抽象。它的存在意义在于:在非阻塞 I/O 环境下,数据是以**分块(Chunked)**形式到达和发送的,需要一个高效的容器来承载这些块。

与传统 byte[]ByteBuffer 相比,DataBuffer 有三个关键特性:

分离的读写指针 。传统 ByteBuffer 使用单一的 position 指针,读写操作需要手动 flip() 切换模式,容易出错且不方便同时进行读写。DataBuffer 分别维护 readPositionwritePosition,读取操作不会影响写入位置,这在协议解析(如逐字节解析 WebSocket 帧)时极其便利。

动态扩容能力 。通过 capacity(int)ensureCapacity(int) 方法,DataBuffer 可以根据需要自动扩展容量,而不像 ByteBuffer 那样需要创建新缓冲区并拷贝旧数据。这在编码(如字符串转字节)时尤为重要,因为编码后的字节长度往往无法精确预知。

工厂驱动的创建模式 。DataBuffer 本身不提供构造函数,所有实例必须通过 DataBufferFactory 创建。这种设计将内存分配策略(池化或非池化)与数据操作逻辑解耦,使得运行时可以灵活切换底层实现。

3.2 工厂模式的深层含义

DataBufferFactory 接口定义了四种核心方法:allocateBuffer(分配新缓冲区)、allocateBuffer(int)(分配指定初始大小)、wrap(ByteBuffer)(包装已有数据)、wrap(byte[])(包装字节数组)以及 join(合并多个缓冲区)。

其中 wrap 方法体现了零拷贝设计理念。当服务端需要将已存在的内存数据(如序列化后的对象)写入响应时,无需复制数据到新的缓冲区,只需包装已有的 byte[]ByteBuffer。Netty 的实现中,wrap 甚至可以直接使用 Netty 的 ByteBuf,实现跨组件的数据共享。

join 方法则揭示了响应式数据流的碎片化特征。在 HTTP 请求处理中,一个完整的请求体可能被拆分为多个 TCP 数据包到达,每个包产生一个 DataBuffer。当需要整体读取时(如解析完整的 JSON 请求体),join 方法可以高效地将这些碎片合并为连续的缓冲区,而无需多次数据复制。

3.3 字符编码的响应式处理

DataBuffer 的 write(CharSequence, Charset) 方法实现了一个精妙的编码循环。该方法接受一个字符序列和字符集,将字符串编码为字节并写入缓冲区。其实现细节体现了响应式思维:编码过程可能因缓冲区空间不足而多次迭代,每次迭代都尝试编码尽可能多的字符,空间不足时动态扩容。

这种处理方式与传统 String.getBytes() 截然不同。后者一次性分配足够容纳所有编码后字节的数组,对于大文本会导致内存峰值飙升。而 DataBuffer 的流式编码可以在数据到达时逐步写入,配合 Netty 的零拷贝机制,可以大幅降低内存占用。


四、响应式数据流在各层协议中的应用

4.1 HTTP 协议层的流式处理

在 Spring WebFlux 中,HTTP 请求体不再是一个完整的字节数组或输入流,而是 Flux<DataBuffer>。这个 Flux 在请求数据到达时异步发出 DataBuffer,每个 DataBuffer 对应一个或多个 TCP 数据包。

这种设计使得服务端可以在请求体尚未完全到达时就开始处理。例如,文件上传时可以边接收边写入磁盘,JSON 解析器可以边接收边增量解析。这对于大文件上传或流式数据处理至关重要。

响应体同样以 Flux<DataBuffer> 形式输出。Controller 方法返回的 Mono<DataBuffer>Flux<DataBuffer> 会被框架逐步写入网络通道。这种"生成即发送"的模型避免了将完整响应体缓存在内存中。

4.2 WebSocket 协议的消息帧处理

WebSocket 消息天然是分帧的,每一帧包含部分或完整的消息内容。WebFlux 的 WebSocketSession 将接收到的帧转换为 Flux<WebSocketMessage>,每条消息内部封装了 DataBuffer。

由于 WebSocket 允许消息分帧发送,单条应用层消息可能由多个 WebSocket 帧组成。WebFlux 负责将这些帧重组为完整的 WebSocketMessage,但消息体依然以 DataBuffer 形式呈现。对于超大消息(如文件传输),服务端可以按帧处理而无需等待完整消息,实现流式转发。

4.3 中间件与过滤器的数据拦截

在 WebFilter 中,开发者可以拦截请求的 Flux<DataBuffer> 进行预处理,如解密、解压、日志记录。由于 DataBuffer 的不可变特性(实现类通常确保写入后读取不会影响已写入数据),过滤器可以安全地读取缓冲区内容而不影响后续处理链。

但需注意,DataBuffer 可能是池化的(Netty 的 ByteBuf 实现),在过滤器读取后必须确保缓冲区不会被提前释放。这正是 DataBufferUtils.retain() 方法存在的意义,它增加引用计数,防止在消费完成前被回收。


五、编程思维的根本转变

5.1 从"拉取"到"推送"的思考方式

传统编程中,开发者习惯主动"拉取"数据:调用 read() 方法获取数据,调用 write() 方法发送数据。在响应式编程中,思维转变为"推送":数据就绪时,它会被推送给注册的消费者;消费者通过订阅和请求机制控制推速。

这种转变体现在代码结构上:不再是循环读取-处理-发送,而是定义数据流如何转换、合并、分流。开发者的职责从"控制流程"转变为"定义流程",框架和底层引擎负责调度和执行。

5.2 错误处理的异步化

传统编程的错误处理通过 try-catch 块在调用栈中捕获异常,简单直接但阻塞。响应式编程中,错误是数据流的一部分,通过 onError 操作符处理。这使得错误可以沿数据流传递,在合适的层级统一处理。

在 IM 场景中,消息发送可能因多种原因失败(网络中断、认证失败、目标离线)。响应式错误处理允许将错误转换为相应的错误消息返回给客户端,同时不影响其他消息的处理。

5.3 资源管理的响应式化

传统编程的资源管理(如文件句柄、数据库连接)依赖 try-with-resources 或 finally 块确保释放。响应式编程中,资源通常与数据流生命周期绑定,当流终止时资源自动清理。

DataBuffer 的引用计数机制正是这种思维的体现。池化的 DataBuffer 在使用完毕后必须释放,否则会导致内存泄漏。WebFlux 框架会在写入响应或完成消费后自动释放,但在自定义的流处理中,开发者需要通过 DataBufferUtils.release()using 操作符来确保释放。


六、总结与展望

通过对 Spring WebFlux 核心抽象 DataBuffer 及其工厂模式的深度剖析,我们可以清晰地看到响应式编程的本质:它不是简单的 API 替换,而是一套完整的、从线程模型到内存管理的体系化变革。

在即时通讯软件的开发中,这种变革带来的收益是显著的:更高的连接密度、更低的资源消耗、更优雅的流量控制。同时,它也对开发者的思维方式提出更高要求 ------ 从命令式控制到声明式组合,从同步阻塞到异步非阻塞。

现有开源项目虽未完美覆盖 "WebFlux + JavaFX" 的技术栈组合,但已展示出各部件独立运作的成熟模式。将 WebFlux 强大的非阻塞后端能力,与 JavaFX 或 Electron 等富有表现力的前端框架结合,构建现代化 IM 系统的技术路径已经清晰可见。

响应式编程的演进仍在继续,随着 Project Loom(虚拟线程)等新技术的出现,未来的并发模型可能更加多元。但流式处理、背压控制、声明式组合这些响应式编程的核心思想,将继续在软件架构中发挥不可替代的作用。

引入依赖

要在 Gradle 项目中引入 Spring WebFlux,最直接的方式是添加 spring-boot-starter-webflux 依赖。

⚙️ 核心依赖配置

在你的项目根目录下的 build.gradle 文件中,添加以下依赖:

gradle 复制代码
dependencies {
    // 引入 Spring WebFlux
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
}

这个依赖会自动引入 Reactor Netty,它是 WebFlux 默认的高性能、非阻塞式网络引擎。

请注意spring-boot-starter-webflux 默认使用的 Spring Boot 版本由你的项目(或父项目)的 springBootVersion 决定。如需指定版本,可以这样写:implementation 'org.springframework.boot:spring-boot-starter-webflux:3.1.4'

🚀 完整的 build.gradle 示例

以下是一个最简化的 build.gradle 配置示例,供你参考:

gradle 复制代码
plugins {
    id 'org.springframework.boot' version '3.1.4' // 或你使用的版本
    id 'io.spring.dependency-management' version '1.1.3'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17' // WebFlux 通常需要 Java 8 以上

repositories {
    mavenCentral()
}

dependencies {
    // 核心 WebFlux 依赖
    implementation 'org.springframework.boot:spring-boot-starter-webflux'

    // 测试依赖(可选)
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

💡 进阶配置:更换底层服务器

WebFlux 默认使用 Netty,但如果你希望更换为 Tomcat、Jetty 或 Undertow,可以这样做:

gradle 复制代码
configurations {
    implementation {
        // 排除默认的 Reactor Netty
        exclude module: 'spring-boot-starter-reactor-netty'
    }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    // 添加你选择的服务器依赖
    implementation 'org.springframework.boot:spring-boot-starter-undertow'
    // 或 implementation 'org.springframework.boot:spring-boot-starter-jetty'
    // 或 implementation 'org.springframework.boot:spring-boot-starter-tomcat'
}

📝 验证与使用

依赖添加并刷新 Gradle 项目后,你就可以开始编写响应式的 Controller 了:

java 复制代码
@RestController
public class HelloController {
    @GetMapping("/hello")
    public Mono<String> sayHello() {
        return Mono.just("Hello, WebFlux!"); // Mono 表示包含 0 或 1 个元素的异步序列
    }
}

最后,别忘了在 application.propertiesapplication.yml 中确认应用类型为响应式:

properties 复制代码
spring.main.web-application-type=reactive

完成以上配置,就可以开始你的 Spring WebFlux 响应式编程之旅了。

DataBuffer 是 Spring WebFlux 中响应式数据流的最小载体 ,它的设计初衷是在非阻塞 I/O 环境下高效地处理二进制或文本数据块。而 DataBufferFactory 则是生产 DataBuffer 实例的工厂,负责创建、包装和组合这些数据块。下面我将从使用场景工厂接口解析实际代码示例 以及工厂与 DataBuffer 的关系四个方面进行深入说明。


1. DataBuffer 的核心使用场景

在 Spring WebFlux 中,所有基于流的 HTTP 交互(请求/响应)、WebSocket 消息、文件上传等,底层都是通过 Flux<DataBuffer> 传递的。具体场景包括:

  • HTTP 请求体读取ServerRequest.body(BodyExtractor) 内部会将请求体拆分为 Flux<DataBuffer>,然后由 BodyExtractor(如 BodyExtractors.toMono(String.class))将其解码为 Java 对象。
  • HTTP 响应体写入 :控制器返回 Flux<DataBuffer> 或通过 ServerResponse.body() 写入数据,框架会逐块将 DataBuffer 写入底层网络通道(如 Netty)。
  • 文件上传与下载MultipartBody 中的文件部分也是 DataBuffer 流,可以流式写入磁盘,实现零拷贝。
  • WebSocket 通信WebSocketSession.receive() 返回 Flux<WebSocketMessage>,每条消息内部封装了 DataBuffer,发送时也通过 WebSocketMessage 封装。
  • 中间件处理 :过滤器(Filter)可以拦截 DataBuffer 流,进行日志、加密、压缩等操作。

2. DataBufferFactory 接口详解

java 复制代码
public interface DataBufferFactory {
    // 分配一个默认大小的缓冲区(通常为 256 或 4KB)
    DataBuffer allocateBuffer();

    // 分配指定初始容量的缓冲区(可根据需要动态扩容)
    DataBuffer allocateBuffer(int initialCapacity);

    // 包装一个已经存在的 ByteBuffer(只读或可读写),不复制数据
    DataBuffer wrap(ByteBuffer byteBuffer);

    // 包装一个字节数组,同样不复制(注意:修改数组会影响缓冲区)
    DataBuffer wrap(byte[] bytes);

    // 将多个 DataBuffer 连接成一个新的 DataBuffer(通常用于合并分片)
    DataBuffer join(List<? extends DataBuffer> dataBuffers);
}

关键点

  • allocateBuffer:创建新的、空的缓冲区,用于写入数据。
  • wrap :将已有的数据包装为 DataBuffer,避免拷贝,常用于将内存数据(如字符串转换后的字节)转换为响应流。
  • join :将多个 DataBuffer 合并成单个连续的缓冲区。这在需要将分片数据合并为完整消息时很有用(如累积 HTTP 请求体)。

常见的实现类包括:

  • DefaultDataBufferFactory(基于 byte[],非池化,但简单可靠)。
  • NettyDataBufferFactory(基于 Netty 的 ByteBuf,支持池化,性能更高,是 WebFlux 在 Netty 运行时的默认实现)。

3. 实际代码使用示例

场景一:手动创建 DataBuffer 并写入响应

java 复制代码
@RestController
public class EchoHandler {
    private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();

    @GetMapping("/echo")
    public Mono<ResponseEntity<Flux<DataBuffer>>> echo() {
        String message = "Hello, WebFlux!";
        // 使用工厂包装字节数组
        DataBuffer buffer = bufferFactory.wrap(message.getBytes(StandardCharsets.UTF_8));
        return Mono.just(ResponseEntity.ok(Flux.just(buffer)));
    }
}

场景二:在 Filter 中读取并修改请求体

java 复制代码
@Component
public class LoggingFilter implements WebFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        Flux<DataBuffer> body = exchange.getRequest().getBody();
        // 使用 DataBufferUtils 将整个请求体收集为一个 DataBuffer
        Mono<DataBuffer> merged = DataBufferUtils.join(body);
        return merged.flatMap(buffer -> {
            // 读取内容(假设是文本)
            String content = buffer.toString(StandardCharsets.UTF_8);
            System.out.println("Request body: " + content);
            // 注意:不能直接使用原来的 buffer,因为已消费;需要重新包装或复制
            DataBuffer newBuffer = exchange.getResponse().bufferFactory().wrap(content.getBytes());
            // 重新构建请求(实际场景需用 ServerHttpRequestDecorator 替换 body)
            return chain.filter(exchange);
        });
    }
}

场景三:在服务端接收 WebSocket 消息

java 复制代码
@Configuration
public class WebSocketConfig {
    @Bean
    public HandlerMapping webSocketMapping() {
        Map<String, WebSocketHandler> map = new HashMap<>();
        map.put("/chat", new ChatWebSocketHandler());
        return new SimpleUrlHandlerMapping(map, 0);
    }
}

class ChatWebSocketHandler implements WebSocketHandler {
    @Override
    public Mono<Void> handle(WebSocketSession session) {
        Flux<WebSocketMessage> output = session.receive()
            .map(WebSocketMessage::getPayload)              // 获取 DataBuffer
            .map(buffer -> buffer.toString(StandardCharsets.UTF_8))
            .map(text -> "Echo: " + text)
            .map(str -> session.textMessage(str));          // 创建响应消息
        return session.send(output);
    }
}

场景四:使用 join 合并多个分片

java 复制代码
List<DataBuffer> fragments = ...; // 从多个网络包获得
DataBuffer full = factory.join(fragments); // 合并成完整数据
byte[] allBytes = new byte[full.readableByteCount()];
full.read(allBytes); // 读取全部数据

4. DataBuffer 与 DataBufferFactory 的联系与区别

维度 DataBufferFactory DataBuffer
职责 创建、包装、组合 DataBuffer 实例 承载数据、读写操作、切片、编码等
生命周期 单例或全局持有(如 ServerResponse.bufferFactory() 由工厂创建,用完需释放(池化时)
关系 工厂模式,一个工厂可以生成无数个 DataBuffer 每个 DataBuffer 都持有对其工厂的引用(通过 factory() 方法)
典型使用 在 Controller 或 Filter 中获取 bufferFactory(从 ServerWebExchangeResponse 获取) 直接操作 DataBuffer 进行读写或转换成其他形式

区别

  • DataBufferFactory 是"创建者",DataBuffer 是"产品"。
  • DataBufferFactory 负责底层资源的分配策略(是否池化),而 DataBuffer 只关心数据操作。
  • 工厂的 wrap 方法不会复制数据,只是包装;allocateBuffer 则分配新的内存。

联系

  • 每个 DataBuffer 都可以通过 factory() 方法获取其创建者,以便后续创建新缓冲区(例如在扩容或转换时需要同工厂的新实例)。
  • 在 WebFlux 中,ServerHttpResponseServerHttpRequest 都会提供 bufferFactory() 方法,确保整个请求/响应使用同一工厂,保持内存管理一致性。

5. 如何正确使用 DataBuffer?

获取工厂实例的典型方式:

  • 在控制器中,通过 ServerRequestServerResponse.bufferFactory() 方法获取。
  • 在过滤器或处理器中,通过 ServerWebExchangegetResponse().bufferFactory() 获取。
  • 单元测试时,可以直接 new DefaultDataBufferFactory()

注意事项:

  1. 及时释放 :如果使用池化工厂(如 Netty),需要确保 DataBuffer 在使用后被释放(通过 DataBufferUtils.release(buffer))以避免内存泄漏。框架会自动释放(如写入响应后),但手动操作(如 join)后需谨慎。
  2. 不可共享DataBuffer 不是线程安全的,不应在多个线程中同时读写。
  3. 读取前检查位置readableByteCount() 可获取有效数据长度,避免读超出。
  4. 编码处理 :写入 CharSequence 时,注意字符集,并使用 write(CharSequence, Charset) 方法,它会自动处理扩容。

常用工具类 DataBufferUtils

  • DataBufferUtils.join(Flux<DataBuffer>):将多个缓冲区合并为一个 Mono<DataBuffer>
  • DataBufferUtils.retain(DataBuffer):增加引用计数(池化时)。
  • DataBufferUtils.release(DataBuffer):减少引用计数并在计数器归零时释放内存。
  • DataBufferUtils.toInputStream(Flux<DataBuffer>):将流转换为标准 InputStream(阻塞操作,慎用)。

总结

DataBuffer 是 WebFlux 响应式数据处理的基石,它提供了一套与底层 NIO 解耦的 API,使开发者能够以非阻塞方式高效处理字节流。DataBufferFactory 则是这套生态的"引擎",负责创建和管理数据容器的生命周期。在实际开发中,我们通常不需要直接创建 DataBuffer(除非处理底层流或测试),而是通过框架提供的工厂,在请求/响应上下文中操作 Flux<DataBuffer>,从而享受响应式编程带来的高吞吐量和低资源消耗。

如果你有更具体的场景(比如如何对 DataBuffer 进行加密或压缩),可以继续追问,我会给出更详细的实现指南。

相关推荐
风中芦苇啊15 小时前
Java EasyExcel 导入通用工具类:自定义注解映射字段 + 反射机制
java·开发语言
XUHUOJUN15 小时前
Azure Stack Hub 报修与技术支持:Dell + Microsoft 双供应商协同支持流程
架构·azure stack
家有娇妻张兔兔15 小时前
Java 对接 PLC 主流型号最合适的方案:Apache PLC4X 实战指南
java·开发语言·plc·modbus·数据缓存·西门子s7·apache plc4x
储能李大坤16 小时前
125kW SiC双向储能变流器模块技术拆解:三电平拓扑 + SiC功率器件 + 双DSP控制架构详解
架构
XUHUOJUN16 小时前
Azure Stack Hub 租户日常操作:从订阅到 VM、VMSS、监控与 ARM 模板
架构·azure stack
摇滚侠16 小时前
Codebuddy 官网 Codebuddy IntelliJ IDEA 插件 阅读笔记 2
java·笔记·intellij-idea
gugucoding17 小时前
24. 【Java】枚举:更安全的常量
java·开发语言
Kyrie_kk17 小时前
Java--BigInteger 超大整数计算
java·算法
红莲凪是17 小时前
夜莺监控的几种架构模式详解
架构
开发者联盟league17 小时前
Java 通过 JNA 调用 C++ DLL:关键流程总结
java·c++·jna