จะตอบกลับด้วยข้อผิดพลาด HTTP 400 ในวิธี Spring MVC @ResponseBody ที่ส่งคืนสตริงได้อย่างไร


389

ฉันกำลังใช้ Spring MVC สำหรับ JSON API อย่างง่ายด้วย@ResponseBodyวิธีการที่ใช้เป็นพื้นฐานดังต่อไปนี้ (ฉันมีชั้นบริการที่ผลิต JSON โดยตรง)

@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public String match(@PathVariable String matchId) {
    String json = matchService.getMatchJson(matchId);
    if (json == null) {
        // TODO: how to respond with e.g. 400 "bad request"?
    }
    return json;
}

คำถามคือในสถานการณ์ที่กำหนดวิธีที่ง่ายที่สุดและสะอาดที่สุดในการตอบสนองกับข้อผิดพลาด HTTP 400คืออะไร

ฉันเจอวิธีเช่น:

return new ResponseEntity(HttpStatus.BAD_REQUEST);

... แต่ฉันไม่สามารถใช้ที่นี่ได้เนื่องจากวิธีการส่งคืนของฉันคือ String ไม่ใช่ ResponseEntity

คำตอบ:


624

เปลี่ยนประเภทการส่งคืนเป็นResponseEntity<>แล้วคุณสามารถใช้ด้านล่างสำหรับ 400

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

และสำหรับคำขอที่ถูกต้อง

return new ResponseEntity<>(json,HttpStatus.OK);

อัพเดท 1

หลังจากฤดูใบไม้ผลิ 4.1 มีวิธีการช่วยเหลือใน ResponseEntity สามารถใช้เป็น

return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);

และ

return ResponseEntity.ok(json);

อาดังนั้นคุณสามารถใช้ResponseEntityสิ่งนี้เช่นกัน มันทำงานได้ดีและเป็นการเปลี่ยนรหัสต้นฉบับง่าย ๆ - ขอบคุณ!
Jonik

คุณยินดีต้อนรับเมื่อใดก็ได้ที่คุณสามารถเพิ่มส่วนหัวที่กำหนดเองได้เช่นกันตรวจสอบ Constructor ของ ResponseEntity ทั้งหมด
Bassem Reda Zohdy

7
เกิดอะไรขึ้นถ้าคุณกำลังส่งผ่านสิ่งอื่นที่ไม่ใช่สตริงกลับ? เช่นเดียวกับ POJO หรือวัตถุอื่น ๆ ?
mrshickadance

11
มันจะเป็น 'ResponseEntity <YourClass>'
Bassem Reda Zohdy

5
การใช้วิธีการนี้คุณไม่จำเป็นต้องมีคำอธิบายประกอบ @ResponseBody อีกต่อไป
Lu55

108

สิ่งนี้ควรใช้งานได้ฉันไม่แน่ใจว่าจะมีวิธีที่ง่ายกว่านี้หรือไม่:

@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public String match(@PathVariable String matchId, @RequestBody String body,
            HttpServletRequest request, HttpServletResponse response) {
    String json = matchService.getMatchJson(matchId);
    if (json == null) {
        response.setStatus( HttpServletResponse.SC_BAD_REQUEST  );
    }
    return json;
}

5
ขอบคุณ! มันใช้งานได้และค่อนข้างเรียบง่ายเช่นกัน (ในกรณีนี้มันอาจจะทำให้ง่ายขึ้นโดยการลบที่ไม่ได้ใช้bodyและrequestparams.)
Jonik

54

ไม่จำเป็นต้องเป็นวิธีที่กะทัดรัดที่สุดในการทำเช่นนี้ แต่ค่อนข้างสะอาด IMO

if(json == null) {
    throw new BadThingException();
}
...

@ExceptionHandler(BadThingException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public @ResponseBody MyError handleException(BadThingException e) {
    return new MyError("That doesnt work");
}

แก้ไขคุณสามารถใช้ @ResponseBody ในเมธอดตัวจัดการข้อยกเว้นหากใช้ Spring 3.1+ มิฉะนั้นจะใช้ a ModelAndViewหรืออย่างอื่น

https://jira.springsource.org/browse/SPR-6902


1
ขออภัยดูเหมือนจะไม่ทำงาน มันสร้าง HTTP 500 "ข้อผิดพลาดเซิร์ฟเวอร์" ที่มีการติดตามสแต็คยาวในบันทึก: ERROR org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver - Failed to invoke @ExceptionHandler method: public controller.TestController$MyError controller.TestController.handleException(controller.TestController$BadThingException) org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representationมีบางสิ่งที่ขาดหายไปจากคำตอบ?
Jonik

นอกจากนี้ฉันยังไม่เข้าใจจุดที่กำหนดประเภทอื่นที่กำหนดเองอย่างสมบูรณ์ (MyError) จำเป็นไหม? ฉันใช้สปริงตัวล่าสุด (3.2.2)
Jonik

1
มันใช้งานได้สำหรับฉัน ฉันใช้javax.validation.ValidationExceptionแทน (ฤดูใบไม้ผลิ 3.1.4)
Jerry Chen

สิ่งนี้มีประโยชน์มากในสถานการณ์ที่คุณมีเลเยอร์กลางระหว่างบริการของคุณและไคลเอนต์ที่เลเยอร์กลางมีความสามารถในการจัดการข้อผิดพลาดของตัวเอง ขอบคุณสำหรับตัวอย่างนี้ @Zutty
StormeHawke

นี่ควรเป็นคำตอบที่ยอมรับได้เนื่องจากมันเคลื่อนย้ายข้อยกเว้นในการจัดการโค้ดออกจากโฟลว์ปกติและจะซ่อน HttpServlet *
lilalinux

48

ฉันจะเปลี่ยนการดำเนินการเล็กน้อย:

ก่อนอื่นฉันสร้างUnknownMatchException:

@ResponseStatus(HttpStatus.NOT_FOUND)
public class UnknownMatchException extends RuntimeException {
    public UnknownMatchException(String matchId) {
        super("Unknown match: " + matchId);
    }
}

หมายเหตุการใช้งานของ@ResponseStatusResponseStatusExceptionResolverซึ่งจะได้รับการยอมรับจากฤดูใบไม้ผลิ หากมีการโยนข้อยกเว้นจะสร้างการตอบกลับที่มีสถานะการตอบกลับที่สอดคล้องกัน (ฉันยังใช้เสรีภาพในการเปลี่ยนรหัสสถานะ404 - Not Foundซึ่งฉันเห็นว่าเหมาะสมกว่าสำหรับกรณีการใช้งานนี้ แต่คุณสามารถติดHttpStatus.BAD_REQUESTถ้าคุณต้องการ)


ต่อไปฉันจะเปลี่ยนMatchServiceให้มีลายเซ็นต่อไปนี้:

interface MatchService {
    public Match findMatch(String matchId);
}

ในที่สุดฉันจะอัปเดตคอนโทรลเลอร์และมอบสิทธิ์ให้ Spring's MappingJackson2HttpMessageConverterเพื่อจัดการการทำให้เป็นอนุกรม JSON โดยอัตโนมัติ (จะถูกเพิ่มโดยค่าเริ่มต้นหากคุณเพิ่ม Jackson ใน classpath และเพิ่มอย่างใดอย่างหนึ่ง@EnableWebMvcหรือ<mvc:annotation-driven />เพื่อกำหนดค่าของคุณดูเอกสารอ้างอิง ):

@RequestMapping(value = "/matches/{matchId}", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public Match match(@PathVariable String matchId) {
    // throws an UnknownMatchException if the matchId is not known 
    return matchService.findMatch(matchId);
}

หมายเหตุมันเป็นเรื่องธรรมดามากที่จะแยกวัตถุโดเมนจากวัตถุมุมมองหรือวัตถุ DTO สิ่งนี้สามารถทำได้อย่างง่ายดายโดยการเพิ่มโรงงาน DTO ขนาดเล็กที่ส่งคืนวัตถุ JSON ที่ปรับแต่งได้:

@RequestMapping(value = "/matches/{matchId}", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public MatchDTO match(@PathVariable String matchId) {
    Match match = matchService.findMatch(matchId);
    return MatchDtoFactory.createDTO(match);
}

ฉันมีบันทึกของฉัน 500 รายการ: ay 28, 2015 5:23:31 PM org.apache.cxf.interceptor.AbstractFaultChainInitiatorObserver onMessage SEVERE: เกิดข้อผิดพลาดระหว่างจัดการข้อผิดพลาดยอมแพ้! org.apache.cxf.interceptor.Fault
มีดโกน

โซลูชั่นที่สมบูรณ์แบบฉันต้องการเพียงเพิ่มที่ฉันหวังว่า DTO เป็นองค์ประกอบของMatchและวัตถุอื่น ๆ
Marco Sulla

32

นี่คือวิธีการที่แตกต่างกัน สร้างExceptionคำอธิบายประกอบที่กำหนดเองด้วย@ResponseStatusเช่นสิ่งต่อไปนี้

@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Not Found")
public class NotFoundException extends Exception {

    public NotFoundException() {
    }
}

และโยนมันเมื่อจำเป็น

@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public String match(@PathVariable String matchId) {
    String json = matchService.getMatchJson(matchId);
    if (json == null) {
        throw new NotFoundException();
    }
    return json;
}

ตรวจสอบเอกสารฤดูใบไม้ผลิที่นี่: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions


วิธีการนี้ช่วยให้คุณสามารถยุติการดำเนินการทุกที่ที่คุณอยู่ในสแต็คเทรซโดยไม่ต้องส่งคืน "ค่าพิเศษ" ที่ควรระบุรหัสสถานะ HTTP ที่คุณต้องการส่งคืน
มูฮัมหมัด Gelbana

21

ดังที่กล่าวไว้ในคำตอบบางข้อมีความสามารถในการสร้างคลาสยกเว้นสำหรับแต่ละสถานะ HTTP ที่คุณต้องการส่งคืน ฉันไม่ชอบความคิดในการสร้างคลาสต่อสถานะสำหรับแต่ละโครงการ นี่คือสิ่งที่ฉันคิดขึ้นมาแทน

  • สร้างข้อยกเว้นทั่วไปที่ยอมรับสถานะ HTTP
  • สร้างตัวจัดการข้อยกเว้นคำแนะนำของตัวควบคุม

มาดูรหัสกันดีกว่า

package com.javaninja.cam.exception;

import org.springframework.http.HttpStatus;


/**
 * The exception used to return a status and a message to the calling system.
 * @author norrisshelton
 */
@SuppressWarnings("ClassWithoutNoArgConstructor")
public class ResourceException extends RuntimeException {

    private HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;

    /**
     * Gets the HTTP status code to be returned to the calling system.
     * @return http status code.  Defaults to HttpStatus.INTERNAL_SERVER_ERROR (500).
     * @see HttpStatus
     */
    public HttpStatus getHttpStatus() {
        return httpStatus;
    }

    /**
     * Constructs a new runtime exception with the specified HttpStatus code and detail message.
     * The cause is not initialized, and may subsequently be initialized by a call to {@link #initCause}.
     * @param httpStatus the http status.  The detail message is saved for later retrieval by the {@link
     *                   #getHttpStatus()} method.
     * @param message    the detail message. The detail message is saved for later retrieval by the {@link
     *                   #getMessage()} method.
     * @see HttpStatus
     */
    public ResourceException(HttpStatus httpStatus, String message) {
        super(message);
        this.httpStatus = httpStatus;
    }
}

จากนั้นฉันก็สร้างคลาสคำแนะนำสำหรับผู้ควบคุม

package com.javaninja.cam.spring;


import com.javaninja.cam.exception.ResourceException;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;


/**
 * Exception handler advice class for all SpringMVC controllers.
 * @author norrisshelton
 * @see org.springframework.web.bind.annotation.ControllerAdvice
 */
@org.springframework.web.bind.annotation.ControllerAdvice
public class ControllerAdvice {

    /**
     * Handles ResourceExceptions for the SpringMVC controllers.
     * @param e SpringMVC controller exception.
     * @return http response entity
     * @see ExceptionHandler
     */
    @ExceptionHandler(ResourceException.class)
    public ResponseEntity handleException(ResourceException e) {
        return ResponseEntity.status(e.getHttpStatus()).body(e.getMessage());
    }
}

ที่จะใช้มัน

throw new ResourceException(HttpStatus.BAD_REQUEST, "My message");

http://javaninja.net/2016/06/throwing-exceptions-messages-spring-mvc-controller/


วิธีการที่ดีมาก .. แทนที่จะใช้สตริงแบบง่ายฉันต้องการคืนค่า jSON ด้วย errorCode และช่องข้อความ ..
İsmail Yavuz

1
นี่ควรเป็นคำตอบที่ถูกต้องตัวจัดการข้อยกเว้นทั่วไปและสากลพร้อมรหัสสถานะและข้อความที่กำหนดเอง: D
Pedro Silva

10

ฉันใช้สิ่งนี้ในแอพพลิเคชั่นบู๊ทสปริง

@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public ResponseEntity<?> match(@PathVariable String matchId, @RequestBody String body,
            HttpServletRequest request, HttpServletResponse response) {

    Product p;
    try {
      p = service.getProduct(request.getProductId());
    } catch(Exception ex) {
       return new ResponseEntity<String>(HttpStatus.BAD_REQUEST);
    }

    return new ResponseEntity(p, HttpStatus.OK);
}

9

วิธีที่ง่ายที่สุดคือการโยน ResponseStatusException

    @RequestMapping(value = "/matches/{matchId}", produces = "application/json")
    @ResponseBody
    public String match(@PathVariable String matchId, @RequestBody String body) {
        String json = matchService.getMatchJson(matchId);
        if (json == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);
        }
        return json;
    }

3
คำตอบที่ดีที่สุด: ไม่จำเป็นต้องเปลี่ยนประเภทผลตอบแทนและไม่จำเป็นต้องสร้างข้อยกเว้นของคุณเอง นอกจากนี้ ResponseStatusException ยังอนุญาตให้เพิ่มข้อความเหตุผลหากจำเป็น
Migs

สิ่งสำคัญที่ควรทราบคือ ResponseStatusException นั้นมีเฉพาะในรุ่นสปริง 5+
Ethan Conner

2

ด้วย Spring Boot ฉันไม่แน่ใจว่าทำไมสิ่งนี้จึงเป็นสิ่งจำเป็น (ฉันได้รับทาง/errorเลือกแม้ว่าจะ@ResponseBodyมีการระบุไว้ในข้อ@ExceptionHandler) แต่สิ่งต่อไปนี้ในตัวมันเองไม่ทำงาน:

@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorMessage handleIllegalArguments(HttpServletRequest httpServletRequest, IllegalArgumentException e) {
    log.error("Illegal arguments received.", e);
    ErrorMessage errorMessage = new ErrorMessage();
    errorMessage.code = 400;
    errorMessage.message = e.getMessage();
    return errorMessage;
}

มันยังคงโยนข้อยกเว้นอย่างเห็นได้ชัดเพราะไม่มีการกำหนดประเภทสื่อที่ผลิตได้เป็นแอตทริบิวต์คำขอ:

// AbstractMessageConverterMethodProcessor
@SuppressWarnings("unchecked")
protected <T> void writeWithMessageConverters(T value, MethodParameter returnType,
        ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

    Class<?> valueType = getReturnValueType(value, returnType);
    Type declaredType = getGenericType(returnType);
    HttpServletRequest request = inputMessage.getServletRequest();
    List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
    List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
if (value != null && producibleMediaTypes.isEmpty()) {
        throw new IllegalArgumentException("No converter found for return value of type: " + valueType);   // <-- throws
    }

// ....

@SuppressWarnings("unchecked")
protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, Type declaredType) {
    Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
    if (!CollectionUtils.isEmpty(mediaTypes)) {
        return new ArrayList<MediaType>(mediaTypes);

ดังนั้นฉันจึงเพิ่มพวกเขา

@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorMessage handleIllegalArguments(HttpServletRequest httpServletRequest, IllegalArgumentException e) {
    Set<MediaType> mediaTypes = new HashSet<>();
    mediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
    httpServletRequest.setAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
    log.error("Illegal arguments received.", e);
    ErrorMessage errorMessage = new ErrorMessage();
    errorMessage.code = 400;
    errorMessage.message = e.getMessage();
    return errorMessage;
}

และนี่ทำให้ฉันผ่านการมี "ประเภทสื่อที่รองรับที่รองรับ" แต่ก็ยังใช้งานไม่ได้เพราะฉันErrorMessageผิด:

public class ErrorMessage {
    int code;

    String message;
}

JacksonMapper ไม่ได้จัดการเป็น "เปลี่ยนแปลงได้" ดังนั้นฉันจึงต้องเพิ่ม getters / setters และฉันยังเพิ่ม@JsonPropertyคำอธิบายประกอบ

public class ErrorMessage {
    @JsonProperty("code")
    private int code;

    @JsonProperty("message")
    private String message;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

จากนั้นฉันได้รับข้อความตามที่ตั้งใจ

{"code":400,"message":"An \"url\" parameter must be defined."}

0

นอกจากนี้คุณยังสามารถเพียงแค่throw new HttpMessageNotReadableException("error description")ได้รับประโยชน์จากฤดูใบไม้ผลิจัดการข้อผิดพลาดเริ่มต้น

อย่างไรก็ตามเช่นเดียวกับกรณีที่เกิดข้อผิดพลาดเริ่มต้นจะไม่มีการตั้งค่าเนื้อหาการตอบกลับ

ฉันพบว่าสิ่งเหล่านี้มีประโยชน์เมื่อปฏิเสธคำขอที่มีเพียงในแบบฉบับที่มีเหตุผลเท่านั้นที่อาจบ่งบอกถึงเจตนาที่มุ่งร้ายเนื่องจากพวกเขาปิดบังความจริงที่ว่าคำขอนั้นถูกปฏิเสธโดยอิงตามการตรวจสอบความถูกต้องที่กำหนดเองและเกณฑ์

อืม


HttpMessageNotReadableException("error description")เลิกใช้แล้ว
Kuba Šimonovský

0

อีกวิธีหนึ่งคือการใช้@ExceptionHandlerกับ@ControllerAdviceการรวมศูนย์รถขนของคุณทั้งหมดในระดับเดียวกันถ้าไม่ได้คุณต้องใส่วิธีการจัดการในทุกตัวควบคุมที่คุณต้องการในการจัดการข้อยกเว้น

คลาสตัวจัดการของคุณ:

@ControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {

  @ExceptionHandler(MyBadRequestException.class)
  public ResponseEntity<MyError> handleException(MyBadRequestException e) {
    return ResponseEntity
        .badRequest()
        .body(new MyError(HttpStatus.BAD_REQUEST, e.getDescription()));
  }
}

ข้อยกเว้นที่กำหนดเองของคุณ:

public class MyBadRequestException extends RuntimeException {

  private String description;

  public MyBadRequestException(String description) {
    this.description = description;
  }

  public String getDescription() {
    return this.description;
  }
}

ตอนนี้คุณสามารถโยนข้อยกเว้นจากตัวควบคุมใด ๆ ของคุณและคุณสามารถกำหนดตัวจัดการอื่น ๆ ในชั้นเรียนของคุณ


โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.