É seguro iniciar um novo encadeamento em um bean gerenciado pelo JSF?

Eu não consegui encontrar uma resposta definitiva para saber se é seguro gerar threads dentro de beans gerenciados pelo JSF com escopo de session. O encadeamento precisa chamar methods na instância EJB sem estado (que foi injetada com dependência no bean gerenciado).

O pano de fundo é que temos um relatório que leva muito tempo para gerar. Isso causou o tempo limite do pedido HTTP devido às configurações do servidor que não podemos alterar. Portanto, a ideia é iniciar um novo thread e deixá-lo gerar o relatório e armazená-lo temporariamente. Enquanto isso, a página JSF mostra uma barra de progresso, pesquisa o bean gerenciado até que a geração seja concluída e, em seguida, faz uma segunda solicitação para baixar o relatório armazenado. Isso parece funcionar, mas gostaria de ter certeza de que o que estou fazendo não é um hack.

Introdução

Gerar threads de dentro de um bean gerenciado com escopo de session não é necessariamente um hack, desde que faça o trabalho desejado. Mas a criação de threads em suas próprias necessidades deve ser feita com extremo cuidado. O código não deve ser escrito dessa forma que um único usuário possa, por exemplo, gerar uma quantidade ilimitada de threads por session e / ou que os threads continuem sendo executados mesmo após a session ser destruída. Isso explodiria seu aplicativo mais cedo ou mais tarde.

O código precisa ser escrito dessa forma que você possa garantir que um usuário, por exemplo, nunca possa gerar mais de um thread de segundo por session e que o thread tenha a garantia de ser interrompido sempre que a session for destruída. Para várias tarefas em uma session, você precisa enfileirar as tarefas.

Além disso, todos esses encadeamentos devem, de preferência, ser atendidos por um conjunto de encadeamentos comuns, de modo que você possa colocar um limite na quantidade total de encadeamentos gerados no nível do aplicativo. O servidor de aplicativos Java EE médio oferece um conjunto de encadeamentos gerenciados por contêiner que você pode utilizar via @Asynchronous e @Asynchronous do EJB, entre outros. Para ser independente do contêiner, você também pode usar o Util ExecutorService e ScheduledExecutorService do Java 1.5 para isso.

Abaixo, os exemplos assumem o Java EE 6+ com EJB.

Disparar e esquecer uma tarefa no envio de formulário

 @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() { // ... } } 

Obter de forma assíncrona o modelo no carregamento da 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); } } 

Caso você esteja usando a biblioteca de utilitários JSF OmniFaces , isso pode ser feito ainda mais rápido se você anotar o bean gerenciado com @Eager .

Agendar trabalhos em segundo plano no início do aplicativo

 @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) } } 

Atualize continuamente o modelo amplo da aplicação em segundo plano

 @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; } } 

Veja também:

  • Gerando encadeamentos em um bean gerenciado JSF para tarefas planejadas usando um timer

Verifique os @Asynchronous methods EJB 3.1 @Asynchronous methods . Isso é exatamente o que eles são.

Pequeno exemplo que usa OpenEJB 4.0.0-SNAPSHOTs. Aqui nós temos um bean @Singleton com um método marcado como @Asynchronous . Toda vez que esse método é chamado por qualquer pessoa, neste caso, seu bean gerenciado pelo JSF, ele retornará imediatamente, independentemente de quanto tempo o método realmente leva.

 @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); } } } 

Aqui está um pequeno testcase que invoca o método @Asynchronous várias vezes seguidas.

Cada chamada retorna um object Futuro que essencialmente começa vazio e, posteriormente, terá seu valor preenchido pelo contêiner quando a chamada do método relacionado for realmente concluída.

 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); } } 

Exemplo de código fonte

Sob as cobertas, o que faz esse trabalho é:

  • O JobProcessor o chamador vê não é realmente uma instância do JobProcessor . Em vez disso, é uma subclass ou proxy que possui todos os methods substituídos. Os methods supostamente asynchronouss são tratados de maneira diferente.
  • As chamadas para um método asynchronous simplesmente resultam na criação de um Runnable que envolve o método e os parâmetros fornecidos. Este runnable é dado a um executor que é simplesmente uma fila de trabalho anexada a um pool de threads.
  • Depois de adicionar o trabalho à fila, a versão com proxy do método retorna uma implementação de Future que está vinculada ao Runnable que agora está aguardando na fila.
  • Quando o Runnable finalmente executa o método na instância real do JobProcessor , ele JobProcessor o valor de retorno e o colocará no Future tornando-o disponível para o chamador.

Importante observar que o object AsyncResult o JobProcessor retorna não é o mesmo object Future o chamador está mantendo. Teria sido legal se o JobProcessor real pudesse retornar String e a versão de JobProcessor do JobProcessor pudesse retornar Future , mas não vimos nenhuma maneira de fazer isso sem adicionar mais complexidade. Então, o AsyncResult é um object wrapper simples. O contêiner puxará a String , jogará o AsyncResult fora e colocará a String no Future real que o chamador está segurando.

Para obter progresso ao longo do caminho, simplesmente passe um object thread-safe como AtomicInteger para o método @Asynchronous e faça o código do bean periodicamente atualizá-lo com a porcentagem concluída.

Eu tentei isso e funciona muito bem com o meu bean gerenciado 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(); }