简体   繁体   中英

Custom parameter names with a bean for request parameters in Spring 5

I'm trying to use Spring 5 to have a custom bean for my request parameters. In theory this is easy, but I want to have the field names be different from the parameter names.

I can do that trivially with normal @RequestParam parameters, but I can't seem to get it to work with a bean.

I've found this question asked before, and the answer seems to be "Do it manually", with various different options for automating that, eg using Argument Resolvers. But is this really still the case in Spring 5?

My code (It's Kotlin btw, but that shouldn't matter) is like this:

data class AuthorizationCodeParams(
    @RequestParam("client_id") val clientIdValue: String?,
    @RequestParam("redirect_uri") val redirectUriValue: String?,
    @RequestParam("scope") val scopes: String?,
    @RequestParam("state") val state: String?
)

fun startAuthorizationCode(params: AuthorizationCodeParams): ModelAndView {

It seems there's no Spring-provided way of doing this.

There's an open issue on the Spring GitHub page: https://github.com/spring-projects/spring-framework/issues/25815

I found some solutions on StackOverflow, for example https://stackoverflow.com/a/16520399/4161471 , but they seemed quite complex, or not re-usable. So I developed a workaround.

Workaround: Parse as JSON

I resolved this by creating a custom annotation and a HandlerMethodArgumentResolver . The argument resolver will translate the request parameters using Jackson. (Using Jackson is not required, but it was nice and generic and safe.)

import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.core.MethodParameter
import org.springframework.stereotype.Component
import org.springframework.web.bind.support.WebDataBinderFactory
import org.springframework.web.context.request.NativeWebRequest
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.method.support.ModelAndViewContainer

/**
 * Parses all request parameters and maps them to a JSON-serializable class.
 *
 * This is useful, because it allows multiple `@RequestParam`s to be
 * encapsulated in a single object, the values will be type-safe, and
 * the parameter names can be set using (for example, with `@JsonProperty`).
 */
@Target(AnnotationTarget.VALUE_PARAMETER)
@MustBeDocumented
annotation class JsonRequestParam {

  @Component
  class ArgumentResolver(
    private val objectMapper: ObjectMapper
  ) : HandlerMethodArgumentResolver {
  
    // only resolve parameters that are annotated with JsonRequestParam
    override fun supportsParameter(parameter: MethodParameter): Boolean {
      return parameter.hasParameterAnnotation(JsonRequestParam::class.java)
    }
  
    override fun resolveArgument(
      parameter: MethodParameter,
      mavContainer: ModelAndViewContainer?,
      webRequest: NativeWebRequest,
      binderFactory: WebDataBinderFactory?
    ): Any? {
      // `webRequest.parameterMap` is a `Map<String, String[]>`, so join
      // the values to a string
      val params: Map<String, String> = webRequest.parameterMap.mapValues { (_, v) -> v.joinToString() }
      return objectMapper.convertValue(params, parameter.parameterType)
    }
  }
}

Note that joining the parameter map values together with webRequest.parameterMap.mapValues { (_, v) -> v.joinToString() } is a quick fix. It's probably not correct and will only work for classes with primitive parameters - please suggest improvements!

Example usage

So if I have a DTO...

data class MyDataObject(
  @JsonProperty("n")
  val name: String,
  val age: Int,
)

I've used @JsonProperty to override the property name, which means the request parameter for 'name' will be n .

Now in my @Controller I can use the DTO in a @GetMapping method, and annotate it with the @JsonRequestParam I created.

import my.project.JsonRequestParam
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api")
class MyDataController {

  @GetMapping(value = ["/data"])
  fun getData(
    @JsonRequestParam
    request: MyDataObject
  ): ResponseEntity<String> {
    return "get data $request"
  }
}

So if I GET /api/data?n=MyName&age=22 , then I'll get a response:

get data MyDataObject(name = "MyName", age = 22)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM