jobukkit
jobukkit

Reputation: 2670

Mac OS X-specific operations make application crash on other OS's despite checking for OS first

I'm using the following code for integrating my Java app with Mac OS X:

if (System.getProperty("os.name").equals("Mac OS X"))
{
    System.setProperty("com.apple.mrj.application.apple.menu.about.name", "JoText"); // Program name
    com.apple.eawt.Application.getApplication().setDockIconImage(new javax.swing.ImageIcon( JotextMain.class.getResource("icons/Icon.png")  ).getImage()); // Dock icon
    System.setProperty("apple.laf.useScreenMenuBar", "true"); // Use global menu bar
    com.apple.eawt.Application.getApplication().addApplicationListener(new MacAboutAndQuit()); //Make "Quit" and "About" options work
}

On Mac OS X, it works fine. On Ubuntu however:

Exception in thread "main" java.lang.NoClassDefFoundError: com/apple/eawt/ApplicationListener
    at java.lang.Class.getDeclaredMethods0(Native Method)
    at java.lang.Class.privateGetDeclaredMethods(Class.java:2451)
    at java.lang.Class.getMethod0(Class.java:2694)
    at java.lang.Class.getMethod(Class.java:1622)
    at sun.launcher.LauncherHelper.getMainMethod(LauncherHelper.java:494)
    at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:486)
Caused by: java.lang.ClassNotFoundException: com.apple.eawt.ApplicationListener
    at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:423)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:356)
    ... 6 more

When I comment out that code, it launches fine on Ubuntu. Strange, because it checks for Mac OS X first before executing Mac OS X specific operations. I don't have Windows, but friend of me using Windows also reported my .jar didn't launch and gives a "A Java Exception has occured" message from the Java Virtual Machine Launcher instead. On a Windows PC at school, same problem.

What's causing this?

Upvotes: 1

Views: 2158

Answers (1)

CodeBlind
CodeBlind

Reputation: 4569

The second line of your if block is the problem (as well as the fourth). Even though your code doesn't ever call that function, the JVM still has to try to load the com.apple.eawt.Application class at initialization of whatever class contains that if statement, simply because of the reference to it. When you're not on Mac, that class doesn't exist, hence the error.

A workaround would be to use Reflection to make that function call. This should prevent the class loader from trying to load that class until the call is actually made.

Example

javax.swing.ImageIcon icon = ...
Class c = Class.forName("com.apple.eawt.Application");
Method m = c.getMethod("getApplication");
Object applicationInstance = m.invoke(null);
m = applicationInstance.getClass().getMethod("setDockIconImage", javax.swing.ImageIcon.class);
m.invoke(applicationInstance,icon);

This is the Reflection equivalent of that second line in your if block. There's a lot about this that flies in the face of good development practice - you won't get compiler warnings if class/method names change, but it does allow you to make calls on specific types without directly referring to them in your source code. This prevents the class loader from loading those OS-specific classes until you're actually inside the if statement.

To clean this up a bit, you could make a special class that handles all of the Mac-OS specific stuff, wrap up that functionality in a single method, then call that method with Reflection like this example - this will save you one of the Method m = ...-styled calls.

Reflection Caveats

Aside from the fact that this code is ugly and not easy to maintain, Reflection is VERY slow. It's best used in cases like this where you might be initializing something once for the duration of your program. Typically, Reflection is something you want to stay away from for performance reasons, if not for the maintainability issue.

Read this for more info on Reflection performance:

Java Reflection Performance

A more-elegant solution

Rather than replace everything inside that if block with Reflection code, I would create an interface, like the following:

interface SpecificPlatform{
    void initialize(javax.swing.ImageIcon icon);
}

Then, I'd implement it for Mac:

class MacOSX implements SpecificPlatform{
    @Override
    public void initialize(javax.swing.ImageIcon icon){
        System.setProperty("com.apple.mrj.application.apple.menu.about.name", "JoText");
        Application.getApplication().setDockIconImage(new javax.swing.ImageIcon(
            JotextMain.class.getResource("icons/Icon.png")  ).getImage()); // Dock icon
        System.setProperty("apple.laf.useScreenMenuBar", "true"); // Use global menu bar
        Application.getApplication().addApplicationListener(new MacAboutAndQuit());
    }
}

Now... change your original code to something like this:

javax.swing.ImageIcon icon = ...
SpecificPlatform sp = null;

if (System.getProperty("os.name").equals("Mac OS X"))
    sp = (SpecificPlatform)(Class.forName("pkg.name.MacOSX").getConstructor().
            newInstance());
}

//Check for other OS-specific classes here ...

if(sp != null) sp.initialize(icon);

Now you only have one ugly Reflection call per OS, instead of making a whole bunch of ugly Reflection calls for every OS-specific method call you have to make.

Upvotes: 4

Related Questions