Подтвердить что ты не робот

NullPointerException в invokeLater при запуске через Java Webstart

После обновления с JRE 1.7.0_21 до 1.7.0_25-b15 мое приложение начало бросать NullPointerException в SwingUtilities.invokeLater(...), когда оно запускается из Java WebStart. Удивительно, когда он выполняется как отдельное приложение (вне JWS), он отлично работает.

Вот вершина стека:

Exception in thread "AWT-EventQueue-2" java.lang.NullPointerException
at sun.awt.SunToolkit.getSystemEventQueueImplPP(SunToolkit.java:1011)
at sun.awt.SunToolkit.getSystemEventQueueImplPP(SunToolkit.java:1007)
at sun.awt.SunToolkit.getSystemEventQueueImpl(SunToolkit.java:1002)
at java.awt.Toolkit.getEventQueue(Toolkit.java:1730)
at java.awt.EventQueue.invokeLater(EventQueue.java:1217)
at javax.swing.SwingUtilities.invokeLater(SwingUtilities.java:1290)
at AppletView$8.setBaseUnits(AppletView.java:536)
    (...)

Чтобы получить полную картину: метод setBaseUnits (..) вызывается как обратный вызов RMI удаленным сервером. Полная трассировка стека довольно длинная.

Есть ли что-то в модели безопасности, которая изменилась в RMI или JWS, что может сломать вещи? Если это так, я бы ожидал некоторого исключения безопасности, но это могло быть что-то, что не было правильно обнаружено в JRE и привело к NPE.

Любые предложения приветствуются.


---- Обновление1:

Есть аналогичные проблемы с обновлением JRE 1.7.0_25, вероятно, в отношении некоторых изменений безопасности и объектов AppContext: https://forums.oracle.com/message/11080621 https://forums.oracle.com/thread/2552799. Я попробовал исправить ошибку: https://forums.oracle.com/message/11082162#11082162, но без каких-либо успехов.

Я вижу 3 приложения AWT-EventQueue в приложении с номерами от 0 до 2. Похоже, JRE создает дополнительные очереди событий для разных контекстов приложения, если программа запускается JWS. В JWS есть 3 AppContext и 3 EVT, и есть только один контекст и EVT, если программа выполнена из IDE.


---- Обновление2:

Существует обходное решение, предложенное гуруманом ниже (спасибо большое). К сожалению, все вызовы SwingUtilities.invokeLater(..) из потоков RMI должны быть заменены, и программа начинает зависеть от внутреннего API Sun JRE.

Я все еще ищу более общий подход, не относящийся к Sun JRE. Я думаю, что это ошибка JRE. Возможно, это может быть исправлено каким-то образом: AppContext не должен быть пустым в потоке RMI.


---- Обновление3:

Я сделал простой тестовый пример, чтобы показать проблему. Он состоит из 4 файлов. Для запуска этого тестового примера необходимо подписать целевую банку (TestCase.jar). Прежде всего укажите правильную базу кода в launch.jnlp, затем запустите сервер с помощью Java Web Start (например, с помощью javaws launch.jnlp). На экране должен появиться следующий кадр:

The server application frame after start

Затем клиент RMI может быть выполнен. После успешного выполнения кадр должен состоять из:

The server application frame after successful RMI call

но если вы попытаетесь выполнить сервер с помощью JWS, вы получите следующее исключение в клиентской программе (исключение распространяется с сервера RMI на клиент RMI):

Exception in thread "main" java.lang.NullPointerException
    at sun.awt.SunToolkit.getSystemEventQueueImplPP(SunToolkit.java:1011)
    at sun.awt.SunToolkit.getSystemEventQueueImplPP(SunToolkit.java:1007)
    at sun.awt.SunToolkit.getSystemEventQueueImpl(SunToolkit.java:1002)
    at java.awt.Toolkit.getEventQueue(Toolkit.java:1730)
    at java.awt.EventQueue.invokeLater(EventQueue.java:1217)
    at javax.swing.SwingUtilities.invokeLater(SwingUtilities.java:1290)
    at testcase.RmiServiceImpl.callBack(RmiServiceImpl.java:70)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:322)
    at sun.rmi.transport.Transport$1.run(Transport.java:177)
    at sun.rmi.transport.Transport$1.run(Transport.java:174)
    at java.security.AccessController.doPrivileged(Native Method)
    at sun.rmi.transport.Transport.serviceCall(Transport.java:173)
    at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:553)
    at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:808)
    at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:667)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:724)
    at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:273)
    at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:251)
    at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:160)
    at java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod(RemoteObjectInvocationHandler.java:194)
    at java.rmi.server.RemoteObjectInvocationHandler.invoke(RemoteObjectInvocationHandler.java:148)
    at com.sun.proxy.$Proxy0.callBack(Unknown Source)
    at testcase.RmiClient.main(RmiClient.java:22)

Итак, вот файлы тестовых файлов:

1) Определение файла JNLP launch.jnlp:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<jnlp codebase="file:/home/user/NetBeansProjects/TestCase/dist/" href="launch.jnlp" spec="1.0+">
    <information>
        <title>TestCase</title>
        <vendor>digital_infinity</vendor>
        <homepage href=""/>
        <description>TestCase</description>
        <description kind="short">TestCase</description>
    </information>
<security>
  <all-permissions/>
</security>
    <update check="always"/>
    <resources>
        <j2se version="1.7+"/>
        <jar href="TestCase.jar" main="true"/>
    </resources>
    <application-desc main-class="testcase.RmiServiceImpl">
    </application-desc>
</jnlp>

2) Определение интерфейса RMI (RmiService.java):

package testcase;    
public interface RmiService extends java.rmi.Remote  {
    void callBack() throws java.rmi.RemoteException;
}

3) Код службы RMI и основной класс службы:

package testcase;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.swing.JFrame;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

/**
 */
public class RmiServiceImpl extends java.rmi.server.UnicastRemoteObject 
implements RmiService {

    final static int PORT = 1099;

    static JFrame frame;
    static JTextField textField;

    public RmiServiceImpl() throws RemoteException {
        super(PORT);
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) throws Exception {
        Registry reg;
        RmiServiceImpl service = new RmiServiceImpl();
        try {
            reg = LocateRegistry.getRegistry(PORT);
            reg.rebind("test", service);
        } catch (RemoteException ex) {
            reg = LocateRegistry.createRegistry(PORT);
            reg.rebind("test", service);
        }
        SwingUtilities.invokeAndWait(new Runnable() {
            @Override
            public void run() {
                frame = new JFrame("Test App");
                textField = new JTextField("Before call to callBack");
                frame.getContentPane().add(textField);
                frame.pack();
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setVisible(true);
            }
        });
    }

    /** RMI callback */
    public void callBack() {
        Runnable rn = new Runnable() {
            public void run() {
                textField.setText("CallBack succesfully called.");
                frame.pack();
            }
        };
        SwingUtilities.invokeLater(rn);
    }
}

4) Простой код клиента:

package testcase;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RmiClient {
    public static void main(String[] args) throws Exception {
        //now we trying to communicate with object through RMI
        Registry reg = LocateRegistry.getRegistry(RmiServiceImpl.PORT);
        //after got the registry, lookup the object and finally do call
        RmiService serv = (RmiService) reg.lookup("test");
        serv.callBack();
    }
}

---- Обновление4:

JRE Bug Я отправил: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=8019272

Другие связанные ошибки:

4b9b3361

Ответ 1

Проблема возникает в среде Webstart. До версии Webstart Java 7u25 AppContext был установлен в группе системных потоков. Тем не менее он установлен в основной группе потоков.

Если у вас есть нить, основанная на группе потоков, где ее родитель или дедушка не является основной группой нитей, у нее нет sun.awt.AppContext.

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

Runnable task = ....
ThreadGroup threadGroup = System.getSecurityManager() != null
                                    ? System.getSecurityManager().getThreadGroup()
                                    : Thread.currentThread().getThreadGroup();
Thread t = new Thread(threadGroup, task, "my thread", 0);

Ответ 2

Я нашел то, что считаю лучшим решением этой ошибки.

Я просто добавил следующий код перед вызовом SwingUtilities или любого связанного с Swing компонента. Он создает новый AppContext для потока RMI (поток RMI должен быть текущим потоком при запуске кода ниже).

if(AppContext.getAppContext() == null){
    SunToolkit.createNewAppContext();
}

В связи с потребностями моего приложения я смог добавить его по одному методу, использующему SwingUtilities, но вам может потребоваться добавить его ко всем методам в свой RMI Callable Object.

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

Ответ 3

Ниже приведено обходное решение для JDK-8019274, упакованное в класс утилиты. Для нас invokeAndWait() по-прежнему остается проблемой. В этом примере существующее исправление для invokeLater() и новое исправление для invokeAndWait().

Примечания:

  • Вам нужно включить jnlp.jar в свой проект.
  • Вызов init() в начале вашего метода main() перед вызовом invokeLater()
  • Заменить все ваши вызовы на SwingUtilities invokeLater() и invokeAndWait() с помощью этих вызовов

(Отказ от ответственности: это из нашего продукта. Некоторые аспекты этого решения могут не относиться к вам.)

public class JreFix {
    private static String badVersionInfo = null;
    private static AppContext awtEventDispatchContext = null;
    private static AppContext mainThreadContext = null;
    private static Boolean isWebStart = null;
    private static BasicService basicService = null;
    private static IntegrationService integrationService = null;

    /**
     * Call this early in main().  
     */
    public static void init() {
        if (isWebstart() && isApplicableJvmType()) {
            String javaVersion = System.getProperty("java.version");

            if ("1.7.0_25".equals(javaVersion)) {
                badVersionInfo = "7u25";
            }
            else if ("1.7.0_40".equals(javaVersion)) {
                badVersionInfo = "7u40";
            }
            else if (javaVersion != null && "1.6.0_51".equals(javaVersion.substring(0,8))) {
                badVersionInfo = "6u51";
            }
            else if ("javaws-10.25.2.16".equals(System.getProperty("javawebstart.version"))) {
                badVersionInfo = "Web Start 10.25.2.16";
            }
        }

        if (badVersionInfo != null) {
            mainThreadContext = AppContext.getAppContext();
            try {
                SwingUtilities.invokeAndWait(new Runnable() {
                    public void run() {
                        awtEventDispatchContext = AppContext.getAppContext();
                    }
                });
            }
            catch (Exception e) {
                displayErrorAndExit(null);
            }

            if (mainThreadContext == null || awtEventDispatchContext == null) {
                 displayErrorAndExit(null);
            }
        }
    }

    public static void invokeNowOrLater(Runnable runnable) {
        if (hasAppContextBug()) {
            invokeLaterOnAwtEventDispatchThreadContext(runnable);
        }
        else {
            SwingUtilities.invokeLater(runnable);
        }
    }

    public static void invokeNowOrWait(Runnable runnable) {
        if (hasAppContextBug()) {
            fixThreadAppContext(null);
        }

        try {
            SwingUtilities.invokeAndWait(runnable);
        } 
        catch (Exception e) {
            // handle it
        }
    }

    public static boolean hasAppContextBug() {
        return isJreWithAppContextBug() && AppContext.getAppContext() == null;
    }

    public static void invokeLaterOnAwtEventDispatchThreadContext(Runnable runnable) {
        sun.awt.SunToolkit.invokeLaterOnAppContext(awtEventDispatchContext, runnable);
    }

    public static void fixThreadAppContext(Component parent) {
        try {
            final Field field = AppContext.class.getDeclaredField("threadGroup2appContext");
            field.setAccessible(true);
            Map<ThreadGroup, AppContext> threadGroup2appContext = (Map<ThreadGroup, AppContext>)field.get(null);
            final ThreadGroup currentThreadGroup = Thread.currentThread().getThreadGroup();
            threadGroup2appContext.put(currentThreadGroup, mainThreadContext);
        } 
        catch (Exception e) {
            displayErrorAndExit(parent);
        }

        if (AppContext.getAppContext() == null) {
             displayErrorAndExit(parent);
        }
    }

    private static boolean isJreWithAppContextBug() {
        return badVersionInfo != null;
    }

    private static void displayErrorAndExit(Component parent) {
        JLabel msgLabel = new JLabel("<html>" + 
                "Our application cannot run using <b>Web Start</b> with this version of Java.<p><p>" +
                "Java " + badVersionInfo + " contains a bug acknowledged by Oracle (JDK-8019274).");
        JOptionPane.showMessageDialog(parent, msgLabel, "Java Version Error", JOptionPane.ERROR_MESSAGE);
        System.exit(1);
    }

    private static boolean isApplicableJvmType() {
        String vendor = System.getProperty("java.vendor");
        String vmName = System.getProperty("java.vm.name");
        if (vendor != null && vmName != null) {
            return vmName.contains("Java HotSpot") &&
                    (vendor.equals("Oracle Corporation") || 
                     vendor.equals("Sun Microsystems Inc."));
        }

        return false;
    }

    private static boolean isWebstart() {
        if (isWebStart == null) {
            try { 
                basicService = (BasicService) ServiceManager.lookup("javax.jnlp.BasicService");             
                isWebStart = true;
            } 
            catch (UnavailableServiceException e) { 
                isWebStart = false;
            }           

            try {
                integrationService = (IntegrationService) ServiceManager.lookup("javax.jnlp.IntegrationService");
            } 
            catch (UnavailableServiceException e) {
            }
        }
        return isWebStart;
    }
}

Ответ 4

Java 7u65, который вышел вчера (2014-07-15), утверждает, что исправил эту или очень похожую проблему в JDK-8019724. Я тестирую прямо сейчас, чтобы узнать - драйвер от одного из наших поставщиков не работает в Java Web Start, и он удерживает нас на Java 6.

ETA: Да, похоже, что это устраняет наши проблемы!