diff --git a/java/README.MD b/java/README.MD
new file mode 100644
index 0000000..d94c924
--- /dev/null
+++ b/java/README.MD
@@ -0,0 +1,13 @@
+# WeChatFerry Java 客户端
+## 配置环境
+选择 32 位的 JDK [temurin](https://adoptium.net/zh-CN/temurin/releases)。
+
+## 重新生成 gRPC 文件
+[gRPC 文件生成工具](https://repo.maven.apache.org/maven2/io/grpc/protoc-gen-grpc-java/)。
+```sh
+cd java/src/main/java/
+protoc --java_out=. -I=../../../../proto wcf.proto
+protoc --grpc-java_out=. --plugin=protoc-gen-grpc-java="C:/Tools/bin/protoc-gen-grpc-java-1.49.2-windows-x86_32.exe" -I=../../../../proto wcf.proto
+```
+
+`C:/Tools/bin` 为工具存放路径。
diff --git a/java/pom.xml b/java/pom.xml
new file mode 100644
index 0000000..1318de0
--- /dev/null
+++ b/java/pom.xml
@@ -0,0 +1,77 @@
+
+
+ 4.0.0
+
+ com.iamteer.demo
+ gRpcDemo
+ 1.0-SNAPSHOT
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ 1.6.2
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 17
+ 17
+
+
+
+ org.xolstice.maven.plugins
+ protobuf-maven-plugin
+ 0.6.1
+
+ com.google.protobuf:protoc:3.21.7:exe:${os.detected.classifier}
+ grpc-java
+ io.grpc:protoc-gen-grpc-java:1.50.0:exe:${os.detected.classifier}
+
+
+
+
+ compile
+ compile-custom
+
+
+
+
+
+
+
+
+ net.java.dev.jna
+ jna
+ 5.3.1
+
+
+ io.grpc
+ grpc-netty-shaded
+ 1.50.0
+ runtime
+
+
+ io.grpc
+ grpc-protobuf
+ 1.50.0
+
+
+ io.grpc
+ grpc-stub
+ 1.50.0
+
+
+ org.apache.tomcat
+ annotations-api
+ 6.0.53
+ provided
+
+
+
+
\ No newline at end of file
diff --git a/java/src/main/java/com/iamteer/wcf/Client.java b/java/src/main/java/com/iamteer/wcf/Client.java
new file mode 100644
index 0000000..666415e
--- /dev/null
+++ b/java/src/main/java/com/iamteer/wcf/Client.java
@@ -0,0 +1,144 @@
+package com.iamteer.wcf;
+
+import java.util.List;
+import java.util.Map;
+import com.sun.jna.Library;
+import com.sun.jna.Native;
+
+import io.grpc.ManagedChannel;
+import io.grpc.stub.StreamObserver;
+import io.grpc.ManagedChannelBuilder;
+
+public class Client {
+ private interface JnaLibrary extends Library {
+ JnaLibrary INSTANCE = Native.load("sdk", JnaLibrary.class);
+
+ void WxInitSDK();
+
+ void WxDestroySDK();
+ }
+
+ public void InitClient(String hostPort) {
+ JnaLibrary.INSTANCE.WxInitSDK();
+ this.connect(hostPort);
+ }
+
+ public void CleanupClient() {
+ this.DisableRecvMsg();
+ JnaLibrary.INSTANCE.WxDestroySDK();
+ }
+
+ private void connect(String hostPort) {
+ ManagedChannel managedChannel = ManagedChannelBuilder.forTarget(hostPort)
+ .usePlaintext()
+ .build();
+ this.wcfBlokingStub = WcfGrpc.newBlockingStub(managedChannel);
+ this.wcfStub = WcfGrpc.newStub(managedChannel);
+ }
+
+ public int IsLogin() {
+ WcfOuterClass.Empty empty = WcfOuterClass.Empty.newBuilder().build();
+ WcfOuterClass.Response response = this.wcfBlokingStub.rpcIsLogin(empty);
+ return response.getStatus();
+ }
+
+ public String GetSelfWxid() {
+ WcfOuterClass.Empty empty = WcfOuterClass.Empty.newBuilder().build();
+ WcfOuterClass.String rsp = this.wcfBlokingStub.rpcGetSelfWxid(empty);
+ return rsp.getStr();
+ }
+
+ public void EnableRecvMsg() {
+ if (isReceivingMsg) {
+ return;
+ }
+
+ isReceivingMsg = true;
+ WcfOuterClass.Empty empty = WcfOuterClass.Empty.newBuilder().build();
+ this.wcfStub.rpcEnableRecvMsg(empty, new StreamObserver() {
+ @Override
+ public void onNext(WcfOuterClass.WxMsg value) {
+ System.out.printf("New Message:\n%s", value);
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ System.err.println("EnableRecvMsg Error");
+ }
+
+ @Override
+ public void onCompleted() {
+ System.out.println("EnableRecvMsg Complete");
+ }
+ });
+ }
+
+ public int DisableRecvMsg() {
+ if (!isReceivingMsg) {
+ return 0;
+ }
+
+ WcfOuterClass.Empty empty = WcfOuterClass.Empty.newBuilder().build();
+ WcfOuterClass.Response response = this.wcfBlokingStub.rpcDisableRecvMsg(empty);
+ if (response.getStatus() == 0) {
+ isReceivingMsg = false;
+ return 0;
+ }
+
+ return -1;
+ }
+
+ public int SendText(String msg, String receiver, String aters) {
+ WcfOuterClass.TextMsg textMsg = WcfOuterClass.TextMsg.newBuilder().setMsg(msg).setReceiver(receiver)
+ .setAters(aters).build();
+ WcfOuterClass.Response response = this.wcfBlokingStub.rpcSendTextMsg(textMsg);
+ return response.getStatus();
+ }
+
+ public int SendImage(String path, String receiver) {
+ WcfOuterClass.ImageMsg imageMsg = WcfOuterClass.ImageMsg.newBuilder().setPath(path).setReceiver(receiver)
+ .build();
+ WcfOuterClass.Response response = this.wcfBlokingStub.rpcSendImageMsg(imageMsg);
+ return response.getStatus();
+ }
+
+ public Map GetMsgTypes() {
+ WcfOuterClass.Empty empty = WcfOuterClass.Empty.newBuilder().build();
+ WcfOuterClass.MsgTypes msgTypes = this.wcfBlokingStub.rpcGetMsgTypes(empty);
+ return msgTypes.getTypesMap();
+ }
+
+ public List GetContacts() {
+ WcfOuterClass.Empty empty = WcfOuterClass.Empty.newBuilder().build();
+ WcfOuterClass.Contacts contacts = this.wcfBlokingStub.rpcGetContacts(empty);
+ return contacts.getContactsList();
+ }
+
+ public List GetDbs() {
+ WcfOuterClass.Empty empty = WcfOuterClass.Empty.newBuilder().build();
+ WcfOuterClass.DbNames dbs = this.wcfBlokingStub.rpcGetDbNames(empty);
+ return dbs.getNamesList();
+ }
+
+ public List GetTables(String db) {
+ WcfOuterClass.String str = WcfOuterClass.String.newBuilder().setStr(db).build();
+ WcfOuterClass.DbTables tables = this.wcfBlokingStub.rpcGetDbTables(str);
+ return tables.getTablesList();
+ }
+
+ public List QuerySql(String db, String sql) {
+ WcfOuterClass.DbQuery query = WcfOuterClass.DbQuery.newBuilder().setDb(db).setSql(sql).build();
+ WcfOuterClass.DbRows rows = this.wcfBlokingStub.rpcExecDbQuery(query);
+ return rows.getRowsList();
+ }
+
+ public int AcceptNewFriend(String v3, String v4) {
+ WcfOuterClass.Verification v = WcfOuterClass.Verification.newBuilder().setV3(v3).setV4(v4).build();
+ WcfOuterClass.Response response = this.wcfBlokingStub.rpcAcceptNewFriend(v);
+ return response.getStatus();
+ }
+
+ private boolean isReceivingMsg = false;
+ private WcfGrpc.WcfBlockingStub wcfBlokingStub;
+ private WcfGrpc.WcfStub wcfStub;
+}
diff --git a/java/src/main/java/com/iamteer/wcf/Main.java b/java/src/main/java/com/iamteer/wcf/Main.java
new file mode 100644
index 0000000..4b939be
--- /dev/null
+++ b/java/src/main/java/com/iamteer/wcf/Main.java
@@ -0,0 +1,64 @@
+package com.iamteer.wcf;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Iterator;
+
+public class Main {
+ public static void main(String[] args) throws Exception {
+ String hostPort = "localhost:10086";
+ Client client = new Client();
+ client.InitClient(hostPort);
+ System.out.println("Connecting to " + hostPort);
+
+ int status = client.IsLogin();
+ System.out.println(status);
+
+ String wxid = client.GetSelfWxid();
+ System.out.println(wxid);
+
+ client.EnableRecvMsg(); // Receive Message
+
+ Map msgTypes = client.GetMsgTypes();
+ Iterator> iterTypes = msgTypes.entrySet().iterator();
+ while (iterTypes.hasNext()) {
+ Map.Entry entry = iterTypes.next();
+ System.out.println(entry.getKey() + ": " + entry.getValue());
+ }
+
+ List contacts = client.GetContacts();
+ Iterator iterContacts = contacts.iterator();
+ while (iterContacts.hasNext()) {
+ WcfOuterClass.Contact contact = iterContacts.next();
+ System.out.println(contact);
+ }
+
+ List dbs = client.GetDbs();
+ Iterator iterDbs = dbs.iterator();
+ while (iterDbs.hasNext()) {
+ String db = iterDbs.next();
+ System.out.println(db);
+ }
+
+ List tables = client.GetTables("MicroMsg.db");
+ Iterator iterTables = tables.iterator();
+ while (iterTables.hasNext()) {
+ WcfOuterClass.DbTable table = iterTables.next();
+ System.out.println(table);
+ }
+
+ List rows = client.QuerySql("MicroMsg.db", "SELECT * FROM Contact LIMIT 1;");
+ Iterator iterRows = rows.iterator();
+ while (iterRows.hasNext()) {
+ WcfOuterClass.DbRow row = iterRows.next();
+ System.out.println(row);
+ }
+
+ // status = client.AcceptNewFriend("v3", "v4"); // 需要真实的数据
+ // System.out.println(status);
+
+ Thread.sleep(1000);
+ client.CleanupClient();
+ }
+
+}
diff --git a/proto/wcf.proto b/proto/wcf.proto
index b327bf8..b2c2ed7 100644
--- a/proto/wcf.proto
+++ b/proto/wcf.proto
@@ -1,6 +1,7 @@
syntax = "proto3";
package wcf;
+option java_package = "com.iamteer.wcf";
service Wcf {
rpc RpcIsLogin(Empty) returns (Response) {}