API de concurrence Java EE

Bonjour à tous!

Et ici, nous nous livrons à des petits pains et lançons le deuxième flux du cours "Java Enterprise Developer" . Le créateur et professeur permanent du cours - Vitaly Ivanov , a même écrit un article sur ce sujet, qui, nous l'espérons, vous paraîtra utile :)

Alors allons-y :)

Cet article explore l'API de spécification de concurrence d'accès JavaEE ( JSR 236 ), qui définit la norme pour les tâches parallèles dans un conteneur JavaEE en utilisant le concept de ressources gérées. La sortie de la septième version de JavaEE a permis d'exécuter des tâches parallèles dans des conteneurs d'entreprise, fournissant au développeur des outils et des utilitaires pratiques pour travailler avec le multitâche. Jusqu'à ce moment, tout le multitâche était laissé à l'implémentation spécifique du serveur d'applications utilisé, qui décide indépendamment de l'optimisation des tâches. La violation de ce principe a été considérée comme une mauvaise pratique dans la construction de l'architecture des applications d'entreprise. En conséquence, il n'a pas été recommandé au développeur de créer de nouveaux threads, et parfois un tel comportement au niveau du conteneur était interdit.


Le bean entreprise ne doit pas tenter de gérer les threads. Le bean entreprise ne doit pas tenter de démarrer, arrêter, suspendre ou reprendre un thread, ni modifier la priorité ou le nom d'un thread. Le bean entreprise ne doit pas tenter de gérer les groupes de threads.

(traduction gratuite de l'auteur: les EJB ne doivent pas essayer de gérer les threads, à savoir essayer de démarrer, arrêter, suspendre et restaurer leur exécution, ou changer la priorité ou changer le nom d'un thread. De plus, les EJB ne doivent pas essayer de gérer les groupes de threads).

En fait, interdire la création de leurs propres threads dans des conteneurs JavaEE est assez problématique, cependant, avec cette approche, les services d'arrière-plan du conteneur ne peuvent pas garantir l'exactitude de leur travail. Par exemple, la fermeture d'une transaction à la fin de la méthode EJB pourrait potentiellement ne pas fonctionner correctement si des tâches étaient démarrées dans un nouveau thread à l'aide d'héritiers Threads (ou d'implémentations Runnable) de JavaSE. En outre, l'utilisation des types d'interface de base fournis par l'API Executor, tels que ExecutorService et ScheduledExecutorService, lorsqu'ils sont créés via les méthodes statiques de la classe Executors, entraîneraient des erreurs potentielles et perturberaient l'exécution des services de conteneur.

Parmi les outils recommandés par la spécification JavaEE pour l'exécution asynchrone des tâches, le développeur a dû utiliser des EJB asynchrones sans état / avec état et / ou Message Driven, dont les capacités sont suffisantes pour une certaine gamme de tâches, et surtout, dont la gestion est initialement complètement et complètement contrôlée par le serveur d'applications, à savoir un conteneur EJB.

Cependant, comme indiqué précédemment, grâce à JSR 236 , des ressources gérées par conteneur sont apparues qui implémentent la prise en charge de l'exécution de tâches multithreading et asynchrone, étendant les capacités du package java.util.concurrent de JavaSE. Pour la pile JavaEE, les classes de ressources gérées se trouvent dans le package javax.enterprise.concurrent et l'accès aux objets de ces classes est fourni via l'injection de ressources à l'aide de l'annotation @Resource ou via le contexte JNDI (en particulier, InitialContext). Parallèlement, les possibilités d'utilisation des objets Future / ScheduledFuture / CompletableFuture familiers à un environnement multithread dans les applications JavaEE ont été ajoutées.

Donc, il y a suffisamment de paroles et examinons de plus près chacune des ressources gérées fournies par la spécification d'un point de vue pratique, à savoir dans le contexte de l'utilisation de l'application dans le code de l'application, ainsi que du point de vue de la configuration des ressources en utilisant le serveur d'application Glassfish 5 comme exemple.

Eh bien, la classe ManagedExecutorService a été la première à être prise en compte, ce qui (déjà compris par le nom) étend les capacités du JavaSE familier ExecutorService et est conçu pour l'exécution asynchrone des tâches dans l'environnement JavaEE.

Pour configurer non seulement ce type d'ExecutorService au sein du serveur d'applications Glassfish, vous devez vous référer au fichier de configuration domain.xml, dont l'emplacement est déterminé par le répertoire $ {GLASSFISH_HOME} / domaines / <nom de domaine> / config. Un fragment de ce fichier est présenté ci-dessous:

 <domain application-root="${com.sun.aas.instanceRoot}/applications" version="25" log-root="${com.sun.aas.instanceRoot}/logs"> <resources> <context-service object-type="system-all" jndi-name="concurrent/__defaultContextService" /> <managed-executor-service object-type="system-all" jndi-name="concurrent/__defaultManagedExecutorService" /> <managed-scheduled-executor-service object-type="system-all" jndi-name="concurrent/__defaultManagedScheduledExecutorService" /> <managed-thread-factory object-type="system-all" jndi-name="concurrent/__defaultManagedThreadFactory" /> </resources> <servers> <server config-ref="server-config" name="server"> <resource-ref ref="concurrent/__defaultContextService" /> <resource-ref ref="concurrent/__defaultManagedExecutorService" /> <resource-ref ref="concurrent/__defaultManagedScheduledExecutorService" /> <resource-ref ref="concurrent/__defaultManagedThreadFactory" /> </server> </servers> </domain> 

Entrer dans l'interface du panneau d'administration de Glassfish 5, configurer

ManagedExecutorService est le suivant:



Cette section permet la création de nouvelles ressources du même type, la gestion des ressources existantes, la suppression, ainsi que le verrouillage et le déverrouillage.

Pour les fans de l'administration de console dans Glassfish, un puissant utilitaire asadmin est présenté, en utilisant la commande create-managed-executor-service , vous pouvez créer de nouvelles ressources ManagedExecutorService:



Dans le code d'application, il est plus pratique d'utiliser l'injection de ressources pour obtenir une référence à l'objet créé par ManagedExecutorService, mais vous pouvez également utiliser les outils JNDI, comme indiqué ci-dessous:

 @Resource(lookup = "concurrent/OtusExecutorService") ManagedExecutorService executor; InitialContext context = new InitialContext(); ManagedExecutorService managedExecutorServiceWithContext = (ManagedExecutorService) context.lookup( "concurrent/OtusExecutorService"); 

Je voudrais attirer l'attention du lecteur sur le fait que le paramètre de recherche est facultatif pour l'annotation @Resource , et s'il n'est pas défini par le développeur dans le code d'application, le conteneur injecte des ressources par défaut avec le préfixe __default dans leur __default . Dans ce cas, pour le développeur, le code devient encore plus concis:

 @Resource ManagedExecutorService executor; 

Après avoir reçu une référence à cet objet, à l'aide des méthodes execute() et submit() , vous pouvez exécuter des tâches implémentant l'interface Runnable ou Callable à l'intérieur du conteneur.

Passant à un exemple, je voudrais noter que parmi toute la variété des cas possibles, les plus intéressants sont les tâches effectuées dans un environnement JavaEE distribué et dans lesquelles il est important de fournir un support transactionnel dans un environnement multi-thread. Comme vous le savez, JavaEE a développé la spécification JTA (Java Transaction API), qui vous permet de déterminer les limites d'une transaction en la démarrant explicitement avec la méthode begin() et en terminant par les méthodes commit() , en validant les modifications ou en rollback() , qui annule les actions effectuées.

Prenons un exemple de tâche qui renvoie un message à partir d'une liste de cent éléments par index dans une transaction utilisateur:

 public class TransactionSupportCallableTask implements Callable<String> { private int messageIndex; public TransactionSupportCallableTask(int messageId) { this. messageIndex = messageId; } public String call() { UserTransaction tx = lookupUserTransaction(); String message = null; try { tx.begin(); message = getMessage(messageIndex); tx.commit(); } catch (Exception e) { e.printStackTrace(); try { tx.rollback(); } catch (Exception e1) { e1.printStackTrace(); } } return message; } private void getMessage(int index) { … } private UserTransaction lookupUserTransaction () { … } } 

Le code de la servlet qui affiche un message de la liste à un index sélectionné au hasard:

 @WebServlet("/task") public class ManagedExecutorServiceServlet extends HttpServlet { @Resource(lookup = "concurrent/OtusExecutorService") ManagedExecutorService executor; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Future<String> futureResult = executor.submit(new TransactionSupportCallableTask(Random.nextInt(100))); while (!futureResult.isDone()) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } try { response.getWriter().write("Callable task has received message with following content '" + futureResult.get() + "'"); } catch(Exception e) { e.printStackTrace(); } } } 

Le prochain à considérer est la ressource ManagedScheduledExecutorService, dont le but principal est de planifier des tâches qui sont répétées à intervalles réguliers ou qui nécessitent une exécution retardée.



Du point de vue de la configuration de cette ressource via la console d'administration GlassFish, aucune modification particulière n'a été trouvée par rapport au type précédent:



Pour créer rapidement une ressource de type ManagedScheduledExecutorService, asadmin a la asadmin create-managed-scheduled-executor-service



Dans le code de l'application, nous utilisons toujours l'injection de ressources:

 @Resource(lookup = "concurrent/OtusScheduledExecutorService") ManagedScheduledExecutorService scheduledExecutor; 

Les principales méthodes d'exécution des tâches pour ce type d'ExecutorService sont schedule() , qui reçoit en entrée les tâches de type Runnable ou Callable, et scheduleAtFixedRate() , qui détermine en outre le délai initial de la tâche et définit l'intervalle de répétition dans TimeUnit (secondes, minutes, etc.). .).

Le cas précédent peut être réécrit comme suit:

 @WebServlet("/scheduledTask") public class ManagedScheduledExecutorServiceServlet extends HttpServlet { @Resource(lookup = "concurrent/OtusScheduledExecutorService") ManagedScheduledExecutorService scheduledExecutor; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ScheduledFuture<String> futureResult = scheduledExecutor.schedule( new TransactionSupportCallableTask(Random.nextInt(100)), 5, TimeUnit.SECONDS); while (!futureResult.isDone()) { try { Thread.sleep(50); // Wait } catch (InterruptedException e) { e.printStackTrace(); } } try { response.getWriter().write("Callable task received message with following content '" + futureResult.get() + "'"); } catch ( Exception e) { e.printStackTrace(); } } } 

De plus, l'API d'accès concurrentiel pour l'environnement Enterpise offre la possibilité de créer des flux contrôlés. Pour ces tâches, vous devez utiliser les capacités d'une fabrique de threads gérés qui implémente ses fonctionnalités via la classe ManagedThreadFactory du même nom et est également accessible via le service JNDI:

 @Resource ManagedThreadFactory factory; 

La fenêtre d'administration de la console Glassfish semble "à l'ancienne":



En utilisant une fabrique de threads gérés, il devient possible non seulement de fournir au conteneur des mécanismes de contrôle de flux, mais aussi d'initialiser les propriétés des threads générés: définir des noms et établir des priorités, ce qui à l'avenir peut grandement simplifier la recherche de problèmes lors de l'analyse d'un vidage de thread en détectant facilement l'ordre d'exécution des threads précédemment nommés.

Dans notre cas, nous définissons la classe du flux qui affiche des informations sur un ami avec lequel cette tâche est inextricablement liée à la console:

 public class SimpleThreadTask implements Runnable { private String friend; public SimpleThreadTask(String friend){ this.friend = friend; } @Override public void run() { System.out.println("Hello, " + friend); } } 

Laissez le servlet démarrer le thread et signalez-le à la sortie:

 @WebServlet("/thread") public class ManagedThreadFactoryServlet extends HttpServlet { @Resource ManagedThreadFactory factory; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Thread thread = factory.newThread(new SimpleThreadTask("Otus")); thread.setName("ManagedThreadFromPool"); thread.setPriority(7); thread.start(); response.getWriter().write("Custom thread has been running."); } } 

En ce qui concerne la dernière fonctionnalité de JavaEE dans le domaine du multithreading - Context Services, il convient de noter que ces services créent des objets proxy contextuels dynamiques. Nous connaissons tous les capacités des proxys dynamiques de JavaSE ( java.lang.reflect.Proxy ), qui vous permettent de générer une implémentation dynamique des interfaces requises, dont les capacités sont activement utilisées pour créer des connexions à la base de données et la gestion des transactions, sont utilisées pour toutes sortes d'intercepteurs AOP, etc. De plus, pour les mandataires créés via des services de contexte JavaEE, il est supposé qu'ils peuvent fonctionner dans le cadre d'un contexte JNDI, d'un contexte de sécurité et d'une classe de conteneur communs.

Pour connecter le service, utilisez simplement le code:

 @Resource ContextService service; 

Du point de vue de l'administration et de la configuration de cette ressource, tout est extrêmement familier et similaire aux types déjà considérés:



Voici un exemple d'un thread qui démarre une tâche proxy dans le contexte d'un conteneur:

 public class SampleProxyTask implements Runnable { @Override public void run() { //  Subject subject = Subject.getSubject(AccessController.getContext()); logInfo(subject.getPrincipals()); //    calculateSmth(); } private void calculateSmth() { … } private void logInfo(Set<Principal> subject) { … } } 

Bean EJB sans état pour créer des proxy contextuels:

 @Stateless public class ContextServiceBean { @Resource ContextService service; @Resource ManagedExecutorService executor; public void perform(Runnable task) { Runnable proxy = service.createContextualProxy(task, Runnable.class); executor.submit(proxy); } } 

Et enfin, le code du servlet exécutant la tâche:

 @WebServlet("/context") public class ContextServiceServlet extends HttpServlet { @Inject ContextServiceBean contextServiceBean; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { contextServiceBean.perform(new SampleProxyTask()); } } 

Cela met en fait fin aux capacités du développeur JavaEE de fonctionner dans un environnement multi-thread. Grâce à eux, tous les processus et services qui se produisent dans le conteneur seront sous le contrôle strict du serveur, coordonnant leur travail et ne violant pas l'ordre d'exécution habituel. Pour les objectifs du développeur d'entreprise, ces capacités sont souvent suffisantes et dans la huitième version, cette API n'a pas changé.

LA FIN

Comme toujours, nous attendons des questions et des commentaires et assurez-vous de consulter Vitaly pour une leçon ouverte , où il peut également poser des questions et écouter / participer au sujet «CDI en action @

Source: https://habr.com/ru/post/fr421809/


All Articles