Najlepszy sposób na definiowanie kodów / ciągów błędów w Javie?

118

Piszę usługę internetową w Javie i próbuję znaleźć najlepszy sposób definiowania kodów błędów i powiązanych z nimi ciągów błędów . Muszę mieć zgrupowany numeryczny kod błędu i ciąg błędu. Zarówno kod błędu, jak i ciąg błędu zostaną wysłane do klienta uzyskującego dostęp do usługi internetowej. Na przykład, gdy wystąpi SQLException, mogę chcieć wykonać następujące czynności:

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

Program kliencki może zobaczyć komunikat:

„Wystąpił błąd nr 1: Wystąpił problem z dostępem do bazy danych”.

Moją pierwszą myślą było użycie jednego Enumz kodów błędów i zastąpienie toStringmetod zwracania ciągów błędów. Oto co wymyśliłem:

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
}

Moje pytanie brzmi: czy istnieje lepszy sposób na zrobienie tego? Wolałbym rozwiązanie w kodzie, zamiast czytać z zewnętrznego pliku. Używam Javadoc w tym projekcie i możliwość udokumentowania kodów błędów w linii i automatycznego ich aktualizacji w dokumentacji byłaby pomocna.

William Brendel
źródło
Późny komentarz, ale warto wspomnieć, że ... 1) Czy naprawdę potrzebujesz kodów błędów tutaj w wyjątku? Zobacz odpowiedź blabla999 poniżej. 2) Należy zachować ostrożność, przekazując użytkownikowi zbyt wiele informacji o błędzie. Przydatne informacje o błędach powinny być zapisywane w logach serwera, ale klient powinien być poinformowany o minimum (np. „Wystąpił problem z logowaniem”). Jest to kwestia bezpieczeństwa i zapobiegania przedostawaniu się spooferów.
wmorrison365,

Odpowiedzi:

161

Cóż, z pewnością jest lepsza implementacja rozwiązania wyliczeniowego (co jest ogólnie całkiem przyjemne):

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

Możesz zastąpić toString (), aby zamiast tego po prostu zwrócił opis - nie jestem pewien. W każdym razie głównym celem jest to, że nie musisz nadpisywać osobno dla każdego kodu błędu. Zwróć również uwagę, że wyraźnie określiłem kod zamiast używać wartości porządkowej - ułatwia to późniejszą zmianę kolejności i dodawanie / usuwanie błędów.

Nie zapominaj, że to wcale nie jest umiędzynarodowione - ale jeśli twój klient usługi sieciowej nie wyśle ​​ci opisu lokalizacji, i tak nie możesz łatwo umiędzynarodowić go samodzielnie. Przynajmniej będą mieli kod błędu do użycia dla i18n po stronie klienta ...

Jon Skeet
źródło
13
Aby umiędzynarodowić, zastąpić pole opisu kodem w postaci łańcucha, który można wyszukać w pakiecie zasobów?
Marcus Downing
@Marcus: Podoba mi się ten pomysł. Koncentruję się na wyciągnięciu tego wszystkiego z drzwi, ale kiedy spojrzymy na internacjonalizację, myślę, że zrobię to, co sugerowałeś. Dzięki!
William Brendel
@marcus, jeśli toString () nie jest nadpisane (co nie musi być), to kod ciągu może być po prostu wartością wyliczenia toString (), która w tym przypadku będzie DATABASE lub DUPLICATE_USER.
rubel
@Jon Skeet! Podoba mi się to rozwiązanie, jak można stworzyć rozwiązanie łatwe do zlokalizowania (lub przetłumaczenia na inne języki itp.) Myśląc o używaniu go w Androidzie, czy mogę użyć R.string.IDS_XXXX zamiast sztywno zakodowanych łańcuchów?
AB
1
@AB: Cóż, kiedy już masz wyliczenie, możesz łatwo napisać klasę, aby wyodrębnić odpowiedni zlokalizowany zasób z wartości wyliczenia, poprzez pliki właściwości lub cokolwiek innego.
Jon Skeet
34

Jeśli o mnie chodzi, wolę uzewnętrznić komunikaty o błędach w plikach właściwości. Będzie to bardzo pomocne w przypadku internacjonalizacji aplikacji (jeden plik właściwości na język). Łatwiej jest także zmodyfikować komunikat o błędzie i nie będzie potrzebna żadna ponowna kompilacja źródeł Java.

W moich projektach generalnie mam interfejs, który zawiera kody błędów (ciąg znaków lub liczba całkowita, to nie obchodzi), który zawiera klucz w plikach właściwości dla tego błędu:

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

w pliku właściwości:

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

Kolejnym problemem związanym z twoim rozwiązaniem jest łatwość konserwacji: masz tylko 2 błędy i już 12 linii kodu. Wyobraź sobie więc plik wyliczenia, kiedy będziesz miał setki błędów do zarządzania!

Romain Linsolas
źródło
2
Zrobiłbym to więcej niż 1, gdybym mógł. Zakodowanie ciągów na sztywno jest brzydkie w utrzymaniu.
Robin
3
Przechowywanie stałych typu String w interfejsie to zły pomysł. Możesz używać wyliczeń lub stałych typu String w końcowych klasach z prywatnym konstruktorem, na pakiet lub obszar pokrewny. Proszę Johna Skeetsa odpowiedzieć wyliczeniami. Proszę sprawdzić. stackoverflow.com/questions/320588/…
Anand Varkey Philips
21

Przeciążanie metody toString () wydaje się nieco nieprzyjemne - wydaje się to trochę naciągnięciem normalnego użycia metody toString ().

Co powiesz na:

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

wydaje mi się dużo czystszy ... i mniej szczegółowy.

Cowan
źródło
5
Przeciążanie funkcji toString () na dowolnych obiektach (nie mówiąc już o wyliczeniach) jest całkiem normalne.
cletus
+1 Nie tak elastyczne jak rozwiązanie Jona Skeeta, ale nadal ładnie rozwiązuje problem. Dzięki!
William Brendel
2
Chodziło mi o to, że toString () jest najczęściej i użytecznie używane do podania wystarczającej ilości informacji do zidentyfikowania obiektu - często zawiera nazwę klasy lub jakiś sposób sensownego określenia typu obiektu. Funkcja toString (), która zwraca tylko „Wystąpił błąd bazy danych”, byłaby zaskakująca w wielu kontekstach.
Cowan
1
Zgadzam się z Cowanem, użycie metody toString () w ten sposób wydaje się nieco „hakerskie”. Tylko szybki strzał za grosze, a nie normalne użytkowanie. W przypadku wyliczenia toString () powinno zwrócić nazwę stałej wyliczenia. Wyglądałoby to interesująco w debugerze, gdy chcesz uzyskać wartość zmiennej.
Robin
19

W mojej ostatniej pracy poszedłem trochę głębiej w wersji enum:

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

@Error, @Info, @Warning są zachowywane w pliku klasy i są dostępne w czasie wykonywania. (Mieliśmy też kilka innych adnotacji, które pomogły w opisie dostarczania wiadomości)

@Text to adnotacja w czasie kompilacji.

Napisałem do tego procesor adnotacji, który wykonał następujące czynności:

  • Sprawdź, czy nie ma zduplikowanych numerów wiadomości (część przed pierwszym podkreśleniem)
  • Sprawdzenie składni tekstu wiadomości
  • Wygeneruj plik messages.properties zawierający tekst z kluczem według wartości wyliczenia.

Napisałem kilka programów narzędziowych, które pomogły w rejestrowaniu błędów, opakowywaniu ich jako wyjątków (w razie potrzeby) i tak dalej.

Próbuję sprawić, by pozwolili mi to otworzyć ... - Scott

Scott Stanchfield
źródło
Niezły sposób obsługi komunikatów o błędach. Czy już to wykorzystałeś?
bobbel
5

Polecam zapoznać się z java.util.ResourceBundle. Powinieneś przejmować się I18N, ale warto, nawet jeśli nie masz. Eksternalizacja wiadomości to bardzo dobry pomysł. Przekonałem się, że warto było dać przedsiębiorcom arkusz kalkulacyjny, który pozwoliłby im na wprowadzenie dokładnego języka, który chcieli zobaczyć. Napisaliśmy zadanie Ant, aby wygenerować pliki .properties w czasie kompilacji. To sprawia, że ​​I18N jest trywialny.

Jeśli używasz także Springa, tym lepiej. Ich klasa MessageSource jest przydatna do tego rodzaju rzeczy.

duffymo
źródło
4

Tylko po to, by dalej chłostać tego konkretnego martwego konia - dobrze wykorzystaliśmy numeryczne kody błędów, gdy błędy są wyświetlane klientom końcowym, ponieważ często zapominają lub błędnie odczytują faktyczny komunikat o błędzie, ale czasami mogą zachować i zgłosić wartość liczbową, która może dać masz wskazówkę, co się właściwie wydarzyło.

telcopro
źródło
3

Istnieje wiele sposobów rozwiązania tego problemu. Moim preferowanym podejściem jest posiadanie interfejsów:

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

public interface IMessage {
    ICode code();
}

Teraz możesz zdefiniować dowolną liczbę wyliczeń, które dostarczają wiadomości:

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

Teraz masz kilka opcji, aby zamienić je w ciągi. Możesz skompilować ciągi do swojego kodu (używając adnotacji lub parametrów konstruktora wyliczenia) lub możesz je odczytać z pliku config / property lub z tabeli bazy danych lub ich mieszaniny. To drugie jest moim preferowanym podejściem, ponieważ zawsze będziesz potrzebować wiadomości, które możesz zamienić na tekst bardzo wcześnie (np. Podczas łączenia się z bazą danych lub czytania konfiguracji).

Używam testów jednostkowych i struktur odbicia, aby znaleźć wszystkie typy, które implementują moje interfejsy, aby upewnić się, że każdy kod jest gdzieś używany i że pliki konfiguracyjne zawierają wszystkie oczekiwane komunikaty itp.

Korzystając z frameworków, które mogą analizować Javę, takich jak https://github.com/javaparser/javaparser lub ten z Eclipse , możesz nawet sprawdzić, gdzie są używane wyliczenia i znaleźć nieużywane.

Aaron Digulla
źródło
2

Ja (i reszta naszego zespołu w mojej firmie) wolę zgłaszać wyjątki zamiast zwracać kody błędów. Kody błędów muszą być sprawdzane wszędzie, przekazywane i powodują, że kod staje się nieczytelny, gdy ilość kodu staje się większa.

Klasa błędu definiowałaby wtedy komunikat.

PS: a właściwie również dbaj o internacjonalizację!
PPS: możesz również przedefiniować metodę podnoszenia i dodać logowanie, filtrowanie itp. W razie potrzeby (przynajmniej w środowiskach, w których klasy Exception i przyjaciele są rozszerzalne / zmienialne)

blabla999
źródło
przepraszam Robin, ale w takim razie (przynajmniej z powyższego przykładu) powinny to być dwa wyjątki - „błąd bazy danych” i „zduplikowany użytkownik” są na tyle różne, że należy utworzyć dwie osobne podklasy błędów, które można wyłapać indywidualnie ( jeden to system, drugi to błąd administratora)
blabla999
a do czego służą kody błędów, jeśli nie do rozróżnienia między jednym a drugim wyjątkiem? Tak więc przynajmniej powyżej obsługi jest dokładnie tym: radzi sobie z kodami błędów, które są przekazywane i włączane, jeśli są włączane.
blabla999
Myślę, że nazwa wyjątku byłaby znacznie bardziej ilustracyjna i samoopisująca niż kod błędu. Lepiej poświęcić więcej uwagi odkrywaniu dobrych nazw wyjątków, IMO.
duffymo
@ blabla999 ah, dokładnie moje myśli. Po co łapać gruboziarnisty wyjątek, a następnie testować „jeśli kod błędu == x, lub y lub z”. Taki ból i idzie pod prąd. Wtedy też nie możesz złapać różnych wyjątków na różnych poziomach swojego stosu. Musiałbyś złapać na każdym poziomie i przetestować kod błędu na każdym. To sprawia, że ​​kod klienta jest o wiele bardziej szczegółowy ... +1 + więcej, gdybym mógł. To powiedziawszy, myślę, że musimy odpowiedzieć na pytanie PO.
wmorrison365,
2
Pamiętaj, że dotyczy to usługi internetowej. Klient może analizować tylko ciągi. Po stronie serwera nadal byłyby zgłaszane wyjątki, które mają element członkowski errorCode, którego można użyć w ostatecznej odpowiedzi dla klienta.
pkrish
1

Trochę późno, ale szukałem dla siebie ładnego rozwiązania. Jeśli masz inny rodzaj błędu wiadomości, możesz dodać prostą, niestandardową fabrykę wiadomości, aby określić więcej szczegółów i format, który chcesz później.

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

EDYCJA: ok, użycie wyliczenia tutaj jest trochę niebezpieczne, ponieważ zmieniasz dane wyliczenie na stałe. Myślę, że lepiej byłoby zmienić klasę i użyć pól statycznych, ale nie możesz już używać '=='. Więc myślę, że to dobry przykład czego nie robić (lub robić to tylko podczas inicjalizacji) :)

pprzemek
źródło
1
Całkowicie zgadzasz się z EDYCJĄ, nie jest dobrą praktyką zmienianie pola wyliczenia w czasie wykonywania. Dzięki temu projektowi każdy może edytować komunikat o błędzie. To jest dość niebezpieczne. Pola wyliczenia powinny zawsze być końcowe.
b3nyc
0

wyliczenie dla definicji kodu / komunikatu błędu jest nadal dobrym rozwiązaniem, chociaż ma obawy i18n. Właściwie możemy mieć dwie sytuacje: kod / komunikat jest wyświetlany użytkownikowi końcowemu lub integratorowi systemu. W drugim przypadku I18N nie jest potrzebny. Myślę, że usługi internetowe są najprawdopodobniej późniejszym przypadkiem.

Jimmy
źródło
0

Używanie interfacejako stałej wiadomości jest generalnie złym pomysłem. Wyciek na stałe do programu klienta jako część eksportowanego API. Kto wie, że późniejsi programiści mogą analizować te komunikaty o błędach (publiczne) jako część swojego programu.

Będziesz na zawsze zablokowany, aby to obsługiwać, ponieważ zmiany w formacie ciągu mogą / mogą uszkodzić program klienta.

Awan Biru
źródło
0

Postępuj zgodnie z poniższym przykładem:

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

};

W swoim kodzie nazwij to tak:

fileResponse.setErrorCode(ErrorCodes.NO_FILE.errordesc());
Chinmoy
źródło
0

Używam PropertyResourceBundle do definiowania kodów błędów w aplikacji korporacyjnej do zarządzania lokalnymi zasobami kodów błędów. Jest to najlepszy sposób obsługi kodów błędów zamiast pisania kodu (może się sprawdzać w przypadku kilku kodów błędów), gdy liczba kodów błędów jest ogromna i uporządkowana.

Więcej informacji na temat PropertyResourceBundle można znaleźć w dokumentacji java

Mujibur Rahman
źródło