在线程中使用 Javafx TimeLine

Using Javafx TimeLine In a Thread

我正在尝试创建一个互联网检查器 class,它将检查与某个 url 的连接并相应地更新状态 属性。为了避免 ui 冻结,我想使用一个线程和一个计时器在一定时间间隔后重新检查。问题是时间轴关键帧中的 CHECK 方法调用仍然是从 FX 线程调用的。我如何在线程中使用时间轴?

代码:

public class InternetChecker {
    private String baseUrl;
    /***Properties***/
    private ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.ACTIVE);

    /****************************************************************
     **********                  CONSTRUCTORS            ************
     ****************************************************************/
    public InternetChecker(String baseUrl) {
        this(baseUrl, 1000);
    }

    public InternetChecker(String baseUrl, int millisCheckInterval) {
        this.baseUrl = baseUrl;
        new Thread(() -> {
            Timeline timelineCheck = new Timeline();
            timelineCheck.getKeyFrames().add(
                    new KeyFrame(Duration.millis(millisCheckInterval), e -> {
                        check();
                    }));
            timelineCheck.setCycleCount(Animation.INDEFINITE);
            timelineCheck.play();
        }).start();
    }

    /*******************************
     * Will check if there is an internet connection present
     * and update the status accordingly
     *******************************/
    public void check() {
        // Check if base internet connection
        // is working, if it is we continue
        // to see if domain connection is working
        try {
            if ("127.0.0.1".equals(InetAddress.getLocalHost().getHostAddress())) {
                setStatus(Status.INTERNET_DISCONNECTED);
                return;
            }
        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        }
        // Check if base domain connection is working
        try {
            final URL url = new URL(baseUrl);
            final URLConnection conn = url.openConnection();
            conn.connect();
            conn.getInputStream().close();
            setStatus(Status.ACTIVE);
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            setStatus(Status.BASE_URL_UNREACHABLE);
        }
    }

    /****************************************************************
     **********                  ACCESSORS               ************
     ****************************************************************/

    public Status getStatus() {
        return status.get();
    }

    public ObjectProperty<Status> statusProperty() {
        return status;
    }

    private void setStatus(Status status) {
        this.status.set(status);
    }

    /*******************************
     *  ACTIVE (Base url reachable)
     *  BASE_URL_UNREACHABLE (Internet available, but base url is unreachable)
     *  INTERNET_DISCONNECTED (Internet is not available)
     ********************************/
    public enum Status {
        ACTIVE,
        BASE_URL_UNREACHABLE,
        INTERNET_DISCONNECTED;
    }
}

由于您需要执行与 JavaFX 应用程序线程 通信的周期性 background 任务,因此最好使用 ScheduledService. This class executes a (new) Task 定期使用可由开发人员定义的 Executor。请注意 ScheduledService 扩展了 javafx.concurrent.Service.

以下是您需要执行的操作的框架示例:

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;

public class ConnectionStatusService extends ScheduledService<Status> {

    // Property allows you to change the "baseUrl" between executions
    private final StringProperty baseUrl = new SimpleStringProperty(this, "baseUrl");

    // property getter and setters omitted...

    @Override
    protected Task<Status> createTask() {
        // creates a new Task and gives the current "baseUrl"
        // as an argument. This is called every cycle
        return new ConnectionStatusTask(getBaseUrl());
    }

    private static class ConnectionStatusTask extends Task<Status> {

        // A Task is a one-shot thing and its initial state should be
        // immutable (or at least encapsulated from external modification).
        private final String baseUrl;

        private ConnectionStatusTask(String baseUrl) {
            this.baseUrl = baseUrl;
        }

        @Override
        protected Status call() throws Exception {
            // Do what you need to determine connection status
            return computedStatus;
        }
    }

}

然后你 listen/bind 到 lastValue 属性。

public void initService() {
    ConnectionStatusService service = new ConnectionStatusService();
    service.setBaseUrl(/* your URL */);
    service.setPeriod(Duration.seconds(1)); // run every 1 seconds
    service.lastValueProperty().addListener(/* your listener */); // or bind to this property

    // you may also want to add EventHandlers/Listeners to handle when the
    // service fails and such.
    service.start();
}

观察 lastValue 属性 而不是 value 属性 很重要。原因在lastValue的Javadoc中给出:

The last successfully computed value. During each iteration, the "value" of the ScheduledService will be reset to null, as with any other Service. The "lastValue" however will be set to the most recently successfully computed value, even across iterations. It is reset however whenever you manually call reset or restart.

我建议阅读 TaskServiceScheduledService 的 Javadoc 以获取更多信息。这三个 类 都实现了 javafx.concurrent.Worker 接口。

您只想在 JavaFX 应用程序线程上执行一条语句,即 status.set(status);。由于您计划 运行 此语句之间有一些延迟,您可以简单地使用 Platform.runLater 来执行此操作。

至于重复执行检查:ScheduledExecutorService就是为了这个目的而设计的。

public class InternetChecker implements Runnable {
    private final String baseUrl;
    /***Properties***/

    // use readonly wrapper here to restrict outside access to the property
    private final ReadOnlyObjectWrapper<Status> status = new ReadOnlyObjectWrapper<>(Status.ACTIVE);

    /****************************************************************
     **********                  CONSTRUCTORS            ************
     ****************************************************************/
    public InternetChecker(String baseUrl) {
        this.baseUrl = baseUrl;
    }

    /*******************************
     * Will check if there is an internet connection present
     * and update the status accordingly
     *******************************/
    @Override
    public void run() {
        // Check if base internet connection
        // is working, if it is we continue
        // to see if domain connection is working
        try {
            if ("127.0.0.1".equals(InetAddress.getLocalHost().getHostAddress())) {
                setStatus(Status.INTERNET_DISCONNECTED);
                return;
            }
        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        }
        // Check if base domain connection is working
        try {
            final URL url = new URL(baseUrl);
            final URLConnection conn = url.openConnection();
            conn.connect();
            conn.getInputStream().close();
            setStatus(Status.ACTIVE);
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            setStatus(Status.BASE_URL_UNREACHABLE);
        }
    }

    /****************************************************************
     **********                  ACCESSORS               ************
     ****************************************************************/

    public Status getStatus() {
        return status.get();
    }

    public ReadOnlyObjectProperty<Status> statusProperty() {
        return status.getReadOnlyProperty​();
    }

    private void setStatus(final Status status) {
        Platform.runLater(() -> this.status.set(status));
    }

    /*******************************
     *  ACTIVE (Base url reachable)
     *  BASE_URL_UNREACHABLE (Internet available, but base url is unreachable)
     *  INTERNET_DISCONNECTED (Internet is not available)
     ********************************/
    public enum Status {
        ACTIVE,
        BASE_URL_UNREACHABLE,
        INTERNET_DISCONNECTED;
    }
}
InternetChecker checker = new InternetChecker(url);
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor​();

// use delay here to avoid race condition
executorService.scheduleAtFixedDelay(checker, 0, millisCheckInterval, TimeUnit.MILLISECONDS);

请注意,您需要关闭服务 "manually" 或使用 ThreadFactory 返回守护线程:

ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor​(r -> {
    Thread t = new Thread(r);
    t.setDaemon(true);
    return t;
});