web-dev-qa-db-fra.com

Comment changer le type de contenu dans le gestionnaire d'exceptions

Supposons que j'ai un contrôleur qui sert la requête GET et renvoie le bean à sérialiser en JSON et fournit également un gestionnaire d'exceptions pour IllegalArgumentException qui peut être déclenché en service:

@RequestMapping(value = "/meta/{itemId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public MetaInformation getMetaInformation(@PathVariable int itemId) {
    return myService.getMetaInformation(itemId);
}

@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ResponseBody
public String handleIllegalArgumentException(IllegalArgumentException ex) {
    return ExceptionUtils.getStackTrace(ex);
}

Les convertisseurs de messages sont:

<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
        <bean class="org.springframework.http.converter.StringHttpMessageConverter" />
    </mvc:message-converters>
</mvc:annotation-driven>

Maintenant, lorsque je demande l'URL donnée dans le navigateur, je vois la bonne réponse JSON. Cependant, si une exception est déclenchée, l'exception stringifiée est également convertie en JSON, mais j'aimerais qu'elle soit traitée par StringHttpMessageConverter (résultant text/plain type mime). Comment puis-je y aller?

Pour rendre l'image plus complète (et compliquée), supposons que j'ai également le gestionnaire suivant:

@RequestMapping(value = "/version", method = RequestMethod.GET)
@ResponseBody
public String getApplicationVersion() {
    return "1.0.12";
}

Ce gestionnaire permet de sérialiser la chaîne de retour par les deux MappingJackson2HttpMessageConverter et StringHttpMessageConverter en fonction du passé Accept-type par le client. Les types et valeurs de retour doivent être les suivants:

 + ---- + --------------------- + ----------------- ------ + ------------------ + ------------------------ ------------- + 
 | NN | URL | Type d'acceptation | Type de contenu | Convertisseur de messages | 
 | | | en-tête de demande | en-tête de réponse | | 
 + ---- + --------------------- + ---------------- ------- + ------------------ + ----------------------- -------------- + 
 | 1. |/version | text/html; */* | texte/clair | StringHttpMessageConverter | 
 | 2. |/version | application/json; */* | application/json | MappingJackson2HttpMessageConverter | 
 | 3. |/meta/1 | text/html; */* | application/json | MappingJackson2HttpMessageConverter | 
 | 4. |/meta/1 | application/json; */* | application/json | MappingJackson2HttpMessageConverter | 
 | 5. |/meta/0 (exception) | text/html; */* | texte/clair | StringHttpMessageConverter | 
 | 6. |/meta/0 (exception) | application/json; */* | texte/clair | StringHttpMessageConverter | 
 + ---- + --------------------- + --------------- -------- + ------------------ + ---------------------- --------------- + 
30
dma_k

Je pense que la suppression du produces = MediaType.APPLICATION_JSON_VALUE de @RequestMapping du getMetaInformation vous donnera le résultat souhaité.

Le type de réponse sera négocié en fonction de la valeur du type de contenu dans l'en-tête Accept.


modifier

Comme cela ne couvre pas le scénario 3,4, voici une solution fonctionnant avec ResponseEntity.class directement:

@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleIllegalArgumentException(Exception ex) {
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.TEXT_PLAIN);
    return new ResponseEntity<String>(ex.getMessage(), headers, HttpStatus.BAD_REQUEST);
}
20
oehmiche

Il y a plusieurs aspects liés au problème:

  • StringHttpMessageConverter ajoute le type mime fourre-tout */* à la liste des types de supports pris en charge, tandis que MappingJackson2HttpMessageConverter est lié à application/json uniquement.
  • Lorsque @RequestMapping Fournit produces = ..., Cette valeur est stockée dans HttpServletRequest (voir RequestMappingInfoHandlerMapping.handleMatch()) et lorsque le gestionnaire d'erreurs est appelé, ce type MIME est automatiquement hérité et utilisé.

La solution dans un cas simple serait de mettre StringHttpMessageConverter en premier dans la liste:

<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.StringHttpMessageConverter">
            <property name="supportedMediaTypes">
                <array>
                    <util:constant static-field="org.springframework.http.MediaType.TEXT_PLAIN" />
                </array>
            </property>
        </bean>
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
    </mvc:message-converters>
</mvc:annotation-driven>

et supprimez également produces de l'annotation @RequestMapping:

@RequestMapping(value = "/meta/{itemId}", method = RequestMethod.GET)
@ResponseBody
public MetaInformation getMetaInformation(@PathVariable int itemId) {
    return myService.getMetaInformation(itemId);
}

Maintenant:

  • StringHttpMessageConverter supprimera tous les types que seuls MappingJackson2HttpMessageConverter peuvent gérer (MetaInformation, Java.util.Collection, etc.), ce qui permet de les passer plus loin.
  • En cas d'exception dans le scénario (5, 6) StringHttpMessageConverter aura la priorité.

Jusqu'ici tout va bien, mais malheureusement, les choses se compliquent avec ObjectToStringHttpMessageConverter . Pour le type de retour du gestionnaire Java.util.Collection<MetaInformation>, Ce convertisseur de message indiquera qu'il peut convertir ce type en Java.lang.String. La limitation vient du fait que les types d'éléments de collection sont effacés et que la méthode AbstractHttpMessageConverter.canWrite(Class<?> clazz, MediaType mediaType) obtient la classe Java.util.Collection<?> Pour vérification, cependant quand il s'agit de l'étape de conversion ObjectToStringHttpMessageConverter échoue. Pour résoudre le problème, nous conservons produces pour l'annotation @RequestMapping Où le convertisseur JSON doit être utilisé, mais pour forcer le type de contenu correct pour le gestionnaire d'exceptions, nous effacerons l'attribut HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE De HttpServletRequest:

@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ResponseBody
public String handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException ex) {
    request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
    return ExceptionUtils.getStackTrace(ex);
}

@RequestMapping(value = "/meta", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public Collection<MetaInformation> getMetaInformations() {
    return myService.getMetaInformations();
}

Le contexte reste le même qu'à l'origine:

<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
        <bean class="org.springframework.http.converter.ObjectToStringHttpMessageConverter">
            <property name="conversionService">
                <bean class="org.springframework.context.support.ConversionServiceFactoryBean" />
            </property>
            <property name="supportedMediaTypes">
                <array>
                    <util:constant static-field="org.springframework.http.MediaType.TEXT_PLAIN" />
                </array>
            </property>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

Désormais, les scénarios (1, 2, 3, 4) sont traités correctement en raison de la négociation du type de contenu, et les scénarios (5, 6) sont traités dans le gestionnaire d'exceptions.

Alternativement, on peut remplacer le type de retour de collection par des tableaux, puis la solution # 1 est à nouveau applicable:

@RequestMapping(value = "/meta", method = RequestMethod.GET)
@ResponseBody
public MetaInformation[] getMetaInformations() {
    return myService.getMetaInformations().toArray();
}

Pour discuter:

Je pense que AbstractMessageConverterMethodProcessor.writeWithMessageConverters() ne devrait pas hériter la classe de la valeur, mais plutôt de la signature de la méthode:

Type returnValueType = returnType.getGenericParameterType();

et HttpMessageConverter.canWrite(Class<?> clazz, MediaType mediaType) doit être changé en:

canWrite(Type returnType, MediaType mediaType)

ou (dans le cas où cela limite trop les convertisseurs potentiels basés sur la classe) à

canWrite(Class<?> valueClazz, Type returnType, MediaType mediaType)

Les types paramétrés pourraient alors être traités correctement et la solution # 1 serait à nouveau applicable.

12
dma_k