Создание кросс-платформенного приложения Java SWT

Я написал графический интерфейс Java с использованием SWT. Я упакую приложение, используя ANT script (фрагмент ниже).

<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>

Это создает единую банку, которую в Windows я могу просто дважды щелкнуть, чтобы запустить мой графический интерфейс. Недостатком является то, что я должен был явно упаковать пакет SWT Windows в свою банку.

Я хотел бы иметь возможность запускать свое приложение на других платформах (в первую очередь Linux и OS X). Самый простой способ сделать это - создать специфичные для платформы банки, которые упаковали соответствующие SWT файлы в отдельные JAR файлы.

Есть ли лучший способ сделать это? Возможно ли создать один JAR, который будет работать на нескольких платформах?

Ответы

Ответ 1

У меня есть рабочая реализация, которая теперь ссылается на FAQ SWT.

Теперь этот подход можно использовать в качестве задачи ANT: SWTJar

[EDIT] SWTJar теперь обновлен, чтобы использовать решение Алексея Романова, как описано выше.

build.xml

Сначала я создаю банку, содержащую все мои классы приложений.

<!-- 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 файлы
    • Кувшин, который я только что построил
    • Все баки SWT
  • Классы
    • Классы классов классов 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, который загружает классы из двух банок.

  • Правильная коробка SWT
  • Обертка приложения

Как только у нас есть этот загрузчик классов, мы можем запустить основной метод приложения, используя отражение.

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;
  }
}

[EDIT] Как указано выше, для тех, кто ищет "jar-in-jar classloader": он включен в Eclipse JDT (Java IDE, построенный на Eclipse). Откройте файл org.eclipse.jdt.ui_ * version_number *.jar с архиватором, и вы найдете файл jar-in-jar-loader.zip внутри. Я переименовал его в jar-in-jar-loader.jar.

intrace-ui.jar - это банка, которую я построил, используя описанный выше процесс. Вы должны иметь возможность запускать эту единственную банку на любом из win32/64, linux32/64 и osx32/64.

[EDIT] Этот ответ теперь ссылается на FAQ SWT.

Ответ 2

Я столкнулся с той же проблемой. Я еще не пробовал, но планирую включать версии swt.jar для всех платформ и динамически загружать их в начале метода main.

UPDATE: Это сработало. build.xml включает все банки:

<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);
    }
}

[EDIT] Для тех, кто ищет "jar-in-jar classloader": он включен в Eclipse JDT (Java IDE, построенный на Eclipse). Откройте org.eclipse.jdt.ui_*version_number*.jar с помощью архиватора, и вы найдете файл jar-in-jar-loader.zip внутри.

Ответ 3

если вы не хотите свертывать все в один файл jar и использовать jar-in-jar, вы также можете решить эту проблему, включив в развернутый каталог приложений lib названные SWT-банки для каждой целевой платформы:

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" во время выполнения, используя System.getProperty(String name), чтобы создать правильное имя файла jar.

Затем вы можете использовать немного непослушный бит отражения (очистители OO отвратитесь сейчас!), вызвав обычно защищенный метод URLClassloader.addURL(URL url), чтобы добавить правильный jar к пути класса systemloader системы до того, как понадобится первый класс SWT.

Если вы можете выдержать запах кода, здесь я приведу рабочий пример http://www.chrisnewland.com/select-correct-swt-jar-for-your-os-and-jvm-at-runtime-191

Ответ 4

Очень странно, что все ответы здесь просто советуют упаковать все SWT JAR в один гигантский JAR файл приложения. IMHO, это строго против цели SWT: для каждой платформы есть библиотека SWT, поэтому она должна упаковывать только соответствующую SWT-библиотеку для каждой платформы. Это очень просто сделать, просто определите 5 профилей сборки в вашей конструкции ANT: win32, win64, linux32, linux64 и mac64 (вы также можете делать mac32, но все современные Mac - 64-разрядные).

В любом случае, если вы хотите иметь хорошую интеграцию приложений в ОС, вам нужно будет сделать некоторые вещи, специфичные для ОС, и вы снова получите профили сборки. Для настольных приложений неудобно иметь один пакет приложений для всех платформ как для разработчика, так и для его пользователей.

Ответ 5

Заменить выделенный выделенный текст в src= "lib/ org.eclipse.swt.win32.win32.x86_3.5.2.v3557f.jar" с указанным linux файлом jt файла swt