Spring MVC - วิธีคืนค่าสตริงธรรมดาเป็น JSON ใน Rest Controller


146

คำถามของฉันคือการติดตามนี้คำถาม

@RestController
public class TestController
{
    @RequestMapping("/getString")
    public String getString()
    {
        return "Hello World";
    }
}

ในข้างต้น Spring จะเพิ่ม "Hello World" ลงในเนื้อหาตอบกลับ ฉันจะส่งคืนสตริงเป็นการตอบสนอง JSON ได้อย่างไร ฉันเข้าใจว่าฉันสามารถเพิ่มคำพูดได้ แต่มันให้ความรู้สึกเหมือนแฮ็คมากกว่า

โปรดยกตัวอย่างเพื่อช่วยอธิบายแนวคิดนี้

หมายเหตุ:ฉันไม่ต้องการให้สิ่งนี้เขียนตรงไปยังเนื้อหาการตอบสนองของ HTTP ฉันต้องการส่งคืน String ในรูปแบบ JSON (ฉันใช้ตัวควบคุมของฉันกับRestyGWTซึ่งต้องการให้การตอบกลับอยู่ในรูปแบบ JSON ที่ถูกต้อง)


คุณสามารถส่งคืนแผนที่หรือวัตถุ / เอนทิตีใด ๆ ที่มีสตริงของคุณ
Denys Denysiuk

คุณหมายความว่าคุณต้องการให้ค่า String เป็นอนุกรมกับสตริง JSON หรือไม่?
Sotirios Delimanolis

คำตอบ:


164

ไม่ว่าจะส่งคืนtext/plain(ดังในข้อความสตริง Return only จาก Spring MVC 3 Controller ) หรือตัด String ของคุณเป็นวัตถุบางอย่าง

public class StringResponse {

    private String response;

    public StringResponse(String s) { 
       this.response = s;
    }

    // get/set omitted...
}


ตั้งค่าประเภทการตอบกลับของคุณเป็นMediaType.APPLICATION_JSON_VALUE(= "application/json")

@RequestMapping(value = "/getString", method = RequestMethod.GET,
                produces = MediaType.APPLICATION_JSON_VALUE)

และคุณจะมี JSON ที่ดูเหมือน

{  "response" : "your string value" }

126
คุณยังสามารถกลับมาCollections.singletonMap("response", "your string value")เพื่อให้ได้ผลลัพธ์เดียวกันโดยไม่ต้องสร้างคลาส Wrapper
Bohuslav Burghardt

@Bohuslav นั่นเป็นเคล็ดลับที่ดี
Shaun

7
ไม่เป็นความจริงที่ต้องใช้คีย์และค่า สตริงเดียวหรืออาร์เรย์ของสตริงเป็น JSON ที่ถูกต้องทั้งคู่ หากคุณไม่เห็นด้วยคุณสามารถอธิบายได้ว่าเหตุใดเว็บไซต์ jsonlint จึงยอมรับทั้งสองอย่างนี้เป็น JSON ที่ถูกต้อง
KyleM

2
คลาส Wrapper ถูกแปลงเป็น JSON ได้อย่างไร
Rocky Inde

3
ฉันคิดว่ามันก็เพียงพอที่จะกลับCollections.singleton("your string value")
gauee

58

JSON เป็นสตริงในบริบท PHP หรือ JAVA นั่นหมายถึงสตริงซึ่งเป็น JSON ที่ถูกต้องสามารถส่งคืนในการตอบสนอง ต่อไปนี้ควรใช้งานได้

  @RequestMapping(value="/user/addUser", method=RequestMethod.POST)
  @ResponseBody
  public String addUser(@ModelAttribute("user") User user) {

    if (user != null) {
      logger.info("Inside addIssuer, adding: " + user.toString());
    } else {
      logger.info("Inside addIssuer...");
    }
    users.put(user.getUsername(), user);
    return "{\"success\":1}";
  }

ไม่เป็นไรสำหรับการตอบกลับสตริงธรรมดา แต่สำหรับการตอบสนอง JSON ที่ซับซ้อนคุณควรใช้คลาส wrapper ตามที่ Shaun อธิบายไว้


8
นี่ควรเป็นคำตอบที่ยอมรับได้เนื่องจากนี่เป็นคำตอบที่แน่นอนสำหรับคำถามของ OP
SRy

ขอบคุณ @ResponseBody คือสิ่งที่ฉันต้องการ
riskop

อยากรู้ไหมว่าตำแหน่งไหน "ดีกว่า" สำหรับ @ResponseBody ก่อนหรือหลังคีย์เวิร์ดสาธารณะ ฉันมักจะใส่หลังจากนั้นเนื่องจากมีการระบุมูลค่าส่งคืนมากขึ้น
David Bradley

แต่ฉันได้รับการหลบหนีนี้ในการตอบสนองของฉัน สิ่งแปลก ๆ
Ivan

28

ในโครงการหนึ่งเราแก้ไขปัญหานี้โดยใช้JSONObject ( ข้อมูลการพึ่งพา maven ) เราเลือกสิ่งนี้เนื่องจากเราต้องการส่งคืนสตริงแบบง่ายมากกว่าอ็อบเจ็กต์ wrapper สามารถใช้คลาสตัวช่วยภายในแทนได้อย่างง่ายดายหากคุณไม่ต้องการเพิ่มการอ้างอิงใหม่

ตัวอย่างการใช้งาน:

@RestController
public class TestController
{
    @RequestMapping("/getString")
    public String getString()
    {
        return JSONObject.quote("Hello World");
    }
}

1
บางทีคุณควรพูดถึงในคำตอบของคุณซึ่ง"\"Hello World\""จะได้ผลเช่นกันโดยไม่มีการพึ่งพาเพิ่มเติมนั่นคือสิ่งที่JSONObject.quote()ใช่ไหม?
jerico

ฉันไม่ชอบวิธีแก้ปัญหา แต่มันใช้ได้ผลสำหรับฉัน :-)
Michael Hegner

23

คุณสามารถส่งคืนได้อย่างง่ายดายJSONด้วยStringทรัพย์สินresponseดังต่อไปนี้

@RestController
public class TestController {
    @RequestMapping(value = "/getString", produces = MediaType.APPLICATION_JSON_VALUE)
    public Map getString() {
        return Collections.singletonMap("response", "Hello World");
    }
}

2
เมื่อใดก็ตามที่คุณใช้ '@RestController' คุณไม่จำเป็นต้องใช้ '@ResponseBody'
jitendra varshney

13

เพียงแค่ยกเลิกการลงทะเบียนStringHttpMessageConverterอินสแตนซ์เริ่มต้น:

@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
  /**
   * Unregister the default {@link StringHttpMessageConverter} as we want Strings
   * to be handled by the JSON converter.
   *
   * @param converters List of already configured converters
   * @see WebMvcConfigurationSupport#addDefaultHttpMessageConverters(List)
   */
  @Override
  protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.stream()
      .filter(c -> c instanceof StringHttpMessageConverter)
      .findFirst().ifPresent(converters::remove);
  }
}

ทดสอบด้วยวิธีการจัดการการกระทำของคอนโทรลเลอร์และตัวจัดการข้อยกเว้นของคอนโทรลเลอร์:

@RequestMapping("/foo")
public String produceFoo() {
  return "foo";
}

@ExceptionHandler(FooApiException.class)
public String fooException(HttpServletRequest request, Throwable e) {
  return e.getMessage();
}

หมายเหตุสุดท้าย:

  • extendMessageConvertersพร้อมใช้งานตั้งแต่ฤดูใบไม้ผลิ 4.1.3 หากใช้งานในเวอร์ชันก่อนหน้าคุณสามารถใช้เทคนิคเดียวกันนี้ได้โดยใช้configureMessageConvertersเวลาทำงานเพิ่มขึ้นเล็กน้อย
  • นี่เป็นแนวทางหนึ่งของวิธีการอื่น ๆ ที่เป็นไปได้หากแอปพลิเคชันของคุณส่งคืน JSON เท่านั้นและไม่มีเนื้อหาประเภทอื่นคุณควรข้ามตัวแปลงเริ่มต้นและเพิ่มตัวแปลงแจ็คสันตัวเดียว อีกวิธีหนึ่งคือการเพิ่มตัวแปลงเริ่มต้น แต่เรียงลำดับต่างกันเพื่อให้ตัวแปลงแจ็คสันอยู่ก่อนสตริง สิ่งนี้ควรอนุญาตให้วิธีการดำเนินการของคอนโทรลเลอร์กำหนดวิธีที่พวกเขาต้องการให้ String ถูกแปลงขึ้นอยู่กับประเภทสื่อของการตอบสนอง

1
จะเป็นการดีที่จะมีโค้ดตัวอย่างเกี่ยวกับบันทึกสุดท้ายที่ 2 ของคุณ
Tony Baguette

1
converters.removeIf(c -> c instanceof StringHttpMessageConverter)
chrylis -cautiouslyoptimistic-

13

ฉันรู้ว่าคำถามนี้เก่า แต่ฉันอยากจะมีส่วนร่วมด้วย:

ความแตกต่างหลักระหว่างการตอบกลับอื่น ๆ คือการส่งคืนแฮชแมป

@GetMapping("...")
@ResponseBody
public Map<String, Object> endPointExample(...) {

    Map<String, Object> rtn = new LinkedHashMap<>();

    rtn.put("pic", image);
    rtn.put("potato", "King Potato");

    return rtn;

}

สิ่งนี้จะกลับมา:

{"pic":"a17fefab83517fb...beb8ac5a2ae8f0449","potato":"King Potato"}

7

เพิ่มproduces = "application/json"ใน@RequestMappingบันทึกย่อที่ชอบ:

@RequestMapping(value = "api/login", method = RequestMethod.GET, produces = "application/json")

คำแนะนำ: เป็นค่าส่งคืนฉันแนะนำให้ใช้ResponseEntity<List<T>>ประเภท เนื่องจากข้อมูลที่ผลิตในเนื้อความ JSON จำเป็นต้องเป็นอาร์เรย์หรือออบเจ็กต์ตามข้อกำหนดแทนที่จะเป็นสตริงธรรมดาเส้นเดียว บางครั้งอาจทำให้เกิดปัญหา (เช่น Observables ใน Angular2)

ความแตกต่าง:

ส่งคืนStringเป็น json:"example"

ส่งคืนList<String>เป็น json:["example"]


7

ทำให้ง่าย:

    @GetMapping("/health")
    public ResponseEntity<String> healthCheck() {
        LOG.info("REST request health check");
        return new ResponseEntity<>("{\"status\" : \"UP\"}", HttpStatus.OK);
    }

การใช้ ResponseEntity ดูเหมือนจะทันสมัยสำหรับฉัน +1
Alexander

3

เพิ่ม@ResponseBodyคำอธิบายประกอบซึ่งจะเขียนข้อมูลส่งคืนในเอาต์พุตสตรีม


1
สิ่งนี้ไม่ได้ผลสำหรับฉัน ฉันมี@PostMapping(value = "/some-url", produces = APPLICATION_JSON_UTF8_VALUE)
aliopi

0

ปัญหานี้ทำให้ฉันคลั่งไคล้ Spring เป็นเครื่องมือที่มีศักยภาพ แต่สิ่งง่ายๆเช่นการเขียนสตริงเอาต์พุตเนื่องจาก JSON ดูเหมือนจะเป็นไปไม่ได้หากไม่มีแฮ็กที่น่าเกลียด

วิธีแก้ปัญหาของฉัน (ใน Kotlin) ที่ฉันพบว่าสิ่งที่ล่วงล้ำน้อยที่สุดและโปร่งใสที่สุดคือการใช้คำแนะนำของคอนโทรลเลอร์และตรวจสอบว่าคำขอนั้นไปยังจุดสิ้นสุดชุดใดชุดหนึ่งหรือไม่ (โดยทั่วไปแล้ว REST API เนื่องจากเรามักต้องการส่งคืนคำตอบทั้งหมดจากที่นี่เป็น JSON และไม่สร้างความเชี่ยวชาญพิเศษในส่วนหน้าโดยพิจารณาว่าข้อมูลที่ส่งคืนเป็นสตริงธรรมดา ("Don't do JSON deserialization!") หรืออย่างอื่น ("Do JSON deserialization!")) ด้านบวกของสิ่งนี้คือคอนโทรลเลอร์ยังคงเหมือนเดิมและไม่มีแฮ็ก

supportsวิธีการทำให้แน่ใจว่าคำขอทั้งหมดที่ได้รับการจัดการโดยStringHttpMessageConverter(เช่นแปลงที่จับเอาท์พุทของตัวควบคุมทั้งหมดที่ส่งกลับสตริงธรรมดา) จะประมวลผลและในbeforeBodyWriteวิธีการที่เราควบคุมในกรณีที่เราต้องการที่จะขัดขวางและแปลงออกไป JSON (และแก้ไขส่วนหัวตามลำดับ)

@ControllerAdvice
class StringToJsonAdvice(val ob: ObjectMapper) : ResponseBodyAdvice<Any?> {
    
    override fun supports(returnType: MethodParameter, converterType: Class<out HttpMessageConverter<*>>): Boolean =
        converterType === StringHttpMessageConverter::class.java

    override fun beforeBodyWrite(
        body: Any?,
        returnType: MethodParameter,
        selectedContentType: MediaType,
        selectedConverterType: Class<out HttpMessageConverter<*>>,
        request: ServerHttpRequest,
        response: ServerHttpResponse
    ): Any? {
        return if (request.uri.path.contains("api")) {
            response.getHeaders().contentType = MediaType.APPLICATION_JSON
            ob.writeValueAsString(body)
        } else body
    }
}

ฉันหวังว่าในอนาคตเราจะได้คำอธิบายประกอบง่ายๆซึ่งเราสามารถลบล้างสิ่งที่HttpMessageConverterควรใช้สำหรับผลลัพธ์ได้


-5

เพิ่มคำอธิบายประกอบนี้ในวิธีการของคุณ

@RequestMapping(value = "/getString", method = RequestMethod.GET, produces = "application/json")
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.