需求
设备会产生一个 cex 后缀的文件,cex 文件经过特殊的加密处理,不能像之前的 txt、dat、xml、json 文件直接解析的方式去获取数据,必须通过设备供应商提供的函数去获取 cex 文件的箱号、通道号、下位机、充放电容量等数据。
而且,设备产生的 cex 文件是增量文件(设备更新数据只是在原有 cex 文件基础上追加数据,跟 MySQL 的写日志那种类似)
设备供应商提供了《数据接口Dll使用说明》、LANDCex_x64.dll、LANHE.sys、CexConst.h 等核心文件,还提供了 VS2015 和 ExcelVBA 使用接口的案例
过程
之前本来不是这么麻烦的,只是做一个接口,去接收设备供应商传过来的 json 数据。但是,接口开发完了,才知道要收费,为了省钱,只能让我去“卖艺”😭 😭 😭
先理一下整体的思考方向:
- 获取 cex 文件中之前需要传递 json 数据,包括箱号、通道号、下位机、充放电容量等
- 改造下之前已经写好但是废弃的接口(毕竟逻辑还在那里可以用)
- ……
仔细阅读设备供应商提供的《数据接口Dll使用说明》
首先是数据:了解这种数据的逻辑结构,对于准确理解和使用输出函数非常重要
测试数据在逻辑上可以理解为由“t_cycle”、“t_step”和“t_rec”三张数据表相互交错而行成的一个复合数据表。一个“cycle”包含多个“step步”,而一个“工step”又包含多个“rec”
所以要定位数据,需要确定的是:数据在哪张表、行号、列号。设备供应商为了简化接口,将三张表所有数据的列进行了统一的编号 columnID 来唯一标识,这样定位数据就不需要确定是哪张表了,这个 columnID 记录在 CexConst.h 头文件
然后就是一堆的函数接口及使用说明,并且给出了基本的调用流程:
- 检查 dll 版本:
CheckDllVersion()
- 加载 cex 数据到内存中:
LoadData()
- 操作数据:
GetRows()
、GetDataEx()
、GetDataAsFloat()
- 是否数据:
FreeData()
那么,Java 怎么调用 dll 的函数呢?简单百度了解到,主要有 JNative、JNI、JNA 等
最后选用好用的 JNA:
-
导入 JNA 依赖
<!-- JNA --> <dependency> <groupId>net.java.dev.jna</groupId> <artifactId>jna</artifactId> <version>5.0.9</version> </dependency>
-
创建 dll 接口
public interface DllUtil extends Library { DllUtil instance = (DllUtil) Native.loadLibrary("Cex_x64", DllUtil.class); /** * 获取 DLL 的接口版本号 * @return */ Long GetDllVersion(); }
-
调用 dll 接口
DllUtil.instance.GetDllVersion()
探知
- dll:Dynamic Link Library,动态链接库,windows 中许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,放置于系统。当我们执行某一个程序时,相应的 dll 文件就会被调用
- JNA:(Java Native Access )提供一组 Java 工具类用于在运行期间动态访问系统本地库(Native Library:Dll)而不需要编写任何 Native/JNI 代码。我们只要在一个 Java 接口中描述目标 Native Library 的函数与结构,JNA 将自动实现 Java 接口到 Native Function 的映射。JNA 建立在 JNI 的基础之上的一个框架,比 JNI 更好用
参考博文:
实践
前期准备
-
引入依赖
<dependency> <groupId>net.java.dev.jna</groupId> <artifactId>jna</artifactId> <version>5.9.0<ersion> </dependency>
-
存放 Dll
参考很多资料,有的说是放项目路径下,有的说放 JDK 的 bin 目录中,其实都可以,参考 Native.loadLibrary 源码其实这些路径都是会去找的,这里把 Dll 存放在项目 resources 路径,使用 Native.loadLibrary 方式加载(要确认 Dll 在 64 位还是 32 位机使用,并选择对应版本)
添加 dll 后要重启系统,我之前没有重启调用 load 方法一直 load 不了,后来看了哪篇博文说是要重启下才能加载 dll
参考
Native.loadLibrary
源码,会去获取 dll 接口类的类加载器,然后通过类加载器和获取的 handler 生成一个 Library 的代理对象,最后返回这个代理对象public final class Native { public static Object loadLibrary(String name, Class interfaceClass, Map options) { Handler handler = new Handler(name, interfaceClass, options); ClassLoader loader = interfaceClass.getClassLoader(); Library proxy = (Library)Proxy.newProxyInstance(loader, new Class[]{interfaceClass}, handler); cacheOptions(interfaceClass, options, proxy); return proxy; } }
如使用的那样
Native.loadLibrary("Cex_x64", DllUtil.class)
,第一个参数传的是 dll 不带后缀的名称,即参数 name ,这个 name 在 loadLibrary 中用作new Handler
,看到new Handler
的部分,主要追踪参数 libname,先是对 libname 做了合法校验,经过处理填充 options,通过NativeLibrary.getInstance
得到本地 Library 的实例public final class Native { public static class Handler implements InvocationHandler { public Handler(String libname, Class interfaceClass, Map options) { if (libname != null && "".equals(libname.trim())) { throw new IllegalArgumentException("Invalid library name \"" + libname + "\""); } else { this.interfaceClass = interfaceClass; Map options = new HashMap(options); int callingConvention = AltCallingConvention.class.isAssignableFrom(interfaceClass)?1:0; if (options.get("calling-convention") == null) { options.put("calling-convention", new Integer(callingConvention)); } this.options = options; this.nativeLibrary = NativeLibrary.getInstance(libname, options); this.functionMapper = (FunctionMapper)options.get("function-mapper"); if (this.functionMapper == null) { this.functionMapper = new Library.Handler.FunctionNameMap(options); } this.invocationMapper = (InvocationMapper)options.get("invocation-mapper"); } } } }
A.isAssignableFrom(B)
确定一个类(B)是不是继承来自于另一个父类(A),一个接口(A)是不是实现了另外一个接口(B),或者两个类相同。主要,这里比较的维度不是实例对象,而是类本身其实再追踪
NativeLibrary.getInstance
会发现,先会根据 libraries 得到一个弱引用 WeakReference 的 ref 尝试去获取 library,如果是首次加载的话,会走到 library == null 的 if 里面,这里就是为 libraries 填充信息,看到libraries.put(file.getAbsolutePath() + options, ref);
往 libraries 填充library.getFile()
的绝对路径。但是看到这个也不太对啊,我本来是要找 dll 的路径,系统如果有多个同名的 dll ……public class NativeLibrary { private static final Map libraries = new HashMap(); public static final NativeLibrary getInstance(String libraryName, Map options) { Map options = new HashMap(options); if (options.get("calling-convention") == null) { options.put("calling-convention", new Integer(0)); } if (Platform.isLinux() && "c".equals(libraryName)) { libraryName = null; } synchronized(libraries) { WeakReference ref = (WeakReference)libraries.get(libraryName + options); NativeLibrary library = ref != null ? (NativeLibrary)ref.get() : null; if (library == null) { if (libraryName == null) { library = new NativeLibrary("<process>", (String)null, Native.open((String)null), options); } else { library = loadLibrary(libraryName, options); } ref = new WeakReference(library); libraries.put(library.getName() + options, ref); File file = library.getFile(); if (file != null) { libraries.put(file.getAbsolutePath() + options, ref); libraries.put(file.getName() + options, ref); } } return library; } } }
所以,我肯定是看漏了那里,慢慢往上找,回到 library == null 的 if 里面,还要判断 libraryName == null,libraryName,libraryName 是我们传进来的 dll 名称,肯定不是 null,所以会调用 loadLibrary(libraryName, options) 补充 library
跳转到 NativeLibrary.loadLibrary 方法,看到很多路径,我就感觉有戏
先设置了 searchPath 作为搜索到的路径
搜索可能的路径
- 是不是 web 环境:Native.getWebStartLibraryPath(libraryName)
- customPaths 这里没看懂啥意思
- 将 "jna.library.path" 所有路径加载到 searchPath,去找有无 libraryName 的路径
查找 searchPath 中 libraryName 的路径
String libraryPath = findLibraryPath(libraryName, searchPath);
尝试各个系统下这个 libraryPath 是否存在
注意:这里的 handle 在 C++ 函数转 Java 接口有用
long handle = 0L; try { handle = Native.open(libraryPath); } catch (UnsatisfiedLinkError var13) { searchPath.addAll(librarySearchPath); }
public class NativeLibrary { private static NativeLibrary loadLibrary(String libraryName, Map options) { List searchPath = new LinkedList(); String webstartPath = Native.getWebStartLibraryPath(libraryName); if (webstartPath != null) { searchPath.add(webstartPath); } List customPaths = (List)searchPaths.get(libraryName); if (customPaths != null) { synchronized(customPaths) { searchPath.addAll(0, customPaths); } } searchPath.addAll(initPaths("jna.library.path")); String libraryPath = findLibraryPath(libraryName, searchPath); long handle = 0L; try { handle = Native.open(libraryPath); } catch (UnsatisfiedLinkError var13) { searchPath.addAll(librarySearchPath); } try { if (handle == 0L) { libraryPath = findLibraryPath(libraryName, searchPath); handle = Native.open(libraryPath); if (handle == 0L) { throw new UnsatisfiedLinkError("Failed to load library '" + libraryName + "'"); } } } catch (UnsatisfiedLinkError var15) { UnsatisfiedLinkError e = var15; if (Platform.isLinux()) { libraryPath = matchLibrary(libraryName, searchPath); if (libraryPath != null) { try { handle = Native.open(libraryPath); } catch (UnsatisfiedLinkError var12) { e = var12; } } } else if (Platform.isMac() && !libraryName.endsWith(".dylib")) { libraryPath = "/System/Library/Frameworks/" + libraryName + ".framework/" + libraryName; if ((new File(libraryPath)).exists()) { try { handle = Native.open(libraryPath); } catch (UnsatisfiedLinkError var11) { e = var11; } } } else if (Platform.isWindows()) { libraryPath = findLibraryPath("lib" + libraryName, searchPath); try { handle = Native.open(libraryPath); } catch (UnsatisfiedLinkError var10) { e = var10; } } if (handle == 0L) { throw new UnsatisfiedLinkError("Unable to load library '" + libraryName + "': " + e.getMessage()); } } return new NativeLibrary(libraryName, libraryPath, handle, options); } }
创建 Java 接口
其实这些个接口就是 native 修饰的接口,想想也是,我现在做的事不就是 Java 调用 C++ 的函数嘛,相比于 JNA 来说 JNI 更适合说明这个问题。
native:
- 一个 native 方法就是一个 Java 调用非 Java 代码的接口。即该方法实现非 Java,比如用 C 或 C++
- 定义一个 native 方法时,并不提供具体实现,因为本来就由 C 或 C++ 实现,Java 只是“声明”
通过 JNI,我们就可以通过 Java 调用到操作系统相关的技术实现的库函数,从而与其他技术和系统交互,使用其他技术实现的系统的功能,实现不同编程语言的相互调用
在 windows 系统上,可执行的应用程序是基于 native 的 PE 结构,JVM 也是基于 native 结构实现。Java 应用体系构建于 JVM 之上

这里也引发了相关的思考:Java 跨平台主要依靠 JVM,那么使用 JNI 不就使得程序破坏了跨平台的特性了吗?
接口名称
接口的方法名称与 Dll 提供函数名称一致,包括大小写:C/C++ 提供什么就是什么,如,Dll 提供 CheckDllVersion() 函数,那么在接口中也必须声明的是 CheckDllVersion()
接口参数
参数的名称无所谓的,主要是返回参数很折磨人。
Java 是值传递的,没有指针的概念,但是 C/C++ 有指针,如,bool GetDescriptionOfDownId(VARIANT& vDesc, DWORD dwDownId, bool bDotSet=true),函数作用是通过 dwDownId 返回描述信息 vDesc。这里就是用到的指针,好在 JNA 中引入了 Pointer 类,用来表示 native 方法中的指针。
当然 C/C++ 中对结构体使用得很是频繁,JNA 中也引入了 Structure 类与之对应。C++ 中的结构体类似于 Java 中的实体类,但在 JNA 中 Java 实体类的每个属性都要与 C++ 结构体的每一个字段对应
// C++ 结构体
typedef struct _testStruct {
char id[18];
char name[48];
int intValue;
float floatValue;
long longValue;
} testStruct_t;
// Java 类
public class TestStruct extends Structure {
public byte[] id = new byte[18]; // 长度也是需要对应的,否则数据会发生错乱
public byte[] name = new byte[48];
public int intValue;
public float floatValue;
public NativeLong longValue;
@Override
protected List<String> getFieldOrder() {
return Arrays.asList(new String[] {
"id", "name", "intValue", "floatValue", "longValue"
});
}
public static class ByReference extends TestStruct implements Structure.ByReference {}
public static class ByValue extends TestStruct implements Structure.ByValue {}
}
数据类型
-
基本数据类型
-
指针类型
用于返回数据的参数不能使用基本数据类型,而是要用到 com.sun.jna.ptr 包下的 ByReference 的具体实现
- ByteByReference
- IntByReference
- LongByReference
- DoubleByReference
- FloatByReference、
- ativeLongByReference
- ShortByReference
- PointerByReference
JNA 中用 Point 映射 C++ 指针,一般情况下一级指针用 Point(msg.getInt(0),0表示偏移量从0开始),二级指针用 PointerByRefrence 比较方便, Pointer 也可以用二级指针(msg.getPointer(0).getString(0),表示指针的指针再取值;多级指针以此类推)
C++ :void GetListTest(char**name, int* number, long* ier) Java:void GetListTest(PointerByReference name, IntByReference number, LongByReference ier);
char** 是二级指针,JNA 中用 PointerByReference 映射;int* 是一级指针,JNA 中用 IntByReference,如果这个一级指针数据类型是 long,则用 PointerByReference
接口示例
public interface DllUtil extends Library {
/** 加载 Dll */
DllUtil instance = (DllUtil) Native.loadLibrary("DLL_NAME", DllUtil.class);
/** 获取 DLL 的接口版本号 */
// DWORD __stdcall GetDllVersion (void)
Long GetDllVersion();
/** 验证 DLL 的接口版本的兼容性 */
// bool __stdcall CheckDllVersion (DWORD dwVerRequested)
Boolean CheckDllVersion(Long dwVerRequested);
/** 将测试数据文件的详细数据调入内存 */
// HANDLE __stdcall LoadData (const char*pszPath, int nHopeUnitScheme=USCH_BaseOn_Auto)
Long LoadData(String pszPath, Long nHopeUnitScheme);
/** 释放由 LoadData 调入内存中的详细数据 */
// void __stdcall FreeData (HANDLE hDataObj)
void FreeData(Long hBriefInfo);
/** 获取测试开始时间 */
// __time32_t __stdcall GetStartTime (HANDLE hBriefInfoOrDataObj)
Long GetStartTime(Long hBriefInfo);
/** 获取概要信息中的测试通道信息 */
// bool __stdcall GetChannel (HANDLE hBriefInfoOrDataObj, BYTE& cBoxNo, BYTE &cChl, DWORD& dwDownId, WORD& wDownVer)
Boolean GetChannel(Long hBriefInfo, ByteByReference cBoxNo, ByteByReference cChl, LongByReference dwDownId, LongByReference wDownVer);
/** 根据 下位机ID 获取描述信息 */
// bool GetDescriptionOfDownId(VARIANT& vDesc, DWORD dwDownId, bool bDotSet=true)
Boolean GetDescriptionOfDownId(Pointer vDesc, Long dwDownId, Boolean bDotSept);
/** 获取“列”ID 的文字描述 */
// bool __stdcall GetDescriptionOfColumn (UINT columnID, VARIANT& vDesc)
Boolean GetDescriptionOfColumn(Long columnID, Pointer vDesc);
/** 获取指定数据列的行数,即数据的个数 */
// int __stdcall GetRows (HANDLE hDataObj, UINT columnID)
int GetRows(Long hBriefInfo, Long columnID);
/** 只能用于访问 float 类型(即 VT_R4 型)的数据,如电压、电流等 */
// float __stdcall GetDataAsFloat(HANDLE hDataObj, UINT columnID, int nRow)
Float GetDataAsFloat(Long hBriefInfo, Long columnID, Long nRow);
}
注意:Dll 函数接口文档声明了很多种类型,如 HANDLE、UINT、VARIANT 几个用的比较频繁的类型说说
- HANDLE:句柄,对应 Java 的 Long 类型,句柄的含义参考:什么是句柄(handle)
- UNIT:对应于 32 位无符号整数
- VARIANT:特殊的数据类型,没有类型声明字符,能够在运行期间动态的改变类型,支持所有简单数据类型,如整型、浮点、字符串、布尔型、日期时间、货币等
调用 Java 接口
理清楚 JNA 使用规则,就可以编写业务代码了,就是按照文档提供的函数去操作 cex 文件,获取需要的数据
还有一个问题就是,设备产生的 cex 文件是增量文件需要处理,还是先理个思路:
写个定时任务监听存放 cex 路径下的 cex 文件,如果文件有“变化”就操作变化的部分,这里的变化就是增量的记录,使用 ThreadLocal 存储上一次读取的行数 oldRows,如果这次的行数 newRows 相较于 ThreadLocal 存储的行数 oldRows 大,那么就操作 oldRows 到 newRows 记录
public class DllDemo {
public static void main(String[] args) throws UnsupportedEncodingException {
System.out.println("=================================================");
System.out.println(DllUtils.instance.toString());
System.out.println("=================================================");
// 1. 校验 Dll 版本
Long dllVersion = DllUtils.instance.GetDllVersion();
System.out.println("当前 Dll 版本: 0x000" + Long.toHexString(dllVersion));
if (!DllUtils.instance.CheckDllVersion(DllConstant.LANDCEX_DLL_VER_0_3_1_0)) {
System.out.println("Dll版本不匹配);
}
// 2. 获取 cex 文件全路径
String cexPath = "C:\\Users\\Kangyh\\Desktop\\-D221018_017_6.cex";
// 3. 载入数据
Long loadData = DllUtils.instance.LoadData(cexPath, DllConstant.USCH_BASEON_MA);
// 使用 GetStartTime 测试一下 Dll 和 cex 文件
Long startTimeSecond = DllUtils.instance.GetStartTime(loadData);
Date date = new Date();
date.setTime(startTimeSecond * 1000);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("startTime: " + simpleDateFormat.format(date)); // 2022-10-18 08:50:13
// 4. 处理数据
ByteByReference cBoxNo = new ByteByReference();
ByteByReference cChl = new ByteByReference();
LongByReference dwDownId = new LongByReference();
LongByReference wDownVer = new LongByReference();
if (DllUtils.instance.GetChannel(loadData, cBoxNo, cChl, dwDownId, wDownVer)) {
System.out.println("cBoxNo: " + cBoxNo.getValue()); // 16
System.out.println("cChl: " + cChl.getValue()); // 5
System.out.println("dwDownId: " + dwDownId.getValue()); // 705888533
System.out.println("wDownVer: " + wDownVer.getValue()); // 111
}
// 2021.07.11.266
// 2021.01.19.042
Pointer downIdDesc = new Memory(20);
if (DllUtils.instance.GetDescriptionOfDownId(downIdDesc, dwDownId.getValue(), false)) {
int pointerOffset = downIdDesc.getInt(0);
char[] data = downIdDesc.getPointer(pointerOffset).getCharArray(0, 12);
System.out.println("downIdDesc:" + new String(data)); // 20210119042
}
Native.free(Pointer.nativeValue(downIdDesc));
Pointer.nativeValue(downIdDesc, 0);
Pointer desc = new Memory(20);
if (DllUtils.instance.GetDescriptionOfColumn(DllConstant.CYCLE_CapC, desc)) {
char[] data = desc.getPointer(8).getCharArray(0, 20);
String descBydownId = new String(data);
System.out.println("GetDescriptionOfColumn:" + descBydownId.substring(0,4)); // 充电容量
}
Native.free(Pointer.nativeValue(desc));
Pointer.nativeValue(desc, 0);
Map<Integer, Float> cycleCapCMap = new HashMap<>();
int cycleCapCLength = DllUtils.instance.GetRows(loadData, DllConstant.CYCLE_CapC);
System.out.println("cycleCapCLength:" + cycleCapCLength); // 201
for (int i = 0; i < cycleCapCLength; i++) {
Float cycleCapC = DllUtils.instance.GetDataAsFloat(loadData, DllConstant.CYCLE_CapC, (long) i);
cycleCapCMap.put(i, cycleCapC);
}
Map<Integer, Float> cycleCapDMap = new HashMap<>();
int cycleCapDLength = DllUtils.instance.GetRows(loadData, DllConstant.CYCLE_CapD);
System.out.println("cycleCapDLength:" + cycleCapDLength); // 201
for (int i = 0; i < cycleCapDLength; i++) {
Float cycleCapD = DllUtils.instance.GetDataAsFloat(loadData, DllConstant.CYCLE_CapD, (long) i);
cycleCapDMap.put(i, cycleCapD);
}
cycleCapCMap.forEach((keyC, valueC) -> {
cycleCapDMap.forEach((keyD, valueD) -> {
if (keyC.equals(keyD)) {
System.out.println("充电容量" + valueC.toString() + "-- 放电容量" + valueD.toString());
}
});
});
// 5. 释放数据
DllUtils.instance.FreeData(loadData);
}
}
常见问题
Unable to load library
检查 Dll 文件路径访问是否存在问题,没有找到动态库文件,有些路径好像是要重启电脑才能加载
Invalid memory access
访问无效的内存,应该是在调 getPointer 方法时出现的问题
访问数据乱码问题
要么是真的字符集不对应,要么就是使用 Pointer 取的数据不对,后者可能性居多。还有一点就是如果每次运行的数据都不对,可能是这个数据是 Pointer,所以一直在变化
标题:Java 使用 JNA 调用 DLL 动态库的函数操作文件
作者:Hefery
地址:http://hefery.icu/articles/2022/10/13/1665654134166.html