Posted in

Go调用Java服务的12种方案深度评测(JNIGraalVMQuarkus全对比)

第一章:Go调用Java服务的技术全景与选型哲学

在云原生与多语言协同演进的背景下,Go 与 Java 的跨语言协作已成企业级系统架构的常态。Go 凭借高并发、低内存开销与快速部署优势常作为网关、边车或数据管道主力;Java 则依托 Spring 生态、成熟中间件支持与强事务能力持续承担核心业务逻辑。二者并非替代关系,而是互补共生——关键在于选择契合场景、可控性强、可观测性完备的集成路径。

主流集成模式概览

  • HTTP/RESTful 调用:最轻量、零语言耦合,适合松耦合微服务通信;需关注序列化一致性(如统一使用 JSON 或 Protobuf)与重试熔断策略。
  • gRPC + Protocol Buffers:跨语言性能最优选,需 Java 侧提供 .proto 定义并生成 gRPC Server(如使用 grpc-java),Go 侧通过 protoc-gen-go 生成客户端。
  • JNI 直接调用:仅限同进程内、低延迟严苛场景(如实时风控计算),但牺牲可移植性与稳定性,不推荐生产环境常规使用。
  • 消息队列桥接:通过 Kafka/RocketMQ 解耦时序与生命周期,天然支持异步、削峰、重放,适合事件驱动架构。

关键选型决策维度

维度 HTTP/REST gRPC 消息队列
延迟 极低 高(毫秒级)
协议复杂度 中(需 IDL 管理) 中(需 Topic 治理)
错误传播能力 弱(仅 HTTP 状态码) 强(自定义 Status + Details) 弱(需业务层封装)
运维可观测性 易(标准 HTTP tracing) 需适配 OpenTelemetry gRPC 插件 依赖 Broker 指标与消费位点监控

快速验证 gRPC 集成示例

Java 服务端(Spring Boot + grpc-spring-boot-starter)暴露 UserService 后,Go 客户端可执行:

# 1. 从 Java 工程导出 user.proto,置于 ./proto/
# 2. 生成 Go 代码
protoc --go_out=. --go-grpc_out=. ./proto/user.proto
# 3. 初始化连接(含 TLS 与超时配置)
conn, err := grpc.Dial("java-service:9090",
    grpc.WithTransportCredentials(insecure.NewCredentials()),
    grpc.WithTimeout(5*time.Second),
)
if err != nil { panic(err) }
client := pb.NewUserServiceClient(conn)

该流程将 Java 服务能力以类型安全方式注入 Go 生态,规避反射与字符串解析风险。

第二章:基于JNI的原生互操作方案深度实践

2.1 JNI架构原理与Go侧Cgo桥接机制解析

JNI(Java Native Interface)是JVM与本地代码交互的标准契约,其核心依赖JNIEnv*环境指针和jobject等类型映射。Go通过cgo调用C函数时,需绕过JVM直接与native层协同。

Go调用Java的典型路径

  • Go → C wrapper(cgo导出)→ JNI invocation API → JVM → Java method
  • Java回调Go需借助全局引用+函数指针注册(如JavaVM->AttachCurrentThread

关键数据结构对齐表

Java Type C/JNI Type Go CGO Type
int jint C.jint
String jstring *C.jstring
Object jobject C.jobject
// JNI_OnLoad中获取JavaVM指针(供Go后续Attach/Detach)
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
    g_jvm = vm; // 全局缓存,cgo可安全访问
    return JNI_VERSION_1_8;
}

该函数在JVM加载共享库时自动调用;g_jvm被Go侧通过//export暴露的C函数引用,用于线程绑定与对象生命周期管理。

// Go侧主动触发Java方法(简化示意)
func CallJavaMethod() {
    C.attach_thread() // 调用C封装的AttachCurrentThread
    defer C.detach_thread()
    C.call_java_print(C.CString("Hello from Go"))
}

attach_thread内部调用(*JavaVM).AttachCurrentThread,确保当前goroutine拥有合法JNIEnv上下文;call_java_print为JNI层C函数,完成FindClassGetMethodIDCallVoidMethod完整链路。

2.2 Java端JVM生命周期管理与线程模型适配

JVM生命周期需与宿主环境(如Android Runtime或嵌入式容器)协同演进,尤其在线程模型耦合场景下。

线程绑定策略

  • 主线程必须绑定 JNI_OnLoad 所在线程,确保 JavaVM* 全局可访问
  • 新生线程需显式调用 AttachCurrentThread,避免 GetEnv 返回 NULL

JVM启动参数关键配置

参数 说明 推荐值
-Xms/-Xmx 堆初始/最大容量 128m/512m(受限环境)
-XX:+UseG1GC 启用低延迟GC 必选(响应敏感场景)
-Djava.class.path 指定启动类路径 绝对路径,避免相对路径解析失败
// 在JNI层安全获取JNIEnv并绑定当前线程
JavaVM* g_jvm = nullptr; // 全局持有,由JNI_OnLoad初始化

jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    g_jvm = vm; // 仅此一次赋值,线程安全
    return JNI_VERSION_1_8;
}

JNIEnv* get_jni_env() {
    JNIEnv* env = nullptr;
    jint status = g_jvm->GetEnv((void**)&env, JNI_VERSION_1_8);
    if (status == JNI_EDETACHED) {
        // 当前线程未附加:必须Attach(不可AttachCurrentThreadAsDaemon)
        if (g_jvm->AttachCurrentThread(&env, nullptr) != JNI_OK) {
            return nullptr;
        }
    }
    return env;
}

该函数确保任意线程均可安全获取有效 JNIEnv*:先尝试 GetEnv,失败则自动 AttachCurrentThread;注意 AttachCurrentThreadAsDaemon 不触发 GC 友好性,故禁用。返回前无需 Detach——由线程退出时JVM自动清理。

2.3 高频场景下的对象序列化与内存安全实践

序列化选型对比

方案 吞吐量(万 ops/s) 内存开销 安全性保障
JSON(Jackson) 8.2 无原生反序列化防护
Protobuf 24.6 类型强约束,规避反射攻击
Kryo(禁用注册) 31.1 ❌ 默认启用不安全反射

安全序列化实现示例

// 使用Protobuf + 自定义解析器,禁用未知字段与动态类型
Parser<TradeOrder> safeParser = TradeOrder.parser()
    .withUnknownFieldHandler((tag, input) -> {
        throw new SecurityException("拒绝未知字段: " + tag); // 主动拦截非法字段
    });

逻辑分析:withUnknownFieldHandler 拦截未定义字段,防止攻击者注入恶意扩展字段;parser() 返回不可变解析器实例,避免线程间状态污染。参数 tag 为字段编号,input 为原始字节流上下文。

内存安全关键路径

graph TD
    A[高频请求] --> B{序列化入口}
    B --> C[字段白名单校验]
    C --> D[Protobuf二进制解析]
    D --> E[对象池复用实例]
    E --> F[零拷贝响应写入]

2.4 JNI异常传播、错误码映射与Go错误处理统一

JNI调用中,Java端抛出的RuntimeException需同步透传至Go层,而非静默吞没。核心挑战在于三类错误语义的对齐:Java异常类型、JNI返回的jint错误码(如JNI_ERR)、Go的error接口。

异常捕获与跨语言转换

// 在Cgo桥接函数中检查并转换JNI异常
if jni.ExceptionCheck(env) != 0 {
    exc := jni.ExceptionOccurred(env)
    jni.ExceptionClear(env)
    msg := jni.GetStringUTFChars(env, jni.CallObjectMethod(env, exc, msgMid), nil)
    err := fmt.Errorf("jni: %s", C.GoString(msg))
    jni.ReleaseStringUTFChars(env, jni.CallObjectMethod(env, exc, msgMid), msg)
    return nil, err
}

该段逻辑主动轮询JNI异常状态,获取异常消息字符串并构造Go原生error。关键点:ExceptionClear()必须在ExceptionOccurred()后立即调用,否则后续JNI调用将失败;ReleaseStringUTFChars防止内存泄漏。

错误码映射表

JNI返回值 含义 Go error构造方式
JNI_OK 成功 nil
JNI_ERR 通用错误 fmt.Errorf("jni: generic error")
JNI_EDETACHED 线程未附加 errors.New("jni: thread not attached")

统一错误处理流程

graph TD
    A[JNI调用] --> B{ExceptionCheck?}
    B -->|Yes| C[Fetch & Clear Exception → Go error]
    B -->|No| D{Return code == JNI_ERR?}
    D -->|Yes| E[Map to predefined Go error]
    D -->|No| F[Success → nil error]

2.5 生产级JNI封装库设计:golang-jni-wrapper实战构建

golang-jni-wrapper 以零拷贝内存共享与异常透传为核心,解决传统 JNI 调用中 GC 干扰、线程绑定僵化、错误信息丢失三大痛点。

核心设计原则

  • 线程安全的 JNIEnv* 缓存池(避免 AttachCurrentThread 频繁调用)
  • Go struct 字段与 Java ByteBuffer 直接内存映射(unsafe.PointerDirectBuffer.address
  • Java 异常自动转为 Go error,含完整 StackTraceElement 上下文

内存同步机制

// Java 层声明:public static native void processInPlace(ByteBuffer buf, int len);
func (j *JNI) ProcessInPlace(buf *jni.DirectByteBuffer, length int) error {
    env := j.getEnv() // 复用已 Attach 的 JNIEnv
    ptr := jni.GetDirectBufferAddress(env, buf) // 零拷贝获取原生地址
    // ... C/C++ 算法逻辑直接操作 ptr
    return j.checkJavaException(env) // 自动捕获并转换 Exception
}

getEnv() 通过 pthread_getspecific 检索线程局部 JNIEnv*GetDirectBufferAddress 绕过 JVM 边界检查,需确保 buf 为 direct buffer。

特性 传统 cgo-JNI golang-jni-wrapper
跨线程调用开销 高(每次 Attach/Detach) 低(LRU 缓存 + 自动管理)
Java 异常可观测性 丢失堆栈 完整 Throwable.toString() + printStackTrace() 截获
graph TD
    A[Go goroutine] -->|调用| B[j.ProcessInPlace]
    B --> C{JNIEnv 已 Attach?}
    C -->|是| D[直接获取 ptr]
    C -->|否| E[Attach + 缓存]
    D --> F[C 算法处理]
    F --> G[checkJavaException]
    G -->|有异常| H[构造 Go error]

第三章:GraalVM Native Image跨语言融合方案

3.1 GraalVM Polyglot API在Go宿主环境中的嵌入式调用

GraalVM 并未原生支持 Go 作为宿主语言,但可通过 C API(libgraalvmnative)桥接。核心路径是:Go 调用 C 封装层 → C 加载 GraalVM 运行时 → 创建 PolyglotEngine 实例。

关键依赖链

  • Go 使用 cgo 链接 libgraalvmnative.so(Linux)或 .dylib(macOS)
  • 必须设置 GRAALVM_HOMELD_LIBRARY_PATH
  • 启动参数需显式启用 polyglot 模式:--polyglot --jvm

示例:初始化引擎(C 封装接口)

// engine.h —— Go 通过#cgo 调用
extern PolyglotEngine* create_engine(const char** options, int opt_count);

Go 调用片段(含错误检查)

/*
#cgo LDFLAGS: -L${SRCDIR}/lib -lgraalvmnative
#include "engine.h"
*/
import "C"
import "unsafe"

opts := []*C.char{
    C.CString("--polyglot"),
    C.CString("--jvm"),
}
engine := C.create_engine(&opts[0], C.int(len(opts)))
if engine == nil {
    panic("failed to create polyglot engine")
}

逻辑说明create_engine 接收 C 字符串数组与长度,内部调用 org.graalvm.polyglot.Engine.newBuilder().build()--jvm 参数确保 JVM 模式启动,支撑 Java/JS/Python 等语言互操作。

组件 作用
libgraalvmnative GraalVM 提供的 C 兼容运行时绑定
cgo Go 与 C ABI 交互的唯一标准机制
--polyglot 启用跨语言上下文隔离与值转换能力
graph TD
    A[Go main] --> B[cgo call]
    B --> C[libgraalvmnative.so]
    C --> D[GraalVM Engine]
    D --> E[JS/Python/Ruby Context]

3.2 Java代码AOT编译为动态库供Go直接dlopen调用

GraalVM 提供 native-image 工具,支持将 Java 代码编译为平台原生动态库(.so / .dylib),使 Go 可通过 dlopen 直接调用。

编译流程关键步骤

  • 使用 --shared--no-fallback 参数生成动态库
  • 导出函数需用 @CEntryPoint 标注,并禁用 JVM 启动开销
  • Java 类需显式注册为 --initialize-at-build-time

示例导出函数

// HelloService.java
import org.graalvm.nativeimage.c.function.CEntryPoint;
import org.graalvm.nativeimage.c.type.CCharPointer;

public class HelloService {
    @CEntryPoint(name = "say_hello")
    public static int say_hello(CCharPointer name) {
        System.out.println("Hello, " + fromCString(name));
        return 0;
    }
}

@CEntryPoint 声明 C 兼容入口;fromCString() 将 C 字符串转为 Java 字符串;返回 int 便于 Go 错误判断。

Go 调用示意

// 使用 syscall.LazyDLL 加载 .so,再 GetProcAddress 获取 say_hello 符号
选项 作用 必选
--shared 生成动态库而非可执行文件
--no-fallback 禁用解释执行回退,确保纯 AOT
-H:Name=libhello 指定输出库名
graph TD
    A[Java源码] --> B[GraalVM native-image --shared]
    B --> C[libhello.so]
    C --> D[Go dlopen + dlsym]
    D --> E[直接调用 say_hello]

3.3 GraalVM Truffle语言实现与Go扩展接口协同模式

GraalVM 的 Truffle 框架允许以 Java 实现高性能动态语言,而 Go 扩展则通过 libgraal 或 JNI 边界调用宿主能力。二者协同需绕过跨语言内存模型差异。

数据同步机制

Truffle 语言(如 SimpleLanguage)通过 @ExportMessage 暴露操作接口,Go 侧经 C FFI 封装调用:

// Go 侧 C 接口桥接(CGO)
extern void* sl_interop_create_context();
extern int sl_interop_eval(void*, const char*);

sl_interop_create_context() 返回 Truffle LanguageContext 指针,生命周期由 Java GC 管理;sl_interop_eval() 触发 AST 解释执行,参数为 UTF-8 字符串,返回 0 表示成功。

协同架构对比

维度 直接 JNI 调用 Truffle Interop API
类型转换开销 高(jobject ↔ Go struct) 低(自动 boxing/unboxing)
异常传播 需手动 ExceptionCheck 自动映射为 Go error
graph TD
    A[Go 主程序] -->|C FFI call| B[Truffle Context]
    B --> C[SL Parser]
    C --> D[AST Node]
    D -->|@ExportMessage| E[Java Host Object]

第四章:基于HTTP/gRPC/消息中间件的松耦合集成方案

4.1 Spring Boot RESTful服务 + Go标准net/http高性能消费实践

Spring Boot 提供轻量级 RESTful 接口,而 Go 的 net/http 客户端凭借无协程调度开销、连接复用与零分配设计,在高并发消费场景中表现优异。

连接池调优策略

  • 复用 http.Transport 实例,设置 MaxIdleConns=100MaxIdleConnsPerHost=100
  • 启用 KeepAlive(默认开启),降低 TCP 握手开销
  • 超时统一配置:Timeout=5sIdleConnTimeout=30s

Go 客户端核心实现

client := &http.Client{
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 100,
        IdleConnTimeout:     30 * time.Second,
        TLSHandshakeTimeout: 5 * time.Second,
    },
}
// 逻辑说明:复用连接池避免频繁建连;IdleConnTimeout 防止服务端过早关闭空闲连接导致"connection reset"

性能对比(QPS,16核/32GB)

客户端类型 平均延迟 吞吐量
Java RestTemplate 42 ms 1850
Go net/http 9.3 ms 8900

graph TD A[Go HTTP Client] –> B[复用 Transport] B –> C[长连接保活] C –> D[无反射/无 GC 压力] D –> E[单机万级 QPS]

4.2 Quarkus gRPC Server + Go gRPC Client双向流式通信优化

双向流式通信在实时数据同步、IoT设备协同等场景中面临高吞吐与低延迟双重挑战。Quarkus 以 native image 和反应式运行时显著降低服务端内存占用与启动延迟,而 Go 客户端凭借轻量协程(goroutine)和零拷贝 []byte 处理能力,天然适配高频流式交互。

数据同步机制

客户端按需发送设备心跳与遥测批次,服务端动态响应策略更新与配置下发,形成闭环反馈流。

性能调优关键点

  • 启用 gRPC HTTP/2 流控窗口调大(--grpc-max-send-msg-size=8388608
  • Quarkus 中配置 quarkus.grpc.server.max-inbound-message-size=8388608
  • Go 客户端复用 ClientConn 并启用 keepalive
// Go 客户端双向流初始化(带超时与重试)
stream, err := client.BidirectionalSync(ctx,
    grpc.WaitForReady(true),
    grpc.MaxCallSendMsgSize(8*1024*1024),
)

该调用启用等待服务端就绪、扩大单消息上限至 8MB;WaitForReady 避免流建立初期的 UNAVAILABLE 错误,提升流稳定性。

参数 Quarkus Server 值 Go Client 值 作用
max-inbound-message-size 8388608 限制接收消息最大尺寸
MaxCallSendMsgSize 8388608 控制发送消息上限
KeepAliveTime 30s 30s 维持长连接活跃
// Quarkus Server 端流处理器(响应式)
public void handleBidirectionalStream(StreamObserver<SyncResponse> responseObserver) {
    // 使用 Mutiny Multi 实现背压感知的流式响应
}

基于 Mutiny 的 Multi 自动适配下游消费速率,避免 OOM;StreamObserver 封装线程安全的异步写入,确保多 goroutine 场景下响应不丢序。

4.3 Kafka事件驱动架构下Go与Java服务的Schema演进协同

在跨语言微服务中,Avro Schema注册中心(Confluent Schema Registry)成为Go(github.com/linkedin/goavro/v2)与Java(io.confluent:kafka-avro-serializer)协同演进的核心枢纽。

数据同步机制

双方共享同一Schema ID,通过schema.id嵌入消息头实现版本可追溯:

// Go生产者:显式声明兼容性策略
cfg := avro.NewConfig()
cfg.SetSchemaRegistryURL("http://schema-registry:8081")
cfg.SetAutoRegisterSchemas(true)
cfg.SetCompatibilityLevel("BACKWARD") // 允许新增可选字段

SetCompatibilityLevel("BACKWARD")确保Java消费者能解析旧版Go消息;AutoRegisterSchemas=true触发首次写入时自动注册Schema并返回ID,避免硬编码版本。

演进约束对比

语言 支持演进操作 禁止操作 工具链校验时机
Java 字段重命名、默认值添加 删除必填字段 编译期(maven-plugin)
Go 新增可选字段、类型宽松转换 修改枚举值顺序 运行时注册阶段

协同流程

graph TD
    A[Go服务发布v2 Schema] --> B{Schema Registry校验}
    B -->|兼容| C[分配新ID,更新全局索引]
    B -->|冲突| D[拒绝注册,触发告警]
    C --> E[Java消费者拉取v2 Schema]
    E --> F[反序列化时自动适配字段缺失]

关键在于统一采用BACKWARD_TRANSITIVE策略,使三方服务(Go/Java/Python)均能安全消费任意历史版本消息。

4.4 OpenTelemetry统一观测体系:跨语言Trace透传与Metrics对齐

OpenTelemetry(OTel)通过标准化上下文传播协议,实现 Java、Go、Python 等异构服务间 Trace ID 与 Span ID 的无损透传。

数据同步机制

OTel SDK 自动注入 traceparent HTTP 头(W3C Trace Context 格式),下游服务解析后延续调用链:

# Python 服务端自动提取并续传(需 otel-instrumentation-wsgi)
from opentelemetry import trace
from opentelemetry.propagate import extract, inject

# 从入站请求提取上下文
carrier = {"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}
ctx = extract(carrier)  # 解析出 TraceID=4bf92f3577b34da6a3ce929d0e0e4736,SpanID=00f067aa0ba902b7
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("process_order", context=ctx):
    # 新 Span 继承父上下文,保障链路连续性

逻辑分析extract() 依据 W3C 规范解析 traceparent 字段,还原分布式追踪上下文;start_as_current_span(..., context=ctx) 显式继承父 Span,确保跨进程 Span 关系可溯。tracestate 头可扩展传递 vendor 特定元数据。

Metrics 与 Trace 对齐策略

维度 Trace 层级 Metrics 层级 对齐方式
时间精度 微秒级(span.start_time) 秒级(instrument.record()) 使用 time_unix_nano 对齐时间戳
标签(Attributes) http.status_code, db.name 同名 attributes 作为 metric label OTel SDK 自动复用 attribute 键值
graph TD
    A[Client HTTP Request] -->|inject traceparent| B[Java Service]
    B -->|extract + inject| C[Go Service]
    C -->|extract + inject| D[Python Service]
    D --> E[DB Query Span]
    E --> F[Counter: db.calls.total<br>attributes: {db.system: \"postgresql\"}]

第五章:未来演进路径与企业级落地建议

技术栈协同演进的三维驱动模型

现代企业AI平台已从单点模型部署转向“数据—算力—治理”三维协同演进。某国有银行在2023年完成MLOps平台升级时,将特征平台(Feast)与Kubeflow Pipelines深度集成,实现特征版本自动绑定模型训练流水线;同时引入NVIDIA Triton推理服务器统一调度CPU/GPU资源,在信用卡欺诈识别场景中将端到端延迟从860ms压降至192ms。该实践验证了基础设施层解耦与业务层强耦合并存的演进逻辑。

混合云架构下的模型生命周期管理

企业需构建跨公有云(AWS SageMaker)、私有云(OpenShift AI)与边缘节点(NVIDIA Jetson)的统一管控平面。下表对比了三类环境在模型监控维度的关键能力:

监控维度 公有云环境 私有云环境 边缘节点
实时推理日志采集 支持CloudWatch流式推送 需自建Fluentd+ES集群 本地环形缓冲+定时同步
模型漂移检测频率 每小时全量样本扫描 每日抽样5%生产流量 仅支持阈值触发式轻量检测
自动化重训触发 支持SageMaker Autopilot联动 需人工审批后触发Airflow DAG 不支持自动重训

合规性嵌入式开发流程

某医疗影像AI公司通过将GDPR与《人工智能监管办法》条款转化为代码约束,在CI/CD流水线中嵌入三项强制检查:

  • pre-commit钩子校验所有数据加载函数是否调用pandas.read_csv(..., dtype_backend='pyarrow')以启用隐私保护型类型推断
  • pytest测试套件包含test_pii_redaction.py,要求对DICOM元数据字段执行正则匹配覆盖率≥99.7%
  • 模型导出阶段自动注入ONNX Runtime的OrtSessionOptions.add_session_config_entry("session.intra_op_thread_count", "2")以满足国产化信创环境线程数限制
graph LR
A[生产环境API调用] --> B{请求头含X-Region-Code?}
B -->|是| C[路由至合规专用推理集群]
B -->|否| D[拒绝请求并返回HTTP 403]
C --> E[自动注入审计水印至响应体base64字段]
E --> F[写入区块链存证服务 Hyperledger Fabric]

组织能力适配的渐进式转型路径

某制造集团采用“双轨制”推进AI工程化:传统IT部门维持原有Jira+Confluence工作流,负责基础设施运维;新成立的AI卓越中心(AICoE)独立使用GitLab CI+MLflow+Prometheus技术栈,但通过标准化API网关(Kong)与主干系统对接。18个月内完成37个预测性维护模型上线,其中21个模型实现从POC到产线部署周期压缩至11天——关键突破在于将模型注册表(Model Registry)与ERP设备台账ID建立双向映射,使工程师可直接在SAP GUI中查看对应电机的健康度衰减曲线。

供应链风险应对的冗余设计原则

当某车企遭遇CUDA 12.2兼容性危机时,其预置的异构推理方案立即生效:核心感知模型在NVIDIA A100集群运行TensorRT优化版本,同时在Intel Gaudi2集群部署相同ONNX模型的Habana SynapseAI编译版本。两套结果经加权融合后输出最终决策,保障L4级自动驾驶系统连续运行217天无降级。该冗余机制已在采购合同中明确要求供应商提供至少两种硬件平台的性能基准报告。

从 Consensus 到容错,持续探索分布式系统的本质。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注