Créez votre propre ObjectPool en Java, partie 1

L'idée du pool d'objets est similaire au fonctionnement de votre bibliothèque locale: lorsque vous voulez lire un livre, vous savez qu'il est moins coûteux d'emprunter un exemplaire à la bibliothèque plutôt que d'acheter votre propre exemplaire. De même, il est moins coûteux (en termes de mémoire et de vitesse) pour un processus d' emprunter un objet plutôt que de créer sa propre copie. En d'autres termes, les livres de la bibliothèque représentent des objets et les clients de la bibliothèque représentent les processus. Lorsqu'un processus a besoin d'un objet, il extrait une copie d'un pool d'objets plutôt que d'en instancier un nouveau. Le processus renvoie ensuite l'objet au pool lorsqu'il n'est plus nécessaire.

Il y a, cependant, quelques distinctions mineures entre le pool d'objets et l'analogie de la bibliothèque qui doivent être comprises. Si un usager de bibliothèque veut un livre particulier, mais que toutes les copies de ce livre sont extraites, l'usager doit attendre jusqu'à ce qu'une copie soit retournée. Nous ne voulons jamais qu'un processus doive attendre un objet, donc le pool d'objets instanciera de nouvelles copies si nécessaire. Cela pourrait entraîner une quantité excessive d'objets qui traînent dans la piscine, de sorte qu'il gardera également un compte sur les objets inutilisés et les nettoiera périodiquement.

Ma conception de pool d'objets est suffisamment générique pour gérer les délais de stockage, de suivi et d'expiration, mais l'instanciation, la validation et la destruction de types d'objets spécifiques doivent être gérées par sous-classement.

Maintenant que les bases sont écartées, passons au code. C'est l'objet squelettique:

 public abstract class ObjectPool { private long expirationTime; private Hashtable locked, unlocked; abstract Object create(); abstract boolean validate( Object o ); abstract void expire( Object o ); synchronized Object checkOut(){...} synchronized void checkIn( Object o ){...} } 

Le stockage interne des objets regroupés sera géré avec deux Hashtableobjets, l'un pour les objets verrouillés et l'autre pour les objets déverrouillés. Les objets eux-mêmes seront les clés de la table de hachage et leur durée de dernière utilisation (en millisecondes d'époque) sera la valeur. En stockant la dernière fois qu'un objet a été utilisé, le pool peut l'expirer et libérer de la mémoire après une durée d'inactivité spécifiée.

En fin de compte, le pool d'objets permettrait à la sous-classe de spécifier la taille initiale des tables de hachage ainsi que leur taux de croissance et le délai d'expiration, mais j'essaie de rester simple pour les besoins de cet article en codant en dur ces valeurs dans le constructeur.

 ObjectPool() { expirationTime = 30000; // 30 seconds locked = new Hashtable(); unlocked = new Hashtable(); } 

La checkOut()méthode vérifie d'abord s'il y a des objets dans la table de hachage déverrouillée. Si tel est le cas, il les parcourt et en recherche un valide. La validation dépend de deux choses. Tout d'abord, le pool d'objets vérifie que l'heure de dernière utilisation de l'objet ne dépasse pas l'heure d'expiration spécifiée par la sous-classe. Deuxièmement, le pool d'objets appelle la validate()méthode abstraite , qui effectue toute vérification ou réinitialisation spécifique à la classe nécessaire pour réutiliser l'objet. Si l'objet échoue à la validation, il est libéré et la boucle passe à l'objet suivant dans la table de hachage. Lorsqu'un objet qui réussit la validation est trouvé, il est déplacé dans la table de hachage verrouillée et renvoyé au processus qui l'a demandé. Si la table de hachage déverrouillée est vide ou qu'aucun de ses objets ne passe la validation, un nouvel objet est instancié et renvoyé.

 synchronized Object checkOut() { long now = System.currentTimeMillis(); Object o; if( unlocked.size() > 0 ) { Enumeration e = unlocked.keys(); while( e.hasMoreElements() ) { o = e.nextElement(); if( ( now - ( ( Long ) unlocked.get( o ) ).longValue() ) > expirationTime ) { // object has expired unlocked.remove( o ); expire( o ); o = null; } else { if( validate( o ) ) { unlocked.remove( o ); locked.put( o, new Long( now ) ); return( o ); } else { // object failed validation unlocked.remove( o ); expire( o ); o = null; } } } } // no objects available, create a new one o = create(); locked.put( o, new Long( now ) ); return( o ); } 

C'est la méthode la plus complexe de la ObjectPoolclasse, tout est en descente à partir d'ici. La checkIn()méthode déplace simplement l'objet transmis de la table de hachage verrouillée vers la table de hachage déverrouillée.

synchronized void checkIn( Object o ) { locked.remove( o ); unlocked.put( o, new Long( System.currentTimeMillis() ) ); } 

Les trois méthodes restantes sont abstraites et doivent donc être implémentées par la sous-classe. Pour les besoins de cet article, je vais créer un pool de connexion à la base de données appelé JDBCConnectionPool. Voici le squelette:

 public class JDBCConnectionPool extends ObjectPool { private String dsn, usr, pwd; public JDBCConnectionPool(){...} create(){...} validate(){...} expire(){...} public Connection borrowConnection(){...} public void returnConnection(){...} } 

Il JDBCConnectionPoolfaudra que l'application spécifie le pilote de base de données, le DSN, le nom d'utilisateur et le mot de passe lors de l'instanciation (via le constructeur). (Si tout cela est grec pour vous, ne vous inquiétez pas, JDBC est un autre sujet. Restez avec moi jusqu'à ce que nous revenions à la mise en commun.)

 public JDBCConnectionPool( String driver, String dsn, String usr, String pwd ) { try { Class.forName( driver ).newInstance(); } catch( Exception e ) { e.printStackTrace(); } this.dsn = dsn; this.usr = usr; this.pwd = pwd; } 

Nous pouvons maintenant plonger dans l'implémentation des méthodes abstraites. Comme vous l'avez vu dans la checkOut()méthode, ObjectPoolappellera create () à partir de sa sous-classe lorsqu'il a besoin d'instancier un nouvel objet. Car JDBCConnectionPool, il suffit de créer un nouvel Connectionobjet et de le renvoyer. Encore une fois, pour garder cet article simple, je jette la prudence au vent et j'ignore toutes les exceptions et conditions de pointeur nul.

 Object create() { try { return( DriverManager.getConnection( dsn, usr, pwd ) ); } catch( SQLException e ) { e.printStackTrace(); return( null ); } } 

Avant de ObjectPoollibérer un objet expiré (ou non valide) pour le garbage collection, il le passe à sa expire()méthode sous- classée pour tout nettoyage de dernière minute nécessaire (très similaire à la finalize()méthode appelée par le garbage collector). Dans le cas de JDBCConnectionPool, tout ce que nous devons faire est de fermer la connexion.

void expire( Object o ) { try { ( ( Connection ) o ).close(); } catch( SQLException e ) { e.printStackTrace(); } } 

Et enfin, nous devons implémenter la méthode validate () qui ObjectPoolappelle pour s'assurer qu'un objet est toujours valide pour une utilisation. C'est également à cet endroit que toute réinitialisation doit avoir lieu. Car JDBCConnectionPool, nous vérifions simplement que la connexion est toujours ouverte.

 boolean validate( Object o ) { try { return( ! ( ( Connection ) o ).isClosed() ); } catch( SQLException e ) { e.printStackTrace(); return( false ); } } 

Voilà pour la fonctionnalité interne. JDBCConnectionPoolpermettra à l'application d'emprunter et de renvoyer des connexions de base de données via ces méthodes incroyablement simples et bien nommées.

 public Connection borrowConnection() { return( ( Connection ) super.checkOut() ); } public void returnConnection( Connection c ) { super.checkIn( c ); } 

Cette conception a quelques défauts. Le plus important est peut-être la possibilité de créer un grand pool d'objets qui ne sont jamais libérés. Par exemple, si un groupe de processus demande un objet du pool simultanément, le pool créera toutes les instances nécessaires. Ensuite, si tous les processus renvoient les objets au pool, mais checkOut()ne sont plus jamais appelés, aucun des objets n'est nettoyé. Il s'agit d'un événement rare pour les applications actives, mais certains processus dorsaux qui ont un temps «d'inactivité» peuvent produire ce scénario. J'ai résolu ce problème de conception avec un fil de discussion «nettoyer», mais je conserverai cette discussion pour la seconde moitié de cet article. Je couvrirai également la gestion appropriée des erreurs et la propagation des exceptions pour rendre le pool plus robuste pour les applications critiques.

Thomas E. Davis est un programmeur Java certifié Sun. Il réside actuellement dans le sud de la Floride ensoleillée, mais souffre d'un bourreau de travail et passe la plupart de son temps à l'intérieur.

Cette histoire, "Construisez votre propre ObjectPool en Java, partie 1" a été initialement publiée par JavaWorld.