La meilleure façon de définir des codes / chaînes d'erreur en Java?

118

J'écris un service Web en Java et j'essaie de trouver le meilleur moyen de définir les codes d'erreur et leurs chaînes d'erreur associées . J'ai besoin d'avoir un code d'erreur numérique et une chaîne d'erreur regroupés. Le code d'erreur et la chaîne d'erreur seront envoyés au client accédant au service Web. Par exemple, lorsqu'une exception SQLException se produit, je pourrais souhaiter effectuer les opérations suivantes:

// Example: errorCode = 1, 
//          errorString = "There was a problem accessing the database."
throw new SomeWebServiceException(errorCode, errorString);

Le programme client peut afficher le message:

"L'erreur n ° 1 s'est produite: un problème est survenu lors de l'accès à la base de données."

Ma première pensée a été d'utiliser l'un Enumdes codes d'erreur et de remplacer les toStringméthodes pour renvoyer les chaînes d'erreur. Voici ce que j'ai trouvé:

public enum Errors {
  DATABASE {
    @Override
    public String toString() {
      return "A database error has occured.";
    }
  },

  DUPLICATE_USER {
    @Override
    public String toString() {
      return "This user already exists.";
    }
  },

  // more errors follow
}

Ma question est la suivante: y a-t-il une meilleure façon de procéder? Je préférerais une solution dans le code, plutôt que de lire à partir d'un fichier externe. J'utilise Javadoc pour ce projet et il serait utile de pouvoir documenter les codes d'erreur en ligne et de les mettre à jour automatiquement dans la documentation.

William Brendel
la source
Commentaire tardif mais mérite une mention Je pense ... 1) Avez-vous vraiment besoin de codes d'erreur ici dans l'exception? Voir la réponse blabla999 ci-dessous. 2) Vous devez être prudent en renvoyant trop d'informations d'erreur à l'utilisateur. Les informations d'erreur utiles doivent être écrites dans les journaux du serveur, mais le client doit être informé du strict minimum (par exemple "il y a eu un problème de connexion"). C'est une question de sécurité et d'empêcher les spoofers de prendre pied.
wmorrison365

Réponses:

161

Eh bien, il y a certainement une meilleure implémentation de la solution enum (qui est généralement assez agréable):

public enum Error {
  DATABASE(0, "A database error has occured."),
  DUPLICATE_USER(1, "This user already exists.");

  private final int code;
  private final String description;

  private Error(int code, String description) {
    this.code = code;
    this.description = description;
  }

  public String getDescription() {
     return description;
  }

  public int getCode() {
     return code;
  }

  @Override
  public String toString() {
    return code + ": " + description;
  }
}

Vous pouvez remplacer toString () pour simplement renvoyer la description à la place - pas sûr. Quoi qu'il en soit, le point principal est que vous n'avez pas besoin de remplacer séparément pour chaque code d'erreur. Notez également que j'ai explicitement spécifié le code au lieu d'utiliser la valeur ordinale - cela facilite la modification de l'ordre et l'ajout / la suppression d'erreurs plus tard.

N'oubliez pas que ce n'est pas du tout internationalisé - mais à moins que votre client de service Web ne vous envoie une description des paramètres régionaux, vous ne pouvez pas l'internationaliser facilement de toute façon. Au moins, ils auront le code d'erreur à utiliser pour i18n côté client ...

Jon Skeet
la source
13
Pour internationaliser, remplacer le champ de description par un code de chaîne qui peut être recherché dans un ensemble de ressources?
Marcus Downing
@Marcus: J'aime cette idée. Je me concentre sur le fait de faire sortir ce truc, mais quand on regarde l'internationalisation, je pense que je vais faire ce que vous avez suggéré. Merci!
William Brendel
@marcus, si toString () n'est pas surchargé (ce qu'il n'a pas besoin d'être), alors le code de chaîne pourrait simplement être la valeur enum toString () qui serait DATABASE, ou DUPLICATE_USER dans ce cas.
rouble
@Jon Skeet! J'aime cette solution, comment on pourrait produire une solution facile à localiser (ou à traduire dans d'autres langues, etc.) En pensant à l'utiliser sous Android, puis-je utiliser le R.string.IDS_XXXX au lieu de chaînes codées en dur?
AB
1
@AB: Eh bien, une fois que vous avez l'énumération, vous pouvez facilement écrire une classe pour extraire la ressource localisée pertinente de la valeur enum, via des fichiers de propriétés ou autre.
Jon Skeet
34

En ce qui me concerne, je préfère externaliser les messages d'erreur dans un fichier de propriétés. Ce sera vraiment utile en cas d'internationalisation de votre application (un fichier de propriétés par langue). Il est également plus facile de modifier un message d'erreur et il ne sera pas nécessaire de recompiler les sources Java.

Sur mes projets, j'ai généralement une interface qui contient des codes d'erreur (chaîne ou entier, peu importe), qui contient la clé dans les fichiers de propriétés pour cette erreur:

public interface ErrorCodes {
    String DATABASE_ERROR = "DATABASE_ERROR";
    String DUPLICATE_USER = "DUPLICATE_USER";
    ...
}

dans le fichier de propriétés:

DATABASE_ERROR=An error occurred in the database.
DUPLICATE_USER=The user already exists.
...

Un autre problème avec votre solution est la maintenabilité: vous n'avez que 2 erreurs, et déjà 12 lignes de code. Alors imaginez votre fichier d'énumération alors que vous aurez des centaines d'erreurs à gérer!

Romain Linsolas
la source
2
Je ferais plus de 1 si je pouvais. Le codage en dur des chaînes est moche pour la maintenance.
Robin
3
Le stockage des constantes String dans l'interface est une mauvaise idée. Vous pouvez utiliser des énumérations ou des constantes String dans les classes finales avec un constructeur privé, par package ou zone associée. Veuillez répondre à John Skeets avec Enums. Vérifiez s'il vous plaît. stackoverflow.com/questions/320588/…
Anand Varkey Philips
21

La surcharge de toString () semble un peu dégoûtante - cela semble un peu une extension de l'utilisation normale de toString ().

Qu'en est-il de:

public enum Errors {
  DATABASE(1, "A database error has occured."),
  DUPLICATE_USER(5007, "This user already exists.");
  //... add more cases here ...

  private final int id;
  private final String message;

  Errors(int id, String message) {
     this.id = id;
     this.message = message;
  }

  public int getId() { return id; }
  public String getMessage() { return message; }
}

me semble beaucoup plus propre ... et moins verbeux.

Cowan
la source
5
La surcharge de toString () sur tous les objets (sans parler des énumérations) est tout à fait normale.
cletus
+1 Pas aussi flexible que la solution de Jon Skeet, mais elle résout toujours bien le problème. Merci!
William Brendel
2
Je voulais dire que toString () est le plus couramment et utilement utilisé pour donner suffisamment d'informations pour identifier l'objet - il inclut souvent le nom de la classe, ou un moyen de dire de manière significative le type d'objet. Un toString () qui renvoie simplement «Une erreur de base de données s'est produite» serait surprenant dans de nombreux contextes.
Cowan
1
Je suis d'accord avec Cowan, utiliser toString () de cette manière semble un peu «hackish». Juste un coup rapide pour l'argent et pas une utilisation normale. Pour l'énumération, toString () doit renvoyer le nom de la constante d'énumération. Cela semblerait intéressant dans un débogueur lorsque vous voulez la valeur d'une variable.
Robin
19

Lors de mon dernier travail, je suis allé un peu plus loin dans la version enum:

public enum Messages {
    @Error
    @Text("You can''t put a {0} in a {1}")
    XYZ00001_CONTAINMENT_NOT_ALLOWED,
    ...
}

@Error, @Info, @Warning sont conservés dans le fichier de classe et sont disponibles au moment de l'exécution. (Nous avons eu quelques autres annotations pour aider à décrire également la livraison des messages)

@Text est une annotation à la compilation.

J'ai écrit un processeur d'annotations pour cela qui a fait ce qui suit:

  • Vérifiez qu'il n'y a pas de numéros de message en double (la partie avant le premier trait de soulignement)
  • Vérifiez la syntaxe du texte du message
  • Générez un fichier messages.properties contenant le texte, indexé par la valeur d'énumération.

J'ai écrit quelques routines utilitaires qui ont aidé à consigner les erreurs, à les envelopper comme des exceptions (si vous le souhaitez) et ainsi de suite.

J'essaye de les amener à me laisser l'open-source ... - Scott

Scott Stanchfield
la source
Belle façon de gérer les messages d'erreur. L'avez-vous déjà open-source?
bobbel
5

Je vous recommande de jeter un œil à java.util.ResourceBundle. Vous devriez vous soucier de I18N, mais cela en vaut la peine même si vous ne le faites pas. Externaliser les messages est une très bonne idée. J'ai trouvé qu'il était utile de pouvoir donner une feuille de calcul aux gens d'affaires qui leur permettait de mettre dans la langue exacte qu'ils voulaient voir. Nous avons écrit une tâche Ant pour générer les fichiers .properties au moment de la compilation. Cela rend I18N trivial.

Si vous utilisez également Spring, tant mieux. Leur classe MessageSource est utile pour ce genre de choses.

duffymo
la source
4

Juste pour continuer à fouetter ce cheval mort particulier, nous avons eu une bonne utilisation des codes d'erreur numériques lorsque des erreurs sont montrées aux clients finaux, car ils oublient ou lisent fréquemment le message d'erreur réel, mais peuvent parfois conserver et signaler une valeur numérique qui peut donner vous un indice de ce qui s'est réellement passé.

telcopro
la source
3

Il existe de nombreuses façons de résoudre ce problème. Mon approche préférée est d'avoir des interfaces:

public interface ICode {
     /*your preferred code type here, can be int or string or whatever*/ id();
}

public interface IMessage {
    ICode code();
}

Vous pouvez maintenant définir n'importe quel nombre d'énumérations qui fournissent des messages:

public enum DatabaseMessage implements IMessage {
     CONNECTION_FAILURE(DatabaseCode.CONNECTION_FAILURE, ...);
}

Vous avez maintenant plusieurs options pour les transformer en chaînes. Vous pouvez compiler les chaînes dans votre code (en utilisant des annotations ou des paramètres de constructeur enum) ou vous pouvez les lire à partir d'un fichier de configuration / propriété ou d'une table de base de données ou d'un mélange. Cette dernière approche est mon approche préférée car vous aurez toujours besoin de messages que vous pourrez transformer en texte très tôt (c'est-à-dire pendant que vous vous connectez à la base de données ou que vous lisez la configuration).

J'utilise des tests unitaires et des frameworks de réflexion pour trouver tous les types qui implémentent mes interfaces pour m'assurer que chaque code est utilisé quelque part et que les fichiers de configuration contiennent tous les messages attendus, etc.

En utilisant des frameworks qui peuvent analyser Java comme https://github.com/javaparser/javaparser ou celui d'Eclipse , vous pouvez même vérifier où les énumérations sont utilisées et trouver celles qui ne sont pas utilisées.

Aaron Digulla
la source
2

Moi (et le reste de notre équipe dans mon entreprise) préfère lever des exceptions au lieu de renvoyer des codes d'erreur. Les codes d'erreur doivent être vérifiés partout, transmis et ont tendance à rendre le code illisible lorsque la quantité de code augmente.

La classe d'erreur définirait alors le message.

PS: et se soucient aussi de l'internationalisation!
PPS: vous pouvez également redéfinir la méthode de montée et ajouter la journalisation, le filtrage, etc. si nécessaire (au moins dans les environnements, où les classes d'exception et les amis sont extensibles / modifiables)

blabla999
la source
désolé, Robin, mais alors (au moins à partir de l'exemple ci-dessus), il devrait y avoir deux exceptions - "erreur de base de données" et "utilisateur en double" sont si complètement différentes que deux sous-classes d'erreur distinctes devraient être créées, qui sont individuellement capturables ( l'un étant un système, l'autre étant une erreur d'administration)
blabla999
et à quoi servent les codes d'erreur, sinon pour différencier l'une ou l'autre exception? Donc, au moins au-dessus du gestionnaire, il est exactement cela: traiter des codes d'erreur qui sont passés et activés.
blabla999
Je pense que le nom de l'exception serait beaucoup plus illustratif et auto-descriptif qu'un code d'erreur. Mieux vaut réfléchir davantage à la découverte de bons noms d'exceptions, IMO.
duffymo
@ blabla999 ah, mes pensées exactement. Pourquoi attraper une exception grossière et ensuite tester "if code d'erreur == x, ou y, ou z". Une telle douleur et va à contre-courant. Ensuite, vous ne pouvez pas non plus intercepter différentes exceptions à différents niveaux de votre pile. Vous devrez attraper à tous les niveaux et tester le code d'erreur à chacun. Cela rend le code client tellement plus verbeux ... +1 + plus si je pouvais. Cela dit, je suppose que nous devons répondre à la question des PO.
wmorrison365
2
Gardez à l'esprit qu'il s'agit d'un service Web. Le client ne peut analyser que des chaînes. Du côté du serveur, il y aurait toujours des exceptions levées qui ont un membre errorCode, qui peut être utilisé dans la réponse finale au client.
pkrish
1

Un peu tard mais, je cherchais juste une jolie solution pour moi-même. Si vous avez un type d'erreur de message différent, vous pouvez ajouter une fabrique de messages simple et personnalisée afin de pouvoir spécifier plus de détails et de format que vous souhaitez plus tard.

public enum Error {
    DATABASE(0, "A database error has occured. "), 
    DUPLICATE_USER(1, "User already exists. ");
    ....
    private String description = "";
    public Error changeDescription(String description) {
        this.description = description;
        return this;
    }
    ....
}

Error genericError = Error.DATABASE;
Error specific = Error.DUPLICATE_USER.changeDescription("(Call Admin)");

EDIT: ok, utiliser enum ici est un peu dangereux car vous modifiez définitivement une énumération particulière. Je suppose que mieux serait de changer de classe et d'utiliser des champs statiques, mais que vous ne pouvez plus utiliser '=='. Donc je suppose que c'est un bon exemple de ce qu'il ne faut pas faire (ou ne le faire que lors de l'initialisation) :)

pprzemek
la source
1
Tout à fait d'accord avec votre EDIT, ce n'est pas une bonne pratique de modifier un champ d'énumération au moment de l'exécution. Avec cette conception, tout le monde peut modifier le message d'erreur. C'est assez dangereux. Les champs d'énumération doivent toujours être définitifs.
b3nyc
0

enum pour le code d'erreur / la définition du message est toujours une bonne solution bien qu'elle ait un problème i18n. En fait, nous pouvons avoir deux situations: le code / message est affiché à l'utilisateur final ou à l'intégrateur système. Pour le dernier cas, I18N n'est pas nécessaire. Je pense que les services Web sont probablement le dernier cas.

Jimmy
la source
0

L'utilisation interfacecomme constante de message est généralement une mauvaise idée. Il s'infiltrera définitivement dans le programme client dans le cadre de l'API exportée. Qui sait, que les programmeurs clients ultérieurs pourraient analyser ces messages d'erreur (publics) dans le cadre de leur programme.

Vous serez verrouillé pour toujours pour prendre en charge cela, car les changements de format de chaîne vont / peuvent interrompre le programme client.

Awan Biru
la source
0

Veuillez suivre l'exemple ci-dessous:

public enum ErrorCodes {
NO_File("No file found. "),
private ErrorCodes(String value) { 
    this.errordesc = value; 
    }
private String errordesc = ""; 
public String errordesc() {
    return errordesc;
}
public void setValue(String errordesc) {
    this.errordesc = errordesc;
}

};

Dans votre code, appelez-le comme:

fileResponse.setErrorCode(ErrorCodes.NO_FILE.errordesc());
Chinmoy
la source
0

J'utilise PropertyResourceBundle pour définir les codes d'erreur dans une application d'entreprise afin de gérer les ressources de code d'erreur locales. C'est la meilleure façon de gérer les codes d'erreur au lieu d'écrire du code (cela peut être valable pour quelques codes d'erreur) lorsque le nombre de codes d'erreur est énorme et structuré.

Consultez la documentation java pour plus d'informations sur PropertyResourceBundle

Mujibur Rahman
la source