Table of Contents
Le modèle Singleton est l'un des modèles de conception créationnels les plus utilisés en Java. Il garantit qu'une classe a exactement une instance et fournit un point d'accès global à cette instance. Dans les applications d'entreprise, ce modèle est couramment utilisé pour gérer les connexions de base de données – une ressource critique qui, si mal géré, peut conduire à la dégradation des performances, des fuites de ressources et un comportement imprévisible.
Pourquoi utiliser le modèle Singleton pour les connexions de base de données?
Chaque connexion consomme des ressources de mémoire, de sockets réseau et de serveur de base de données. Sans gestion prudente, une application peut rapidement épuiser le pool de connexion, ce qui entraîne des temps d'attente et des défaillances. Le modèle Singleton s'en occupe en s'assurant qu'une seule instance de connexion existe tout au long du cycle de vie de l'application.
- Gestion des ressources: Limite le nombre de connexions actives, réduisant les frais généraux sur le client et le serveur de base de données.
- Consistance:[ Tous les composants partagent la même connexion, évitant les divergences d'état causées par plusieurs instances de connexion.
- Facile de maintenance: Centralise la création, la configuration et la démontage de la connexion en une seule classe, simplifie les mises à jour et les tests.
- Problème de vie prévisible:[ La connexion est créée une fois et réutilisée, éliminant le besoin d'authentification répétée et poignée de main en hauteur.
Cependant, il est important de noter qu'un seul n'est pas sûr de thread par défaut. Si plusieurs threads utilisent simultanément la même connexion, vous devez soit synchroniser l'accès soit vous appuyer sur un pool de connexion. Nous traiterons ces nuances plus tard.
Les fondamentaux du modèle Singleton en Java
Une classe de Singleton a trois caractéristiques principales:
- Un constructeur privé pour empêcher l'invocation de l'extérieur de la classe.
- Une variable statique privée qui détient la seule instance.
- Une méthode statique publique[ (souvent appelée ) qui renvoie l'instance, la créant si nécessaire.
Lorsqu'elle est appliquée aux connexions de base de données, la classe singleton gère également l'objet de connexion et fournit une méthode pour le récupérer (par exemple, . Commençons par l'implémentation la plus simple et puis améliorez-la pour la sécurité des fils.
Base (non-fil-sûre) Singleton
L'implémentation la plus simple est l'initialisation avide, où l'instance est créée lorsque la classe est chargée. Cela évite la synchronisation entièrement mais peut gaspiller les ressources si la connexion n'est jamais utilisée.
public class DatabaseConnection {
private static final DatabaseConnection INSTANCE = new DatabaseConnection();
private final Connection connection;
private DatabaseConnection() {
this.connection = createConnection();
}
public static DatabaseConnection getInstance() {
return INSTANCE;
}
public Connection getConnection() {
return connection;
}
private Connection createConnection() {
// JDBC connection logic (simplified)
try {
Class.forName("com.mysql.cj.jdbc.Driver");
return DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mydb", "user", "password");
} catch (ClassNotFoundException | SQLException e) {
throw new RuntimeException("Failed to create database connection", e);
}
}
}
Cette approche paresseuse n'est pas sûre de thread car deux threads peuvent simultanément voir et chaque fois créer une nouvelle instance. Pour les systèmes de production, vous avez besoin de thread security. Ensuite, nous explorons trois implémentations robustes.
Mise en œuvre de fils et de fils de fer à simpleton
1. Méthode synchronisée
L'ajout du mot clé à la méthode permet de s'assurer qu'un seul thread peut l'exécuter à la fois. Ceci est simple mais introduit une pénalité de performance parce que chaque appel à acquiert un verrou, même après la création de l'instance. Pour une connexion de base de données qui est fréquemment accessible, ce frais généraux peut être significatif.
public class DatabaseConnection {
private static DatabaseConnection instance;
private Connection connection;
private DatabaseConnection() {
connection = createConnection();
}
public static synchronized DatabaseConnection getInstance() {
if (instance == null) {
instance = new DatabaseConnection();
}
return instance;
}
public synchronized Connection getConnection() {
return connection;
}
private Connection createConnection() { /* ... */ }
}
Notez que même après l'initialisation, chaque appel à nécessite une synchronisation. Cette méthode n'est acceptable que si le singleton est créé très rarement, mais pour les connexions à la base de données, il vaut mieux utiliser l'une des approches suivantes.
2. Verrouillage double-coché (DCL)
DCL réduit la synchronisation en vérifiant d'abord la variable d'instance sans synchronisation, et seulement en synchronisant lorsque l'instance est réellement . Cependant, il est notoirement difficile d'implémenter correctement dans les anciennes versions Java en raison du modèle Java Memory. Depuis Java 5, l'utilisation d'un mot clé permet de s'assurer que la variable d'instance est lue à partir de la mémoire principale et que l'objet partiellement construit n'est pas visible pour d'autres threads.
public class DatabaseConnection {
private static volatile DatabaseConnection instance;
private Connection connection;
private DatabaseConnection() {
connection = createConnection();
}
public static DatabaseConnection getInstance() {
if (instance == null) {
synchronized (DatabaseConnection.class) {
if (instance == null) {
instance = new DatabaseConnection();
}
}
}
return instance;
}
public Connection getConnection() {
return connection;
}
private Connection createConnection() { /* ... */ }
}
Ce modèle est largement utilisé et offre de bonnes performances car la synchronisation n'arrive qu'une seule fois. Cependant, le mot clé a une légère barrière de mémoire en tête, qui est négligeable pour la plupart des applications.
3. Bill Pugh Singleton (Idiome de l'initiateur sur demande)
Cette technique utilise une classe intérieure statique privée qui tient l'instance de singleton. La classe intérieure n'est pas chargée avant que soit appelée, ce qui rend l'initialisation paresseuse. Elle est sans fil par conception parce que le JVM garantit que l'initialisation de classe est sérialisée. Aucune synchronisation explicite ou n'est nécessaire.
public class DatabaseConnection {
private Connection connection;
private DatabaseConnection() {
connection = createConnection();
}
private static class Holder {
static final DatabaseConnection INSTANCE = new DatabaseConnection();
}
public static DatabaseConnection getInstance() {
return Holder.INSTANCE;
}
public Connection getConnection() {
return connection;
}
private Connection createConnection() {
// JDBC setup
try {
Class.forName("com.mysql.cj.jdbc.Driver");
return DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mydb", "user", "password");
} catch (ClassNotFoundException | SQLException e) {
throw new RuntimeException("Failed to create database connection", e);
}
}
}
Ce modèle est recommandé pour la plupart des scénarios car il combine initialisation paresseuse avec sécurité garantie du fil et synchronisation zéro frais généraux. Il fonctionne également correctement avec sérialisation si vous implémentez .
4. Enum Singleton
Joshua Bloch préconise un singleton basé sur dans son livre Java efficace. Les enums sont intrinsèquement sérialisables et offrent une protection contre les attaques de réflexion. Ils sont également sans fil et implicitement paresseux. Cependant, l'utilisation d'un enum pour une connexion à une base de données nécessite la connexion à l'intérieur du constructeur de la constante enum, qui est exécutée lorsque la classe enum est accessible pour la première fois.
public enum DatabaseConnection {
INSTANCE;
private Connection connection;
DatabaseConnection() {
connection = createConnection();
}
public Connection getConnection() {
return connection;
}
private Connection createConnection() {
// JDBC setup
try {
Class.forName("com.mysql.cj.jdbc.Driver");
return DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mydb", "user", "password");
} catch (ClassNotFoundException | SQLException e) {
throw new RuntimeException("Failed to create database connection", e);
}
}
}
Utilisation : . L'approche énum est concise et robuste, bien qu'elle charge la connexion dès que toute référence à est faite. Pour de nombreuses applications, ceci est acceptable.
Le regroupement des connexions : une meilleure alternative?
Bien que le modèle Singleton soit approprié pour limiter une connexion unique, la plupart des applications du monde réel nécessitent la mise en commun des connexions. Un pool de connexion maintient un ensemble de connexions qui peuvent être empruntées et retournées, offrant une meilleure évolutivité et une meilleure utilisation des ressources.
Les implémentations de pool de connexion populaires incluent HikariCP, Apache DBCP[, et Vibur DBCP[. L'utilisation d'un simpleton pour tenir un soutenu par un pool est un modèle commun.
public class DataSourceSingleton {
private static final HikariConfig config = new HikariConfig();
private static final HikariDataSource dataSource;
static {
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(10);
config.setConnectionTimeout(30000);
dataSource = new HikariDataSource(config);
}
private DataSourceSingleton() {}
public static DataSource getDataSource() {
return dataSource;
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}
Ce singleton fournit un global]. Lorsque vous avez besoin d'une connexion, vous appelez , qui emprunte à la piscine. Après utilisation, vous fermez la connexion (qui la retourne à la piscine). Ceci est beaucoup plus évolutive qu'une seule connexion et bénéficie encore d'une gestion centralisée.
Quand utiliser Singleton vs. Poing
- Connectation unique:[ Acceptable uniquement pour les bases de données intégrées (p. ex., H2 en mémoire), les applications très peu routières, ou lorsque la base de données est un simple magasin à valeur clé et que les transactions ne sont pas simultanées.
- Exemple de simpleton d'un pool:[ L'approche recommandée pour les applications multifilaires. Le pool est un simpleton, mais les connexions sont multiples. Utilisez le modèle de Singleton pour tenir le .
Meilleures pratiques et considérations
- Sécurité des fils:[ Faites toujours votre filetage simpleton sûr. Utilisez l'idiome de support de Bill Pugh ou enum pour la plupart des cas. Évitez la méthode synchronisée simple pour les connexions qui sont récupérées fréquemment.
- Configuration de connexion:[ Externaliser les identifiants de base de données. Utilisez des variables d'environnement, des fichiers de configuration ou une gestion de secrets.
- Resource Cleanup:[ Dans un seul monoton de connexion, vous devez vous assurer que la connexion est fermée lorsque l'application s'arrête. Implémenter un crochet d'arrêt ou utiliser un cycle de vie géré par un conteneur.
- Error Recovery:[ Les connexions de base de données peuvent tomber. Votre monotone devrait détecter les connexions discontinues et les rétablir. L'approche du pool gère automatiquement cela; avec une seule connexion, vous pourriez avoir besoin d'implémenter un mécanisme de réessayer.
- Sérialisation: Si votre application sérialise le singleton (par exemple, dans un environnement distribué), implémentez pour retourner l'instance existante. L'approche énum évite complètement cette question.
- L'API de réflexion peut appeler des constructeurs privés. Pour empêcher cela, vous pouvez lancer une exception dans le constructeur si l'instance existe déjà. L'approche énum est immunisée.
Pièges fréquents
- Utiliser une connexion unique dans une application web multifils:[ Cela conduit à la dispute de thread et à de mauvaises performances.
- Pour éviter de fermer les connexions: Avec une connexion à un seul bouton, vous pouvez la garder ouverte pour toujours.
- Improper double-coché verrouillage sans volatile: Dans les anciennes versions Java, cela peut causer des bugs subtils. Utilisez l'idiome ou l'enum du support pour éviter la complexité.
- Ordre d'initialisation statique: Si la classe de singleton fait référence à d'autres champs statiques qui ne sont pas encore initialisés, vous pouvez faire face à des dépendances circulaires.
- Fillets d'essai :Les Singletons peuvent rendre les tests unitaires difficiles parce qu'ils ont un état global. Utilisez l'injection de dépendance pour passer une connexion ou une source de données, et moquez-vous du Singleton pour les tests.
Exemple du monde réel avec le pool de connexion
Pour illustrer une approche prête à la production, voici un simpleton qui gère un pool de connexion HikariCP en utilisant l'idiome de détenteur de Bill Pugh :
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class ConnectionPoolManager {
private final HikariDataSource dataSource;
private ConnectionPoolManager() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(System.getenv("DB_URL"));
config.setUsername(System.getenv("DB_USER"));
config.setPassword(System.getenv("DB_PASS"));
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setConnectionTimeout(5000);
config.setIdleTimeout(300000);
config.setMaxLifetime(600000);
config.setPoolName("MyAppPool");
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
dataSource = new HikariDataSource(config);
}
private static class Holder {
static final ConnectionPoolManager INSTANCE = new ConnectionPoolManager();
}
public static ConnectionPoolManager getInstance() {
return Holder.INSTANCE;
}
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
public void shutdown() {
if (dataSource != null && !dataSource.isClosed()) {
dataSource.close();
}
}
// Optional: shutdown hook
public void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown));
}
}
Utilisation :
try (Connection conn = ConnectionPoolManager.getInstance().getConnection()) {
// use connection
} catch (SQLException e) {
// handle
}
Ce modèle est robuste, sûr des fils et testé de production. Il sépare la configuration du code et assure une réutilisation efficace des connexions.
Tester les connexions de base de données Singleton
Le code de test qui dépend d'un simpleton peut être difficile parce que le singleton conserve l'état à travers les tests. Pour surmonter cela, envisager les stratégies suivantes:
- Utilisez une interface: Définissez une interface pour votre fournisseur de connexion, puis demandez à la personne qui l'applique. Dans les tests, vous pouvez remplacer une implémentation simulée.
- Réinitialiser le singleton:[ Ajouter une méthode privée ou protégée pour réinitialiser l'instance (seulement pour les essais).Utiliser la réflexion si nécessaire, mais être conscient de la sécurité du fil.
- Utilisez l'injection de dépendance:[ Au lieu d'appeler directement, injectez l'instance via un constructeur ou un setter. Cela découple le code et rend les tests simples.
public class UserService {
private final Connection connection;
public UserService(Connection connection) {
this.connection = connection;
}
// business methods
}
Dans la production, vous appelez . Dans les tests, vous pouvez passer une connexion simulée.
Conclusion
Le modèle Singleton est un outil précieux pour la gestion des connexions de base de données en Java, mais il doit être implémenté avec la sécurité des fils et la gestion des ressources en tête. Pour la plupart des applications, un pool de connexion enveloppé par un singleton fournit le meilleur équilibre de performance et de fiabilité. Choisissez l'idiome de support Bill Pugh ou enum pour leur simplicité et sécurité, et toujours externaliser la configuration. Rappelez-vous qu'une connexion unique est rarement appropriée pour la production de systèmes multi-fils. En suivant les pratiques décrites dans cet article, vous pouvez concevoir une couche de gestion de connexion de base de données robuste qui s'échelle avec votre application.
Pour plus de détails, consultez le tutoriel officiel Oracle JDBC et la documentation HikariCP.Pour une plongée plus profonde dans la concurrence Java et le modèle Singleton, consultez Baeldung's guide complet.