JAR捆绑器使用OSXAdapter导致应用程序延迟或终止。

4
我已经创建了一个简单的Java应用程序,每秒钟连续10秒向JTable添加一行新数据。它由三个类组成。 主类在程序启动时被调用
public class JarBundlerProblem {
    public static void main(String[] args)
    {
        System.err.println("Initializing controller");
        new Controller();
    }
}

一个控制器创建 GUI 并通过 doWork() 进行修改。
public class Controller {
    public Controller()
    {
        doWork(null);
    }
    public static void doWork(String s)
    {
        GUI gui = new GUI();
        
        for (int i=0; i<10; i++)
        {
            gui.addRow("Line "+(i+1));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

最后是 GUI

(GUI指的是图形用户界面)。
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;

public class GUI {
    private JFrame frame = new JFrame();
    private DefaultTableModel model = new DefaultTableModel();
    private JTable table = new JTable(model);
    private JScrollPane pane = new JScrollPane(table);
    
    public GUI()
    {
        model.addColumn("Name");
        
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(pane);
        frame.pack();
        frame.setVisible(true);
    }
    public void addRow(String name)
    {
        model.addRow(new Object[]{name});
    }
}

因为我正在开发适用于OS X的应用程序,并且需要将应用程序与特定文件类型(比如说.jarbundlerproblem)关联起来,所以我需要使用苹果Jar Bundler将我的JAR文件打包成APP文件。我已经成功完成了这个过程,我的应用程序能够打开并运行,每秒钟都会计数。

现在是问题

默认情况下,在OS X中双击一个.jarbundlerproblem文件并将其关联到我的应用程序时,不会将我双击的文件作为参数传递给应用程序。显然,这是由于 Java 在 OS X 上的工作方式。

因为我需要能够看到双击的文件,所以我使用了OSXAdapter库,这是苹果专门为此目的制作的 Java 库。我通过修改Controller类的构造函数并添加另一个方法registerForMacOSXEvents()来实现这一点:

public Controller()
{
    registerForMacOSXEvents();
    //doWork(null);
}
public void registerForMacOSXEvents() {
    try {
        OSXAdapter.setFileHandler(this, getClass().getDeclaredMethod("doWork", new Class[] { String.class }));
    } catch (Exception e) {
        System.err.println("Error while loading the OSXAdapter:");
        e.printStackTrace();
    }
}

但是在这个(小)修改后,我的应用开始出现问题。有时候,它不会打开,即使我可以在控制台中看到它刚刚启动了(正在初始化控制器已经被写入),但经过几次尝试后,它最终会启动,但窗口在前10秒钟内完全空白,在此之后,10行将被添加。

帮助

现在,我已经为此苦苦挣扎了很长时间,似乎没有关于OSXAdapter或Jar Bundler的文档。我做错了什么?或者我一开始就不应该使用OSXAdapter或Jar Bundler吗?


只是为了确保我理解OSXAdapter(不熟悉Mac):基本上它是一个在你控制之外运行的线程,通过向doWork发送一个字符串(文件名?)来报告其结果,直到准备就绪,注册后立即启动。它如何发出准备就绪的信号? - kleopatra
很高兴能帮忙 :-) 请您展示一下您的解决方案(不包括待处理列表)好吗? - kleopatra
@kleopatra 我不确定 OSXAdapter 是如何工作的,我只是熟悉它而已。如果你想了解更多关于它的信息,可以在 http://developer.apple.com/library/mac/#samplecode/OSXAdapter/Listings/src_OSXAdapter_java.html#//apple_ref/doc/uid/DTS10000685-src_OSXAdapter_java-DontLinkElementID_5 找到它的源代码。 - kba
@kleopatra 我的解决方案是基于您的想法并将其实现到我的应用程序中,代码量比上面展示的要多得多,因此展示我的解决方案会很困难。不过我可以解释一下我所做的:您在 run() 中的所有工作都已经被我移动到了 doInBackground() 中,并且在 run() 中添加了 execute()doWork() 已经完全删除。每当 Controller.doInBackground() 中的一个迭代完成时,它只是调用 publish() - kba
嗯...不太理解,但如果它能工作的话 :-) 只是要注意:文档说明SwingWorker被设计为仅执行一次 - 如果您反复调用它可能会产生副作用。 - kleopatra
重点是你在run()方法中完成了所有的工作,然后调用doWork()方法并让它调用doInBackground()方法。其实不需要三个方法,一个就足够了。此外,如果从doInBackground()方法中删除Thread.sleep(),那么while循环可能会停止,doInBackground()方法终止,因此最后几行不会被发布到GUI上。这也不再发生。 - kba
3个回答

6
看起来你正在阻塞事件分发线程(EDT)。SwingWorker 是更好的选择,但这个例子实现了Runnable
补充:你可以查看这个项目作为MVC架构的一个例子。它还展示了如何构建Mac OS应用程序包而不使用JAR Bundler。更多关于MVC的内容可以在这里找到。
另外,这个例子展示了自动滚动JTable的一种方法。点击拇指暂停滚动;释放以恢复。
补充说明:您的应用程序在启动时会有10秒钟的延迟。由于这正是Controller休眠的时间,所以它肯定正在EDT上休眠。一个sscce将是确凿无疑的证据。相反,您可以在另一个线程上完成工作,并在EDT上更新模型。 SwingWorker具有自动执行此操作的process()方法,或者您可以像下面示例那样使用invokeLater()。在正确同步您的应用程序之前,很难让Apple事件正常工作。
补充说明:您可以在Controller中调用isDispatchThread()进行检查。引用的项目包括一个带有Mac应用程序和一个ant文件的.dmg,通过目标dist2在现场构建捆绑包。
补充说明:还可以参见此处显示的其他方法。

enter image description here

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;

/** @seehttps://dev59.com/KVvUa4cB1Zd3GeqPuYrx */
public class TableAddTest extends JPanel implements Runnable {

    private static final int N_ROWS = 8;
    private static String[] header = {"ID", "String", "Number", "Boolean"};
    private DefaultTableModel dtm = new DefaultTableModel(null, header) {

        @Override
        public Class<?> getColumnClass(int col) {
            return getValueAt(0, col).getClass();
        }
    };
    private JTable table = new JTable(dtm);
    private JScrollPane scrollPane = new JScrollPane(table);
    private JScrollBar vScroll = scrollPane.getVerticalScrollBar();
    private JProgressBar jpb = new JProgressBar();
    private int row;
    private boolean isAutoScroll;

    public TableAddTest() {
        this.setLayout(new BorderLayout());
        jpb.setIndeterminate(true);
        this.add(jpb, BorderLayout.NORTH);
        Dimension d = new Dimension(320, N_ROWS * table.getRowHeight());
        table.setPreferredScrollableViewportSize(d);
        for (int i = 0; i < N_ROWS; i++) {
            addRow();
        }
        scrollPane.setVerticalScrollBarPolicy(
            JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        vScroll.addAdjustmentListener(new AdjustmentListener() {

            @Override
            public void adjustmentValueChanged(AdjustmentEvent e) {
                isAutoScroll = !e.getValueIsAdjusting();
            }
        });
        this.add(scrollPane, BorderLayout.CENTER);
        JPanel panel = new JPanel();
        panel.add(new JButton(new AbstractAction("Add Row") {

            @Override
            public void actionPerformed(ActionEvent e) {
                addRow();
            }
        }));
        this.add(panel, BorderLayout.SOUTH);
    }

    private void addRow() {
        char c = (char) ('A' + row++ % 26);
        dtm.addRow(new Object[]{
                Character.valueOf(c),
                String.valueOf(c) + String.valueOf(row),
                Integer.valueOf(row),
                Boolean.valueOf(row % 2 == 0)
            });
    }

    private void scrollToLast() {
        if (isAutoScroll) {
            int last = table.getModel().getRowCount() - 1;
            Rectangle r = table.getCellRect(last, 0, true);
            table.scrollRectToVisible(r);
        }
    }

    @Override
    public void run() {
        while (true) {
            EventQueue.invokeLater(new Runnable() {

                @Override
                public void run() {
                    addRow();
                }
            });
            EventQueue.invokeLater(new Runnable() {

                @Override
                public void run() {
                    scrollToLast();
                }
            });
            try {
                Thread.sleep(1000); // simulate latency
            } catch (InterruptedException ex) {
                System.err.println(ex);
            }
        }
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                JFrame f = new JFrame();
                f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                TableAddTest nlt = new TableAddTest();
                f.add(nlt);
                f.pack();
                f.setLocationRelativeTo(null);
                f.setVisible(true);
                new Thread(nlt).start();
            }
        });
    }
}

抱歉,我不明白。我的更新方法?你是指 GUI.addRow() 吗?并且你想让我重写 modeladdRow() 方法吗?我有点不理解。 - kba
@trashgod:重写DefaultTableModel.addRow()有什么帮助吗?问题是这个方法在应该被调用时没有被调用。而且,我也更喜欢保持分离,我喜欢MVC模式,但我仍然不明白您建议我在我的应用程序中做什么。 - kba
不,你说我正在EDT上工作,但事实并非如此。所有的工作都在控制器中完成。此外,你建议制作一个没有JAR Bundler的OS X应用程序,但你链接的项目只是JAR文件;一个未打包的应用程序,这意味着它不涉及我依赖的OSXAdapter。 - kba
我相信你在这两个方面都弄错了;更重要的是。 - trashgod
1
@KristianAntonsen 你做错的是在EDT之外访问UI。所有的更改都必须在EDT上进行,没有例外。Trashgod展示了如何使用invokeLater来实现,另一个(在你的情况下更好的选择,我个人认为)是实现SwingWorker - 阅读它的API文档以查看一个完美适用于你问题的示例。 - kleopatra
显示剩余5条评论

4
完成后,我并不完全相信SwingWorker是一个更简单(也就是更好)的解决方案 - 它仍需要额外的线程同步(在工作线程和传递文件/名称的“外部”线程之间)。 无论如何(借此机会学习,并通过错误:),以下是基本想法的简陋概念验证示例:

  • 将控制器实现为SwingWorker,从而将来自外部线程的输入导入EDT
  • 使其通过方法doWork(...)接受输入(例如来自适配器等),该方法将输入排队以进行发布
  • 实现doInBackground以依次发布输入

未解决问题:

  • 同步访问本地列表(不是并发专家,但相当确定需要这样做)
  • 可靠地检测到外部线程的结束(这里仅在输入队列为空时停止)

欢迎反馈 :-)

public class GUI {
    private JFrame frame = new JFrame();
    private DefaultTableModel model = new DefaultTableModel();
    private JTable table = new JTable(model);
    private JScrollPane pane = new JScrollPane(table);

    public GUI() {
        model.addColumn("Name");

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(pane);
        frame.pack();
        frame.setVisible(true);
    }

    public void addRow(String name) {
        model.addRow(new Object[] { name });
    }

    /**
     * Controller is a SwingWorker.
     */
    public static class Controller extends SwingWorker<Void, String> {
        private GUI gui;

        private List<String> pending;

        public Controller() {
            gui = new GUI();
        }

        public void doWork(String newLine) {
            if (pending == null) {
                pending = new ArrayList<String>();
                pending.add(newLine);
                execute();
            } else {
                pending.add(newLine);
            }
        }

        @Override
        protected Void doInBackground() throws Exception {
            while (pending.size() > 0) {
                publish(pending.remove(0));
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return null;
        }

        /**
         * @inherited <p>
         */
        @Override
        protected void process(List<String> chunks) {
            for (String object : chunks) {
                gui.addRow(object);
            }
        }

    }

    /** 
     * Simulating the adapter.
     * 
     *  Obviously, the real-thingy wouldn't have a reference 
     *  to the controller, but message the doWork refectively 
     */
    public static class Adapter implements Runnable {

        Controller controller;

        public Adapter(Controller controller) {
            this.controller = controller;
        }

        @Override
        public void run() {
            for (int i=0; i<10; i++)
            {
                controller.doWork("Line "+(i+1));
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }
    public static void main(String[] args)
    {
        System.err.println("Initializing controller");
        new Adapter(new Controller()).run();
    }

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger.getLogger(GUI.class.getName());
}

+1 重新打开问题:在 doWork() 上使用 synchronized 应该足够了,而 SwingWorkerget() 方法将等待其后台线程退出。我已经添加了附近的连续变化。 - trashgod
谢谢,kleopatra。使用您的示例,我终于成功地让它全部运行起来了。不过,我将类分开,并将所有工作移动到“doInBackground”中,并完全摆脱了“pending”列表。 - kba
我同意SwingWorker是更好的选择;听起来似乎没有需要等待队列。 - trashgod

2
这是 @kleopatra 的 示例 的一种变体,在这个变体中,一个持续运行的 ControllerdoWork() 中接受新条目,而 SwingWorker 在其后台线程异步处理这些 pending 条目。ArrayBlockingQueue 处理同步。
import java.awt.EventQueue;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingWorker;
import javax.swing.table.DefaultTableModel;

public class GUI {

    private static final Random rnd = new Random();
    private JFrame frame = new JFrame();
    private DefaultTableModel model = new DefaultTableModel();
    private JTable table = new JTable(model);
    private JScrollPane pane = new JScrollPane(table);

    public GUI() {
        model.addColumn("Name");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(pane);
        frame.pack();
        frame.setVisible(true);
    }

    public void addRow(String name) {
        model.addRow(new Object[]{name});
    }

    /**
     * Controller is a SwingWorker.
     */
    private static class Controller extends SwingWorker<Void, String> {

        private static final int MAX = 5;
        private GUI gui;
        private BlockingQueue<String> pending =
            new ArrayBlockingQueue<String>(MAX);

        public Controller() {
            EventQueue.invokeLater(new Runnable() {

                @Override
                public void run() {
                    gui = new GUI();
                }
            });
        }

        private void doWork(String newLine) {
            try {
                pending.put(newLine);
            } catch (InterruptedException e) {
                e.printStackTrace(System.err);
            }
        }

        @Override
        protected Void doInBackground() throws Exception {
            while (true) {
                // may block if nothing pending
                publish(pending.take());
                try {
                    Thread.sleep(rnd.nextInt(500)); // simulate latency
                } catch (InterruptedException e) {
                    e.printStackTrace(System.err);
                }
            }
        }

        @Override
        protected void process(List<String> chunks) {
            for (String object : chunks) {
                gui.addRow(object);
            }
        }
    }

    /** 
     * Exercise the Controller.
     */
    private static class Adapter implements Runnable {

        private Controller controller;

        private Adapter(Controller controller) {
            this.controller = controller;
        }

        @Override
        public void run() {
            controller.execute();
            int i = 0;
            while (true) {
                // may block if Controller busy
                controller.doWork("Line " + (++i));
                try {
                    Thread.sleep(rnd.nextInt(500)); // simulate latency
                } catch (InterruptedException e) {
                    e.printStackTrace(System.err);
                }
            }
        }
    }

    public static void main(String[] args) {
        System.out.println("Initializing controller");
        // Could run on inital thread via
        // new Adapter(new Controller()).run();
        // but we'll start a new one
        new Thread(new Adapter(new Controller())).start();
    }
}

谢谢你的回答,但是我已经完全从我的实际项目中删除了 pending。 :-) - kba
谢谢 - ArrayBlockingQueue 是我今天学到的东西 :-) 并且 +1 为在 EDT 上创建 UI,我的健忘是出了名的... - kleopatra
@kleopatra:我非常欢迎您的见解。很高兴您觉得ArrayBlockingQueue有趣。我已经更新了代码,使用了接口BlockingQueue,该接口针对各种用例都有实现。 - trashgod

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接