¿La mejor manera de definir códigos / cadenas de error en Java?

118

Estoy escribiendo un servicio web en Java y estoy tratando de encontrar la mejor manera de definir códigos de error y sus cadenas de error asociadas . Necesito tener un código de error numérico y una cadena de error agrupados. Tanto el código de error como la cadena de error se enviarán al cliente que accede al servicio web. Por ejemplo, cuando ocurre una SQLException, es posible que desee hacer lo siguiente:

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

El programa cliente puede mostrar el mensaje:

"Se ha producido el error nº 1: se ha producido un problema al acceder a la base de datos".

Mi primer pensamiento fue utilizar uno Enumde los códigos de error y anular los toStringmétodos para devolver las cadenas de error. Esto es lo que se me ocurrió:

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
}

Mi pregunta es: ¿Existe una mejor manera de hacer esto? Preferiría una solución en código, en lugar de leer desde un archivo externo. Estoy usando Javadoc para este proyecto, y sería útil poder documentar los códigos de error en línea y hacer que se actualicen automáticamente en la documentación.

William Brendel
fuente
Comentario tardío, pero vale la pena mencionarlo ... 1) ¿Realmente necesitas códigos de error aquí en la excepción? Vea la respuesta de blabla999 a continuación. 2) Debe tener cuidado de transmitir demasiada información de error al usuario. Se debe escribir información de error útil en los registros del servidor, pero se debe informar al cliente lo mínimo (por ejemplo, "hubo un problema al iniciar sesión"). Se trata de una cuestión de seguridad y de evitar que los spoofers se establezcan.
wmorrison365

Respuestas:

161

Bueno, ciertamente hay una mejor implementación de la solución de enumeración (que generalmente es bastante buena):

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;
  }
}

Es posible que desee anular toString () para devolver la descripción en su lugar, no estoy seguro. De todos modos, el punto principal es que no necesita anular por separado para cada código de error. También tenga en cuenta que he especificado explícitamente el código en lugar de usar el valor ordinal; esto hace que sea más fácil cambiar el orden y agregar / eliminar errores más adelante.

No olvide que esto no está internacionalizado en absoluto, pero a menos que su cliente de servicio web le envíe una descripción de la configuración regional, no podrá internacionalizarlo fácilmente usted mismo de todos modos. Al menos tendrán el código de error para usar para i18n en el lado del cliente ...

Jon Skeet
fuente
13
Para internacionalizar, reemplace el campo de descripción con un código de cadena que se pueda buscar en un paquete de recursos.
Marcus Downing
@Marcus: Me gusta esa idea. Me estoy concentrando en sacar esto por la puerta, pero cuando veamos la internacionalización, creo que haré lo que sugieres. ¡Gracias!
William Brendel
@marcus, si toString () no se anula (lo cual no es necesario), entonces el código de cadena podría ser simplemente el valor de enumeración toString () que sería DATABASE, o DUPLICATE_USER en este caso.
rublo
@Jon Skeet! Me gusta esta solución, ¿cómo se podría producir una solución que sea fácil de localizar (o traducir a otros idiomas, etc.)? Pensando en usarla en Android, ¿puedo usar R.string.IDS_XXXX en lugar de cadenas codificadas allí?
AB
1
@AB: Bueno, una vez que tenga la enumeración, podría escribir fácilmente una clase para extraer el recurso localizado relevante del valor de la enumeración, a través de archivos de propiedades o lo que sea.
Jon Skeet
34

En lo que a mí respecta, prefiero externalizar los mensajes de error en archivos de propiedades. Esto será realmente útil en caso de internacionalización de su aplicación (un archivo de propiedades por idioma). También es más fácil modificar un mensaje de error y no será necesario volver a compilar las fuentes de Java.

En mis proyectos, generalmente tengo una interfaz que contiene códigos de error (String o entero, no le importa mucho), que contiene la clave en los archivos de propiedades para este error:

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

en el archivo de propiedades:

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

Otro problema con su solución es la capacidad de mantenimiento: solo tiene 2 errores y ya 12 líneas de código. ¡Así que imagine su archivo de enumeración cuando tenga que gestionar cientos de errores!

Romain Linsolas
fuente
2
Subiría esto más de 1 si pudiera. Codificar las cadenas es feo para el mantenimiento.
Robin
3
Almacenar constantes de cadena en la interfaz es una mala idea. Puede usar enumeraciones o usar constantes de cadena en clases finales con constructor privado, por paquete o área relacionada. Por favor, John Skeets responda con Enums. Por favor, compruebe. stackoverflow.com/questions/320588/…
Anand Varkey Philips
21

La sobrecarga de toString () parece un poco asquerosa, parece un poco exagerada del uso normal de toString ().

Qué pasa:

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 parece mucho más limpio ... y menos detallado.

Cowan
fuente
5
Sobrecargar toString () en cualquier objeto (por no hablar de enumeraciones) es bastante normal.
cletus
+1 No es tan flexible como la solución de Jon Skeet, pero aún resuelve el problema muy bien. ¡Gracias!
William Brendel
2
Quise decir que toString () se usa con mayor frecuencia y utilidad para proporcionar suficiente información para identificar el objeto; a menudo incluye el nombre de la clase o alguna forma de decir de manera significativa el tipo de objeto. Un toString () que devuelve solo 'Se ha producido un error en la base de datos' sería sorprendente en muchos contextos.
Cowan
1
Estoy de acuerdo con Cowan, usar toString () de esta manera parece un poco 'hackish'. Solo un golpe rápido por el dinero y no un uso normal. Para la enumeración, toString () debe devolver el nombre de la constante de enumeración. Esto se vería interesante en un depurador cuando desee el valor de una variable.
Robin
19

En mi último trabajo, profundicé un poco más en la versión de enumeración:

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

@Error, @Info, @Warning se conservan en el archivo de clase y están disponibles en tiempo de ejecución. (Tuvimos un par de otras anotaciones para ayudar a describir la entrega de mensajes también)

@Text es una anotación en tiempo de compilación.

Escribí un procesador de anotaciones para esto que hizo lo siguiente:

  • Verifique que no haya números de mensaje duplicados (la parte antes del primer guión bajo)
  • Sintaxis: compruebe el texto del mensaje
  • Genere un archivo messages.properties que contenga el texto, codificado por el valor de enumeración.

Escribí algunas rutinas de utilidad que ayudaron a registrar errores, envolverlos como excepciones (si lo desea) y así sucesivamente.

Estoy tratando de que me dejen abrir el código ... - Scott

Scott Stanchfield
fuente
Buena forma de manejar los mensajes de error. ¿Ya lo hiciste de código abierto?
bobbel
5

Te recomiendo que eches un vistazo a java.util.ResourceBundle. Debería preocuparse por I18N, pero vale la pena incluso si no lo hace. Exteriorizar los mensajes es una muy buena idea. Descubrí que era útil poder entregar una hoja de cálculo a los empresarios que les permitiera escribir el idioma exacto que querían ver. Escribimos una tarea Ant para generar los archivos .properties en tiempo de compilación. Hace que I18N sea trivial.

Si también usa Spring, mucho mejor. Su clase MessageSource es útil para este tipo de cosas.

duffymo
fuente
4

Solo para seguir azotando a este caballo muerto en particular, hemos hecho un buen uso de los códigos de error numéricos cuando se muestran errores a los clientes finales, ya que con frecuencia olvidan o leen mal el mensaje de error real, pero a veces pueden retener e informar un valor numérico que puede dar usted una pista de lo que realmente sucedió.

telcopro
fuente
3

Hay muchas formas de solucionar este problema. Mi enfoque preferido es tener interfaces:

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

public interface IMessage {
    ICode code();
}

Ahora puede definir cualquier número de enumeraciones que proporcionen mensajes:

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

Ahora tienes varias opciones para convertirlas en cadenas. Puede compilar las cadenas en su código (usando anotaciones o parámetros del constructor de enumeración) o puede leerlas desde un archivo de configuración / propiedad o desde una tabla de base de datos o una mezcla. Este último es mi enfoque preferido porque siempre necesitará algunos mensajes que pueda convertir en texto muy pronto (es decir, mientras se conecta a la base de datos o lee la configuración).

Estoy usando pruebas unitarias y marcos de reflexión para encontrar todos los tipos que implementan mis interfaces para asegurarme de que cada código se use en algún lugar y que los archivos de configuración contengan todos los mensajes esperados, etc.

Al usar marcos que pueden analizar Java como https://github.com/javaparser/javaparser o el de Eclipse , incluso puede verificar dónde se usan las enumeraciones y encontrar las que no se usan.

Aaron Digulla
fuente
2

Yo (y el resto de nuestro equipo en mi empresa) preferimos generar excepciones en lugar de devolver códigos de error. Los códigos de error deben verificarse en todas partes, pasarse y tienden a hacer que el código sea ilegible cuando la cantidad de código aumenta.

La clase de error luego definiría el mensaje.

PD: ¡y también nos preocupamos por la internacionalización!
PPS: también puede redefinir el método de aumento y agregar registro, filtrado, etc. si es necesario (al menos en entornos, donde las clases de excepción y los amigos son extensibles / cambiables)

blabla999
fuente
lo siento, Robin, pero entonces (al menos en el ejemplo anterior), estas deberían ser dos excepciones: "error de base de datos" y "usuario duplicado" son tan completamente diferentes que se deben crear dos subclases de error separadas, que se pueden detectar individualmente ( uno es un sistema, el otro es un error de administrador)
blabla999
y ¿para qué se utilizan los códigos de error, si no para diferenciar entre una u otra excepción? Entonces, al menos por encima del manejador, él es exactamente eso: lidiar con códigos de error que se pasan y si se activan.
blabla999
Creo que el nombre de la excepción sería mucho más ilustrativo y autodescriptivo que un código de error. Es mejor pensar más en descubrir buenos nombres de excepción, en mi opinión.
duffymo
@ blabla999 ah, exactamente mis pensamientos. ¿Por qué detectar una excepción de grano grueso y luego probar "si código de error == x, o y, o z"? Qué dolor y va contra la corriente. Entonces, tampoco puede detectar diferentes excepciones en diferentes niveles en su pila. Tendría que detectar en todos los niveles y probar el código de error en cada uno. Hace que el código del cliente sea mucho más detallado ... +1 + más si pudiera. Dicho esto, supongo que tenemos que responder a la pregunta de los PO.
wmorrison365
2
Tenga en cuenta que esto es para un servicio web. El cliente solo puede analizar cadenas. En el lado del servidor, todavía se lanzarían excepciones que tienen un miembro errorCode, que se puede usar en la respuesta final al cliente.
pkrish
1

Un poco tarde, pero solo estaba buscando una solución bonita para mí. Si tiene un tipo diferente de error de mensaje, puede agregar una fábrica de mensajes simple y personalizada para poder especificar más detalles y el formato que le gustaría más adelante.

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)");

EDITAR: ok, usar enum aquí es un poco peligroso ya que altera una enumeración particular permanentemente. Supongo que lo mejor sería cambiar a clase y usar campos estáticos, pero ya no puede usar '=='. Así que supongo que es un buen ejemplo de lo que no se debe hacer (o hacerlo solo durante la inicialización) :)

pprzemek
fuente
1
Totalmente de acuerdo con su EDITAR, no es una buena práctica alterar un campo de enumeración en tiempo de ejecución. Con este diseño, todos pueden editar el mensaje de error. Esto es bastante peligroso. Los campos de enumeración siempre deben ser finales.
b3nyc
0

enum para la definición de código / mensaje de error sigue siendo una buena solución, aunque tiene problemas de i18n. En realidad, podemos tener dos situaciones: el código / mensaje se muestra al usuario final o al integrador del sistema. Para el último caso, I18N no es necesario. Creo que los servicios web son probablemente el último caso.

Palanqueta
fuente
0

Usar interfacecomo constante de mensaje es generalmente una mala idea. Se filtrará en el programa del cliente de forma permanente como parte de la API exportada. Quién sabe, los programadores de clientes posteriores podrían analizar esos mensajes de error (públicos) como parte de su programa.

Estará bloqueado para siempre para admitir esto, ya que los cambios en el formato de cadena pueden romper el programa cliente.

Awan Biru
fuente
0

Siga el siguiente ejemplo:

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;
}

};

En su código, llámelo como:

fileResponse.setErrorCode(ErrorCodes.NO_FILE.errordesc());
Chinmoy
fuente
0

Utilizo PropertyResourceBundle para definir los códigos de error en una aplicación empresarial para administrar los recursos de códigos de error locales. Esta es la mejor manera de manejar códigos de error en lugar de escribir código (puede ser válido para algunos códigos de error) cuando el número de códigos de error es enorme y estructurado.

Consulte java doc para obtener más información sobre PropertyResourceBundle

Mujibur Rahman
fuente