web-dev-qa-db-fra.com

Est-il sûr de démarrer manuellement un nouveau thread dans Java EE?

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.

45
Dmitry Chornyi

Introduction

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.

Tire et oublie une tâche sur le formulaire

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

}

Récupération asynchrone du modèle au chargement de la page

@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 .

Planifier des tâches d'arrière-plan au démarrage de l'application

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

}

Mettre à jour en continu le modèle à l'échelle de l'application en arrière-plan

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

}

Voir également:

44
BalusC

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

Exemple de code source

Sous les couvertures, ce qui fait que ce travail est

  • Le 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.
  • Les appels à une méthode asynchrone entraînent simplement la création d'un 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.
  • Après avoir ajouté le travail à la file d'attente, la version mandatée de la méthode renvoie une implémentation de Future qui est liée à Runnable qui attend maintenant dans la file d'attente.
  • Lorsque 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é.

53
David Blevins