Récemment, j'ai rencontré un bug lié à la gestion des exceptions dans Spring. Voici une description simplifiée du code en cause :
public void insererDonnees() {
try {
Client client = new Client();
client.setCode(9876L); // Clé primaire avec index unique
clientDao.ajouter(client);
} catch (DuplicateKeyException e) {
log.error("Exception de clé dupliquée : {}", e.getMessage(), e);
// Traitement spécifique pour DuplicateKeyException
} catch (DataIntegrityViolationException e) {
log.error("Exception d'intégrité : {}", e.getMessage(), e);
// Traitement spécifique pour DataIntegrityViolationException
} catch (Exception e) {
log.error("Exception générale : {}", e.getMessage(), e);
}
}
Curieusement, ce même code déployé sur différentes machines (avec une base de données unique, sans partitionnement) produisait des résultats incohérents :
- Sur la machine A, un conflit de clé primaire générait une
DuplicateKeyException, exécutant la logique correspondante. - Sur la machine B, le même conflit générait une
DataIntegrityViolationException, exécutant une logique différente. - Après redémarrage de la machine B, le comportement redevenait identique à la machine A.
Analyse des exceptions dans le contexte Spring et JDBC
Encapsulation des exceptions standards par Spring
| Type d'exception | Origine | Scénario de déclenchement |
|---|---|---|
SQLIntegrityConstraintViolationException |
Standard JDBC (sous-classe de java.sql.SQLException) |
Violation d'une contrainte d'intégrité (clé primaire, clé étrangère, index unique, etc.) |
DuplicateKeyException |
Exception Spring (Spring Data ou Spring JDBC) | Insertion ou mise à jour enfreignant un index unique ou une clé primaire |
DataIntegrityViolationException |
Exception Spring (couche d'accès aux données) | Violation générale de l'intégrité des données (incluent clés dupliquées, contraintes non nulles, etc.) |
Spring encapsule les exceptions JDBC standards. La classe SQLErrorCodesFactory charge des fichiers de configuration des codes d'erreur spécifiques aux bases de données :
public static final String CHEMIN_CODES_ERREUR_SQL
= "org/springframework/jdbc/support/sql-error-codes.xml";
Origine du problème
La classe SQLErrorCodeSQLExceptionTranslator effectue la conversion des exceptions. Elle utilise un tableau de codes d'erreur chargé dynamiquement :
if (Arrays.binarySearch(codesErreur.getCodesCleDupliquee(), codeErreur) >= 0) {
return new DuplicateKeyException(message, sqlEx);
} else if (Arrays.binarySearch(codesErreur.getCodesIntegriteDonnees(), codeErreur) >= 0) {
return new DataIntegrityViolationException(message, sqlEx);
}
// ... autres cas
Le tableau codesErreur est initialisé via :
try {
String nomBase = JdbcUtils.extractDatabaseMetaData(sourceDonnees, "getDatabaseProductName");
if (StringUtils.hasLength(nomBase)) {
return enregistrerBase(sourceDonnees, nomBase);
}
} catch (MetaDataAccessException ex) {
logger.warn("Erreur lors de l'extraction du nom de la base - retour aux codes d'erreur vides", ex);
}
return new SQLErrorCodes();
Si une exception survient lors de la connexion initiale (par exemple, une connexion fermée prématurément), le champ codesErreur reste vide. Dans ce cas, Spring utilise une stratégie de repli via SQLExceptionSubclassTranslator, qui convertit toutes les SQLIntegrityConstraintViolationException en DataIntegrityViolationException :
if (ex instanceof SQLIntegrityConstraintViolationException) {
return new DataIntegrityViolationException(message, ex);
}
// ... autres mappings de repli
Reproduction du problème
Cas erroné
Simulons une connexion défaillante au démarrage :
@Transactional
public void testerConnexion() {
try {
Connection connexion = DataSourceUtils.getConnection(sourceDonnees);
connexion.close(); // Fermeture forcée pour corrompre l'initialisation
clientDao.chercherParId(1L);
} catch (DuplicateKeyException e) {
log.error("Exception de clé dupliquée : {}", e.getMessage(), e);
} catch (DataIntegrityViolationException e) {
log.error("Exception d'intégrité : {}", e.getMessage(), e);
} catch (Exception e) {
log.error("Exception : {}", e.getMessage(), e);
}
}
Après exécution, un conflit d'index unique déclenche une DataIntegrityViolationException.
Cas correct
En retirant la ligne connexion.close(), l'initialisation réusit. Un conflit d'index unique génère alors une DuplicateKeyException comme attendu.
Solutions
Ce bug a été rapporté et corrigé dans Spring Framwork (commit 670b9fd). Les solutions sont :
Solution 1 : mise à jour
Passer à Spring Framework version 5.2.9.RELEASE ou ultérieure.
Solution 2 : contournement
Étape 1 : Précharger et vérifier les codes d'erreur au démarrage :
public class PrechargeurMetadonnees {
@PostConstruct
public void initialiser() {
try {
SQLErrorCodes codes = fabriqueCodes.getCodes(sourceDonnees);
log.info("Métadonnées préchargées : {}", JsonUtils.versJson(codes));
String[] codesCleDupliquee = codes.getCodesCleDupliquee();
if (ArrayUtils.isEmpty(codesCleDupliquee)) {
log.error("Aucun code de clé dupliquée trouvé - redémarrage recommandé");
}
} catch (Exception e) {
log.error("Échec du préchargement des métadonnées", e);
}
}
}
Étape 2 : Vérifier la base de données en cas d'exception d'intégrité :
catch (DataIntegrityViolationException e) {
log.error("Exception d'intégrité : {}", e.getMessage(), e);
Client existant = clientDao.chercherParIdentifiant(identifiant);
if (existant != null) {
// Conflit d'index unique
} else {
// Autre violation d'intégrité
}
}