Reglas de subprocesamiento para JavaFX
Hay dos reglas básicas para subprocesos y JavaFX:
- Cualquier código que modifique o acceda al estado de un nodo que sea parte de un escenario gráfico debe ejecutarse en el subproceso de la aplicación JavaFX. Ciertas otras operaciones (por ejemplo, crear un nuevo
Stage
s) también están sujetos a esta regla. - Cualquier código que pueda tardar mucho tiempo en ejecutarse debería ejecutarse en un subproceso en segundo plano (es decir, no en el subproceso de la aplicación FX).
El motivo de la primera regla es que, como la mayoría de los kits de herramientas de IU, el marco se escribe sin ninguna sincronización en el estado de los elementos del escenario gráfico. Agregar sincronización incurre en un costo de rendimiento, y esto resulta ser un costo prohibitivo para los kits de herramientas de interfaz de usuario. Por lo tanto, solo un subproceso puede acceder de forma segura a este estado. Dado que el subproceso de la interfaz de usuario (subproceso de la aplicación FX para JavaFX) necesita acceder a este estado para representar la escena, el subproceso de la aplicación FX es el único subproceso en el que puede acceder al estado del gráfico de escena "en vivo". En JavaFX 8 y versiones posteriores, la mayoría de los métodos sujetos a esta regla realizan comprobaciones y lanzan excepciones de tiempo de ejecución si se infringe la regla. (Esto contrasta con Swing, donde puede escribir código "ilegal" y puede parecer que funciona bien, pero de hecho es propenso a fallas aleatorias e impredecibles en un momento arbitrario). Esta es la causa de la IllegalStateException
estas viendo :estás llamando a courseCodeLbl.setText(...)
desde un subproceso que no sea el subproceso de la aplicación FX.
La razón de la segunda regla es que el subproceso de la aplicación FX, además de ser responsable de procesar los eventos del usuario, también es responsable de representar la escena. Por lo tanto, si realiza una operación de ejecución prolongada en ese subproceso, la interfaz de usuario no se representará hasta que se complete la operación y dejará de responder a los eventos del usuario. Si bien esto no generará excepciones ni causará un estado de objeto corrupto (como lo hará la violación de la regla 1), (en el mejor de los casos) crea una experiencia de usuario deficiente.
Por lo tanto, si tiene una operación de ejecución prolongada (como acceder a una base de datos) que necesita actualizar la interfaz de usuario al finalizar, el plan básico es realizar la operación de ejecución prolongada en un subproceso en segundo plano, devolviendo los resultados de la operación cuando es complete y luego programe una actualización de la interfaz de usuario en el subproceso de la interfaz de usuario (aplicación FX). Todos los kits de herramientas de interfaz de usuario de subproceso único tienen un mecanismo para hacer esto:en JavaFX puede hacerlo llamando a Platform.runLater(Runnable r)
para ejecutar r.run()
en el subproceso de la aplicación FX. (En Swing, puede llamar a SwingUtilities.invokeLater(Runnable r)
para ejecutar r.run()
en el subproceso de envío de eventos AWT). JavaFX (ver más adelante en esta respuesta) también proporciona una API de nivel superior para administrar la comunicación de regreso al subproceso de la aplicación FX.
Buenas prácticas generales para subprocesos múltiples
La mejor práctica para trabajar con varios subprocesos es estructurar el código que se ejecutará en un subproceso "definido por el usuario" como un objeto que se inicializa con algún estado fijo, tiene un método para realizar la operación y, al finalizar, devuelve un objeto. representando el resultado. El uso de objetos inmutables para el estado inicializado y el resultado del cálculo es muy deseable. La idea aquí es eliminar la posibilidad de que cualquier estado mutable sea visible desde múltiples subprocesos en la medida de lo posible. El acceso a los datos de una base de datos se ajusta muy bien a esta expresión:puede inicializar su objeto "trabajador" con los parámetros para el acceso a la base de datos (términos de búsqueda, etc.). Realice la consulta de la base de datos y obtenga un conjunto de resultados, use el conjunto de resultados para completar una colección de objetos de dominio y devuelva la colección al final.
En algunos casos, será necesario compartir el estado mutable entre varios subprocesos. Cuando es absolutamente necesario hacer esto, debe sincronizar cuidadosamente el acceso a ese estado para evitar observar el estado en un estado inconsistente (hay otros problemas más sutiles que deben abordarse, como la vitalidad del estado, etc.). La recomendación fuerte cuando esto es necesario es usar una biblioteca de alto nivel para administrar estas complejidades por usted.
Uso de la API javafx.concurrent
JavaFX proporciona una API de concurrencia
que está diseñado para ejecutar código en un subproceso en segundo plano, con una API diseñada específicamente para actualizar la interfaz de usuario de JavaFX al finalizar (o durante) la ejecución de ese código. Esta API está diseñada para interactuar con java.util.concurrent
API
, que proporciona funciones generales para escribir código multiproceso (pero sin enlaces de interfaz de usuario). La clase clave en javafx.concurrent
es Task
, que representa una única unidad de trabajo única destinada a realizarse en un subproceso en segundo plano. Esta clase define un único método abstracto, call()
, que no toma parámetros, devuelve un resultado y puede generar excepciones comprobadas. Task
implementa Runnable
con su run()
método simplemente invocando call()
. Task
también tiene una colección de métodos que garantizan actualizar el estado en el subproceso de la aplicación FX, como updateProgress(...)
, updateMessage(...)
, etc. Define algunas propiedades observables (por ejemplo, state
y value
):los oyentes de estas propiedades serán notificados de los cambios en el subproceso de la aplicación FX. Finalmente, existen algunos métodos convenientes para registrar controladores (setOnSucceeded(...)
, setOnFailed(...)
, etc); cualquier controlador registrado a través de estos métodos también se invocará en el subproceso de la aplicación FX.
Entonces, la fórmula general para recuperar datos de una base de datos es:
- Crear una
Task
para manejar la llamada a la base de datos. - Inicializar la
Task
con cualquier estado que sea necesario para realizar la llamada a la base de datos. - Implementar la
call()
de la tarea método para realizar la llamada a la base de datos, devolviendo los resultados de la llamada. - Registre un controlador con la tarea para enviar los resultados a la interfaz de usuario cuando se complete.
- Invocar la tarea en un subproceso en segundo plano.
Para acceder a la base de datos, recomiendo encarecidamente encapsular el código de la base de datos real en una clase separada que no sepa nada sobre la interfaz de usuario ( Patrón de diseño de objetos de acceso a datos ). Luego, simplemente haga que la tarea invoque los métodos en el objeto de acceso a datos.
Por lo tanto, es posible que tenga una clase DAO como esta (tenga en cuenta que aquí no hay código de interfaz de usuario):
public class WidgetDAO {
// In real life, you might want a connection pool here, though for
// desktop applications a single connection often suffices:
private Connection conn ;
public WidgetDAO() throws Exception {
conn = ... ; // initialize connection (or connection pool...)
}
public List<Widget> getWidgetsByType(String type) throws SQLException {
try (PreparedStatement pstmt = conn.prepareStatement("select * from widget where type = ?")) {
pstmt.setString(1, type);
ResultSet rs = pstmt.executeQuery();
List<Widget> widgets = new ArrayList<>();
while (rs.next()) {
Widget widget = new Widget();
widget.setName(rs.getString("name"));
widget.setNumberOfBigRedButtons(rs.getString("btnCount"));
// ...
widgets.add(widget);
}
return widgets ;
}
}
// ...
public void shutdown() throws Exception {
conn.close();
}
}
La recuperación de un montón de widgets puede llevar mucho tiempo, por lo que cualquier llamada de una clase de interfaz de usuario (por ejemplo, una clase de controlador) debería programar esto en un subproceso en segundo plano. Una clase de controlador podría verse así:
public class MyController {
private WidgetDAO widgetAccessor ;
// java.util.concurrent.Executor typically provides a pool of threads...
private Executor exec ;
@FXML
private TextField widgetTypeSearchField ;
@FXML
private TableView<Widget> widgetTable ;
public void initialize() throws Exception {
widgetAccessor = new WidgetDAO();
// create executor that uses daemon threads:
exec = Executors.newCachedThreadPool(runnable -> {
Thread t = new Thread(runnable);
t.setDaemon(true);
return t ;
});
}
// handle search button:
@FXML
public void searchWidgets() {
final String searchString = widgetTypeSearchField.getText();
Task<List<Widget>> widgetSearchTask = new Task<List<Widget>>() {
@Override
public List<Widget> call() throws Exception {
return widgetAccessor.getWidgetsByType(searchString);
}
};
widgetSearchTask.setOnFailed(e -> {
widgetSearchTask.getException().printStackTrace();
// inform user of error...
});
widgetSearchTask.setOnSucceeded(e ->
// Task.getValue() gives the value returned from call()...
widgetTable.getItems().setAll(widgetSearchTask.getValue()));
// run the task using a thread from the thread pool:
exec.execute(widgetSearchTask);
}
// ...
}
Observe cómo la llamada al método DAO (potencialmente) de ejecución prolongada está envuelta en una Task
que se ejecuta en un subproceso en segundo plano (a través del descriptor de acceso) para evitar el bloqueo de la interfaz de usuario (regla 2 anterior). La actualización de la interfaz de usuario (widgetTable.setItems(...)
) en realidad se ejecuta de nuevo en el subproceso de la aplicación FX, utilizando la Task
método de devolución de llamada de conveniencia setOnSucceeded(...)
(cumpliendo la regla 1).
En su caso, el acceso a la base de datos que está realizando devuelve un solo resultado, por lo que podría tener un método como
public class MyDAO {
private Connection conn ;
// constructor etc...
public Course getCourseByCode(int code) throws SQLException {
try (PreparedStatement pstmt = conn.prepareStatement("select * from course where c_code = ?")) {
pstmt.setInt(1, code);
ResultSet results = pstmt.executeQuery();
if (results.next()) {
Course course = new Course();
course.setName(results.getString("c_name"));
// etc...
return course ;
} else {
// maybe throw an exception if you want to insist course with given code exists
// or consider using Optional<Course>...
return null ;
}
}
}
// ...
}
Y luego el código de su controlador se vería como
final int courseCode = Integer.valueOf(courseId.getText());
Task<Course> courseTask = new Task<Course>() {
@Override
public Course call() throws Exception {
return myDAO.getCourseByCode(courseCode);
}
};
courseTask.setOnSucceeded(e -> {
Course course = courseTask.getCourse();
if (course != null) {
courseCodeLbl.setText(course.getName());
}
});
exec.execute(courseTask);
Los documentos API para Task
tiene muchos más ejemplos, incluida la actualización del progress
propiedad de la tarea (útil para barras de progreso..., etc.