Cet article présente une solution complète pour la normalisation et l'encapsulation des énumérations dans les applications Java entreprise.
- Standardisation et encapsulation
Les simples spécifications techniques ne suffisent pas sans implémentation concrète et outils associés. Pour garantir la qualité de mise en œuvre, il est essentiel d'encapsuler les meilleures pratiques.
Les raisons principales sont :
- Les spécifications dépendent de l'exécution humaine, ce qui constitue le maillon le plus fragile
- L'encapsulation et la réutilisation constituent la meilleure garantie de qualité
1.1. Normalisation des énumérations
Les énumérations Java natives proposent uniquement les propriétés name et ordinal, qui toutes deux peuvent changer lors de refactorisations. Pour résoudre ces effets secondaires, de nouvelles capacités ont été ajoutées via des interfaces :
- Ajout d'un
codecomme identifiant unique de l'énumération - Ajout d'une
descriptionpour l'affichage standardisé
Deux interfaces ont été définies:
CodeBasedEnum: fournit la méthodegetCodepour l'identifiant uniqueSelfDescribedEnum: fournit la méthodegetDescriptionpour les informations descriptives
À partir de ces deux interfaces, une énumération commune CommonEnum a été construite :
// Définition de l'interface d'énumération unifiée
public interface CommonEnum extends CodeBasedEnum, SelfDescribedEnum {
}
La structure globale se présente comme suit :
LORS DE LA DÉFINITION D'UNE ÉNUMÉRATION, IL EST POSSIBLE D'IMPLÉMENTER DIRECTEMENT L'INTERFACE CommonEnum. VOICI UN EXEMPLE :
public enum CommonOrderStatus implements CommonEnum {
CREATED(1, "En attente de paiement"),
TIMEOUT_CANCELLED(2, "Annulé pour expiration"),
MANUAL_CANCELLED(5, "Annulation manuelle"),
PAID(3, "Paiement réussi"),
FINISHED(4, "Terminé");
private final int code;
private final String description;
CommonOrderStatus(int code, String description) {
this.code = code;
this.description = description;
}
@Override
public String getDescription() {
return description;
}
@Override
public int getCode() {
return this.code;
}
}
1.2. Gestion centralisée des CommonEnum
L'AVANTAGE PRINCIPAL DE CommonEnum EST LA GESTION CENTRALISÉE. La première étape consiste à découvrir et enregistrer toutes les implémentations de CommonEnum.
LE FLUX DE TRAITEMENT PRINCIPAL COMPREND :
- Utilisation de
ResourcePatternResolverde Spring pour analyser le classpath selon le package configuré - Représentation des résultats de scan via des objets
Resource, puis analyse des métadonnées viaMetadataReader - Identification des classes implémentant
CommonEnum - Enregistrement des implémentations dans deux maps pour mise en cache
Note importante : L'utilisation directe de réflexion est à éviter absolument, car elle déclenche le chargement automatique des classes et charge de nombreuses classes inutiles, augmentant la pression sur le metaspace.
Lorsque CommonEnum est nécessaire, il suffit d'injecter le bean CommonEnumRegistry pour accéder facilement à toutes les implémentations.
1.3. Intégration Spring MVC
AVEC CommonEnum UNIFIÉ, IL EST POSSIBLE DE GÉRER CENTRALISEMENT LES ÉNUMÉRATIONS ET DE LAISSER LE CADRE EFFECTUER AUOTMATIQUEMENT L'INTÉGRATION AVEC Spring MVC. Le contenu de l'intégration comprend :
- Utilisation du
codecomme identifiant unique des paramètres d'entrée pour éviter les anomalies métier liées aux changements denameouordinal - INFORMATION DE RETOUR COMPRENANT LE
code, LEnameET LAdescriptionDE L'ÉNUMÉRATION - DICTIONNAIRE D'ÉNUMÉRATIONS COMMUN BASÉ SUR
CommonEnumRegistry
1.3.1. Intégration des paramètres d'entrée
LE PRINCIPE CLÉ EST D'UTILISER LE code COMME IDENTIFIANT UNIQUE DE L'ÉNUMÉRATION ET D'AUTOMATISER LA CONVERSION DU code VERS L'ÉNUMÉRATION.
Spring MVC propose deux extensions pour la conversion de paramètres :
POUR LES PARAMÈTRES STANDARDS (RequestParam ou PathVariable), extension de ConditionalGenericConverter :
- Réécriture des méthodes
matchesetgetConvertibleTypesbasées sur les informations CommonEnum fournies parCommonEnumRegistry - Conversion selon
codeetnameen获取toutes les valeurs d'énumération selon le type cible
POUR LES PARAMÈTRES JSON, extension du framework JSON (exemple avec Jackson) :
- Parcours de tous les CommonEnum fournis par
CommonEnumRegistrypour enregistrement séquentiel - Lecture des informations du JSON et conversion vers une valeur d'énumération définie selon
codeetname
@Order(1)
@Component
public class CommonEnumConverter implements ConditionalGenericConverter {
@Autowired
private CommonEnumRegistry enumRegistry;
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
Class<?> type = targetType.getType();
return enumRegistry.getClassDict().containsKey(type);
}
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return enumRegistry.getClassDict().keySet().stream()
.map(cls -> new ConvertiblePair(String.class, cls))
.collect(Collectors.toSet());
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
String value = (String) source;
List<CommonEnum> commonEnums = this.enumRegistry.getClassDict().get(targetType.getType());
return commonEnums.stream()
.filter(commonEnum -> commonEnum.match(value))
.findFirst()
.orElse(null);
}
}
static class CommonEnumJsonDeserializer extends JsonDeserializer {
private final List<CommonEnum> commonEnums;
CommonEnumJsonDeserializer(List<CommonEnum> commonEnums) {
this.commonEnums = commonEnums;
}
@Override
public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JacksonException {
String value = jsonParser.readValueAs(String.class);
return commonEnums.stream()
.filter(commonEnum -> commonEnum.match(value))
.findFirst()
.orElse(null);
}
}
1.3.2. Intégration des retours
PAR DÉFAUT, POUR LES ÉNUMÉRATIONS CONVERTIES EN JSON, SEUL LE name EST SORTI, ET LES AUTRES INFORMATIONS SONT PERDUES, CE QUI EST PEU PRATIQUE POUR L'AFFICHAGE. UNE AMÉLIORATION DE LA SÉRIALISATION JSON EST DONC NÉCESSAIRE.
D'ABORD, IL FAUT DÉFINIR L'OBJET DE RETOUR CORRESPONDANT À CommonEnum :
@Value
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@ApiModel(description = "Énumération commune")
public class CommonEnumVO {
@ApiModelProperty(notes = "Code")
private final int code;
@ApiModelProperty(notes = "Nom")
private final String name;
@ApiModelProperty(notes = "Description")
private final String desc;
public static CommonEnumVO from(CommonEnum commonEnum) {
if (commonEnum == null) {
return null;
}
return new CommonEnumVO(commonEnum.getCode(), commonEnum.getName(), commonEnum.getDescription());
}
public static List<CommonEnumVO> from(List<CommonEnum> commonEnums) {
if (CollectionUtils.isEmpty(commonEnums)) {
return Collections.emptyList();
}
return commonEnums.stream()
.filter(Objects::nonNull)
.map(CommonEnumVO::from)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}
CommonEnumVO est un POJO standard avec uniquement des annotations Swagger ajoutées.
CommonEnumJsonSerializer est le cœur de la sérialisation personnalisée, encapsulant CommonEnum en CommonEnumVO pour l'écriture :
static class CommonEnumJsonSerializer extends JsonSerializer {
@Override
public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
CommonEnum commonEnum = (CommonEnum) o;
CommonEnumVO commonEnumVO = CommonEnumVO.from(commonEnum);
jsonGenerator.writeObject(commonEnumVO);
}
}
1.3.3. Dictionnaire d'énumérations commun
AVEC CommonEnum, IL EST POSSIBLE DE FOURNIR UNE INTERFACE DE DICTIONNAIRE D'ÉNUMÉRATIONS UNIFIÉE POUR ÉVITER LE DÉVELOPPEMENT RÉPÉTÉ. LORS DE L'AJOUT DE NOUVELLES ÉNUMÉRATIONS, AUCUN CODAGE SUPPLÉMENTAIRE N'EST REQUIS CAR LE SYSTÈME LES IDENTIFIE ET LES AJOUTE AUTOMATIQUEMENT AU DICTIONNAIRE.
L'implémentation de l'interface de dictionnaire commun basée sur CommonEnumRegistry est simple, il suffit de construire le Controller selon les normes :
@Api(tags = "Interface de dictionnaire commun")
@RestController
@RequestMapping("/enumDict")
@Slf4j
public class EnumDictController {
@Autowired
private CommonEnumRegistry commonEnumRegistry;
@GetMapping("all")
public RestResult<Map<String, List<CommonEnumVO>>> allEnums() {
Map<String, List<CommonEnum>> dict = this.commonEnumRegistry.getNameDict();
Map<String, List<CommonEnumVO>> dictVo = Maps.newHashMapWithExpectedSize(dict.size());
for (Map.Entry<String, List<CommonEnum>> entry : dict.entrySet()) {
dictVo.put(entry.getKey(), CommonEnumVO.from(entry.getValue()));
}
return RestResult.success(dictVo);
}
@GetMapping("types")
public RestResult<List<String>> enumTypes() {
Map<String, List<CommonEnum>> dict = this.commonEnumRegistry.getNameDict();
return RestResult.success(Lists.newArrayList(dict.keySet()));
}
@GetMapping("/{type}")
public RestResult<List<CommonEnumVO>> dictByType(@PathVariable("type") String type) {
Map<String, List<CommonEnum>> dict = this.commonEnumRegistry.getNameDict();
List<CommonEnum> commonEnums = dict.get(type);
return RestResult.success(CommonEnumVO.from(commonEnums));
}
}
Ce Controller offre les fonctionnalités suivantes :
- Obtention du dictionnaire complet : récupération de tous les CommonEnum du système en une seule fois
- Obtention de tous les types de dictionnaire : récupération uniquement des types, généralement utilisé pour les tests
- Obtention de toutes les informations d'un type de dictionnaire spécifique : par exemple pour le remplissage des listes déroulantes
1.4. Intégration de la couche de stockage
LA COUCHE DE STOCKAGE NE FOURNIT PAS SUFFISAMMENT DE CAPACITÉS D'EXTENSION ET NE PEUT PAS ENREGISTRER AUTOMATIQUEMENT LES CONVERTISSEURS DE TYPE. CEPENDANT, COMME LA LOGIQUE EST SIMILAIRE, LE CADRE FOURNIT UNE CLASSE PARENTE COMMUNE POUR LA RÉUTILISATION.
1.4.1. Intégration MyBatis
CommonEnumTypeHandler implémente l'interface BaseTypeHandler de MyBatis et fournit une capacité de conversion commune pour CommonEnum :
public abstract class CommonEnumTypeHandler<T extends Enum<T> & CommonEnum>
extends BaseTypeHandler<T> {
private final List<T> commonEnums;
protected CommonEnumTypeHandler(T[] commonEnums) {
this(Arrays.asList(commonEnums));
}
protected CommonEnumTypeHandler(List<T> commonEnums) {
this.commonEnums = commonEnums;
}
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, T t, JdbcType jdbcType)
throws SQLException {
preparedStatement.setInt(i, t.getCode());
}
@Override
public T getNullableResult(ResultSet resultSet, String columnName) throws SQLException {
int code = resultSet.getInt(columnName);
return commonEnums.stream()
.filter(commonEnum -> commonEnum.match(String.valueOf(code)))
.findFirst()
.orElse(null);
}
@Override
public T getNullableResult(ResultSet resultSet, int i) throws SQLException {
int code = resultSet.getInt(i);
return commonEnums.stream()
.filter(commonEnum -> commonEnum.match(String.valueOf(code)))
.findFirst()
.orElse(null);
}
@Override
public T getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
int code = callableStatement.getInt(i);
return commonEnums.stream()
.filter(commonEnum -> commonEnum.match(String.valueOf(code)))
.findFirst()
.orElse(null);
}
}
1.4.2. Intégration JPA
CommonEnumAttributeConverter implémente l'interface AttributeConverter de JPA et fournit une capacité de conversion commune pour CommonEnum :
public abstract class CommonEnumAttributeConverter<E extends Enum<E> & CommonEnum>
implements AttributeConverter<E, Integer> {
private final List<E> commonEnums;
public CommonEnumAttributeConverter(E[] commonEnums) {
this(Arrays.asList(commonEnums));
}
public CommonEnumAttributeConverter(List<E> commonEnums) {
this.commonEnums = commonEnums;
}
@Override
public Integer convertToDatabaseColumn(E e) {
return e.getCode();
}
@Override
public E convertToEntityAttribute(Integer code) {
return (E) commonEnums.stream()
.filter(commonEnum -> commonEnum.match(String.valueOf(code)))
.findFirst()
.orElse(null);
}
}
- Exemples d'application
APRÈS L'ENCAPSULATION, LE CODE MÉTIER DEVIENT TRÈS SIMPLE.
2.1. Configuration du projet
COMME L'ENCAPSULATION EST FAITE DANS LE PROJET LEGO, LA PREMIÈRE ÉTAPE EST D'INTRODUIRE LA DÉPENDANCE LEGO :
<dependency>
<groupId>com.geekhalo.lego</groupId>
<artifactId>lego-starter</artifactId>
<version>0.1.23</version>
</dependency>
POUR RECONNAÎTRE AUTOMATIQUEMENT LES IMPLÉMENTATIONS DE CommonEnum, IL FAUT SPÉCIFIER LE PACKAGE DE SCAN :
baseEnum:
basePackage: com.geekhalo.demo
AU DÉMARRAGE DU PROJET, CommonEnumRegistry ANALYSE AUTOMATIQUEMENT LES IMPLÉMENTATIONS DE CommonEnum SOUS LE PACKAGE ET EFFECTUE L'ENREGISTREMENT.
ENFIN, IL FAUT AJOUTER LA CONFIGURATION SUIVANTE SUR LA CLASSE DE DÉMARRAGE :
@ComponentScan(value = "com.geekhalo.lego.core.enums")
CELA PERMET À SPRING DE CHARGER LES COMPOSANTS PRINCIPAUX.
2.2. Exemple Spring MVC
APRÈS AVOIR TERMINÉ CES CONFIGURATIONS, L'INTÉGRATION AVEC Spring MVC EST TERMINÉE. Créez une énumération OrderStatus :
public enum CommonOrderStatus implements CommonEnum {
CREATED(1, "En attente de paiement"),
TIMEOUT_CANCELLED(2, "Annulé pour expiration"),
MANUAL_CANCELLED(5, "Annulation manuelle"),
PAID(3, "Paiement réussi"),
FINISHED(4, "Terminé");
private final int code;
private final String description;
CommonOrderStatus(int code, String description) {
this.code = code;
this.description = description;
}
@Override
public String getDescription() {
return description;
}
@Override
public int getCode() {
return this.code;
}
}
2.2.1. Exemple de paramètre d'entrée
COMME LE MONTRE LA FIGURE :
ON PEUT VOIR QUE status : 3 EST AUTOMATIQUEMENT CONVERTI EN PAID, RÉUSSISSANT LA CONVERSION DU code VERS CommonOrderStatus.
2.2.2. Exemple de résultat de retour
LE RÉSULTAT DE RETOUR N'EST PLUS SIMPLEMENT LE name, MAIS UN OBJET COMPRENANT LES CHAMPS : code, name, desc, ETC.
2.2.3. Exemple de dictionnaire commun
VIA SWAGGER, ON PEUT VOIR QU'UN CONTROLLER DE DICTIONNAIRE A ÉTÉ AJOUTÉ :
/enumDict/types retourne tous les types de champs chargés, comme ceci :
LE SYSTÈME NE CONTIENT QU'UNE SEULE CLASSE D'IMPLÉMENTATION CommonOrderStatus, LES NOUVELLES IMPLÉMENTATIONS APPARAÎTRONT AUTOMATIQUEMENT ICI.
/enumDict/all retourne toutes les informations du dictionnaire :
RETOURNE TOUTES LES INFORMATIONS DU DICTIONNAIRE EN UNE SEULE FOIS.
/enumDict/{type} retourne les informations du dictionnaire spécifié :
RETOURNE LE DICTIONNAIRE CommonOrderStatus SPÉCIFIÉ.
2.3. Exemple de couche de stockage
AVEC LES CLASSES PARENTES COMMUNES RÉUTILISABLES, LES CONVERTISSEURS DE TYPE DEVIENNENT TRÈS SIMPLES.
2.3.1. Convertisseur de type MyBatis
LE CONVERTISSEUR DE TYPE MyBatis N'A QU'À HÉRITER DE CommonEnumTypeHandler :
@MappedTypes(CommonOrderStatus.class)
public class CommonOrderStatusTypeHandler extends CommonEnumTypeHandler<CommonOrderStatus> {
public CommonOrderStatusTypeHandler() {
super(CommonOrderStatus.values());
}
}
N'OUBLIEZ PAS D'AJOUTER LA CONFIGURATION MyBatis :
mybatis:
type-handlers-package: com.geekhalo.demo.enums.code.fix
2.3.2. Convertisseur de type JPA
LE CONVERTISSEUR DE TYPE JPA N'A QU'À HÉRITER DE CommonEnumAttributeConverter :
@Converter(autoApply = true)
public class CommonOrderStatusAttributeConverter extends CommonEnumAttributeConverter<CommonOrderStatus> {
public CommonOrderStatusAttributeConverter() {
super(CommonOrderStatus.values());
}
}
SI NÉCESSAIRE, IL EST POSSIBLE D'AJOUTER UNE ANNOTATION SUR L'ATTRIBUT DE L'ENTITÉ :
/**
* Spécifier le convertisseur d'énumération
*/
@Convert(converter = CommonOrderStatusAttributeConverter.class)
private CommonOrderStatus status;
- Exemple et code source
DÉPÔT DE CODE : https://gitee.com/ltao851025/learnFromBug
ADRESSE DU CODE : https://gitee.com/ltao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/enums/support