2013-10-11 10 views
19

Stiamo creando un SDK Java per semplificare l'accesso a uno dei nostri servizi che forniscono un'API REST. Questo SDK deve essere utilizzato da sviluppatori di terze parti. Sto facendo fatica a trovare il modello migliore per implementare la gestione degli errori nell'SDK che si adatta meglio al linguaggio Java.Gestione degli errori su Java SDK per il servizio API REST

Diciamo che abbiamo il resto dell'endpoint: GET /photos/{photoId}. Questo può restituire i seguenti codici di stato HTTP:

  • 401: l'utente non è autenticato
  • 403: L'utente non dispone dell'autorizzazione per accedere a questa foto
  • 404: Non c'è foto con quella id

Il servizio simile a questa:

interface RestService { 
    public Photo getPhoto(String photoID); 
} 

I Nel codice sopra non sto ancora affrontando la gestione degli errori. Ovviamente voglio fornire un modo al client del sdk per sapere quale errore si è verificato, per poterci potenzialmente recuperare. La gestione degli errori in Java è fatta usando Eccezioni, quindi andiamo con quello. Tuttavia, qual è il modo migliore per farlo usando le eccezioni?

1. Avere un'unica eccezione con informazioni sull'errore.

public Photo getPhoto(String photoID) throws RestServiceException; 

public class RestServiceException extends Exception { 
    int statusCode; 

    ... 
} 

Il cliente del SDK potrebbe allora fare qualcosa del genere:

try { 
    Photo photo = getPhoto("photo1"); 
} 
catch(RestServiceException e) { 
    swtich(e.getStatusCode()) { 
     case 401 : handleUnauthenticated(); break; 
     case 403 : handleUnauthorized(); break; 
     case 404 : handleNotFound(); break; 
    } 
} 

Comunque io non mi piace molto questa soluzione principalmente per 2 motivi:

  • Osservando la firma del metodo lo sviluppatore non ha idea del tipo di situazioni di errore che potrebbe dover gestire.
  • Lo sviluppatore deve gestire direttamente i codici di stato HTTP e sapere cosa significano nel contesto di questo metodo (ovviamente se vengono utilizzati correttamente, molte volte il significato è noto, tuttavia potrebbe non essere sempre il Astuccio).

2. Avere una gerarchia di classi di errori

La firma del metodo rimane:

public Photo getPhoto(String photoID) throws RestServiceException; 

Ma ora creare eccezioni per ogni tipo di errore:

public class UnauthenticatedException extends RestServiceException; 
public class UnauthorizedException extends RestServiceException; 
public class NotFoundException extends RestServiceException; 

Ora il client dell'SDK potrebbe quindi fare qualcosa del genere:

try { 
    Photo photo = getPhoto("photo1"); 
} 
catch(UnauthenticatedException e) { 
    handleUnauthorized(); 
} 
catch(UnauthorizedException e) { 
    handleUnauthenticated(); 
} 
catch(NotFoundException e) { 
    handleNotFound(); 
} 

Con questo approccio lo sviluppatore non ha bisogno di conoscere i codici di stato HTTP che hanno generato gli errori, deve solo gestire le eccezioni Java. Un altro vantaggio è che lo sviluppatore può solo rilevare le eccezioni che desidera gestire (a differenza della precedente situazione in cui avrebbe dovuto catturare la singola eccezione (RestServiceException) e solo allora decidere se volerlo affrontare o meno).

Tuttavia, c'è ancora un problema. Osservando la firma del metodo, lo sviluppatore non ha ancora idea del tipo di errori che potrebbe dover gestire perché abbiamo solo la super-classe nella firma del metodo.

3. Avere una gerarchia di classi di errori + loro lista nella firma del metodo di

Ok, quindi quello che viene in mente ora è quello di cambiare la firma del metodo per:

public Photo getPhoto(String photoID) throws UnauthenticatedException, UnauthorizedException, NotFoundException; 

Tuttavia, è possibile che in futuro nuove situazioni di errore possano essere aggiunte a questo punto finale di riposo. Ciò significherebbe aggiungere una nuova Eccezione alla firma del metodo e ciò rappresenterebbe una violazione della java api. Ci piacerebbe avere una soluzione più robusta che non comporterebbe la violazione delle modifiche all'API nella situazione descritta.

4. Avere una gerarchia di classi di errori (utilizzando eccezioni unchecked) + elencarli nella firma del metodo di

Quindi, per quanto riguarda le eccezioni non selezionati? Se cambiamo il RestServiceException di estendere la RuntimeException:

public class RestServiceException extends RuntimeException 

E continuiamo la firma del metodo:

public Photo getPhoto(String photoID) throws UnauthenticatedException, UnauthorizedException, NotFoundException; 

In questo modo posso aggiungere nuove eccezioni alla firma del metodo senza infrangere il codice esistente. Tuttavia, con questa soluzione lo sviluppatore non è costretto a rilevare alcuna eccezione e non si accorgerà che ci sono situazioni di errore che deve gestire finché non legge attentamente la documentazione (sì, giusto!) O ha notato le eccezioni presenti nel metodo firma.

Qual è la migliore pratica per la gestione degli errori in questo tipo di situazioni?

Ci sono altre (migliori) alternative a quelle che ho citato?

+0

Stai facendo questo SDK per Android o per tutte le app java? – 4gus71n

+0

La domanda è interessante ma soggettiva, no? Voglio sia votare e votare per chiudere, ma farò solo il primo. –

+0

@MiserableVariable Penso che la SO sia stata specifica, "Ci sono altre (migliori) alternative a quelle che ho menzionato?" non ha detto qualcosa del tipo "Come posso gestire i codici di errore nella mia implementazione del client http?". Non so, è la mia opinione. Inoltre mostra segni di ricerche. – 4gus71n

risposta

2

Ho visto le librerie che combinano i vostri suggerimenti 2 e 3, per esempio

public Photo getPhoto(String photoID) throws RestServiceException, UnauthenticatedException, UnauthorizedException, NotFoundException; 

In questo modo, quando si aggiunge una nuova eccezione controllata che si estende RestServiceException, non stai cambiando il contratto del metodo e qualsiasi codice utilizzando compila ancora.

Rispetto a una soluzione di eccezione di richiamata o non controllata, un vantaggio è che questo garantisce che il nuovo errore venga gestito dal codice client, anche se è solo un errore generale. In un callback, non accadrebbe nulla e, con un'eccezione non controllata, l'applicazione client potrebbe bloccarsi.

+0

Questo in realtà ha finito per essere il modo in cui l'abbiamo fatto;) –

-1

Tutto dipende dal livello di informazioni delle risposte all'errore API. Quanto più è utile la gestione degli errori dell'API, tanto più informativo può essere il trattamento delle eccezioni. Credo che le eccezioni sarebbero solo informative come gli errori restituiti dall'API.

Esempio:

{ "status":404,"code":2001,"message":"Photo could not be found."} 

In seguito alla vostra primo suggerimento, se l'eccezione contenuta sia lo stato e il codice di errore API, lo sviluppatore ha una migliore comprensione di ciò che deve fare e più opzione quando si tratta di la gestione delle eccezioni. Se l'eccezione conteneva anche il messaggio di errore che era stato restituito, lo sviluppatore non avrebbe nemmeno bisogno di fare riferimento alla documentazione.

+0

Grazie per la risposta, ma credo che in realtà non risponda alla mia domanda. L'eccezione ovviamente contiene informazioni (javadoc) sulle situazioni in cui verrà lanciata. Tuttavia, la domanda rimane relativa ai tipi di Eccezioni da utilizzare (Selezionato vs Deselezionato) e alla firma del metodo (Super classe RestServiceException o le eccezioni più specifiche). –

9

Sembra che tu stia facendo le cose con "mano". Ti consiglierei0 di provare a Apache CXF.

È un'implementazione chiara JAX-RS API che consente quasi di dimenticare REST.Funziona bene con (consigliato anche) Spring.

È sufficiente scrivere classi che implementano le interfacce (API). Quello che devi fare è annotare i metodi e i parametri delle tue interfacce con le annotazioni JAX-RS.

Quindi, CXF fa la magia.

Si lanciano normali eccezioni nel codice java e quindi si usa il programma di mapping delle eccezioni su server/nd o client per tradurre tra loro e il codice di stato HTTP.

In questo modo, sul lato server/client Java, si gestisce solo l'eccezione normale al 100% Java e CXF gestisce l'HTTP per conto dell'utente: si hanno entrambi i vantaggi di una chiara API REST e di un client Java pronto per essere utilizzato dai tuoi utenti.

Il client può essere generato dal WDSL o creato in fase di esecuzione dall'introspezione delle annotazioni dell'interfaccia.

See:

  1. http://cxf.apache.org/docs/jax-rs-basics.html#JAX-RSBasics-Exceptionhandling
  2. http://cxf.apache.org/docs/how-do-i-develop-a-client.html

Nella nostra applicazione, abbiamo definito e mappato un insieme di codici di errore e le loro eccezioni di controparte:

  • 4XX Previsto/Eccection funzionale (come argomenti non validi, set vuoti, ecc.)
  • 5XX imprevisto/RuntimeException Unrecovable per errori interni che "non dovrebbe accadere"

Ne consegue entrambi gli standard REST e Java. le alternative di gestione

15

eccezione: le richiamate

Non so se si tratta di un'alternativa migliore, ma si potrebbe utilizzare i callback. È possibile rendere facoltativi alcuni metodi fornendo un'implementazione predefinita. Date un'occhiata a questo:

/** 
    * Example 1. 
    * Some callbacks will be always executed even if they fail or 
    * not, all the request will finish. 
    * */ 
    RestRequest request = RestRequest.get("http://myserver.com/photos/31", 
     Photo.class, new RestCallback(){ 

      //I know that this error could be triggered, so I override the method. 
      @Override 
      public void onUnauthorized() { 
       //Handle this error, maybe pop up a login windows (?) 
      } 

      //I always must override this method. 
      @Override 
      public void onFinish() { 
       //Do some UI updates... 
      } 

     }).send(); 

Questo è come la classe richiamata appare come:

public abstract class RestCallback { 

    public void onUnauthorized() { 
     //Override this method is optional. 
    } 

    public abstract void onFinish(); //Override this method is obligatory. 


    public void onError() { 
     //Override this method is optional. 
    } 

    public void onBadParamsError() { 
     //Override this method is optional. 
    } 

} 

fare qualcosa di simile si potrebbe definire un ciclo di vita richiesta, e gestire ogni stato della richiesta. È possibile rendere alcuni metodi facoltativi da implementare o meno. È possibile ottenere alcuni errori generali e dare la possibilità all'utente di implementare la gestione, come in onError.

Come definire chiaramente quali eccezioni gestiscono?

Se mi chiedete, l'approccio migliore è disegnare il ciclo di vita della richiesta, qualcosa di simile:

Sample exception lifecicle

Questo è solo un esempio di poveri, ma l'importante è tenere a mente che tutti i metodi di implementazione, potrebbero essere o meno, opzionali.Se onAuthenticationError è obbligatorio, non necessariamente sarà lo onBadUsername e viceversa. Questo è il punto che rende queste callback così flessibili.

E come implementare il client Http?

Beh io non ne so molto di client HTTP, io uso sempre il apache HttpClient, ma non c'è un sacco di differenze tra i client HTTP, la maggior parte hanno un po 'più o un po' meno funzioni, ma nel fine, sono tutti uguali. Basta prendere il metodo http, inserire l'url, i parametri e inviare. Per questo esempio userò il HttpClient apache

public class RestRequest { 
    Gson gson = new Gson(); 

    public <T> T post(String url, Class<T> clazz, 
      List<NameValuePair> parameters, RestCallback callback) { 
     // Create a new HttpClient and Post Header 
     HttpClient httpclient = new DefaultHttpClient(); 
     HttpPost httppost = new HttpPost(url); 
     try { 
      // Add your data 
      httppost.setEntity(new UrlEncodedFormEntity(parameters)); 
      // Execute HTTP Post Request 
      HttpResponse response = httpclient.execute(httppost); 
      StringBuilder json = inputStreamToString(response.getEntity() 
        .getContent()); 
      T gsonObject = gson.fromJson(json.toString(), clazz); 
      callback.onSuccess(); // Everything has gone OK 
      return gsonObject; 

     } catch (HttpResponseException e) { 
      // Here are the http error codes! 
      callback.onError(); 
      switch (e.getStatusCode()) { 
      case 401: 
       callback.onAuthorizationError(); 
       break; 
      case 403: 
       callback.onPermissionRefuse(); 
       break; 
      case 404: 
       callback.onNonExistingPhoto(); 
       break; 
      } 
      e.printStackTrace(); 
     } catch (ConnectTimeoutException e) { 
      callback.onTimeOutError(); 
      e.printStackTrace(); 
     } catch (MalformedJsonException e) { 
      callback.onMalformedJson(); 
     } 
     return null; 
    } 

    // Fast Implementation 
    private StringBuilder inputStreamToString(InputStream is) 
      throws IOException { 
     String line = ""; 
     StringBuilder total = new StringBuilder(); 

     // Wrap a BufferedReader around the InputStream 
     BufferedReader rd = new BufferedReader(new InputStreamReader(is)); 

     // Read response until the end 
     while ((line = rd.readLine()) != null) { 
      total.append(line); 
     } 

     // Return full string 
     return total; 
    } 

} 

Questo è un esempio di implementazione del RestRequest. Questo è solo un semplice esempio, ci sono un sacco di argomenti da discutere quando si sta creando il proprio client di riposo. Ad esempio, "che tipo di libreria JSON usa per analizzare?", "Stai lavorando per Android o per Java?" (questo è importante perché non so se Android supporta alcune funzionalità di java 7 come eccezioni multi-catch, e ci sono alcune tecnologie che non sono disponibili per java o android e viceversa).

Ma il meglio che posso dire è il codice sdk api in termini di utente, si noti che le linee per rendere la richiesta di riposo sono poche.

Spero che questo aiuti! Ciao:]

+0

Questo è stato un grande suggerimento ma i motivi per cui non l'ho contrassegnato come la risposta corretta è che ciò comporta il cambio completo del modo in cui questo sdk gestirà gli errori rispetto a quello che stiamo facendo al momento. Troppo grande di un cambiamento. –

0

La soluzione può variare a seconda delle esigenze.

  • Se si suppone che ci potrebbero apparire imprevedibili nuovi tipi di eccezione, in futuro, la tua seconda soluzione con la gerarchia eccezione controllata e metodo che gettano le loro superclasse RestServiceException è il migliore. Tutte le sottoclassi conosciute dovrebbero essere elencate in javadoc come Subclasses: {@link UnauthenticatedException}, ..., per far sapere agli sviluppatori che tipo di eccezioni potrebbero nascondersi. Dovrebbe essere notato che se qualche metodo può lanciare solo poche eccezioni da questo elenco, dovrebbero essere descritte nella javadoc di questo metodo usando @throws.

  • Questa soluzione è applicabile anche nel caso in cui tutte le apparenze di RestServiceException significano che una qualsiasi delle sottoclassi potrebbe nascondersi dietro di essa. Ma in questo caso, se RestServiceException sottoclassi non era della loro campi e metodi specifici, la prima soluzione è preferibile, ma con alcune modifiche:

    public class RestServiceException extends Exception { 
        private final Type type; 
        public Type getType(); 
        ... 
        public static enum Type { 
         UNAUTHENTICATED, 
         UNAUTHORISED, 
         NOT_FOUND; 
        } 
    } 
    
  • Inoltre v'è una buona pratica per creare metodo alternativo che getterà incontrollato eccezione che include l'inietta RestServiceException per l'utilizzo all'interno della logica aziendale "tutto o niente".

    public Photo getPhotoUnchecked(String photoID) { 
        try { 
         return getPhoto(photoID); 
        catch (RestServiceException ex) { 
         throw new RestServiceUncheckedException(ex); 
        } 
    }