¿Es seguro iniciar un nuevo hilo en un bean gestionado JSF?
concurrency java-ee (3)
Introducción
Engendrar subprocesos dentro de un bean administrado con ámbito de sesión no es necesariamente un hack siempre que haga el trabajo que desea. Pero el engendrar hilos en sí mismo debe hacerse con extremo cuidado. El código no debe escribirse de esa manera que un solo usuario pueda, por ejemplo, engendrar una cantidad ilimitada de hilos por sesión y / o que los hilos continúen ejecutándose incluso después de que se destruya la sesión. Estallaría tu aplicación tarde o temprano.
El código debe escribirse de esa manera para garantizar que un usuario, por ejemplo, nunca genere más de un hilo de fondo por sesión y que el hilo quede interrumpido siempre que se destruya la sesión. Para tareas múltiples dentro de una sesión, debe poner en cola las tareas.
Además, todos estos subprocesos deberían ser servidos preferiblemente por un grupo de subprocesos común para que pueda poner un límite en la cantidad total de subprocesos generados en el nivel de aplicación. El servidor de aplicaciones Java EE promedio ofrece un grupo de subprocesos gestionados por contenedor que puede utilizar, entre otros, @Asynchronous
y @Schedule
. Para ser independiente del contenedor, también puede utilizar el Util Execurrent ExecutorService
Java 1.5 y el ScheduledExecutorService
para esto.
A continuación, los ejemplos suponen Java EE 6+ con EJB.
Dispara y olvida una tarea en el envío del formulario
@Named
@RequestScoped // Or @ViewScoped
public class Bean {
@EJB
private SomeService someService;
public void submit() {
someService.asyncTask();
// ... (this code will immediately continue without waiting)
}
}
@Stateless
public class SomeService {
@Asynchronous
public void asyncTask() {
// ...
}
}
Obtener de forma asíncrona el modelo en la carga de la página
@Named
@RequestScoped // Or @ViewScoped
public class Bean {
private Future<List<Entity>> asyncEntities;
@EJB
private EntityService entityService;
@PostConstruct
public void init() {
asyncEntities = entityService.asyncList();
// ... (this code will immediately continue without waiting)
}
public List<Entity> getEntities() {
try {
return asyncEntities.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new FacesException(e);
} catch (ExecutionException e) {
throw new FacesException(e);
}
}
}
@Stateless
public class EntityService {
@PersistenceContext
private EntityManager entityManager;
@Asynchronous
public Future<List<Entity>> asyncList() {
List<Entity> entities = entityManager
.createQuery("SELECT e FROM Entity e", Entity.class)
.getResultList();
return new AsyncResult<>(entities);
}
}
En caso de que esté utilizando la biblioteca de utilidades JSF OmniFaces , esto podría hacerse aún más rápido si anota el bean administrado con @Eager
.
Programar trabajos en segundo plano al inicio de la aplicación
@Singleton
public class BackgroundJobManager {
@Schedule(hour="0", minute="0", second="0", persistent=false)
public void someDailyJob() {
// ... (runs every start of day)
}
@Schedule(hour="*/1", minute="0", second="0", persistent=false)
public void someHourlyJob() {
// ... (runs every hour of day)
}
@Schedule(hour="*", minute="*/15", second="0", persistent=false)
public void someQuarterlyJob() {
// ... (runs every 15th minute of hour)
}
@Schedule(hour="*", minute="*", second="*/30", persistent=false)
public void someHalfminutelyJob() {
// ... (runs every 30th second of minute)
}
}
Continuamente actualiza el modelo de toda la aplicación en el fondo
@Named
@RequestScoped // Or @ViewScoped
public class Bean {
@EJB
private SomeTop100Manager someTop100Manager;
public List<Some> getSomeTop100() {
return someTop100Manager.list();
}
}
@Singleton
@ConcurrencyManagement(BEAN)
public class SomeTop100Manager {
@PersistenceContext
private EntityManager entityManager;
private List<Some> top100;
@PostConstruct
@Schedule(hour="*", minute="*/1", second="0", persistent=false)
public void load() {
top100 = entityManager
.createNamedQuery("Some.top100", Some.class)
.getResultList();
}
public List<Some> list() {
return top100;
}
}
Ver también:
No pude encontrar una respuesta definitiva a si es seguro engendrar hilos dentro de los beans manejados con JSF con alcance de sesión. El hilo necesita llamar a métodos en la instancia EJB sin estado (que fue inyectada por dependencia al bean administrado).
El trasfondo es que tenemos un informe que lleva mucho tiempo generar. Esto provocó que la solicitud HTTP expirara debido a la configuración del servidor que no podemos cambiar. Entonces la idea es comenzar un nuevo hilo y dejarlo generar el informe y almacenarlo temporalmente. Mientras tanto, la página JSF muestra una barra de progreso, sondea el bean administrado hasta que se completa la generación y luego realiza una segunda solicitud para descargar el informe almacenado. Esto parece funcionar, pero me gustaría estar seguro de que lo que estoy haciendo no es un truco.
Consulte EJB 3.1 @Asynchronous methods
. Esto es exactamente para lo que son.
Pequeño ejemplo que usa OpenEJB 4.0.0-SNAPSHOTs. Aquí tenemos un bean @Singleton
con un método marcado @Asynchronous
. Cada vez que ese método es invocado por cualquier persona, en este caso su bean administrado por JSF, regresará inmediatamente sin importar cuánto tiempo tome realmente el método.
@Singleton
public class JobProcessor {
@Asynchronous
@Lock(READ)
@AccessTimeout(-1)
public Future<String> addJob(String jobName) {
// Pretend this job takes a while
doSomeHeavyLifting();
// Return our result
return new AsyncResult<String>(jobName);
}
private void doSomeHeavyLifting() {
try {
Thread.sleep(SECONDS.toMillis(10));
} catch (InterruptedException e) {
Thread.interrupted();
throw new IllegalStateException(e);
}
}
}
Aquí hay un pequeño caso de prueba que invoca ese método @Asynchronous
varias veces seguidas.
Cada invocación devuelve un objeto Future que, en esencia, comienza en vacío y luego su valor será rellenado por el contenedor cuando la llamada al método relacionado realmente se complete.
import javax.ejb.embeddable.EJBContainer;
import javax.naming.Context;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class JobProcessorTest extends TestCase {
public void test() throws Exception {
final Context context = EJBContainer.createEJBContainer().getContext();
final JobProcessor processor = (JobProcessor) context.lookup("java:global/async-methods/JobProcessor");
final long start = System.nanoTime();
// Queue up a bunch of work
final Future<String> red = processor.addJob("red");
final Future<String> orange = processor.addJob("orange");
final Future<String> yellow = processor.addJob("yellow");
final Future<String> green = processor.addJob("green");
final Future<String> blue = processor.addJob("blue");
final Future<String> violet = processor.addJob("violet");
// Wait for the result -- 1 minute worth of work
assertEquals("blue", blue.get());
assertEquals("orange", orange.get());
assertEquals("green", green.get());
assertEquals("red", red.get());
assertEquals("yellow", yellow.get());
assertEquals("violet", violet.get());
// How long did it take?
final long total = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start);
// Execution should be around 9 - 21 seconds
assertTrue("" + total, total > 9);
assertTrue("" + total, total < 21);
}
}
Bajo las cubiertas, lo que hace que este trabajo sea:
- El
JobProcessor
ve la persona que llama no es realmente una instancia deJobProcessor
. Más bien es una subclase o proxy que tiene todos los métodos anulados. Los métodos que se supone que son asincrónicos se manejan de manera diferente. - Las llamadas a un método asíncrono simplemente dan como resultado la
Runnable
que envuelve el método y los parámetros que usted proporcionó. Este ejecutable se le da a un Executor que es simplemente una cola de trabajo adjunta a un grupo de subprocesos. - Después de agregar el trabajo a la cola, la versión proxiada del método devuelve una implementación de
Future
que está vinculada alRunnable
que ahora está esperando en la cola. - Cuando
Runnable
finalmente ejecuta el método en la instancia real deJobProcessor
, tomará el valor de retorno y lo establecerá en elFuture
que esté disponible para la persona que llama.
Es importante tener en cuenta que el objeto AsyncResult
que JobProcessor
devuelve no es el mismo objeto Future
que la persona que llama está reteniendo. Habría sido estupendo si el verdadero JobProcessor
pudiera simplemente devolver String
y la versión del JobProcessor
de JobProcessor
pudiera devolver Future<String>
, pero no vimos ninguna forma de hacerlo sin agregar más complejidad. Entonces AsyncResult
es un objeto contenedor simple. El contenedor AsyncResult
el String
, arrojará el AsyncResult
y luego colocará el String
en el Future
real que sostiene el llamador.
Para avanzar en el camino, simplemente pase un objeto seguro para AtomicInteger como AtomicInteger al método @Asynchronous
y @Asynchronous
que el código del bean lo actualice periódicamente con el porcentaje completado.
Intenté esto y funciona muy bien desde mi frijol administrado JSF
ExecutorService executor = Executors.newFixedThreadPool(1);
@EJB
private IMaterialSvc materialSvc;
private void updateMaterial(Material material, String status, Location position) {
executor.execute(new Runnable() {
public void run() {
synchronized (position) {
// TODO update material in audit? do we need materials in audit?
int index = position.getMaterials().indexOf(material);
Material m = materialSvc.getById(material.getId());
m.setStatus(status);
m = materialSvc.update(m);
if (index != -1) {
position.getMaterials().set(index, m);
}
}
}
});
}
@PreDestroy
public void destory() {
executor.shutdown();
}