Spring MVC - Jak zwrócić prosty ciąg jako JSON w kontrolerze REST

137

Moje pytanie jest zasadniczo kontynuacją tego pytania.

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

W powyższym przykładzie Spring dodałby „Hello World” do treści odpowiedzi. Jak mogę zwrócić ciąg jako odpowiedź JSON? Rozumiem, że mógłbym dodawać cytaty, ale to bardziej przypomina hack.

Proszę podać przykłady, które pomogą wyjaśnić tę koncepcję.

Uwaga: nie chcę, aby było to zapisywane bezpośrednio w treści odpowiedzi HTTP, chcę zwrócić ciąg w formacie JSON (używam kontrolera z RestyGWT, który wymaga odpowiedzi w prawidłowym formacie JSON).

Sztylet Gilberta Arenasa
źródło
Możesz zwrócić Map lub dowolny obiekt / obiekt, który zawiera twój ciąg
Denys Denysiuk
Więc masz na myśli, że chcesz, aby wartość String była serializowana do ciągu JSON?
Sotirios Delimanolis

Odpowiedzi:

150

Albo zwróć text/plain(jak w Zwróć tylko wiadomość tekstową z kontrolera Spring MVC 3 ) LUB zawiń Twój String to jakiś obiekt

public class StringResponse {

    private String response;

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

    // get/set omitted...
}


Ustaw typ odpowiedzi na MediaType.APPLICATION_JSON_VALUE(= "application/json")

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

i będziesz mieć JSON, który wygląda jak

{  "response" : "your string value" }
Shaun
źródło
124
Możesz również powrócić, Collections.singletonMap("response", "your string value")aby osiągnąć ten sam wynik bez konieczności tworzenia klasy opakowania.
Bohuslav Burghardt
@Bohuslav To świetna wskazówka.
Shaun,
6
Nie jest prawdą, że wymaga klucza i wartości. Pojedynczy ciąg lub tablica ciągów są prawidłowymi kodami JSON. Jeśli się nie zgadzasz, może możesz wyjaśnić, dlaczego witryna jsonlint akceptuje oba te elementy jako prawidłowe JSON.
KyleM
2
w jaki sposób klasa opakowania jest konwertowana na JSON?
Rocky Inde
3
Myślę, że wystarczy wrócićCollections.singleton("your string value")
gauee
54

JSON jest zasadniczo ciągiem znaków w kontekście PHP lub JAVA. Oznacza to, że w odpowiedzi można zwrócić ciąg, który jest prawidłowym kodem JSON. Następujące powinno działać.

  @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}";
  }

Jest to w porządku w przypadku prostej odpowiedzi typu string. Ale dla złożonej odpowiedzi JSON powinieneś użyć klasy opakowującej opisanej przez Shauna.

pinkal vansia
źródło
7
Należy to przyjąć odpowiedź, ponieważ była to dokładna odpowiedź na pytanie PO.
SRy,
Dzięki, @ResponseBody było tym, czego potrzebowałem
riskop
Ciekawi Cię, jaka jest „lepsza” pozycja @ResponseBody przed publicznym słowem kluczowym lub po nim? Zawsze umieszczałem to po, ponieważ jest bardziej utożsamiane z wartością zwracaną.
David Bradley
26

W jednym projekcie rozwiązaliśmy ten problem za pomocą JSONObject ( informacje o zależnościach maven ). Wybraliśmy to, ponieważ woleliśmy zwrócić prosty łańcuch zamiast obiektu opakowującego. Zamiast tego można łatwo użyć wewnętrznej klasy pomocniczej, jeśli nie chcesz dodawać nowej zależności.

Przykładowe zastosowanie:

@RestController
public class TestController
{
    @RequestMapping("/getString")
    public String getString()
    {
        return JSONObject.quote("Hello World");
    }
}
Sztylet Gilberta Arenasa
źródło
1
Może powinieneś wspomnieć w swojej odpowiedzi, że "\"Hello World\""działałoby to równie dobrze bez dodatkowej zależności - to właśnie JSONObject.quote()działa, prawda?
jerico
Nie podoba mi się to rozwiązanie, ale zadziałało. :-)
Michael Hegner
22

Możesz łatwo wrócić JSONz Stringwłasnością responsew następujący sposób

@RestController
public class TestController {
    @RequestMapping(value = "/getString", produces = MediaType.APPLICATION_JSON_VALUE)
    public Map getString() {
        return Collections.singletonMap("response", "Hello World");
    }
}
Javasick
źródło
2
za każdym razem, gdy używasz „@RestController”, nie musisz używać „@ResponseBody”
jitendra varshney
12

Po prostu wyrejestruj domyślną StringHttpMessageConverterinstancję:

@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);
  }
}

Przetestowano zarówno z metodami obsługi akcji kontrolera, jak i programami obsługi wyjątków kontrolera:

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

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

Uwagi końcowe:

  • extendMessageConverters jest dostępny od Spring 4.1.3, jeśli korzystasz z poprzedniej wersji, możesz zaimplementować tę samą technikę, używając configureMessageConverters tylko trochę więcej pracy.
  • Było to jedno z wielu innych możliwych podejść, jeśli aplikacja zawsze zwraca tylko JSON i nie zwraca żadnych innych typów zawartości, lepiej jest pominąć domyślne konwertery i dodać jeden konwerter jackson. Innym podejściem jest dodanie domyślnych konwerterów, ale w innej kolejności, tak aby konwerter jackson był przed konwerterem łańcuchowym. Powinno to pozwolić metodom akcji kontrolera na dyktowanie, w jaki sposób mają być konwertowane String w zależności od typu nośnika odpowiedzi.
Amr Mostafa
źródło
1
Byłoby miło mieć przykładowy kod dotyczący drugiej ostatniej notatki.
Tony Baguette,
1
converters.removeIf(c -> c instanceof StringHttpMessageConverter)
chrylis
10

Wiem, że to pytanie jest stare, ale chciałbym również wnieść swój wkład:

Główną różnicą między innymi odpowiedziami jest zwrot hashmap.

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

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

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

    return rtn;

}

To zwróci:

{"pic":"a17fefab83517fb...beb8ac5a2ae8f0449","potato":"King Potato"}
Brenno Leal
źródło
2
Dlaczego deklarujesz metodę jako zwracającą HashMap? LHM wdraża Map.
JL_SO,
6

Uprość:

    @GetMapping("/health")
    public ResponseEntity<String> healthCheck() {
        LOG.info("REST request health check");
        return new ResponseEntity<>("{\"status\" : \"UP\"}", HttpStatus.OK);
    }
samaron
źródło
Wydaje mi się, że korzystanie z ResponseEntity jest najnowocześniejszym rozwiązaniem . +1
Alexander
5

Dodaj produces = "application/json"w @RequestMappingadnotacji, takich jak:

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

Podpowiedź: Jako wartość zwracaną polecam użycie ResponseEntity<List<T>>typu. Ponieważ dane wygenerowane w treści JSON muszą być tablicą lub obiektem zgodnie ze specyfikacjami, a nie pojedynczym prostym ciągiem . Czasami może powodować problemy (np. Observables w Angular2).

Różnica:

zwrócony Stringjako json:"example"

zwrócony List<String>jako json:["example"]

Aybars Yuksel
źródło
3

Dodaj @ResponseBodyadnotację, która zapisze zwracane dane w strumieniu wyjściowym.

hugo
źródło
1
to nie zadziałało dla mnie. Mam@PostMapping(value = "/some-url", produces = APPLICATION_JSON_UTF8_VALUE)
aliopi
0

Ten problem doprowadził mnie do szaleństwa: Spring jest tak potężnym narzędziem, a jednak tak prosta rzecz, jak napisanie wyjściowego ciągu znaków jako JSON, wydaje się niemożliwa bez brzydkich hacków.

Moim rozwiązaniem (w Kotlinie), które uważam za najmniej inwazyjne i najbardziej przejrzyste, jest skorzystanie z porady kontrolera i sprawdzenie, czy żądanie trafiło do określonego zestawu punktów końcowych (zazwyczaj REST API, ponieważ najczęściej chcemy zwrócić WSZYSTKIE odpowiedzi stąd jako JSON i nie twórz specjalizacji w interfejsie użytkownika na podstawie tego, czy zwracane dane są zwykłym ciągiem znaków („Nie wykonuj deserializacji JSON!”), czy czymś innym („Wykonaj deserializację JSON!”)). Pozytywnym aspektem jest to, że kontroler pozostaje ten sam i bez hacków.

supportsMetoda zapewnia, że wszystkie wnioski, które były obsługiwane przez StringHttpMessageConverter(np konwerter, który obsługuje wyjście wszystkich sterowników, które zwracają zwykłe struny) są przetwarzane oraz w beforeBodyWritemetodzie możemy kontrolować, w jakich przypadkach chcemy przerwać i konwertować dane wyjściowe do JSON (i odpowiednio zmodyfikuj nagłówki).

@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
    }
}

Mam nadzieję, że w przyszłości otrzymamy prostą adnotację, w której będziemy mogli nadpisać, która HttpMessageConverterpowinna być użyta do wyniku.

szybki refleks
źródło
-5

Dodaj tę adnotację do swojej metody

@RequestMapping(value = "/getString", method = RequestMethod.GET, produces = "application/json")
Dali
źródło