转转质检桌面应用程序的架构演进

转转质检桌面应用程序的架构演进_第1张图片

质检是转转履约体系中的重要一环,通过对手机、平板、笔记本、耳机、手表等品类商品的软硬件功能、外观成色等进行全方面检测,为买家、卖家把好质量关,让二手交易变得更透明、更靠谱,促进绿色消费。

在质检环节中,通过标准化产线,结合自动化设备、质检 APP、桌面应用程序等,最终输出全面可信的“质检报告”呈现给用户。其中,桌面应用程序发挥着举足轻重的作用,本文将重点介绍桌面应用程序架构的演进及其落地。

1 背景

转转质检的桌面应用程序,前期主要由 Qt 构建,C/C++提供底层支持。这些桌面应用的视图层、应用层、以及底层能力支持,均由 C/C++开发人员承担全部开发迭代工作。其次随着业务的不断发展,部分桌面应用程序逐渐暴露出拓展性差、迭代难的问题。

在转转质检技术团队,除了 C/C++开发同学外,还有配套成熟的前端和 Java 后端,综合来说:对于视图层,前端的技术生态、以及开发人员的技术经验,在视图层开发方面具有很大的优势;在应用层方面,Java 技术生态的优势不言而喻,同时 Java 后端同学对整体业务和系统都有着相对全面深入的了解,简而言之,Java 同学在应用层架构设计和落地方面,是非常合适的。

综上,基于团队实际,笔者团队对桌面应用程序,提出了新的技术架构——EJC(Electron、Java、C/C++)。该架构的主要优势在于:

  • 让 C/C++开发同学更偏向于底层能力的研究,发挥更大的价值。
  • Electron 本质是前端技术栈,Java 同学更理解整体业务,在应用层设计经验方面更擅长;且前端和 Java 资源更容易灵活调配。
  • Electron 和 Java 本身是跨端的,对于后续质检桌面应用各端的整合(Windows&Mac),具有不错的优势。

2 EJC 架构

简单说,EJC 技术架构即:Electron(视图层/用户层),Java(应用层),C/C++(基础能力层)。

转转质检桌面应用程序的架构演进_第2张图片

2.1 Electron

Electron 是一个基于 Chromium 和 Node.js 的框架,一套多端生成 Windows、macOS 和 Linux 的跨平台桌面应用程序。

  • 本质是前端技术栈,内置了 Chromium 内核使得应用程序具有最新的 web 标准,开发人员可以专注应用程序的逻辑和界面设计,不用再束手束足做浏览器兼容性操作。
  • 整包更新和热更新,使程序保持最新的状态,类似混合移动应用(Hybrid APP)的快感。
  • 安全的跨平台运行环境,可以有效地降低程序崩溃和系统错误的机率,实现更加可靠和稳定的应用程序。
  • 丰富的 API 在 C/C++能力的加持下,让软硬件结合更加丝滑,扩展能力更进一步。

2.2 Java

Java 应用层,主要包括:

  • 通讯模块:提供基于 HTTP、WebSocket 等协议的通信能力。
  • 底层交互模块: 封装了 Java 调用本地代码(动态库)的技术(JNI/JNA)。
  • 数据存储:使用轻量级数据库 SQLite 来持久化数据,为数据的高可用及容错提供基础能力。
  • 事件监听:基于 Spring 的事件和监听机制实现了基于事件驱动的编程模型,有松耦合、高扩展性和可测试性等优点。
  • 业务模块:必要的业务逻辑处理。
  • 监控模块:对客户端的实时运行参数、硬件异常情况等数据进行记录,定时上报云端。
  • 配置管理:定时从云端拉取最新的配置,覆盖本地配置。
  • 调度策略:根据设备的历史状态,预判硬件(如货架货位上的 USB 通讯口、USB 集线器等设备)是否出现故障,当判定故障时,可以优先调度其它设备并将故障信息上报。

2.3 C/C++

基础能力层,核心 SDK 的实现。提供与 Windows、IOS、安卓、相机、机械臂等底层通用能力。

基于上述说明,在转转质检中,笔者呈现的 EJC 技术架构,如下:

转转质检桌面应用程序的架构演进_第3张图片

下面重点对 Java 应用层的前端通讯模块、底层通讯模块进行介绍。

2.4 前端通讯模块

在 Java 应用层兼容了 HTTP 协议、WebSocket 协议的通信方式。接下来介绍几种与前端通讯的方案以及我们在 EJC 架构中的选型和考量:

2.4.1 HTTP 短轮询

客户端周期性的向服务器发送请求,以获得最新的数据,这种方式会造成服务器和网络资源的浪费。适用于实时性要求不高的场景: Java 客户端从云端拉取配置时,采用的就是此机制。

转转质检桌面应用程序的架构演进_第4张图片

2.4.2 HTTP 长轮询

与 HTTP 短轮询相比,HTTP 长轮询能够避免客户端频繁向服务器发送请求,节省了网络和服务器资源的开销,同时能实现更及时和可靠的数据推送。

转转质检桌面应用程序的架构演进_第5张图片

2.4.3 SSE(Server-Sent Events)

本质上是一个 HTTP 长连接,服务端发送给客户端不是一个数据包,而是一个 stream 流,格式为 text/stream,所以客户端不会关闭连接,会一直等着服务器发过来新的数据流。适合一些只需要服务端单向推送事件给客户端的场景。

转转质检桌面应用程序的架构演进_第6张图片

 在实际的应用场景中,服务端只需要推送一次信息给前端(如:前端调用 Java 服务端获取系统硬件配置信息),我们选择 SSE 作为前后端的通信方式,有以下优势:

  • 比 http 短轮询性能更好。
  • 比 http 长轮询更可靠。
  • 比 WebSocket 更轻量。
  • 可以在现有的基础设施和技术上使用,而不需要进行任何额外的配置或部署。

2.4.4 WebSocket

WebSocket 是基于 TCP 的双向通信协议,可以实现实时通信。适合实时性要求很高的而且需要双工通信的系统。在实际的应用中,如隐私清除工具,从插入手机到隐私清除完成只需要 3~5 秒,质检人员需要实时的看到手机状态的变更,这时候我们选用 Websocket 实时的将数据的状态推送到前端进行展示。

转转质检桌面应用程序的架构演进_第7张图片

2.5 底层通讯模块

Java 调用 C/C++ 有 JNI (Java Native Interface) 与 JNA (Java Native Access) 两种方式,都是 Java 中用于调用本地底层 SDK 的技术。

下面通过简单的代码示例(获取 IOS 设备名称)来说明 Java 是如何调用底层 SDK 的。为了节约篇幅,仅展示了部分关键代码。

2.5.1 JNI 介绍和使用

Java 语言提供的标准接口,它提供了一组函数和数据类型,允许 Java 应用程序调用和被 C/C++ 语言调用。JNI 通过编写本地方法实现与 C/C++ 语言的交互。

  • 使用 native 关键字声明本地方法。
public class JniDemo {
    /**
     * 获取IOS设备的名称
     * @param udid 设备UDID
     * @return 设备名称
     */
    public native String getDeviceNameByUDID(String udid);
}
复制代码
  • 通过 javah 命令,将代码中的 native 方法生成对应的 C 语言的头文件。
> javah JniDemo
// JDK10+已经移除了javah命令工具,使用以下命令
> javac JniDemo.java -h outputDir
复制代码
  • 执行上述命令后,将生成一个名为 JniDemo.h 的 C/C++ 头文件。
#include 
/* Header for class JniDemo */

#ifndef _Included_JniDemo
#define _Included_JniDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * 包含了 getDeviceNameByUDID 方法的声明
 * Class:     JniDemo
 * Method:    getDeviceNameByUDID
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_JniDemo_getDeviceNameByUDID
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif
复制代码
  • C/C++ 实现头文件来实现 Java_JniDemo_getDeviceNameByUDID 方法,并将其编译为动态库。
#include "jnidemo.h"

JNIEXPORT jstring JNICALL Java_JniDemo_getDeviceNameByUDID(JNIEnv *env, jobject, jstring udid)
{
    string udid_cpp = jstringTostring(env, udid);
    LHW_INFO("udid_cpp = " << udid_cpp);
    IOS_Device_Interface idi;
    string device_name = idi.get_device_name_by_udid(udid_cpp);
    LHW_INFO("device_name = " << device_name);
    return stringTojstring(env, device_name.c_str());
}
复制代码
  • Java 使用
public class JniDemo {
    /**
     * 获取IOS设备的名称
     * @param udid 设备UDID
     * @return 设备名称
     */
    public native String getDeviceNameByUDID(String udid);

    public static void main(String[] args) {
        System.loadLibrary("jniDemo");
        JniDemo obj = new JniDemo();
        String result = obj.getDeviceNameByUDID("00008110-001518392EE3801E");
        System.out.println("Result is " + result);
        // Result is iphone 13 pro
    }
}
复制代码

2.5.2 JNA 介绍和使用

JNA 是在 JNI 基础上实现的编程框架,实现了 Java 类型到 C 类型的自动转换。Java 开发人员只要在一个 Java 接口中描述目标 native library 的函数与结构,不再需要编写任何 Native/JNI 代码,极大的降低了 Java 调用动态库的开发难度。

  • 编写 C/C++代码,声明头文件(需要使用 extern “C”关键字才能被 JNA 调用)。
#pragma once
#include "pch.h"

#ifndef JNADemoAPI
#define JNADemoAPI __declspec(dllexport)
#endif // !_Included_JnaDemo

#ifdef __cplusplus
extern "C" {
#endif // __cplusplus

    JNADemoAPI const char* getDeviceNameByUDID(const char *udid);


#ifdef __cplusplus
}
#endif // __cplusplus

复制代码
  • 实现头文件,并将其编译为动态库。
#include "pch.h"
#include "JnaDemo.h"
#include "IOSDevice/ios_device_pimpl.h"

string IOS_Device_Interface::getDeviceNameByUDID(string udid) {
    return ios_device->get_deviceName_by_udid(udid);
}
复制代码
  • Java 中使用。

首先在项目中引入 JNA 库:


  com.sun.jna
  jna
  5.12.1

复制代码

声明与动态库对应的 Java 接口类:

/**
 *  定义动态库接口
 */
public interface JnaDemo extends Library {
    /**
     * 与 C/C++ 中的函数名对应
     * @param 设备UDID
     * @return 设备名称
     */
    String getDeviceNameByUDID(String udid);
}
复制代码

加载动态库并调用方法:


/**
 * 通过 JNA 调用 C/C++ 函数
 *
 */
public class JnaDemoTest {

    public static void main(String[] args) {
        // 加载名为 jnaDemo 动态库
        JnaDemo jnaDemo = Native.load("JnaDemo", JnaDemo.class);
        // 调用方法并获取结果
        String result = jnaDemo.getDeviceNameByUDID("00008110-001518392EE3801E");
        System.out.println("Result is " + result);
        // Result is iphone 13 pro
    }
}
复制代码

2.5.3 选型和考量

通过上述的示例代码我们对比了两种方案的优缺点,并进行了性能测试。

JNI JNA
优点 本地方法编译后可以选择 C 或 C++ 来实现。 调用本地方法时效率相对 JNA 高。 封装了系统常用的动态库,可以直接使用。开发效率相对 JNI 高,无需 Java 编写本地方法。
缺点 开发效率相对较低,需要 Java 编写本地方法并编译生成 C/C++头文件,C/C++ 需要按照生成的头文件进行编码实现。 不支持 C++编译生成的动态库,需要在 C++ 接口的上层用 C 语言进行一次封装。

从开发者的角度来说:JNA 对 Java 开发者比较友好,JNI 则对 C/C++开发者比较友好。

同时我们分别用 JNI 和 JNA 进行 100 次到 500 次读取 IOS 设备名称的性能测试,得到耗时对比。在 8 核 16G 机器上运行得到如下结果:

计算数量(百次) JNI JNA
1 1197ms 26957ms
2 2196ms 52800ms
3 2759ms 79260ms
4 4573ms 106377ms
5 6299ms 132482ms

通过上面的对比和性能测试,我们制定了如下选型标准:

  • 自研的 SDK:高优使用 JNI 作为底层通信方式。优势在于:JNI 的性能更好,底层数据交互的接口由 Java 定义,C/C++开发者可以选择 C 或 C++进行实现,有更多的选择性及灵活性。
  • 外部厂商提供的 SDK:优先调用厂商自带的 SDK。优势在于:无需 C/C++再次封装一层动态库,减少开发资源的投入。

3 EJC 架构的落地

EJC 架构在转转质检已成功落地了多个应用,下面主要介绍 EJC 在 Windows 笔记本质检工具中的落地。

3.1 项目背景

随着质检业务的发展,笔记本质检量再创新高。早期由 C/C++开发、使用 Qt 构建的笔记本验机工具已不能满足业务的需求,主要体现有以下几点:

  • 维护成本高:代码的复杂性较高,维护需要开发者投入更多的时间和精力。
  • 覆盖率低:功能不够完善,易用性较差,使用覆盖率低。
  • 移植性差:无法移植到 Mac 平台。

基于上述的项目背景,我们使用了 EJC 架构来重构笔记本验机工具。

3.2 架构实现

转转质检桌面应用程序的架构演进_第8张图片

3.2.1 名词解释

  • WMI:Windows Management Instrumentation;是 Windows 系统标准的信息服务。
  • WinAPI:Windows 系统提供的底层接口。
  • DLL(C):即 EJC 中的 C,由我们 C/C++的同学研发的底层 SDK。

3.2.2 流程描述

  • 录入获取数据流程:Electron 启动后 -> 后台异步启动 Java 服务端 -> 开启异步全局扫描笔记本基本数据项注解 -> 获得需读取的电脑属性 -> 调度器分类执行属性获取命令 -> 执行获取命令执行链(可横向扩展获取方式)-> 数据加工 -> 数据纠错 -> 经 SSE 通道推送页面渲染。
  • 质检流程辅助流程:进入质检流程 -> 请求质检项辅助(某个功能,例如指纹是否正常)-> Java 调用底层(Dll、Wmi 等其他方式)-> 返回辅助质检结果 -> 页面回传渲染质检项支持结果。

3.2.3 流程释义

  • 执行链:考虑到电脑某项属性需要多种方式获取并互相就纠正,因此可以针对性的配置其特有的执行链,以达到更好的读取准确率,也更加方便扩展。
  • 数据纠错:某些属性比如电池健康值;默认采取 WMI 读取;但是部分厂商没有按照 WMI 的标准写值,导致获取为空;因此需要调取 DLL(C)的其他获取方式作为补充。

3.3 项目呈现

3.3.1 录入模块

通过 Java、C/C++读取笔记本关键信息,获取笔记本基本情况。通过录入功能,辅助一线人员选择系统标品项,同时与质检码进行关联入库。在此基础上产生原始信息与标品 ID 的映射关系,减少下次相同机型的一个操作步骤,方便一线操作人员在质检相同机型的一个操作便携性。

转转质检桌面应用程序的架构演进_第9张图片

3.3.2 质检模块

通过品牌机型获取系统对应的质检模版,提供自动&辅助质检能力,协助一线质检人员对笔记本的质检能力更快捷、精准。 

转转质检桌面应用程序的架构演进_第10张图片

4 总结

本文对转转质检的 EJC 架构做了一些分享,并给出一些实践经验,希望能为大家解决类似的问题提供一些帮助。目前 EJC 架构体系已经在质检业务中上线了多个桌面应用并稳定运行,未来将会覆盖更多的应用场景,助力业务得到实质发展

你可能感兴趣的:(架构,java,开发语言)