J2SE 1.5 Tiger


précédentsommairesuivant

Les nouvelles APIs majeures de Java 5

Chaque nouvelle version de J2SE apporte son lot de nouvelles API intégrées. J2SE 1.4 nous avait ainsi gratifié d'une API permettant de gérer les entrées/sorties, aussi appelée NIO (New Input Outpout) ou encore de JAXP (Java API for XML Parsing).

Tiger n'est pas en reste avec notamment l'ajout majeur d'une API très complète de gestion de la concurrence à haut niveau, ou encore l'intégration de JMX pour le management et de JVMTI pour la supervision.

La synchronisation de haut niveau : l'API de concurrence

La plate forme Java fournit des primitives basiques et de bas niveau pour écrire des programmes concurrents et synchronisés, mais celles-ci sont difficiles à utiliser correctement.
La plupart des programmes deviennent plus clairs, plus rapides, plus faciles à écrire et plus fiables si une synchronisation de haut niveau est utilisée.

Une librairie étendue d'utilitaires de gestion de la concurrence a été intégrée dans le J2SE 1.5.

Cette API, connue comme le package java.util.concurrent, contient des pools de threads, des queues, des collections synchronisées, des verrous spéciaux (atomiques ou pas), des barrières (ou barrage), et bon nombre d'autres utilitaires, comme un framework d'exécution de taches. L'ajout à la plateforme Java de cette librairie est un apport substantiel qui va bouleverser la façon dont nous écrivons la gestion de la concurrence dans les applications Java.

En fait cette API n'est autre que celle développée par Doug Léa, professeur au State University of New York (SUNY) College d'Oswego et qui est devenue le JSR-166 (anciennement dans le package edu.oswego.cs.dl.util.concurrent).

Nous allons voir par la suite une vue rapide des principaux éléments de cette énorme librairie.

Les exécuteurs de tâches.

L'interface Executor est une super-interface simple et standard qui permet de contrôler des sous-systèmes d'exécution de tâches (en fait des Runnables), comme des pools de threads, des entrées / sorties asynchrones et des frameworks légers de gestion de tâches.
Elle permet de découpler les appels d'exécution des exécutions elles-mêmes, en précisant l'utilisation de chaque thread, l'ordonnancement, etc.
En fonction du type d'exécuteur utilisé, la tache peut être exécutée par un nouveau thread, un thread existant ou même par le thread qui demande l'exécution de celle-ci, et ce de manière séquentielle ou concurrente :

 
Sélectionnez
Executor executor = anExecutor;
...
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
...

L'interface ExecutorService fournit un framework d'exécution de tâches entièrement asynchrone. Celui-ci gère la mise en queue, l'ordonnancement et la terminaison des tâches.
Il fournit à cet effet des méthodes de terminaison de processus et d'autres de monitoring de la progression d'une ou plusieurs tâches asynchrones (via les Futures, retournant l'état du traitement ou thread voulu).

Tiger offre en standard deux implémentations concrètes, très flexibles et hautement configurables, de cette interface : la classe ThreadPoolExecutor et la classe ScheduledThreadPoolExecutor.

D'autres part, une classe utilitaire (helper), Executors, fournit des fabriques et des moyens de configurer les principaux Executors, ainsi que nombre de méthodes utilitaires.

L'exemple de référence crée un serveur en quelques lignes :

 
Sélectionnez
class NetworkService
{
  private final ServerSocket serverSocket;
  private final ExecutorService pool;
  
  public NetworkService(int port, int poolSize) throws IOException
  {
    serverSocket = new ServerSocket(port);
    pool = Executors.newFixedThreadPool(poolSize);
  }
  
  public void serve()
  {
    try
    {
      for (;;)
      {
        pool.execute(new Handler(serverSocket.accept()));
      }
    }
    catch (IOException ex)
    {
      pool.shutdown();
    }
  }
}

class Handler implements Runnable
{
  private final Socket socket;
  
  Handler(Socket socket)
  { 
    this.socket = socket; 
  }
  
  public void run()
  {
    // read and service request
  }
}

Les queues

Tiger introduit une nouvelle interface pour les collections : java.util.Queue.

Cette nouvelle interface comporte un certain nombre de signatures supplémentaires pour ajouter, supprimer des éléments, et parcourir la collection:

 
Sélectionnez
public boolean offer(Object element)
public Object remove()
public Object poll()
public Object element()
public Object peek()

Une queue n'est rien d'autre qu'une structure de données de type FIFO (First In, First Out).
La valeur ajoutée de Tiger est ici sur les moyens mis à disposition pour agir sur la queue.

Par exemple, à la différence de la méthode add(), la méthode offer() ne lève plus d'exception de type unchecked lorsqu'une erreur se produit. Elle retourne simplement false.
De la même manière, la méthode poll() retourne null si nous tentons de supprimer un élément d'une collection vide, à la différence de la méthode remove() que nous connaissons.

Nous distinguons deux types de queues : celles qui implémentent l'interface BlockingQueue et celles qui ne le font pas et qui implémentent uniquement Queue, comme la classe LinkedList existante, ou les nouvelles PriorityQueue (ordonnancement naturel des éléments) et ConcurrentLinkedQueue (thread-safe).

Les BlockingQueue sont des queues qui se bloquent lorsque l'on tente d'ajouter ou de retirer un élément tant que l'espace disponible n'est pas suffisant. Elles fonctionnent dans un mode producteur / consommateur.
Il existe cinq implémentations de ce type de queue dans le package java.util.concurrent :

ArrayBlockingQueue Une queue avec des limites de taille minimale et maximale, dont la structure sous jacente est un tableau.
LinkedBlockingQueue Une queue, que l'on peut limiter, et dont la structure sous jacente est un ensemble de noeuds de liaisons.
PriorityBlockingQueue Une PriorityQueue implémentée comme un segment.
DelayQueue Une queue dont les éléments peuvent expirer , et implémentée comme un segment.
SynchronousQueue Une simple queue attendant qu'un consommateur demande son élément.

Les Map atomiques

Tiger introduit l'interface ConcurrentMap et son implémentation ConcurrentHashMap.
Il s'agit de Map dont les opérations sont atomiques et concurrentes.

Elles sont à distinguer des collections "synchronisées" avec le framework Collections (comme Hashtable et Collections.synchronizedMap(new HashMap())).
Les collections de java.util.concurrent sont "concurrentes", c'est-à-dire qu'elles sont "thread safe", mais pas supervisées par un seul et unique verrou d'exclusion (comme le sont les collections "synchronisées").

Par exemple, ConcurrentHashMap permet non seulement des lectures simultanées mais aussi de décider du nombre d'écritures simultanées, et ce de manière déterministe. Elle possède donc une meilleure capacité de monter en charge que les Hashtables, qui elles ne permettent qu'une lecture ou qu'une écriture simultané.

Elles apportent deux méthodes essentielles pour cela :

  • putIfAbsent(String key, Object value) qui ajoute l'objet uniquement si la clé n'existe pas dans la structure et retourne la valeur insérée. Sinon elle préserve la valeur existante en la retournant.
  • remove(String key, Object Value) qui ne supprime l'entrée que si la valeur de l'objet pour la clé désignée est égale à la valeur passé. Dans ce cas elle retourne true. Sinon elle retourne false.

La représentation du temps et de ses unités: TimeUnit

La classe TimeUnit représente une unité de durée, pouvant aller jusqu'à la nanoseconde, qui fournit différentes méthodes pour déterminer et contrôler des opérations de timing (comme un "ordonnanceur"), d'ajournement ou de durée de vie.

Par exemple, pour faire échoir au bout de 50 millisecondes une tentative de pose d'un verrou sur un bloc, en cas d'échec de celle-ci :

 
Sélectionnez
Lock lock = ...; // création d'un lock
...
if ( lock.tryLock(50L, TimeUnit.MILLISECONDS) ) 
...

Ainsi, si la pose du verrou échoue, le traitement ne se bloque pas au delà de cinquante milliseconde.

Il faut cependant noter que TimeUnit représente uniquement une granularité de temps et ne garantit pas la détection de l'événement dans le même ordre de granularité.

Les synchroniseurs (loquet, barrière, sémaphore et échangeur)

Les synchroniseurs sont des éléments essentiels pour la gestion de la concurrence. En effet, il s'agit d'idiomes de haut niveau mettant en oeuvre des mécanismes ou algorithmes particuliers qui apportent différents moyens de contrôler la mise en concurrence et/ou la synchronisation de différents processus.

Tiger fournit quatre types des synchroniseurs :

  • Les loquets.
  • Les barrières.
  • Les sémaphores.
  • Les échangeurs.

Les loquets (ou latchers) via la classe CountDownLatch, permettent de bloquer (avec la méthode await()) plusieurs threads jusqu'à ce qu'un certain nombre d'opérations soient finies.

Typiquement, chacune de ces opérations va décrémenter un compteur non altérable au sein du loquet en invoquant la méthode countDown(), jusqu'à zéro. A ce moment, le loquet est levé et chaque thread en attente est débloqué et peut donc reprendre son exécution.

Le cas le plus simple de loquet est réalisé par un compteur de valeur un. Il s'agit alors d'un interrupteur, qui lorsqu'il est fermé, débloque le courant (en l'occurrence ici, le traitement).

Un autre cas typique est le découpage d'un traitement en plusieurs micros traitements (le fameux « diviser pour régner »), comme par exemple le découpage en sous requêtes d'une requête vers une base de données :

 
Sélectionnez
class RequestProcessor
{
  // ...
  
  void main() throws InterruptedException
  {
    CountDownLatch latcher = new CountDownLatch(N);
    Executor e = ... ;
    
    for (int i = 0; i < N; ++i)
    {
      // create and start threads
      e.execute(new RequestRunnable(latcher, i));
      
      latcher.await(); // wait for all to finish
    }
  }
}

class RequestRunnable implements Runnable
{
  private final CountDownLatch localLatcher;
  private final int id;
  
  RequestRunnable(CountDownLatch latcher, int id)
  {
    this.localLatcher = latcher;
    this.id = id;
  }
  
  public void run()
  {
    try
    {
      doRequest(id);
      localLatcher.countDown();
    }
    catch (InterruptedException ex) {..};
  }
  
  void doRequest () { ... }
}

Les barrières permettent de définir un point d'attente commun à plusieurs processus. Lorsque tous les processus enregistrés par la barrière ont atteint ce point, ils sont débloqués.

Elles sont particulièrement efficaces pour des traitements divisibles et récursifs, car à la différence des loquets pour lesquels le compteur est fixe, une barrière est réutilisable immédiatement après l'opération de déblocage. Elles sont alors qualifiées de cycliques et Tiger en fournit une implémentation dans la classe CyclicBarrier.
De plus, il est possible de déclarer une tâche optionnelle dans la barrière qui sera à même de changer l'état de celle-ci, car exécutée juste avant le déblocage des processus.

On peut par exemple imaginer un traitement récursif sur des matrices, chaque résultat de traitement sur une matrice en créant une autre, jusqu'au résultat voulu.

Le sémaphore est sans doute le mécanisme de synchronisation le plus connu et le plus classique. C'est un mécanisme permettant d'assurer une forme d'exclusion mutuelle.
Plus précisément, un sémaphore est une valeur entière S associée à une file d'attente. Nous pouvons accéder au sémaphore par deux méthodes, la première P attend que S soit positif et décrémente S (le processus est mis dans la file d'attente si S n'est pas strictement positif), la deuxième V incrémente S ou libère un des processus en attente sur un P.

Pour schématiser, nous pouvons dire que le sémaphore dispose d'un nombre fini de jetons.
Des processus ayant besoin d'une ressource tentent d'acquérir un de ces jetons. S'il n'y a plus de jetons, ils se mettent en attente. Lorsque le traitement est terminé, le processus détenteur d'un jeton relâche ce dernier dans le sémaphore. Le premier processus en attente peut alors acquérir à son tour le jeton ainsi relâché.

Ce pattern simple règle le problème de la section critique avec un seul jeton (sémaphore binaire) :

 
Sélectionnez
private Semaphore s = new Semaphore(1, true);

s.acquireUninterruptibly(); // bloquant
value = balance +1; // section critique exclusive
s.release();

Il peut aussi servir pour réaliser des buffers de 10 éléments partagés entre des producteurs et des consommateurs :

 
Sélectionnez
class Resources
{
  private static final RES_NUMBER = 10;
  private final Semaphore token = new Semaphore(RES_NUMBER, true);
  private List resourcesPool = new ArraylList(RES_NUMBER);
  
  public Object getResource()
      throws InterruptedException
  {
    token.acquire();
    return resourcesPool.get();
  }
  
  public void releaseResource(Object o)
  {
    resourcesPool.put(o);
    token.release();
  }
}

Bien entendu, cet exemple est ridicule. Il est là uniquement pour illustrer le mécanisme.
Il permet surtout de mettre en attente une demande et de la débloquer automatiquement, sans aucune intervention ou test supplémentaires.

Le dernier type de synchroniseur est l'échangeur (Exchanger).

Un échangeur permet à deux processus de s'échanger mutuellement des données à un certain point dans le traitement. Nous l'appelons aussi un « rendez-vous ». Il définit un canal commun qui permet à deux objets de communiquer en s'échangeant d'autres objets.

On peut le voir comme un sablier qui lorsqu'il est prêt (ou arrive au « rendez-vous »), ouvre sa vanne de communication. Le contenu de la partie haute se déverse alors dans la partie basse :

 
Sélectionnez
class SandGlass
{
  Exchanger<Data> exchanger = new Exchanger();
  
  class SandGlassLowRoom implements Runnable
  {  
    public void run()
    {
      Data content = computeContent();
      
      try
      {
        content = exchanger.exchange(content);
      }
      catch (InterruptedException ex){}
    }
  }
  
  class SandGlassHighRoom implements Runnable
  {
    public void run()
    {
      Data content = computeContent();
      
      try
      {
        content = exchanger.exchange(content);
      }
      catch (InterruptedException ex){}
    }
  }
  
  void openRooms()
  {
    new Thread(new SandGlassLowRoom()).start();
    new Thread(new SandGlassHighRoom()).start();
  }
}

Les traitements asynchrones anticipés

Lorsqu'un traitement est long, il peut être intéressant de le déclencher de manière asynchrone et de n'en récupérer le résultat que plus tard, lorsque celui-ci est fini. De plus, il se peut que le développeur souhaite garder la main sur cette tâche, pour pouvoir par exemple l'interrompre à tout moment, et ce sans créer un code bloquant, ou pour notifier un utilisateur de la fin de celle-ci.
C'est ce que propose de définir la nouvelle interface Future.

Future représente le "résultat" d'un traitement asynchrone, c'est-à-dire un objet sur lequel nous conservons une référence, et que nous pouvons utiliser en lieu et place du véritable résultat.
De plus cette interface fournit des méthodes utilitaires permettant d'interroger l'état du traitement (isDone()), d'interrompre le traitement (cancel()) ou encore d'en récupérer le résultat (get()). Une dernière méthode permet de savoir si le traitement a été interrompu (isCancelled()), ce qui permet donc de vérifier la cohérence du résultat.

La méthode get() possède deux formes, dont l'une est bloquante tant que le traitement n'est pas terminé et donc le résultat non disponible, et une autre qui permet de définir un temps maximum d'attente avant de relâcher le verrou (exprimé grâce à un TimeUnit). Cette dernière peut être particulièrement intéressante pour gérer des problématiques d'IHM lourdes (chargement asynchrone d'image par exemple).

Voici un exemple permettant de construire un rapport depuis une base de données :

 
Sélectionnez
class ReportBuilder
{
  Future<ResultSet> result = null;
  SQLService sqlService = …; // un framework de requétage
  
  void build(String reportStatement)
       throws InterruptedException
  {
    result = executor.submit
    (
      new Callable<String>()
      {
        public String call()
        {
          return sqlService.query(reportStatement) ;
        }
      }
    )
  }
}

Combiné à la puissance des Executors, il est très aisé de lancer plusieurs traitements longs et de les mettre en queue pour une utilisation ultérieure, notamment avec les ExecutorCompletionService qui découplent la production de nouvelles tâches asynchrones de la consommation de leurs résultats. Les producteurs présentent (submit()) des tâches à exécuter et les consommateurs récupèrent (take()) les tâches accomplies et traitent leurs résultats.

De la même manière, les Futures peuvent servir à réaliser un cache hautement disponible, puisque la demande d'un élément non existant du cache, qui provoque en général son chargement au sein dudit cache, ne bloque pas l'accès aux autres éléments déjà cachés.

La classe FutureTask est une implémentation par défaut de Future et de Runnable, ce qui permet de simplifier grandement le développement avec des Futures.

Les variables atomiques

Le package java.util.concurrent.atomic contient des classes « conteneurs » atomiques, parmi lesquelles nous trouvons AtomicInteger, AtomicLong, et AtomicReference, qui fournissent des mécanismes sécurisés d'opération basique de mise à jour ou d'interrogation, comme le sont les variables volatiles.

Toutes les opérations ne se font que si un certain nombre de conditions sont réunies et sans qu'un autre processus ne puisse agir sur le conteneur.

Elles sont très pratiques pour implémenter par exemple des compteurs sans recourir à des mécanismes de synchronisation particuliers, puisqu'elles le sont nativement.

De plus, elles sont beaucoup plus performantes que leurs équivalents dans java.lang utilisant la synchronisation, puisqu'elles s'appuient directement sur le nouveau mécanisme interne à la JVM 1.5, le "compare-and-swap".

Les nouveaux verrous "haute performance"

Le package java.util.concurrent.locks fournit de nouveaux mécanismes de verrouillage.
Ils sont similaires aux verrous déjà présents dans l'API du J2SE 1.4, mais y ajoutent des fonctionnalités non supportés par le moniteur actuel comme la faculté d'interrompre un processus bloqué sur un verrou, de définir un temps limite de blocage sur un verrou, ... .

De plus, ils sont beaucoup plus performants que la synchronisation actuelle, au prix il est vrai d'une rigueur dans la syntaxe beaucoup plus présente.

Le package comporte des implémentations de verrous et de conditions de verrouillage (dont se servent les fameux wait(),notify(), notifyAll()).

L'interface Lock supporte des méthodes de verrouillage fonctionnellement différentes (multientrées, propre, etc..). L'implémentation principale est la classe ReentrantLock qui est un verrou multi-entrées (qui peut être lu temporairement par plusieurs utilisateurs ou plusieurs fois par un seul utilisateur) exactement comme l'actuelle synchronisation (exclusion mutuelle). La classe ReadWriteLock permet d'implémenter des verrous pour des objets multiutilisateurs, mono-producteur.

L'interface Condition permet de définir des variables conditionnelles que l'on associe à des verrous. Elles sont utilisées par le moniteur pour placer des conditions de blocage ou de déblocage. Plusieurs conditions peuvent être associées à un seul verrou (à la différence de l'ancien moniteur).

Voici l'exemple du buffer que fournit la documentation du JDK 1.5 :

 
Sélectionnez
class BoundedBuffer
{
  Lock lock = new ReentrantLock();
  final Condition notFull = lock.newCondition();
  final Condition notEmpty = lock.newCondition();
  
  Object[] items = new Object[100];
  int putptr, takeptr, count;

  public void put(Object x) 
       throws InterruptedException
  {
    lock.lock();
    try
    {
      while (count == items.length)
       notFull.await();
       
      items[putptr] = x;
      
      if (++putptr == items.length) 
	    putptr = 0;
	    
      ++count;
      notEmpty.signal();
    }
    finally
    {
      lock.unlock();
    }
  }
  
  public Object take() 
       throws InterruptedException
  {
    lock.lock();
    try
    {
      while (count == 0)
        notEmpty.await();
        
      Object x = items[takeptr];
      
      if (++takeptr == items.length) 
	    takeptr = 0;
      --count;
      
      notFull.signal();
      return x;
    }
    finally
    {
      lock.unlock();
    }
  }
}

La gestion et la supervision de la JVM : l'API de management

Depuis bien longtemps, les utilisateurs d'applications écrites en Java demandaient une extension qui leur permettent de gérer facilement les événements de la JVM (Java Virtual Machine), et plus particulièrement l'état de la mémoire à bas niveau.

J2SE 1.5 fournit pour cela une extension permettant de supporter ces éléments clés garants de la fiabilité, de la disponibilité et du fonctionnement normal des applications Java.

Elle est composée de deux parties principales :

  • L'API JMX (Java Management Extensions) bien connue en tant qu'API externe, qui consiste en un framework d'observation de la JVM. Les caractéristiques internes des données de celle-ci, comme les informations sur les processus et la mémoire sont désormais disponibles pour être publiées via l'interface de MBeans JMX (JSR 003), l'interface distante JMX (JSR 160) ou encore directement via l'espace d'adressage Java. Un ensemble d'outils et de protocoles standard d'accès (JSR163), comme le protocole SNMP (Simple Network Management Protocol), permettent la remontée d'informations vers un moniteur.
  • Un outil de « profiling » natif appelé JVMTI, permettant de profiler les applications Java, mais aussi de les superviser, de les debugger voire de les manipuler directement au niveau du bytecode, c'est-à-dire à chaud, pendant leur exécution.

Non seulement vous pouvez voir les informations de la JVM, mais le framework fournit les outils permettant d'agir sur celles-ci, d'essayer de nouvelles valeurs, comme changer le niveau de traces fonctionnelles (log) dynamiquement.

JPLIS (Java Programming Language Instrumentation Services) permet d'ajouter des points de contrôle ciblés dans le code, à la manière de la POA (Programmation Orienté Aspect), permettant ainsi une supervision plus fine de la JVM, et ce à chaud, au chargement de l'application, ou encore durant la compilation (opération de tissage).

L'autre fonctionnalité phare de cette API de management, est sans nul doute le détecteur de seuil de mémoire. Lorsque le seuil est atteint, une notification peut être envoyée par le bean JMX ou une « trap » SNMP peut être levée. Cela permet par exemple de prévoir une charge importante sur une application et de la gérer avec des machines supplémentaires en répartissant dynamiquement la charge.

La prise en main de cette API est très simple. Par exemple pour définir une supervision de la JVM via le protocole SNMP, on peut très simplement faire :

 
Sélectionnez
java -Dcom.sun.management.snmp.acl=false
     -Dcom.sun.management.snmp.port=161
     -Dcom.sun.management.snmp.trap=162
     -jar TestApplication.jar

Les ports utilisés ici sont les ports standard SNMP. De la même manière, il est possible de définir ces options dans un fichier de configuration (management.properties).

Associer un serveur de MBean JMX est tout aussi simple :

 
Sélectionnez
java -Dcom.sun.management.jmxremote.authenticate=false
     -Dcom.sun.management.jmxremote.port= 5001
     -Dcom.sun.management.jmxremote.ssl=false
     -jar TestApplication.jar

L'exemple qui suit permet de suivre l'évolution de l'utilisation de la mémoire par la JVM.
Comme nous allons pouvoir le constater, cette fonctionnalité est très simplement prise en charge par l'API de management :

 
Sélectionnez
import java.lang.management.*;
import java.util.*;

public class MemTest
{
  public static void main(String args[])
  {
    List<MemoryPoolMXBean> pools = ManagementFactory.getMemoryPoolMXBeans();
    for (MemoryPoolMXBean p: pools)
    {
      System.out.println("Memory type="+p.getType()
      System.out.print("Memory usage="+p.getUsage());
    }
  }
}

Voici le type de sortie que produit le code précédent :

Image non disponible

Une autre fonctionnalité très simple à mettre en œuvre, est la détection des crashs de la JVM HotSpot suite à une erreur fatale. Il est alors possible de déclencher une commande :

 
Sélectionnez
-XX:OnError="command"

Il est aussi possible de brancher un debugger ou un fichier système lors de cette détection :

 
Sélectionnez
-XX:OnError="pmap %p" // %p représente le pid.
-XX:OnError="gdb %p"

Cette API étant très riche et les possibilités qu'elle offre très nombreuses, veuillez vous reporter au site de référence de JMX pour de plus amples informations :

http://java.sun.com/products/JavaManagement/

Les nouveautés pour les clients lourds

Tiger améliore grandement le temps de démarrage et la taille mémoire occupée par les applications développées en client lourd.

De plus il introduit un nouveau thème appelé « Ocean », en plus des thèmes « Windows XP » et « GTK » déjà ajoutés dans le J2SE 1.4.2.

Image non disponible
Image non disponible
Image non disponible

Il faut aussi noter que les apparences graphiques sont désormais personnalisables (principe des skins), via des fichiers XML, ou de manière programmatique.

Tiger permet aussi d'utiliser les capacités d'accélération graphique avec OpenGL pour les applications Java développées en utilisant les composants Java2D.

Il suffit pour cela de le spécifier dans la ligne de commande de lancement de l'application :

 
Sélectionnez
java -Dsun.java2d.opengl=true -jar Java2DApplication.jar

Pour les utilisateurs du système d'exploitation Linux, il est désormais possible d'utiliser le système graphique X11, via XAWT (sun.awt.X11.XToolkit), une librairie graphique très rapide s'appuyant sur X11, et permettant d'utiliser le protocole XDnD (glisser / déposer) entre les applications Java et des applications comme Mozilla et Star Office.

Comme il est d'usage, cela se fait via la ligne de commande :

 
Sélectionnez
java -Dawt.toolkit=sun.awt.motif.MToolkit -jar X11Application.jar

précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur [Lionel Roux]. La copie, modification et/ou distribution, de tout ou partie de celui-ci, par quelque moyen que ce soit, est soumise à l'obtention préalable de l'autorisation de l'auteur.
Tout manquement à cette licence donnerait systématiquement lieu à des poursuites, en accord avec la loi sur l'économie numérique (LEN).