Je n'ai pas pu trouver de réponse définitive à la question de savoir s'il est sûr de générer des threads dans des beans gérés JSF à portée de session. Le thread doit appeler des méthodes sur l'instance EJB sans état (qui a été injectée par dépendance au bean géré).
Le contexte est que nous avons un rapport qui prend beaucoup de temps à générer. Cela a provoqué l'expiration de la demande HTTP en raison de paramètres de serveur que nous ne pouvons pas modifier. L'idée est donc de démarrer un nouveau thread et de le laisser générer le rapport et de le stocker temporairement. En attendant, la page JSF affiche une barre de progression, interroge le bean géré jusqu'à la fin de la génération, puis émet une deuxième demande de téléchargement du rapport stocké. Cela semble fonctionner, mais je voudrais être sûr que ce que je fais n'est pas un hack.
La génération de threads à partir d'un bean géré de portée de session n'est pas nécessairement un hack tant qu'il fait le travail que vous souhaitez. Mais le frai des fils doit être fait avec une extrême prudence. Le code ne doit pas être écrit de cette manière qu'un seul utilisateur peut par exemple générer un nombre illimité de threads par session et/ou que les threads continuent de s'exécuter même après la destruction de la session. Cela ferait exploser votre application tôt ou tard.
Le code doit être écrit de cette façon afin que vous puissiez vous assurer qu'un utilisateur ne peut par exemple jamais générer plus d'un thread d'arrière-plan par session et que le thread est garanti d'être interrompu chaque fois que la session est détruite. Pour plusieurs tâches dans une session, vous devez mettre les tâches en file d'attente. En outre, tous ces threads doivent de préférence être servis par un pool de threads commun afin que vous puissiez limiter la quantité totale de threads générés au niveau de l'application.
La gestion des threads est donc une tâche très délicate. C'est pourquoi vous feriez mieux d'utiliser les installations intégrées plutôt que de cultiver votre propre maison avec new Thread()
et vos amis. Le serveur d'application moyen Java EE propose un pool de threads géré par conteneur que vous pouvez utiliser via, entre autres, les EJB @Asynchronous
et @Schedule
. Pour être indépendant du conteneur (lire: Tomcat-friendly), vous pouvez également utiliser le Java 1.5's Util Concurrent ExecutorService
and ScheduledExecutorService
pour cela.
Les exemples ci-dessous supposent Java EE 6+ avec EJB.
@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() {
// ...
}
}
@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);
}
}
Si vous utilisez la bibliothèque d'utilitaires JSF OmniFaces , cela pourrait être fait encore plus rapidement si vous annotez le bean géré avec @Eager
.
@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)
}
}
@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;
}
}
Découvrez EJB 3.1 @Asynchronous methods
. C'est exactement pour ça qu'ils sont.
Petit exemple qui utilise OpenEJB 4.0.0-SNAPSHOTs. Ici, nous avons un @Singleton
bean avec une méthode marquée @Asynchronous
. Chaque fois que cette méthode est invoquée par quelqu'un, dans ce cas, votre bean géré JSF, elle reviendra immédiatement quelle que soit la durée réelle de la méthode.
@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);
}
}
}
Voici un petit testcase qui invoque que @Asynchronous
méthode plusieurs fois de suite.
Chaque invocation renvoie un objet Future qui commence essentiellement vide et verra plus tard sa valeur remplie par le conteneur lorsque le l'appel de méthode se termine réellement.
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);
}
}
Sous les couvertures, ce qui fait que ce travail est
JobProcessor
que l'appelant voit n'est pas en fait une instance de JobProcessor
. Il s'agit plutôt d'une sous-classe ou d'un proxy qui a toutes les méthodes remplacées. Les méthodes censées être asynchrones sont traitées différemment.Runnable
qui encapsule la méthode et les paramètres que vous avez indiqués. Cette exécutable est donnée à un Executor qui est simplement une file d'attente de travail attachée à un pool de threads.Future
qui est liée à Runnable
qui attend maintenant dans la file d'attente.Runnable
exécute enfin la méthode sur l'instance réelle JobProcessor
, elle prend la valeur de retour et la définit dans le Future
le rendant accessible à l'appelant.Il est important de noter que l'objet AsyncResult
renvoyé par JobProcessor
n'est pas le même objet Future
que l'appelant détient. Cela aurait été bien si le vrai JobProcessor
pouvait simplement retourner String
et la version de l'appelant de JobProcessor
pouvait retourner Future<String>
, mais nous n'avons vu aucun moyen de le faire sans ajouter plus de complexité. Ainsi, le AsyncResult
est un simple objet wrapper. Le conteneur va retirer le String
, jeter le AsyncResult
, puis mettre le String
dans le réel Future
que l'appelant détient.
Pour progresser en cours de route, passez simplement un objet thread-safe comme AtomicInteger au @Asynchronous
et que le code du bean le mette à jour périodiquement avec le pourcentage terminé.