Normalisation des énumérations - Retour d'expérience

Cet article présente une solution complète pour la normalisation et l'encapsulation des énumérations dans les applications Java entreprise.

  1. 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 code comme identifiant unique de l'énumération
  • Ajout d'une description pour l'affichage standardisé

Deux interfaces ont été définies:

  • CodeBasedEnum : fournit la méthode getCode pour l'identifiant unique
  • SelfDescribedEnum : fournit la méthode getDescription pour 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 :

  1. Utilisation de ResourcePatternResolver de Spring pour analyser le classpath selon le package configuré
  2. Représentation des résultats de scan via des objets Resource, puis analyse des métadonnées via MetadataReader
  3. Identification des classes implémentant CommonEnum
  4. 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 code comme identifiant unique des paramètres d'entrée pour éviter les anomalies métier liées aux changements de name ou ordinal
  • INFORMATION DE RETOUR COMPRENANT LE code, LE name ET LA description DE 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 matches et getConvertibleTypes basées sur les informations CommonEnum fournies par CommonEnumRegistry
  • Conversion selon code et name en获取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 CommonEnumRegistry pour enregistrement séquentiel
  • Lecture des informations du JSON et conversion vers une valeur d'énumération définie selon code et name
@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);
    }
}

  1. 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;

  1. 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

Étiquettes: Java enums spring-mvc MyBatis JPA

Publié le 23 juillet à 21h25