ChatGPT解决这个技术问题 Extra ChatGPT

Spring MVC: Complex object as GET @RequestParam

Suppose i have a page that lists the objects on a table and i need to put a form to filter the table. The filter is sent as an Ajax GET to an URL like that: http://foo.com/system/controller/action?page=1&prop1=x&prop2=y&prop3=z

And instead of having lots of parameters on my Controller like:

@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) { ... }

And supposing i have MyObject as:

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

    //Getters and setters
    ...
}

I wanna do something like:

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

Is it possible? How can i do that?

@michal +1. Here is a couple of tutorials showing how to do that: Spring 3 MVC: Handling Forms in Spring 3.0 MVC, What is and how to use @ModelAttribute, Spring MVC Form Handling Example. Just google "Spring MVC form handling" and you'll get a ton of tutorials/examples. But be sure to use modern way of form handling, i.e. Spring v2.5+
remove @RequestParam from the MyObject and also make attributes of the MyObject definable (by adding a no-arg constructor and setter for all the attributes) or (define an all-args constructor without no-args constructor, the setter is not needed in this case).

B
Biju Kunjummen

You can absolutely do that, just remove the @RequestParam annotation, Spring will cleanly bind your request parameters to your class instance:

public @ResponseBody List<MyObject> myAction(
    @RequestParam(value = "page", required = false) int page,
    MyObject myObject)

Note that Spring by default requiers getters/setters for the MyObject to bind it automatically. Otherway it won't bind the myObject.
How can you set fields are optional/non-optional in MyObject? Not sure how to find proper documentation for this..
@Biju, Is there a way to control default values and required for MyObject then, in a similar way we can do with @RequestParam?
@BijuKunjummen How can I update the MyObject to accept query parameters in Snake case and map it to a camel case member of MyObject. For example, ?book_id=4, should be mapped with bookId member of the MyObject?
I have request params as such: page-number and page-size. How will my request DTO object look like? In java, I can't create a field with the name page-number or page-size. Is there a way to tell spring that the request param with the given name should be mapped to this or that field? Please advise.
P
Przemek Nowak

I will add some short example from me.

The DTO class:

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

Request mapping inside controller class:

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

Query:

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

Result:

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

I hope it helps :)

UPDATE / KOTLIN

Because currently I'm working a lot of with Kotlin if someone wants to define similar DTO the class in Kotlin should have the following form:

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

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

With the data class like this one:

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

the Spring (tested in Boot) returns the following error for request mentioned in answer:

"Failed to convert value of type 'java.lang.String[]' to required type 'java.lang.Long[]'; nested exception is java.lang.NumberFormatException: For input string: \"353,234\""

The data class will work only for the following request params form:

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

Be aware of this!


is it possible to set "required" to dto fields?
I suggest to try with Spring MVC validators. Example: codejava.net/frameworks/spring/…
It's very curious that this does not require an annotation! I wonder, is there an explicit annotation for this albeit unnecessary?
This behavior difference with java/kotlin should be an error. I created an issue. Spend few hours to search why it was not working but I used to have no problem in java. See github.com/spring-projects/spring-framework/issues/25815
Thanks for Kotlin way, but there is any way to immutable object and use val instead var to work ?
F
FluffyDestroyerOfCode

Since the question on how to set fields mandatory pops up under each post, I wrote a small example on how to set fields as required:

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

//Add this to your rest controller class
@RequestMapping(value = "/test", method = RequestMethod.GET)
public String testComplexObject (@Valid ExampleDTO e){
    System.out.println(e.getMandatoryParam() + " " + e.getTestDate());
    return "Does this work?";
}

Doest that work today? Tried here, but the validations that I set on the pojo didnt work.
Yes it still works. Maybe this question will help you stackoverflow.com/questions/43436804/spring-valid-doesnt-work If you need more assistance I need some example code to look at it further.
h
hevi

I have a very similar problem. Actually the problem is deeper as I thought. I am using jquery $.post which uses Content-Type:application/x-www-form-urlencoded; charset=UTF-8 as default. Unfortunately I based my system on that and when I needed a complex object as a @RequestParam I couldn't just make it happen.

In my case I am trying to send user preferences with something like;

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

On client side the actual raw data sent to the server is;

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

parsed as;

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

and the server side is;

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

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

I tried @ModelAttribute, added setter/getters, constructors with all possibilities to UserPreferences but no chance as it recognized the sent data as 5 parameters but in fact the mapped method has only 2 parameters. I also tried Biju's solution however what happens is that, spring creates an UserPreferences object with default constructor and doesn't fill in the data.

I solved the problem by sending JSon string of the preferences from the client side and handle it as if it is a String on the server side;

client:

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

server:

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

to brief, I did the conversion manually inside the REST method. In my opinion the reason why spring doesn't recognize the sent data is the content-type.


I am having exactly the same issue too. I did cleaner workaround using @RequestMapping(method = POST, path = "/settings/{projectId}") public void settings(@PathVariable String projectId, @RequestBody ProjectSettings settings)
@Petr Újezdský your solution is not feasible because it completely changes the API (using request body instead of request parameter)
D
DKO

While answers that refer to @ModelAttribute, @RequestParam, @PathParam and the likes are valid, there is a small gotcha I ran into. The resulting method parameter is a proxy that Spring wraps around your DTO. So, if you attempt to use it in a context that requires your own custom type, you may get some unexpected results.

The following will not work:

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

In my case, attempting to use it in Jackson binding resulted in a com.fasterxml.jackson.databind.exc.InvalidDefinitionException.

You will need to create a new object from the dto.


v
vkstream

Yes, You can do it in a simple way. See below code of lines.

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

G
Gabriel Brito

Accepted answer works like a charm but if the object has a list of objects it won't work as expected so here is my solution after some digging.

Following this thread advice, here is how I've done.

Frontend: stringify your object than encode it in base64 for submission.

Backend: decode base64 string then convert the string json into desired object.

It isn't the best for debugging your API with postman but it is working as expected for me.

Original object: { page: 1, size: 5, filters: [{ field: "id", value: 1, comparison: "EQ" } Encoded object: 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)
}

And here the SearchFilterDTO and 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
)