JACOB调用COM组件的多线程问题

java | 2020-05-19 09:30:09

在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的更多详细信息,请参见“ 对象生存期”文档。

登录后即可回复 登录 | 注册
    
关注编程学问公众号