¿Es seguro iniciar un nuevo hilo en un bean gestionado JSF?

No pude encontrar una respuesta definitiva a si es seguro engendrar hilos dentro de los beans manejados con JSF con scope 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.

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> asyncEntities; @EJB private EntityService entityService; @PostConstruct public void init() { asyncEntities = entityService.asyncList(); // ... (this code will immediately continue without waiting) } public List 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> asyncList() { List 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 .

Progtwigr 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 getSomeTop100() { return someTop100Manager.list(); } } 
 @Singleton @ConcurrencyManagement(BEAN) public class SomeTop100Manager { @PersistenceContext private EntityManager entityManager; private List top100; @PostConstruct @Schedule(hour="*", minute="*/1", second="0", persistent=false) public void load() { top100 = entityManager .createNamedQuery("Some.top100", Some.class) .getResultList(); } public List list() { return top100; } } 

Ver también:

  • Engendrar hilos en un frijol administrado JSF para tareas progtwigdas usando un temporizador

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 addJob(String jobName) { // Pretend this job takes a while doSomeHeavyLifting(); // Return our result return new AsyncResult(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 Futuro 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 red = processor.addJob("red"); final Future orange = processor.addJob("orange"); final Future yellow = processor.addJob("yellow"); final Future green = processor.addJob("green"); final Future blue = processor.addJob("blue"); final Future 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); } } 

Ejemplo de código fuente

Bajo las cubiertas, lo que hace que este trabajo sea:

  • El JobProcessor ve la persona que llama no es realmente una instancia de JobProcessor . 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 Ejecutor 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 al Runnable que ahora está esperando en la cola.
  • Cuando Runnable finalmente ejecuta el método en la instancia real de JobProcessor , tomará el valor de retorno y lo establecerá en el Future 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 , 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 subprocesos 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(); }