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

Я написал Java GUI, используя SWT. Я упаковываю приложение, используя скрипт 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 я могу просто дважды щелкнуть, чтобы запустить мой графический интерфейс. Недостатком является то, что мне пришлось явно упаковать пакет Windows SWT в мою банку.

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

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

5 ответов

Решение

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

Этот подход теперь доступен для использования в качестве задачи 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, который загружает классы из двух jar-файлов.

  • Правильная банка 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": он включен в JDT Eclipse (Java IDE, построенная на Eclipse). Откройте org.eclipse.jdt.ui_*version_number*.jar с помощью архиватора, и вы найдете файл jar-in-jar-loader.zip внутри. Я переименовал это в jar-in-jar-loader.jar.

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

[EDIT] На этот ответ теперь ссылаются из SWT FAQ.

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

ОБНОВЛЕНИЕ: это работало. 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": он включен в Eclipse JDT (Java IDE, построенная на Eclipse). открыто org.eclipse.jdt.ui_*version_number*.jar с архиватором и вы найдете файл jar-in-jar-loader.zip внутри.

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

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

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

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

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

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

Другие вопросы по тегам