com4j 是一个类型安全的 Java/COM 桥接器。
vtable
接口(而不是 IDispatch
),以提高性能,并为更多 COM 接口提供扩展器支持。通常,使用 com4j 的第一步是从 COM 类型库生成 Java 类型定义。COM 类型库通常位于 .ocx
、.dll
、.exe
或 .tlb
文件中。除了使用 OleView
预测文件之外,我仍然不知道如何为给定的 COM 库定位类型库。
在本教程中,我们使用 %WINDIR%\system32\wshom.ocx
,其中包含 Windows Scripting Host
的类型库。这种类型的库应该可以在所有现代风格的 Windows 中使用。
要从类型库生成 Java 定义,请执行以下操作:
java -jar tlbimp.jar -o wsh -p test.wsh %WINDIR%\system32\wshom.ocx
这应该在 test.wsh
Java 包中生成 Java 定义,并将所有文件放在 wsh
目录下。
首先,看看生成的 ClassFactory
类。此类包含一系列用于创建 COM 对象新实例的 create***
方法。
public abstract class ClassFactory {
public static IFileSystem3 createFileSystemObject() {
return COM4J.createInstance(IFileSystem3.class, "{0D43FE01-F093-11CF-8940-00A0C9054228}");
}
// ...
}
调用这些方法会导致 COM 对象的实例化,并返回对其包装器的引用。
tlbimp
还为类型库中的每个接口定义生成一个 Java 接口。通常它们看起来如下:
@IID("{2A0B9D10-4B87-11D3-A97A-00104B365C9F}")
public interface IFileSystem3 extends IFileSystem {
@VTID(32)
ITextStream getStandardStream(StandardStreamTypes standardStreamType, boolean unicode);
@VTID(33)
java.lang.String getFileVersion(java.lang.String fileName);
}
当您试图使用从 tlbimp
生成的定义时,可以忽略所有这些注解。这些用于配置 com4j 运行时以正确进行桥接。这些接口由 com4j COM 对象包装器实现,并且在此接口上调用方法会导致运行时调用相应的 COM 方法。
此外,当类型库定义枚举时,tlbimp
会生成枚举。
public enum StandardStreamTypes {
StdIn, // 0
StdOut, // 1
StdErr, // 2
}
使用生成的代码很简单。以下代码说明了如何使用该 IFileSystem3.getFileVersion
方法获取文件的版本字符串。
public class Main {
public static void main(String[] args) {
IFileSystem3 fs = ClassFactory.createFileSystemObject();
for (String file : args) {
System.out.println(fs.getFileVersion(file));
}
}
}
本文档解释了使用 com4j 时应该了解的内容。
在运行时,com4j 自动为带有 com4j 注解的接口生成实现代码(请参阅 此处 获取更多信息)。从现在起我们称之为“代理”。每个代理都拥有对 COM 接口的引用。如下图所示,两个代理可以引用同一对象的相同接口,或者两个代理可以具有相同对象的不同接口。
因此,您不能使用 proxy1 == proxy2
这样的表达式来检查两个代理是否引用同一个 COM 对象。为此,你必须写为 proxy1.equals(proxy2)
。
观察以下 COM 方法:
[helpstring("get the child object.")]
HRESULT GetItem( [int] int index, [out,retval] IFoo** ppItem );
COM 方法不仅返回 “概念” 返回值(IFoo*
),还返回一个 HRESULT
。tlbimp
总是从 Java 中隐藏 HRESULT,因此上述方法必然是:
IFoo GetItem(int index);
当 COM 方法调用失败返回 HRESULT
时,com4j 运行时抛出未检查的 ComException
。
这使得调用者无法知道从该方法返回的实际 HRESULT
成功代码,而有时 COM 方法实际上使用不同的成功代码(例如,使用 S_OK
和 S_FALSE
作为布尔函数)。请参阅 com4j 注解指南
以了解如何将 HRESULT
映射为 Java 方法的返回值。
当 Java 代码引用 IFoo
并且需要获得同一 COM 对象的 IBar
时,您必须使用以下 queryInterface
方法:
IBar bar = fooObject.queryInterface(IBar.class);
换句话说,不能使用像 (IBar) fooObject
这样的普通强制转换运算符。
除了上面列出的那些注意事项之外,您几乎可以像使用普通 Java 对象一样使用所有这些 COM 对象。当然你也可以继续阅读下面的内容,以便更深入地了解 com4j 的工作原理。
com4j.jar
中包含了 com4j-x86.dll
和 com4j-x64.jar
,并能够在运行时正确的加载它。因此,通常只需将 com4j.jar
与应用程序捆绑在一起。这种方便方法的唯一缺点是运行时性能轻微的损失。
或者,你也可以:
com4j*.dll
与 com4j.jar
放在同一目录中。com4j*.dll
放在系统属性 java.library.path
所在的目录。这需要在启动 JVM 时完成,因为属性的值由类加载器缓存。可以使用 Java Web Start
相关技术部署使用 com4j 的应用程序。有关详细信息,请参阅发行版中的 jnlp 示例。
此处内容绍了如何订阅 COM 事件。
要订阅 COM 事件,首先需要一个定义事件接口的 IID 的接口,以及您要订阅的事件方法,例如:
@IID("{5846EB78-317E-4B6F-B0C3-11EE8C8FEEF2}")
public interface _IiTunesEvents {
/**
* 发生数据库更改时触发
*/
@DISPID(1)
void onDatabaseChangedEvent(Object deletedObjectIDs, Object changedObjectIDs);
/**
* 在曲目开始播放时触发
*/
@DISPID(2)
void onPlayerPlayEvent(Object iTrack);
}
事件接口不必列出所有事件方法; 如果省略了某些方法,那么这些事件将被忽略。此外,从技术上讲,事件接口可以是一个类 —— 唯一需要的是 @IID
注解,以及 @DISPID
指定了哪些是事件方法。
为了便于订阅 COM 事件,tlbimp 生成一个具有空方法的类。但您也可以选择手动编写。
您可以使用以下代码订阅 COM 对象:
// 大多是从 Com4jObject 派生的接口
Com4jObject comObject = ...;
EventCookie cookie = comObject.advise(_IiTunesEvents.class, new MyEventReceiver());
// ...
// 终止订阅
cookie.close();
由于 COM 中的引用计数,您必须使用该 close
方法显式执行取消订阅,否则 COM 和 Java 对象都将泄漏。
如果某线程 X
调用一个 COM 方法,而该方法又触发一个事件 Y
,则执行事件方法 Y
的线程将不是 X
,而是 com4j 内部维护的线程。因此,对线程本地资源的访问需要仔细完成。但请注意,此时不需要同步 --- 线程 X
将一直阻塞,直到您的事件代码返回。
对于其他一些事件(例如 iTunes playerStart / playerStop 事件),它们会在没有您首先调用 COM 的情况下发生。这些事件以真正的异步方式提供,因此您需要同步。
对于某些其他类型的事件(如 iTunes PlayerStart/PlayerStop
事件),它们发生时没有先调用 COM。这些事件是以真正异步的方式传递的,因此需要同步。
详细说明了运行时如何将 Java 方法调用桥接到 COM 方法调用中,以及如何使用注解来控制此过程。
在最常见的形式中,Java 方法可以注解如下:
@IID(iid)
public interface INTERFACE {
@VTID(vtid)
@ReturnValue(index=rindex, inout=rio, type=rt)
T foo(
@MarshalAs(t1) T1 param1,
@MarshalAs(t2) T2 param2,
... );
}
注解 IID
的 iid
参数指定 COM 接口的 IID
。方法调用是针对 COM 对象的此接口完成的。
必须的 vtid
参数描述了给定接口中方法的索引。com4j 运行时从不使用方法名信息来决定调用哪个 COM 方法。可以通过计算在该接口上定义的方法来确定虚拟表索引。例如,IUnknown 有 3 个方法,因此 @VTID(3)
将在从 IUnknown 派生的接口上指定第一个方法。IDispatch 定义了 4 个额外的方法,因此从 IDispatch 派生的接口上的第一个方法会有 @VITD(7)
。
使用错误的 VTID 通常会导致 JVM 崩溃,
因为最终使用错误的参数集调用了错误的方法(或不存在的方法)。所以手动调整它时要小心。
在 COM 中,返回值通常通过引用作为参数传递。
因此,当 Java 方法具有返回值时,com4j 将其作为参数桥接。
可选的 rindex
指定在实际参数中传递此参数的位置。 例如,以下 Java 方法:
@ReturnValue(index=0) Tr foo( T1 t1, T2 t2 )
将桥接到以下 COM 方法调用:
HRESULT Foo( [out,retval] Tr* r, T1 t1, T2 t2 );
同样,以下 Java 方法:
@ReturnValue(index=1) Tr foo( T1 t1, T2 t2 )
将桥接到以下 COM 方法调用:
HRESULT Foo( T1 t1, [out,retval] Tr* r, T2 t2 );
当省略 rindex
时,这意味着返回值在最后一个参数之后传递,这是大多数 COM 方法所做的。
虽然很少见,但 COM 方法参数可以具有 [in, out, retval]
语义,这意味着它从调用者获取值,修改它,并将其作为方法的返回值返回。
为 rio 指定 true
将实现此语义。 打开此开关后,com4j 运行时不会在参数中插入返回值,而是将指定参数作为参数和返回值重载。因此,以下 Java 方法:
@ReturnValue(index=1,inout=true) T2 foo( T1 t1, T2 t2 )
将桥接到以下 COM 方法调用:
HRESULT Foo( T1 t1, [int, out, retval] T2* t2 );
可选的 rt
参数指定此方法的 native
返回类型以及返回值如何映射到 Java 的语义。省略时,使用预定义的表来决定从 Java 返回类型使用哪种本机类型。
有关可能的值,它们的语义和允许的 Java 类型,请参阅 NativeType
。
可以通过 MarshalAs
属性选择性地注释参数,以控制 Java 参数如何绑定到本机类型的参数。省略时,使用相同的预定义表来决定使用哪种本机类型。
为返回类型指定的 NativeType
和为参数指定的 NativeType
有时具有稍微不同的语义。
考虑以下 COM 方法:
[helpstring("get the child object.")]
HRESULT GetItem( [int] int index, [out,retval] IFoo** ppItem );
COM 方法不仅返回 “概念” 返回值(IFoo*
),还返回 HRESULT
。tlbimp
总是在 Java
中隐藏 HRESULT
,因此上述方法必然会:
IFoo GetItem( int index );
当 COM 方法调用失败时返回 HRESULT
,com4j 运行时将抛出未检查的 ComException
。
有时,COM 方法实际使用此 HRESULT
返回一个有意义的值。例如,
[helpstring("count the items and returns it, or return a failure code.")]
HRESULT CountItems();
如果要访问 HRESULT
返回值,请使用 NativeType.HRESULT
,如下所示,它将 HRESULT
值作为 Java int
返回:
@ReturnValue(type=NativeType.HRESULT)
int countItems();
COM 对象的生命周期由引用计数控制,而 Java 对象的生命周期由垃圾收集控制。本文档解释了 com4j 如何处理这种差异。
默认情况下,在 JVM 发现代理本身可以被垃圾回收后不久,代理对象就会释放对 COM 对象的引用。这就从用户应用程序中隐藏了生命周期管理的细节,但缺点是您通常无法预测何时释放 COM 对象。
用户应用程序可以显式调用 Com4jObject.dispose
方法以更早地释放对 COM 对象的引用。调用此方法后,代理对象将变为 “diposed”,并且对其任何 COM 方法的所有后续调用都将失败,并抛出 IllegalStateException
。
ComObjectListener
应用程序管理 COM 对象生命周期的另一种方法是使用 ComObjectListener
。监听器可以注册到当前线程,如果已注册,则每次创建新的 com4j 代理时都会接收回调。
当应用程序具有限制 COM 访问的代码块时,这非常有用。其思想是跟踪所有 COM 对象,然后在代码块完成后将它们全部(除了一些超出作用域的对象)销毁。更多信息请参见 ComObjectCollector
的 javadoc。
这对于大型应用程序很有用,因为在单个对象上调用 dispose
方法过于繁琐。
内容声明 | |
---|---|
标题: 【笔记】Java 调用 COM 组件之 com4j 使用说明 | |
链接: https://zixizixi.cn/note-java-com4j | 来源: iTanken |
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可,转载请保留此声明。
|