在java中经常使用JACOB调用COM组件来操作office的word,excel,但真正应用到服务器上,我们还需要多多了解jacob多线程调用的问题,下面是官方文档给出的关于如何多线程使用jacob.
介绍
用于线程的COM模型不同于Java模型。在COM中,每个组件都可以声明其是否支持多线程。
术语"Single Threaded Apartment (STA)"是指一个线程,其中在该线程中创建的所有COM对象都是单线程的。这可以通过两种方式体现出来:
- 对该组件的所有调用均由创建该组件的同一线程进行
- 或者由另一个线程进行的任何调用均由COM进行序列化
通过使用Windows消息循环并将消息发布到隐藏的窗口中,可以完成调用的序列化(我不是在开玩笑)。COM实现此目的的方法是要求任何其他线程通过本地Proxy对象而不是原始对象进行调用(当我们讨论JACOB DispatchProxy类时,将对此进行更多介绍)。
这对于Java应用程序意味着什么?如果您使用的是一个声明为 ThreadingModel“ Apartment”的组件(您可以通过在注册表的CLSID下查找来找到此组件),并且计划在一个线程中创建,使用和销毁该组件-那么您将遵循您可以将该线程声明为STA线程。
另一方面,如果您需要从另一个线程(例如,在servlet中)进行方法调用,那么您有几种选择。通过扩展com.jacob.com.STA
,在其自己的STA中创建组件 ,并使用 com.jacob.com.DispatchProxy
该类在线程之间传递Dispatch指针,或者可以将线程声明为MTA线程。在这种情况下,COM将对运行您的组件的STA进行跨线程调用。如果您在MTA中创建一个Apartment线程组件,COM将自动为您创建一个STA,并将您的组件放入其中,然后将所有调用编组。
这使我们想到了Main STA的概念 。COM要求,如果您的应用程序中有任何Apartment线程组件,则将创建的第一个STA标记为 Main STA。COM使用主STA创建从MTA线程创建的所有Apartment线程组件。问题是,如果您已经创建了一个STA,那么COM将把它选为Main STA,并且如果您退出该线程-整个应用程序将退出。
1.7版之前的JACOB中的COM线程
直到JACOB 1.7版本,JACOB中只有一种模型可用:
- 在1.6版之前:所有线程都自动初始化为STA。
- 在版本1.6中:所有线程都自动初始化为MTA。
更改默认设置的原因是,将Java线程标记为STA可能会导致问题。
任何Java Swing应用程序以及servlet和applet都必须能够从多个线程进行调用。如果您尝试跨STA线程进行COM方法调用-它将失败!
在大多数情况下,JACOB 1.6(MTA)默认值选择可以正常工作,但是有些明显的例外引起了人们的悲伤。MAPI就是这种例外之一。事实证明,如果您尝试从MTA线程创建MAPI对象-它只会失败并退出。这导致某些人使用STA的默认值重新编译JACOB 1.6。
MTA线程还有另一个问题:使用Apartment线程组件时,我们已经注意到COM将在主STA中创建组件。如果不存在,COM将创建它。但是,这意味着 所有 Apartment线程组件都将在同一STA中创建 。这会造成瓶颈,并在不相关的组件之间产生依赖性。同样,如果该STA退出,则所有组件都将被破坏,应用程序很可能崩溃。
JACOB 1.7版中的COM线程
在1.7版中,我们添加了更细粒度的控件,以允许Java程序员控制COM如何创建其组件。不幸的是,这意味着您需要对COM Apartments的黑暗和神秘主题有一个很好的了解。您需要考虑几种不同的情况:
默认情况
如果只运行在版本1.6中创建的代码,忽略COM线程问题,那么您将获得与1.6中相同的行为:每个java线程都是MTA线程,所有单元线程组件都将由COM在其自己的主STA中创建。这通常适用于大多数应用程序(上面提到的例外情况)。
创建自己的线程套件
要声明MTA线程,请使用以下模板:
ComThread.InitMTA();
...
...
ComThread.Release();
如果您希望JACOB创建自己的主STA(而不是让COM为您选择STA),那么您应该使用:
Thread 1:
ComThread.InitMTA(true); // a true tells JACOB to create a Main STA
...
...
ComThread.Release();
...
Thread 2:
ComThread.InitMTA();
...
...
ComThread.Release();
...
...
ComThread.quitMainSTA();
在这种情况下,还可以显式创建主STA:
ComThread.startMainSTA();
...
...
Thread 1:
ComThread.InitMTA();
...
...
ComThread.Release();
...
Thread 2:
ComThread.InitMTA();
...
...
ComThread.Release();
...
...
ComThread.quitMainSTA();
在后一种情况下,所有单元线程组件都将在JACOB的主STA中创建。这仍然存在组件共享同一个主STA并造成瓶颈的所有问题。为了避免这种情况,您还可以自己创建STA线程:
ComThread.startMainSTA();
...
...
Thread 1:
ComThread.InitSTA();
...
...
ComThread.Release();
...
Thread 2:
ComThread.InitMTA();
...
...
ComThread.Release();
...
...
ComThread.quitMainSTA();
在本例中,线程1是STA,线程2是MTA。您可以省略对ComThread.startMainSTA()的调用,但是如果您这样做了,那么COM将使第一个STA成为您的主STA,然后如果您退出该线程,应用程序将崩溃。
实际上,线程1几乎是一个STA。它缺少windows消息循环。因此,只要创建一个组件并在同一线程中使用它,而不是进行事件回调,这种类型的STA就可以了。
JACOB的STA类
如果要创建一个真正的STA,可以在其中创建组件,然后让其他线程在其上调用方法,则需要Windows消息循环。JACOB提供了一个名为:的类 com.jacob.com.STA
。
public class com.jacob.com.STA extends java.lang.Thread
{
public com.jacob.com.STA();
public boolean OnInit(); // you override this
public void OnQuit(); // you override this
public void quit(); // you can call this from ANY thread
}
STA类扩展了 java.lang.Thread
,它为您提供了两个可以覆盖的方法: OnInit
和 OnQuit
。这些方法是从线程的run
方法调用的, 因此它们将在新线程中执行。这些方法使您可以创建COM组件(调度对象)并释放它们。要创建STA,请对其进行子类化并覆盖OnInit。
该 quit
方法是可以从任何线程调用的 唯一其他方法。此方法使用Win32函数 PostThreadMessage
强制STA的Windows消息循环退出,从而终止线程。
然后,您需要调用STA线程中正在运行的组件。如果仅尝试从另一个线程在STA线程中创建的Dispatch对象上进行调用,则将获得COM异常。有关更多详细信息,请参见: Don Box'Effective COM'Rule 29:不要跨单元边界访问原始接口指针。
DispatchProxy类
由于您不能直接在另一个STA中创建的Dispatch对象上调用方法,因此JACOB为创建Dispatch对象的类提供了一种将其编组到线程中的方法。这是通过com.jacob.com.DispatchProxy
类完成的 。
public class DispatchProxy extends JacobObject {
public DispatchProxy(Dispatch);
public Dispatch toDispatch();
public native void release();
public void finalize();
}
此类的工作方式如下:创建Dispatch对象的线程以Dispatch作为参数构造DispatchProxy(Dispatch)的实例。然后可以从另一个线程访问该实例,该线程将调用其 toDispatch
方法代理,就好像它在您的线程本地一样。COM将透明地进行线程间编组。
以下示例是JACOB发行版中samples / test / ScriptTest2.java的一部分。它显示了如何在一个STA线程中创建ScriptControl并从另一个线程对其进行方法调用:
import com.jacob.com.*;
import com.jacob.activeX.*;
class ScriptTest2 extends STA
{
public static ActiveXComponent sC;
public static Dispatch sControl = null;
public static DispatchProxy sCon = null;
public boolean OnInit()
{
try
{
System.out.println("OnInit");
System.out.println(Thread.currentThread());
String lang = "VBScript";
sC = new ActiveXComponent("ScriptControl");
sControl = (Dispatch)sC.getObject();
// sCon can be called from another thread
sCon = new DispatchProxy(sControl);
Dispatch.put(sControl, "Language", lang);
return true;
}
catch (Exception e)
{
e.printStackTrace();
return false;
}
}
public void OnQuit()
{
System.out.println("OnQuit");
}
public static void main(String args[]) throws Exception
{
try {
ComThread.InitSTA();
ScriptTest2 script = new ScriptTest2();
Thread.sleep(1000);
// get a thread-local Dispatch from sCon
Dispatch sc = sCon.toDispatch();
// call a method on the thread-local Dispatch obtained
// from the DispatchProxy. If you try to make the same
// method call on the sControl object - you will get a
// ComException.
Variant result = Dispatch.call(sc, "Eval", args[0]);
System.out.println("eval("+args[0]+") = "+ result);
script.quit();
System.out.println("called quit");
} catch (ComException e) {
e.printStackTrace();
}
finally
{
ComThread.Release();
}
}
}
您可以尝试修改Dispatch.call
主线程中的 调用以sControl
直接使用 ,您将看到它失败。请注意,一旦在主线程中构造了ScriptTest2对象,我们就会睡一秒钟,以允许其他线程有时间对其进行初始化。
STA线程调用 sCon = new DispatchProxy(sControl);
以保存对表示该sControl
对象的DispatchProxy的全局引用 。然后,主线程调用: Dispatch sc = sCon.toDispatch();
从DispatchProxy对象中获取本地Dispatch代理。
最多 一个(!) 线程可以调用toDispatch(),并且该调用只能进行一次。这是因为IStream对象用于传递代理,并且只写入一次并在读取时关闭。如果您需要多个线程来访问Dispatch指针,则创建那么多个DispatchProxy对象。有关更多详细信息,请参阅上面的Don Box参考。
推荐程序
- 建议始终允许JACOB管理主STA,而不要让COM自己创建一个或标记一个。
- 如果对该组件的所有方法调用都将来自同一线程,则使用ComThread.InitSTA()声明STA线程。
- 如果要允许其他线程调用的STA线程,请使用
com.jacob.com.STA
上面概述的 类。 - 如果您有一个COM组件将其ThreadingModel声明为“免费”或“两者”,则请使用MTA。
- 在大多数情况下,如果需要从多个线程进行方法调用,则可以简单地使用MTA线程,并允许COM在主STA中创建组件。如果您对COM有足够的了解,并且知道MTA解决方案何时会失败或存在其他缺点,则仅应创建自己的STA和DispatchProxy。
示例/测试目录中有3个示例演示了这些情况:
- ScriptTest.java-为ScriptControl组件创建一个STA,并从该STA运行其所有方法调用。
- ScriptTest2.java-创建一个单独的STA线程,并使用DispatchProxy从另一个线程对该组件进行方法调用。
- ScriptTest3.java-创建一个单独的MTA线程,并从另一个MTA线程对该组件进行方法调用。对于大多数应用程序,这比ScriptTest2简单。
默认线程模型
如果您创建一个新线程,并且不对其进行调用ComThread.InitSTA()
或 调用 ComThread.InitMTA()
,那么您的Java代码第一次创建JacobObject时,它将尝试向ROT注册自身,并且当它看到当前线程未初始化时,它将将其初始化为MTA。这意味着执行此操作的代码不再位于本机jni代码内-现在位于 com.jacob.com.ROT
类中。有关ROT的更多详细信息,请参见“ 对象生存期”文档。