web-dev-qa-db-fra.com

Comment gérer les exceptions dans Spring MVC différemment pour les requêtes HTML et JSON

J'utilise le gestionnaire d'exceptions suivant dans Spring 4.0.3 pour intercepter les exceptions et afficher une page d'erreur personnalisée pour l'utilisateur:

@ControllerAdvice
public class ExceptionHandlerController
{
    @ExceptionHandler(value = Exception.class)
    public ModelAndView handleError(HttpServletRequest request, Exception e)
    {
        ModelAndView mav = new ModelAndView("/errors/500"));
        mav.addObject("exception", e);
        return mav;
    }
}

Mais maintenant, je veux un traitement différent pour les demandes JSON afin d'obtenir des réponses d'erreur JSON pour ce type de demandes lorsqu'une exception s'est produite. Actuellement, le code ci-dessus est également déclenché par des requêtes JSON (Utilisation d'un en-tête Accept: application/json) et le client JavaScript n'aime pas la réponse HTML.

Comment puis-je gérer les exceptions différemment pour les demandes HTML et JSON?

29
kayahr

L'annotation ControllerAdvice comporte un élément/attribut appelé basePackage qui peut être défini pour déterminer les packages à analyser et à appliquer pour les contrôleurs. Vous pouvez donc séparer les contrôleurs qui gèrent les demandes normales et ceux qui traitent les demandes AJAX dans différents packages, puis écrire 2 contrôleurs de traitement des exceptions avec les annotations ControllerAdvice appropriées. Par exemple:

@ControllerAdvice("com.acme.webapp.ajaxcontrollers")
public class AjaxExceptionHandlingController {
...
@ControllerAdvice("com.acme.webapp.controllers")
public class ExceptionHandlingController {
10
dnang

La meilleure façon de procéder (en particulier dans le servlet 3) consiste à enregistrer une page d'erreur dans le conteneur et à l'utiliser pour appeler un Spring @Controller. De cette façon, vous pouvez gérer différents types de réponse de manière standard Spring MVC (par exemple, en utilisant @RequestMapping avec Produit = ... pour les clients de votre ordinateur).

Je vois dans votre autre question que vous utilisez Spring Boot. Si vous effectuez une mise à niveau vers un instantané (1.1 ou supérieur en d'autres termes), vous obtenez ce problème immédiatement (voir BasicErrorController ). Si vous voulez le remplacer, il vous suffit de mapper le chemin/error sur votre propre @Controller.

7
Dave Syer

Comme vous avez le HttpServletRequest, vous devriez pouvoir obtenir l’en-tête "Accept" de la requête. Ensuite, vous pouvez traiter l'exception en fonction de celle-ci.

Quelque chose comme:

String header = request.getHeader("Accept");
if(header != null && header.equals("application/json")) {
    // Process JSON exception
} else {
    ModelAndView mav = new ModelAndView("/errors/500"));
    mav.addObject("exception", e);
    return mav;
}
2
Daniel

Comme je n’ai trouvé aucune solution à cela, j’ai écrit du code qui vérifie manuellement l’en-tête accept de la demande pour déterminer le format. Je vérifie ensuite si l'utilisateur est connecté et envoie le chemin de pile complet s'il est ou un message d'erreur court.

J'utilise ResponseEntity pour pouvoir retourner à la fois JSON ou HTML comme ici .
Code:

@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleExceptions(Exception ex, HttpServletRequest request) throws Exception {

    final HttpHeaders headers = new HttpHeaders();
    Object answer; // String if HTML, any object if JSON
    if(jsonHasPriority(request.getHeader("accept"))) {
        logger.info("Returning exception to client as json object");
        headers.setContentType(MediaType.APPLICATION_JSON);
        answer = errorJson(ex, isUserLoggedIn());
    } else {
        logger.info("Returning exception to client as html page");
        headers.setContentType(MediaType.TEXT_HTML);
        answer = errorHtml(ex, isUserLoggedIn());
    }
    final HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
    return new ResponseEntity<>(answer, headers, status);
}

private String errorHtml(Exception e, boolean isUserLoggedIn) {
    String error = // html code with exception information here
    return error;
}

private Object errorJson(Exception e, boolean isUserLoggedIn) {
    // return error wrapper object which will be converted to json
    return null;
}

/**
 * @param acceptString - HTTP accept header field, format according to HTTP spec:
 *      "mime1;quality1,mime2;quality2,mime3,mime4,..." (quality is optional)
 * @return true only if json is the MIME type with highest quality of all specified MIME types.
 */
private boolean jsonHasPriority(String acceptString) {
    if (acceptString != null) {
        final String[] mimes = acceptString.split(",");
        Arrays.sort(mimes, new MimeQualityComparator());
        final String firstMime = mimes[0].split(";")[0];
        return firstMime.equals("application/json");
    }
    return false;
}

private static class MimeQualityComparator implements Comparator<String> {
    @Override
    public int compare(String mime1, String mime2) {
        final double m1Quality = getQualityofMime(mime1);
        final double m2Quality = getQualityofMime(mime2);
        return Double.compare(m1Quality, m2Quality) * -1;
    }
}

/**
 * @param mimeAndQuality - "mime;quality" pair from the accept header of a HTTP request,
 *      according to HTTP spec (missing mimeQuality means quality = 1).
 * @return quality of this pair according to HTTP spec.
 */
private static Double getQualityofMime(String mimeAndQuality) {
    //split off quality factor
    final String[] mime = mimeAndQuality.split(";");
    if (mime.length <= 1) {
        return 1.0;
    } else {
        final String quality = mime[1].split("=")[1];
        return Double.parseDouble(quality);
    }
}
2
Katharsas

L'annotation conseiladervice a plusieurs propriétés qui peuvent être définies, à partir du printemps 4. Vous pouvez définir plusieurs conseils de contrôleur appliquant différentes règles.

Une propriété est "les annotations. Vous pouvez probablement utiliser une annotation spécifique sur le mappage de la requête json ou vous pourriez trouver une autre propriété plus utile?

0
Martin Frey

L'astuce consiste à avoir un contrôleur REST avec deux correspondances, dont l'une spécifie "text/html" et renvoie une source HTML valide. L'exemple ci-dessous, qui a été testé dans Spring Boot 2.0, suppose l'existence d'un modèle séparé nommé "error.html".

@RestController
public class CustomErrorController implements ErrorController {

    @Autowired
    private ErrorAttributes errorAttributes;

    private Map<String,Object> getErrorAttributes( HttpServletRequest request ) {
        WebRequest webRequest = new ServletWebRequest(request);
        boolean includeStacktrace = false;
        return errorAttributes.getErrorAttributes(webRequest,includeStacktrace);
    }

    @GetMapping(value="/error", produces="text/html")
    ModelAndView errorHtml(HttpServletRequest request) {
        return new ModelAndView("error.html",getErrorAttributes(request));
    }

    @GetMapping(value="/error")
    Map<String,Object> error(HttpServletRequest request) {
        return getErrorAttributes(request);
    }

    @Override public String getErrorPath() { return "/error"; }

}

Références

0
nobar

Utilisez @ControllerAdvice Laissez le gestionnaire d'exceptions envoyer un DTO contenant les erreurs de champ.

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ValidationErrorDTO processValidationError(MethodArgumentNotValidException ex) {
    BindingResult result = ex.getBindingResult();
    List<FieldError> fieldErrors = result.getFieldErrors();

    return processFieldErrors(fieldErrors);
}

Ce code est de ce site: http://www.petrikainulainen.net/programming/spring-framework/spring-from-the-trenches-adding-validation-validation-to-a-rest-api/ Regardez là pour plus d'infos.

0
Michiel