Create cross platform Java SWT Application
I have written a Java GUI using SWT. I package the application using an ANT script (fragment below).
<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=".开发者_Python百科/build/classes" includes="**/*.class" />
<zipfileset excludes="META-INF/*.SF" src="lib/org.eclipse.swt.win32.win32.x86_3.5.2.v3557f.jar" />
</jar>
This produces a single jar which on Windows I can just double click to run my GUI. The downside is that I have had to explicitly package the windows SWT package into my jar.
I would like to be able to run my application on other platforms (primarily Linux and OS X). The simplest way to do it would be to create platform specific jars which packaged the appropriate SWT files into separate JARs.
Is there a better way to do this? Is it possible to create a single JAR which would run on multiple platforms?
I've just run into the same problem. I haven't tried it yet, but I plan to include versions of swt.jar
for all platforms and load the correct one dynamically in the start of the main
method.
UPDATE: It worked. build.xml
includes all jars:
<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"/>
and my main
method starts with calling this:
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] For those looking for the "jar-in-jar classloader": It's included in Eclipse's JDT (the Java IDE built on Eclipse). Open org.eclipse.jdt.ui_*version_number*.jar
with an archiver and you will find a file jar-in-jar-loader.zip
inside.
I have a working implementation which is now referenced from the SWT FAQ.
This approach is now available to use as an ANT task: SWTJar
[EDIT] SWTJar has now been updated to use Alexey Romanov's solution as described above.
build.xml
First I build a jar containing all of my application classes.
<!-- 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>
Next, I build a jar to contain all of the following:
- JARs
- The jar which I just built
- All the SWT jars
- Classes
- The "Jar-In-Jar" classloader classes
- A special loader class - see below
Here is the fragment from 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
This loader class uses the jar-in-jar-loader to create a ClassLoader which loads classes from two jars.
- The correct SWT jar
- The application wrapper jar
Once we have this classloader we can launch the actual application main method using reflection.
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] As stated above, for those looking for the "jar-in-jar classloader": It's included in Eclipse's JDT (the Java IDE built on Eclipse). Open org.eclipse.jdt.ui_*version_number*.jar with an archiver and you will find a file jar-in-jar-loader.zip inside. I renamed this to jar-in-jar-loader.jar.
intrace-ui.jar - this is the jar which I built using the process described above. You should be able to run this single jar on any of win32/64, linux32/64 and osx32/64.
[EDIT] This answer is now referenced from the SWT FAQ.
if you are not looking to roll up everything into a single jar file and use jar-in-jar then you can also solve this problem by including named SWT jars for each target platform in your deployed application's lib directory:
lib/swt_win_32.jar
lib/swt_win_64.jar
lib/swt_linux_32.jar
lib/swt_linux_64.jar
and loading the correct one dynamically at runtime by inspecting the Java system properties "os.name"
and "os.arch"
at runtime using System.getProperty(String name)
to create the correct jar filename.
You can then use a slightly naughty bit of reflection (OO purists look away now!) by invoking the normally protected method URLClassloader.addURL(URL url)
to add the correct jar to the system classloader's classpath before the first SWT class is needed.
If you can stand the code-smell I've put a working example here http://www.chrisnewland.com/select-correct-swt-jar-for-your-os-and-jvm-at-runtime-191
It's very strange that all the answers here just advise to package all SWT JARs into a single giant application JAR file. IMHO, this is strictly against the purpose of SWT: there is an SWT library for each platform, so it is supposed to package only the appropriate SWT library for each platform. That's very easy to do, just define 5 build profiles in your ANT build: win32, win64, linux32, linux64 and mac64 (you can do mac32 as well, but all modern Macs are 64-bit).
Anyway, if you want to have good application integration into the OS, then you'll have to do some OS-specific things and you're with build profiles again. For desktop application it's inconvenient to have one app package for all platforms both for the developer and for his users.
Replace bold selected text in src="lib/org.eclipse.swt.win32.win32.x86_3.5.2.v3557f.jar" with linux-specified swt jar-file
精彩评论