创建跨平台的Java SWT应用程序

34

我使用 SWT 编写了一个 Java GUI。我使用 ANT 脚本(片段如下)打包应用程序。

<jar destfile="./build/jars/swtgui.jar" filesetmanifest="mergewithoutmain">
  <manifest>
    <attribute name="Main-Class" value="org.swtgui.MainGui" />
    <attribute name="Class-Path" value="." />
  </manifest>
  <fileset dir="./build/classes" includes="**/*.class" />
  <zipfileset excludes="META-INF/*.SF" src="lib/org.eclipse.swt.win32.win32.x86_3.5.2.v3557f.jar" />
</jar>

这将生成一个单独的jar文件,在Windows上,我只需要双击它就可以运行我的GUI。缺点是我必须显式地将Windows SWT包打包到我的jar文件中。

我希望能够在其他平台(主要是Linux和OS X)上运行我的应用程序。最简单的方法是创建特定于平台的jar文件,将适当的SWT文件打包到不同的JAR文件中。

有没有更好的方法来做到这一点?是否可能创建一个单一的JAR文件可在多个平台上运行?

5个回答

29
我刚遇到了同样的问题。虽然我还没有尝试过,但我计划为所有平台包含swt.jar的版本,并在main方法的开始动态加载正确的版本。
更新:它成功了。build.xml包括所有的jar文件:
<zipfileset dir="/home/aromanov/workspace/foo/lib" includes="swt_linux_gtk_x86.jar"/>
<zipfileset dir="/home/aromanov/workspace/foo/lib" includes="swt_macosx_x86.jar"/>
<zipfileset dir="/home/aromanov/workspace/foo/lib" includes="swt_win32_x86.jar"/>
<zipfileset dir="/home/aromanov/workspace/foo/lib" includes="swt_linux_gtk_x64.jar"/>
<zipfileset dir="/home/aromanov/workspace/foo/lib" includes="swt_macosx_x64.jar"/>
<zipfileset dir="/home/aromanov/workspace/foo/lib" includes="swt_win32_x64.jar"/>

我的main方法以调用此内容开始:

private void loadSwtJar() {
    String osName = System.getProperty("os.name").toLowerCase();
    String osArch = System.getProperty("os.arch").toLowerCase();
    String swtFileNameOsPart = 
        osName.contains("win") ? "win32" :
        osName.contains("mac") ? "macosx" :
        osName.contains("linux") || osName.contains("nix") ? "linux_gtk" :
        ""; // throw new RuntimeException("Unknown OS name: "+osName)

    String swtFileNameArchPart = osArch.contains("64") ? "x64" : "x86";
    String swtFileName = "swt_"+swtFileNameOsPart+"_"+swtFileNameArchPart+".jar";

    try {
        URLClassLoader classLoader = (URLClassLoader) getClass().getClassLoader();
        Method addUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
        addUrlMethod.setAccessible(true);

        URL swtFileUrl = new URL("rsrc:"+swtFileName); // I am using Jar-in-Jar class loader which understands this URL; adjust accordingly if you don't
        addUrlMethod.invoke(classLoader, swtFileUrl);
    }
    catch(Exception e) {
        throw new RuntimeException("Unable to add the SWT jar to the class path: "+swtFileName, e);
    }
}

[编辑] 对于那些寻找“jar-in-jar类加载器”的人:它已经包含在Eclipse的JDT(基于Eclipse构建的Java IDE)中了。使用存档工具打开org.eclipse.jdt.ui_*版本号*.jar,您将会在其中找到一个名为jar-in-jar-loader.zip的文件。


或者在您的Eclipse安装中获取最新版本。 - Alexey Romanov
1
使用压缩软件打开 org.eclipse.jdt.ui_*版本号*.jar,里面应该有一个名为 jar-in-jar-loader.zip 的文件。(我这里安装的是3.5.1版本,但我认为3.6没有区别。) - Alexey Romanov
1
我尝试了你的代码,但是出现了这个异常:java.net.MalformedURLException: unknown protocol: rsrc at java.net.URL.<init>(URL.java:574) 完成了...你能帮忙解决一下问题吗? - othman
2
@Alexey - 你可能会感兴趣看到我已经将你的解决方案打包成一个ant任务 - http://mchr3k.github.com/swtjar/ - mchr
@javydreamercsw 您可以使用 sun.arch.data.model 获取JVM架构。https://dev59.com/D3I95IYBdhLWcg3w_zQN - Rossiar
显示剩余16条评论

19

我有一个可用的实现,现在已被引用于SWT FAQ

这个方法现在可以作为ANT任务使用:SWTJar

[编辑] SWTJar现已更新,使用了上述Alexey Romanov的解决方案。

build.xml

首先,我构建了一个包含所有应用程序类的jar文件。

<!-- UI (Stage 1) -->   
<jarjar jarfile="./build/tmp/intrace-ui-wrapper.jar">
  <fileset dir="./build/classes" includes="**/shared/*.class" />
  <fileset dir="./build/classes" includes="**/client/gui/**/*.class" />
  <zipfileset excludes="META-INF/*.MF" src="lib/miglayout-3.7.3.1-swt.jar"/>
</jarjar>

接下来,我要创建一个jar文件来包含以下所有内容:

  • JAR文件
    • 刚刚构建的jar
    • 所有的SWT jar
    • "Jar-In-Jar"类加载器类
    • 一个特殊的加载器类-见下文

这是build.xml中的代码片段。

<!-- UI (Stage 2) -->
<jarjar jarfile="./build/jars/intrace-ui.jar">
  <manifest>
    <attribute name="Main-Class" value="org.intrace.client.loader.TraceClientLoader" />
    <attribute name="Class-Path" value="." />
  </manifest>
  <fileset dir="./build/classes" includes="**/client/loader/*.class" />
  <fileset dir="./build/tmp" includes="intrace-ui-wrapper.jar" />
  <fileset dir="./lib" includes="swt-*.jar" />
  <zipfileset excludes="META-INF/*.MF" src="lib/jar-in-jar-loader.jar"/>
</jarjar>

TraceClientLoader.java

这个加载器类使用jar-in-jar-loader创建了一个ClassLoader,它从两个JAR文件中加载类:

  • 正确的SWT JAR包
  • 应用程序的Wrapper JAR包

一旦我们获取了这个ClassLoader,就可以使用反射来启动实际应用程序的主方法。

public class TraceClientLoader
{
  public static void main(String[] args) throws Throwable
  {    
    ClassLoader cl = getSWTClassloader();
    Thread.currentThread().setContextClassLoader(cl);    
    try
    {
      try
      {
        System.err.println("Launching InTrace UI ...");
        Class<?> c = Class.forName("org.intrace.client.gui.TraceClient", true, cl);
        Method main = c.getMethod("main", new Class[]{args.getClass()});
        main.invoke((Object)null, new Object[]{args});
      }
      catch (InvocationTargetException ex)
      {
        if (ex.getCause() instanceof UnsatisfiedLinkError)
        {
          System.err.println("Launch failed: (UnsatisfiedLinkError)");
          String arch = getArch();
          if ("32".equals(arch))
          {
            System.err.println("Try adding '-d64' to your command line arguments");
          }
          else if ("64".equals(arch))
          {
            System.err.println("Try adding '-d32' to your command line arguments");
          }
        }
        else
        {
          throw ex;
        }
      }
    }
    catch (ClassNotFoundException ex)
    {
      System.err.println("Launch failed: Failed to find main class - org.intrace.client.gui.TraceClient");
    }
    catch (NoSuchMethodException ex)
    {
      System.err.println("Launch failed: Failed to find main method");
    }
    catch (InvocationTargetException ex)
    {
      Throwable th = ex.getCause();
      if ((th.getMessage() != null) &&
          th.getMessage().toLowerCase().contains("invalid thread access"))
      {
        System.err.println("Launch failed: (SWTException: Invalid thread access)");
        System.err.println("Try adding '-XstartOnFirstThread' to your command line arguments");
      }
      else
      {
        throw th;
      }
    }
  }

  private static ClassLoader getSWTClassloader()
  {
    ClassLoader parent = TraceClientLoader.class.getClassLoader();    
    URL.setURLStreamHandlerFactory(new RsrcURLStreamHandlerFactory(parent));
    String swtFileName = getSwtJarName();      
    try
    {
      URL intraceFileUrl = new URL("rsrc:intrace-ui-wrapper.jar");
      URL swtFileUrl = new URL("rsrc:" + swtFileName);
      System.err.println("Using SWT Jar: " + swtFileName);
      ClassLoader cl = new URLClassLoader(new URL[] {intraceFileUrl, swtFileUrl}, parent);

      try
      {
        // Check we can now load the SWT class
        Class.forName("org.eclipse.swt.widgets.Layout", true, cl);
      }
      catch (ClassNotFoundException exx)
      {
        System.err.println("Launch failed: Failed to load SWT class from jar: " + swtFileName);
        throw new RuntimeException(exx);
      }

      return cl;
    }
    catch (MalformedURLException exx)
    {
      throw new RuntimeException(exx);
    }                   
  }

  private static String getSwtJarName()
  {
    // Detect OS
    String osName = System.getProperty("os.name").toLowerCase();    
    String swtFileNameOsPart = osName.contains("win") ? "win" : osName
        .contains("mac") ? "osx" : osName.contains("linux")
        || osName.contains("nix") ? "linux" : "";
    if ("".equals(swtFileNameOsPart))
    {
      throw new RuntimeException("Launch failed: Unknown OS name: " + osName);
    }

    // Detect 32bit vs 64 bit
    String swtFileNameArchPart = getArch();

    String swtFileName = "swt-" + swtFileNameOsPart + swtFileNameArchPart
        + "-3.6.2.jar";
    return swtFileName;
  }

  private static String getArch()
  {
    // Detect 32bit vs 64 bit
    String jvmArch = System.getProperty("os.arch").toLowerCase();
    String arch = (jvmArch.contains("64") ? "64" : "32");
    return arch;
  }
}

[编辑] 如上所述,对于那些寻找“jar-in-jar类加载器”的人:它包含在Eclipse的JDT(基于Eclipse构建的Java IDE)中。使用压缩软件打开org.eclipse.jdt.ui_*version_number*.jar,你会找到一个名为jar-in-jar-loader.zip的文件。我将其重命名为jar-in-jar-loader.jar。

intrace-ui.jar - 这个jar是我使用上面描述的过程构建的。您应该能够在任何win32/64、linux32/64和osx32/64上运行这个单独的jar文件。

[编辑] 现在,这个答案被引用自SWT FAQ


我在你的网站上找不到联系方式,但是我无法让你的swtjar任务正常工作。我收到了错误信息 D:\My Dropbox\Java\kEllyIRClient\swtjar-build.xml:16: The archive swtjar.jar doesn't exist。taskdef有正确的类路径,因为如果更改它,它会抱怨这个问题。我不知道如何让它正常工作,而且我更愿意使用你的任务来完成它,而不是手动实现反射内容。 - Rahat Ahmed
你能否发布你完整的 build.xml 链接(或者至少是你遇到问题的目标)?你还可以告诉我关于你文件/文件夹在磁盘上的布局的细节吗?告诉我你的电子邮件地址,我会直接帮助你。 - mchr
这是我发布的问题:http://stackoverflow.com/questions/9107509/unable-to-make-ant-script-with-swtjar/9112334#comment11467319_9112334,但是自那以后脚本已经更改。我的电子邮件是rahatarmanahmed@gmail.com。 - Rahat Ahmed
我已经在你的SO问题上发布了答案。 - mchr
我下载了swtjar.jar,但是当我使用java -XstartOnFirstThread -jar swtjar.jar启动它时,出现了一个错误,指出“swtjar.jar中没有主清单属性”。 - MetalSnake
显示剩余2条评论

10

如果您不想将所有内容打包成单个jar文件并使用jar-in-jar,那么您也可以通过在部署的应用程序的lib目录中包含每个目标平台的命名SWT jar来解决此问题:

lib/swt_win_32.jar
lib/swt_win_64.jar
lib/swt_linux_32.jar
lib/swt_linux_64.jar

通过检查Java系统属性"os.name"和"os.arch",在运行时动态加载正确的jar文件。使用System.getProperty(String name)方法创建正确的jar文件名。然后可以使用稍微有些调皮的反射技巧(面向对象纯粹主义者请回避!),通过调用通常受保护的方法URLClassloader.addURL(URL url),将正确的jar添加到系统类加载器的类路径中,在需要第一个SWT类之前完成操作。如果您可以忍受代码气味,我在这里放了一个工作示例http://www.chrisnewland.com/select-correct-swt-jar-for-your-os-and-jvm-at-runtime-191

1
很奇怪,这里所有的答案都建议将所有SWT JAR文件打包成一个巨大的应用程序JAR文件。 在我看来,这严格违反了SWT的目的:每个平台都有一个SWT库,因此应该只打包每个平台适当的SWT库。 这很容易做到,只需在ANT构建中定义5个构建配置文件:win32,win64,linux32,linux64和mac64(您也可以做mac32,但所有现代Mac都是64位)。
无论如何,如果您想要良好的应用程序集成到操作系统中,那么您需要做一些特定于操作系统的事情,并再次使用构建配置文件。 对于桌面应用程序而言,开发人员和用户都不方便将一个应用程序包用于所有平台。

0

将src="lib/org.eclipse.swt.win32.win32.x86_3.5.2.v3557f.jar"中的粗体选定文本替换为适用于Linux的swt jar文件


这是我考虑过的方式,但这会让我留下特定于平台的JAR文件。我很想能够生成一个单一的跨平台JAR文件,但我怀疑这可能不可能。 - mchr
很遗憾,这是不可能的。 - user10679754
那正是我所怀疑的。嗯,我可以接受每个平台一个二进制文件 - 我只需要编写一次代码 :) (尽管我知道我需要在所有平台上进行测试)。 - mchr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接