在非 MACOS JRE 上捕获 java.lang.NoClassDefFoundError

Catching java.lang.NoClassDefFoundError on Non-MACOS JRE

我正在使用 java swing 开发一个简单的应用程序,我想在 Windows、MacOS 和 Linux 中使用它。 当然,我正在尽我所能将它与 OS 集成。

对于 MacOS 我有这段代码可以让我在全局菜单中设置应用程序名称和 "About" 按钮的操作。

我正在使用以下代码:

  if(System.getProperty("os.name").toUpperCase().startsWith("MAC")){
        System.setProperty("com.apple.mrj.application.apple.menu.about.name", "My app name");
        System.setProperty("apple.awt.application.name", "My app name");
        //Need for macos global menubar
        System.setProperty("apple.laf.useScreenMenuBar", "true");
        try{
        com.apple.eawt.Application app = com.apple.eawt.Application.getApplication();
        app.setDockIconImage(Toolkit.getDefaultToolkit().getImage(MainGUI.class.getResource("images/icon.png")));
        app.setAboutHandler(new com.apple.eawt.AboutHandler() {
            @Override
            public void handleAbout(com.apple.eawt.AppEvent.AboutEvent aboutEvent) {
                AboutDialog a = new AboutDialog();
                a.setTitle("About");
                a.pack();
                a.setResizable(false);
                centerDialogInScreen(a);
                a.setVisible(true);
            }
        });
        } catch (Throwable e){
            //This means that the application is not being run on MAC OS.
            //Just do nothing and go on...
        }
    }

当我 运行 我的应用程序在非 macOS 上 JAVA 因为它的 JRE 没有 com.apple.eawt.* 类 JVM 应该抛出一个 NoDefClassFoundError我正在抓住并继续,对吗?

它似乎并没有这样做,当我启动我的应用程序“.jar”时,我得到以下信息(在 Windows):

Error: A JNI error has occurred, please check your installation and  try again
Exception in thread "main" java.lang.NoClassDefFoundError:  com/apple/eawt/AboutHandler
       at java.lang.Class.getDeclaredMethods0(Native Method)
       at java.lang.Class.privateGetDeclaredMethods(Unknown Source)
       at java.lang.Class.privateGetMethodRecursive(Unknown Source)
       at java.lang.Class.getMethod0(Unknown Source)
       at java.lang.Class.getMethod(Unknown Source)
       at sun.launcher.LauncherHelper.validateMainClass(Unknown Source)
       at sun.launcher.LauncherHelper.checkAndLoadMain(Unknown Source)
Caused by: java.lang.ClassNotFoundException: com.apple.eawt.AboutHandler
       at java.net.URLClassLoader.findClass(Unknown Source)
       at java.lang.ClassLoader.loadClass(Unknown Source)
       at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
       at java.lang.ClassLoader.loadClass(Unknown Source)
       ... 7 more

我在这里错过了什么?

appleclasses 不是标准的Javaclasses,因此在Mac 平台之外不可用。如果您希望您的应用程序是跨平台的,最好的选择是避免使用那些 classes,即使这意味着与 MacOS.

的集成较少

另一方面,您可以为不同的操作系统创建特定的 'About' class 并针对特定的操作系统扩展它(例如 MacAbout、WinAbout、.. ..)

确定class'名称运行时,使用Class.forName动态加载class,避免JRE需要解析不存在的appleclasses.

但是,老实说,我会选择第一个选项并制作一个真正的平台独立应用程序。

抛出 NoClassDefFoundError 的确切时间不固定,但可能取决于 JVM 实现细节。在你的例子中,它是 class 的验证。在 HotSpot 中,验证器不会加载所有引用的 classes,但显然它试图在您的情况下加载 AboutHandler,这可能是由于您的 class 有一个内部 class 实现了 AboutHandler 并且验证者想要检查类型层次结构的一致性,即 AboutHandler 是否真的是一个接口。确切的细节并不重要,因为即使您设法解决它,结果也很脆弱,并且可能会在其他版本或替代 JVM 实现中中断。

如果您想安全起见,您不能直接引用可能不存在的 classes。因此,您可以使用 Class.forNameMethod.invoke 等反射式地执行整个操作,但这会使任何重要的代码变得麻烦。更简单的解决方案是将整个 MacOS 特定代码放入它自己的 class 中,比如 MacSetup。然后,你只需要通过 Class.forName 加载这个 class (只有在检查你在 MacOS 上是 运行 之后)才能分离它。

public class MacSetup implements Runnable {
    @Override
    public void run() {
        System.setProperty("com.apple.mrj.application.apple.menu.about.name", "My app name");
        System.setProperty("apple.awt.application.name", "My app name");
        //Need for macos global menubar
        System.setProperty("apple.laf.useScreenMenuBar", "true");
        com.apple.eawt.Application app = com.apple.eawt.Application.getApplication();
        app.setDockIconImage(Toolkit.getDefaultToolkit().getImage(MainGUI.class.getResource("images/icon.png")));
        app.setAboutHandler(new com.apple.eawt.AboutHandler() {
            @Override
            public void handleAbout(com.apple.eawt.AppEvent.AboutEvent aboutEvent) {
                AboutDialog a = new AboutDialog();
                a.setTitle("About");
                a.pack();
                a.setResizable(false);
                centerDialogInScreen(a);
                a.setVisible(true);
            }
        });
    }
}

主要class:

if(System.getProperty("os.name").toUpperCase().startsWith("MAC")) {
    try {
        Class.forName("MacSetup").asSubclass(Runnable.class).newInstance().run();
    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException ex) {
        // since this code is only executed when we *are* on MacOS, we should report it
        Logger.getLogger("MacSetup").log(Level.SEVERE, null, ex);
    }
}

所以主 class 与 MacSetup class 及其所有引用分离,因为它反射加载此 class 并通过调用其实现方法始终存在的 Runnable 界面,将反射操作减少到必要的最低限度。

好吧,我设法使用反射解决了部分问题。图标部分工作正常,我认为如果我能够将代码转换为反射,"AboutHandler" 部分会工作得很好。 因为这并不重要,而且我有一些更大的鱼要炸,所以我没有太在意。 如果有人设法使用反射获取 AboutHandler,我会将其标记为正确。

if(System.getProperty("os.name").toUpperCase().startsWith("MAC")) {
System.setProperty("com.apple.mrj.application.apple.menu.about.name", "My App Name");
System.setProperty("apple.awt.application.name", "My App Short name");
//Need for macos global menubar
System.setProperty("apple.laf.useScreenMenuBar", "true");

try {
    Class application = Class.forName("com.apple.eawt.Application");
    Method getApplication = application.getMethod("getApplication");
    Object instance = getApplication.invoke(application);

    Class[] params = new Class[1];
    params[0] = Image.class;
    Method setIcon = application.getMethod("setDockIconImage",params);
    setIcon.invoke(instance,Toolkit.getDefaultToolkit().getImage(MainGUI.class.getResource("images/icon.png")));

} catch (ClassNotFoundException | NoSuchMethodException |
        SecurityException | IllegalAccessException |
        IllegalArgumentException | InvocationTargetException exp) {
    exp.printStackTrace(System.err);
}
}

com.apple java 软件包在 java9 中被弃用并删除(现在是 MacOS high sierra 的默认设置)

http://openjdk.java.net/jeps/272 新 api 使用

package java.awt;

public class Desktop {

    /* ... */

    /**
     * Adds sub-types of {@link AppEventListener} to listen for notifications
     * from the native system.
     *
     * @param listener
     * @see AppForegroundListener
     * @see AppHiddenListener
     * @see AppReOpenedListener
     * @see AppScreenSleepListener
     * @see AppSystemSleepListener
     * @see AppUserSessionListener
     */

    public void addAppEventListener(final AppEventListener listener) {}

    /**
     * Requests user attention to this application (usually through bouncing the Dock icon).
     * Critical requests will continue to bounce the Dock icon until the app is activated.
     *
     */
    public void requestUserAttention(final boolean critical) {}

    /**
     * Attaches the contents of the provided PopupMenu to the application's Dock icon.
     */
    public void setDockMenu(final PopupMenu menu) {}

    /**
     * Changes this application's Dock icon to the provided image.
     */
    public void setDockIconImage(final Image image) {}


    /**
     * Affixes a small system provided badge to this application's Dock icon. Usually a number.
     */
    public void setDockIconBadge(final String badge) {}

    /**
     * Displays or hides a progress bar or other indicator in
     * the dock.
     *
     * @see DockProgressState.NORMAL
     * @see DockProgressState.PAUSED
     * @see DockProgressState.ERROR
     *
     * @see #setDockProgressValue
     */
    public void setDockProgressState(int state) {}

    /**
     * Sets the progress bar's current value to {@code n}.
     */
    public void setDockProgressValue(int n) {}

    /**
     * Tests whether a feature is supported on the current platform.
     */

    public boolean isSupportedFeature(Feature f) {}

    /* ... */
}