子兮子兮 子兮子兮

No can, but will.

目录
【笔记】Java 调用 COM 组件之 com4j 使用说明
/        

【笔记】Java 调用 COM 组件之 com4j 使用说明

一、简介

com4j 是一个类型安全的 Java/COM 桥接器。

目的

  • 作为一个 Java 库,允许 Java 应用程序与 Microsoft 组件对象模型无缝地互操作。
  • 作为一种 Java 工具,用于导入 COM 类型库并生成该库的 Java 定义。

特点

  • 利用 Java5 特性来提高可用性。
  • 直接绑定到 vtable 接口(而不是 IDispatch),以提高性能,并为更多 COM 接口提供扩展器支持。
  • 支持事件回调。
  • 适用于 32 位和 64 位 JVM。

项目地址

二、快速开始

生成类型定义

通常,使用 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)

graph LR subgraph COM com[.<br>COM 对象<br>.] com1(( )) com2(( )) end subgraph Java p1[com4j 代理 1] p2[com4j 代理 2] p3[com4j 代理 3] p1 --> com1 p2 --> com2 p3 --> com2 com1 --- com com2 --- com end classDef bg-green fill:#FFCC99; class p1 bg-green; class p2 bg-green; class p3 bg-green; style com fill:skyblue;

原示意图

COM 错误和异常

观察以下 COM 方法:

[helpstring("get the child object.")]
HRESULT GetItem( [int] int index, [out,retval] IFoo** ppItem );

COM 方法不仅返回 “概念” 返回值(IFoo*),还返回一个 HRESULTtlbimp 总是从 Java 中隐藏 HRESULT,因此上述方法必然是:

IFoo GetItem(int index);

当 COM 方法调用失败返回 HRESULT 时,com4j 运行时抛出未检查的 ComException

这使得调用者无法知道从该方法返回的实际 HRESULT 成功代码,而有时 COM 方法实际上使用不同的成功代码(例如,使用 S_OKS_FALSE 作为布尔函数)。请参阅 com4j 注解指南 以了解如何将 HRESULT 映射为 Java 方法的返回值。

强制转换和查询接口

当 Java 代码引用 IFoo 并且需要获得同一 COM 对象的 IBar 时,您必须使用以下 queryInterface 方法:

IBar bar = fooObject.queryInterface(IBar.class);

换句话说,不能使用像 (IBar) fooObject 这样的普通强制转换运算符。

结论

除了上面列出的那些注意事项之外,您几乎可以像使用普通 Java 对象一样使用所有这些 COM 对象。当然你也可以继续阅读下面的内容,以便更深入地了解 com4j 的工作原理。

四、部署使用

com4j.jar 中包含了 com4j-x86.dllcom4j-x64.jar,并能够在运行时正确的加载它。因此,通常只需将 com4j.jar 与应用程序捆绑在一起。这种方便方法的唯一缺点是运行时性能轻微的损失。

或者,你也可以:

  1. com4j*.dllcom4j.jar 放在同一目录中。
  2. com4j*.dll 放在系统属性 java.library.path 所在的目录。这需要在启动 JVM 时完成,因为属性的值由类加载器缓存。

Java Web Start

可以使用 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

注解 IIDiid 参数指定 COM 接口的 IID。方法调用是针对 COM 对象的此接口完成的。

VTID

必须的 vtid 参数描述了给定接口中方法的索引。com4j 运行时从不使用方法名信息来决定调用哪个 COM 方法。可以通过计算在该接口上定义的方法来确定虚拟表索引。例如,IUnknown 有 3 个方法,因此 @VTID(3) 将在从 IUnknown 派生的接口上指定第一个方法。IDispatch 定义了 4 个额外的方法,因此从 IDispatch 派生的接口上的第一个方法会有 @VITD(7)

使用错误的 VTID 通常会导致 JVM 崩溃,
因为最终使用错误的参数集调用了错误的方法(或不存在的方法)。所以手动调整它时要小心。

rindex

在 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 方法所做的。

rio

虽然很少见,但 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

可选的 rt 参数指定此方法的 native 返回类型以及返回值如何映射到 Java 的语义。省略时,使用预定义的表来决定从 Java 返回类型使用哪种本机类型。

有关可能的值,它们的语义和允许的 Java 类型,请参阅 NativeType

t1, t2, ...

可以通过 MarshalAs 属性选择性地注释参数,以控制 Java 参数如何绑定到本机类型的参数。省略时,使用相同的预定义表来决定使用哪种本机类型。

为返回类型指定的 NativeType 和为参数指定的 NativeType 有时具有稍微不同的语义。

COM 错误和异常

考虑以下 COM 方法:

[helpstring("get the child object.")]
HRESULT GetItem( [int] int index, [out,retval] IFoo** ppItem );

COM 方法不仅返回 “概念” 返回值(IFoo*),还返回 HRESULTtlbimp 总是在 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 如何处理这种差异。

什么时候释放 COM 对象?

默认情况下,在 JVM 发现代理本身可以被垃圾回收后不久,代理对象就会释放对 COM 对象的引用。这就从用户应用程序中隐藏了生命周期管理的细节,但缺点是您通常无法预测何时释放 COM 对象。

提前释放 COM 对象

用户应用程序可以显式调用 Com4jObject.dispose 方法以更早地释放对 COM 对象的引用。调用此方法后,代理对象将变为 “diposed”,并且对其任何 COM 方法的所有后续调用都将失败,并抛出 IllegalStateException

使用 ComObjectListener

应用程序管理 COM 对象生命周期的另一种方法是使用 ComObjectListener。监听器可以注册到当前线程,如果已注册,则每次创建新的 com4j 代理时都会接收回调。

当应用程序具有限制 COM 访问的代码块时,这非常有用。其思想是跟踪所有 COM 对象,然后在代码块完成后将它们全部(除了一些超出作用域的对象)销毁。更多信息请参见 ComObjectCollector 的 javadoc。

这对于大型应用程序很有用,因为在单个对象上调用 dispose 方法过于繁琐。