Wiosna MVC: Obiekt złożony jako GET @RequestParam

192

Załóżmy, że mam stronę z listą obiektów w tabeli i muszę umieścić formularz w celu filtrowania tabeli. Filtr jest wysyłany jako Ajax GET na taki adres URL: http://foo.com/system/controller/action?page=1&prop1=x&prop2=y&prop3=z

I zamiast mieć wiele parametrów na moim kontrolerze, takich jak:

@RequestMapping(value = "/action")
public @ResponseBody List<MyObject> myAction(
    @RequestParam(value = "page", required = false) int page,
    @RequestParam(value = "prop1", required = false) String prop1,
    @RequestParam(value = "prop2", required = false) String prop2,
    @RequestParam(value = "prop3", required = false) String prop3) { ... }

Załóżmy, że mam MyObject jako:

public class MyObject {
    private String prop1;
    private String prop2;
    private String prop3;

    //Getters and setters
    ...
}

Chcę zrobić coś takiego:

@RequestMapping(value = "/action")
public @ResponseBody List<MyObject> myAction(
    @RequestParam(value = "page", required = false) int page,
    @RequestParam(value = "myObject", required = false) MyObject myObject,) { ... }

Czy to możliwe? Jak mogę to zrobić?

renanleandrof
źródło
1
Spróbuj użyć „@ModelAttribute” w połączeniu z „@RequestMapping”, gdzie MyObject będzie Twoim modelem.
Michał
@michal +1. Oto kilka samouczków pokazujących, jak to zrobić: Spring 3 MVC: Obsługa formularzy w Spring 3.0 MVC , Co to jest i jak korzystać@ModelAttribute , Spring MVC Przykład obsługi formularzy . Wystarczy google „ Obsługa formularzy Spring MVC ”, a otrzymasz mnóstwo samouczków / przykładów. Ale pamiętaj, aby użyć nowoczesnego sposobu obsługi formularzy, tj. Spring v2.5 +
informatik01
Przydatne również: Co jest @ModelAttributena wiosnę MVC
informatik01

Odpowiedzi:

249

Możesz to absolutnie zrobić, po prostu usuń @RequestParamadnotację, Spring czysto powiąże parametry żądania z instancją klasy:

public @ResponseBody List<MyObject> myAction(
    @RequestParam(value = "page", required = false) int page,
    MyObject myObject)
Biju Kunjummen
źródło
8
Biju, czy możesz podać przykład, jak to nazwać? Wykonuję podstawowe wywołanie HTTP GET do Rest API i nie ma on żadnych wymyślnych formularzy.
bschandramohan
33
Zauważ, że Spring domyślnie wymaga pobierania / ustawiania przez MyObject, aby automatycznie go powiązał. W przeciwnym razie nie będzie wiązać myObject.
aka_sh,
20
Jak ustawić pola, które są opcjonalne / nie opcjonalne w MyObject? Nie wiem, jak znaleźć odpowiednią dokumentację do tego ..
worldsayshi
6
@Biju, czy istnieje sposób na kontrolowanie wartości domyślnych i jest wymagany do MyObjecttego w podobny sposób, jak w przypadku @RequestParam?
Alexey
7
@BijuKunjummen Jak mogę zaktualizować MyObjectparametr akceptacji zapytania w przypadku węża i odwzorować go na członka przypadku wielbłąda MyObject. Na przykład, ?book_id=4należy zmapować z bookIdczłonkiem MyObject?
Vivek Vardhan
55

Dodaję od siebie krótki przykład.

Klasa DTO:

public class SearchDTO {
    private Long id[];

    public Long[] getId() {
        return id;
    }

    public void setId(Long[] id) {
        this.id = id;
    }
    // reflection toString from apache commons
    @Override
    public String toString() {
        return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
}

Żądanie mapowania wewnątrz klasy kontrolera:

@RequestMapping(value="/handle", method=RequestMethod.GET)
@ResponseBody
public String handleRequest(SearchDTO search) {
    LOG.info("criteria: {}", search);
    return "OK";
}

Pytanie:

http://localhost:8080/app/handle?id=353,234

Wynik:

[http-apr-8080-exec-7] INFO  c.g.g.r.f.w.ExampleController.handleRequest:59 - criteria: SearchDTO[id={353,234}]

Mam nadzieję, że to pomoże :)

AKTUALIZACJA / KOTLIN

Ponieważ obecnie dużo pracuję z Kotlinem, jeśli ktoś chce zdefiniować podobne DTO, klasa w Kotlinie powinna mieć następującą postać:

class SearchDTO {
    var id: Array<Long>? = arrayOf()

    override fun toString(): String {
        // to string implementation
    }
}

Z taką dataklasą jak ta:

data class SearchDTO(var id: Array<Long> = arrayOf())

sprężyna (testowana podczas rozruchu) zwraca następujący błąd dla żądania wymienionego w odpowiedzi:

„Nie udało się przekonwertować wartości typu„ java.lang.String [] ”na wymagany typ„ java.lang.Long [] ”; wyjątek zagnieżdżony to java.lang.NumberFormatException: Dla ciągu wejściowego: \" 353,234 \ ""

Klasa danych będzie działać tylko w przypadku następującego formularza parametrów zapytania:

http://localhost:8080/handle?id=353&id=234

Bądź tego świadomy!

Przemek Nowak
źródło
1
czy można ustawić „wymagane” pola dto?
Normalny
4
Proponuję wypróbować z walidatorami Spring MVC. Przykład: codejava.net/frameworks/spring/…
Przemek Nowak
To bardzo ciekawe, że nie wymaga to adnotacji! Zastanawiam się, czy istnieje wyraźna adnotacja do tego, choć niepotrzebna?
James Watkins
8

Mam bardzo podobny problem. W rzeczywistości problem jest głębszy, jak myślałem. Używam jquery, $.postktóra używa Content-Type:application/x-www-form-urlencoded; charset=UTF-8domyślnie. Niestety oparłem na tym swój system i kiedy potrzebowałem złożonego obiektu, @RequestParamnie mogłem tego po prostu zrealizować.

W moim przypadku próbuję wysłać preferencje użytkownika za pomocą czegoś takiego;

 $.post("/updatePreferences",  
    {id: 'pr', preferences: p}, 
    function (response) {
 ...

Po stronie klienta rzeczywiste surowe dane wysyłane do serwera to;

...
id=pr&preferences%5BuserId%5D=1005012365&preferences%5Baudio%5D=false&preferences%5Btooltip%5D=true&preferences%5Blanguage%5D=en
...

parsowane jako;

id:pr
preferences[userId]:1005012365
preferences[audio]:false
preferences[tooltip]:true
preferences[language]:en

a po stronie serwera jest;

@RequestMapping(value = "/updatePreferences")
public
@ResponseBody
Object updatePreferences(@RequestParam("id") String id, @RequestParam("preferences") UserPreferences preferences) {

    ...
        return someService.call(preferences);
    ...
}

Próbowałem @ModelAttribute, dodałem setter / getters, konstruktory ze wszystkimi możliwościami, UserPreferencesale bez szansy, ponieważ rozpoznał wysłane dane jako 5 parametrów, ale w rzeczywistości metoda mapowana ma tylko 2 parametry. Próbowałem również rozwiązania Biju, jednak zdarza się, że wiosna tworzy obiekt UserPreferences z domyślnym konstruktorem i nie wypełnia danych.

Rozwiązałem problem, wysyłając ciąg JSon preferencji po stronie klienta i traktując go jak ciąg po stronie serwera;

klient:

 $.post("/updatePreferences",  
    {id: 'pr', preferences: JSON.stringify(p)}, 
    function (response) {
 ...

serwer:

@RequestMapping(value = "/updatePreferences")
public
@ResponseBody
Object updatePreferences(@RequestParam("id") String id, @RequestParam("preferences") String preferencesJSon) {


        String ret = null;
        ObjectMapper mapper = new ObjectMapper();
        try {
            UserPreferences userPreferences = mapper.readValue(preferencesJSon, UserPreferences.class);
            return someService.call(userPreferences);
        } catch (IOException e) {
            e.printStackTrace();
        }
}

w skrócie, zrobiłem konwersję ręcznie w metodzie REST. Moim zdaniem powodem, dla którego wiosna nie rozpoznaje przesłanych danych, jest typ zawartości.

hevi
źródło
1
Właśnie spotkałem ten sam problem i rozwiązałem go w ten sam sposób. Nawiasem mówiąc, czy znalazłeś dziś lepsze rozwiązanie?
Shay Elkayam
1
Mam dokładnie ten sam problem. Zrobiłem czystsze obejście, używając@RequestMapping(method = POST, path = "/settings/{projectId}") public void settings(@PathVariable String projectId, @RequestBody ProjectSettings settings)
Petr Újezdský
4

Ponieważ pod każdym postem pojawia się pytanie, jak ustawić pola obowiązkowe, napisałem mały przykład, jak ustawić pola zgodnie z wymaganiami:

public class ExampleDTO {
    @NotNull
    private String mandatoryParam;

    private String optionalParam;

    @DateTimeFormat(iso = ISO.DATE) //accept Dates only in YYYY-MM-DD
    @NotNull
    private LocalDate testDate;

    public String getMandatoryParam() {
        return mandatoryParam;
    }
    public void setMandatoryParam(String mandatoryParam) {
        this.mandatoryParam = mandatoryParam;
    }
    public String getOptionalParam() {
        return optionalParam;
    }
    public void setOptionalParam(String optionalParam) {
        this.optionalParam = optionalParam;
    }
    public LocalDate getTestDate() {
        return testDate;
    }
    public void setTestDate(LocalDate testDate) {
        this.testDate = testDate;
    }
}

@RequestMapping(value = "/test", method = RequestMethod.GET)
public String testComplexObject (@Valid ExampleDTO e){
    System.out.println(e.getMandatoryParam() + " " + e.getTestDate());
    return "Does this work?";
}
FluffyDestroyerOfCode
źródło
0

Chociaż odpowiedzi, które odnoszą się do @ModelAttribute, @RequestParam, @PathParami tacy są ważne, istnieje małe haczyka wpadłem. Wynikowym parametrem metody jest proxy, które Spring otacza DTO. Jeśli więc spróbujesz użyć go w kontekście wymagającym własnego typu niestandardowego, możesz uzyskać nieoczekiwane wyniki.

Następujące elementy nie będą działać:

@GetMapping(produces = APPLICATION_JSON_VALUE)
public ResponseEntity<CustomDto> request(@ModelAttribute CustomDto dto) {
    return ResponseEntity.ok(dto);
}

W moim przypadku próba użycia go w powiązaniu z Jacksonem zakończyła się com.fasterxml.jackson.databind.exc.InvalidDefinitionException.

Musisz utworzyć nowy obiekt z dto.

DKO
źródło
0

Tak, możesz to zrobić w prosty sposób. Zobacz poniżej kod linii.

URL - http: // localhost: 8080 / get / request / multiple / param / by / map? Name = 'abc' & id = '123'

 @GetMapping(path = "/get/request/header/by/map")
    public ResponseEntity<String> getRequestParamInMap(@RequestParam Map<String,String> map){
        // Do your business here 
        return new ResponseEntity<String>(map.toString(),HttpStatus.OK);
    } 
vkstream
źródło
0

Zaakceptowana odpowiedź działa jak urok, ale jeśli obiekt ma listę obiektów, nie będzie działał zgodnie z oczekiwaniami, więc oto moje rozwiązanie po pewnym kopaniu.

Postępując zgodnie z radą wątku , oto jak to zrobiłem.

  • Frontend: skretyzuj swój obiekt niż zakoduj go w base64 do przesłania.
  • Backend: dekoduj ciąg base64, a następnie przekonwertuj ciąg json na pożądany obiekt.

Nie jest najlepszy do debugowania interfejsu API za pomocą listonosza, ale działa zgodnie z oczekiwaniami.

Obiekt oryginalny: {strona: 1, rozmiar: 5, filtry: [{pole: „id”, wartość: 1, porównanie: „EQ”}

Zakodowany obiekt: eyJwYWdlIjoxLCJzaXplIjo1LCJmaWx0ZXJzIjpbeyJmaWVsZCI6ImlkUGFyZW50IiwiY29tcGFyaXNvbiI6Ik5VTEwifV19

@GetMapping
fun list(@RequestParam search: String?): ResponseEntity<ListDTO> {
    val filter: SearchFilterDTO = decodeSearchFieldDTO(search)
    ...
}

private fun decodeSearchFieldDTO(search: String?): SearchFilterDTO {
    if (search.isNullOrEmpty()) return SearchFilterDTO()
    return Gson().fromJson(String(Base64.getDecoder().decode(search)), SearchFilterDTO::class.java)
}

A tutaj SearchFilterDTO i FilterDTO

class SearchFilterDTO(
    var currentPage: Int = 1,
    var pageSize: Int = 10,
    var sort: Sort? = null,
    var column: String? = null,
    var filters: List<FilterDTO> = ArrayList<FilterDTO>(),
    var paged: Boolean = true
)

class FilterDTO(
    var field: String,
    var value: Any,
    var comparison: Comparison
)
Gabriel Brito
źródło