自定义 Spring Web Controller 方法的参数

在 Spring Web Controller 方法中的参数可用 org.springframework.web.bind.annotation 下的各种注解来说明参数值从哪儿获得,比如我们熟知的 @PathVariable, @RequestParam, @RequestHeader, @RequestBody, 还有较少使用的 @ReqeustAttribute, @SessionAttribute, @RequestPart, @MatrixVariable, @ModelAttribute, @AuthenticationPrincipal, @CurrentSecurityContext 等。其实在它们背后工作的是相应的 HandlerMethodArgumentResolver 的子孙们,当然还有 HttpMessageConverter 的各个实现类还默默的对输入数据进入类型转换。

为进一步深入了解 Spring Web 如何获得用户输入,我们先尝试一下不常用的注解,然后实现一个自己的注解参数 @ProductId, 它来从 queryString 或 requestHeader 中获得 productId。写作本文的起因是在上一篇 理解 Spring Boot Security + JWT Token 的简单应用 里, JwtTokenFilter 住 SecurityContextFilter 放一个 Authentication 实例, 在 Controller 方法中便能用 @AuthenticationPrincipal 自动注入 authentication.getPrincipal() 的值。

JwtTokenFilter: SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("foo", "bar"))

Controller 方法: public String hello(@AuthenticationPrincipal String user)    ---- 这样就能获得 JwtTokenFilter 设置的 principal "foo"

@RequestBody 处理输入

我们知道在 Controller 方法中用 hello(@RequestBody String body) 可收到请求的整个 body 字符串,这儿我们尝试用 @RequestBody 直接获取一个 JavaBean

为节约代码使用了 JDK 16 的 record 类型,现在我们来请求一下 POST http://localhost:8080/hello

$ curl -X POST -H "Content-type: application/json" http://localhost:8080/hello -d '{"username": "yanbin", "password": "123"}'
User[username=yanbin, password=123]

目前 @RequestBody 好像只支持 application/json Content-type, 尝试用其他 Content-Type 得到 415: Unsupported Media Type 错误。如果我们注册自己的 HttpMessageConverter,就能实现解析类似

  1. Content-type: application/xml  -> <user><username>yanbin</username><password>123</password></user>
  2. Content-type: application/x-www-form-urlencoded -> username=yanbin&password=123
  3. 等等

不过那些应该交给 @ModelAttribute 处理

@ModelAttribute 注解参数

把上面的 @RequestBody 改为 @ModelAttribute

测试

$ curl -X POST -H "Content-type: application/x-www-form-urlencoded" http://localhost:8080/hello -d 'username=yanbin&password=123'
User[username=yanbin, password=123]

试图用 Content-type: application/json 也不能获得 username 和 password 的值,不报错,但输出的是 User[username=null,password=null]

显然 @ModelAttribute 也支持 GET 请求的 queryString, 把上面的 @PostMapping("/hello") 改成 @GetMapping("/hello"), 再试

$ curl http://localhost:8080/hello\?username\=yanbin\&password\=123
User[username=yanbin, password=123]

注:\ 为 shell 下的转义,实际请求为 http://localhost:8080/hello?username=yanbin&password=123

@MatricVariable 处理 Map 输入

@MatricVariable 是用于从 a=1;b=2;c=3 这种分号分隔的字符串中提取值的,作以下几个测试

为避免 shell 下对 &, =, ; 等字符自动转译而影响阅读,只显示实际请求的 URL 及响应

http://localhost:8080/hello/;username=yanbin;password=123
{username=yanbin, password=123}
http://localhost:8080/hello/username=yanbin;password=123                                       -- 没有前导分号将丢失第一个值
{password=123}

不想要前导分号,还要收集到第一个值,这样定义 API

测试

http://localhost:8080/hello/100;username=yanbin;password=123
100;{username=yanbin, password=123}
http://localhost:8080/hello/100;username=yanbin;password=123
username=yanbin;{password=123}

 

测试

http://localhost:8080/hello/100;first=scott;last=tiger
userId: 100, first: scott, last: tiger

同样需要注意 @MatrixVariable 的值是从第一个分号后开始算起,其实是第一个分号前的整个值赋给了 userId, 所

http://localhost:8080/hello/x=y;first=scott;last=tiger
userId: x=y, first:scott, last:tiger

@MatrixVariable 只能从 GET 请求的路径上拆解值,不能从 Form 或 Post Body 中拆解 a=1;b=2 中的值。

另外,在使用 @MatrixVariable 时如果看到错误

org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";"

那是因为启用了  Spring Security, 它禁上在 URL 中带分号,转义的分号也不行,我们可以通过声明一个 Bean 来重新允许 URL 中带分号

自定义的 @ProductId 参数

预习完基本的 Spring Controller 参数注解后,我们来实现一个自己的 @ProductId 参数注解,它所要达成的功能是,在 Controller 方法中加上

productId 会自动从 queryString 或 header 中获得值,相当于每次调用如下方法

需要自己获得 productId 的  controller 方法很多,所以每次调用  getProductId(request) 就不现实了。这时候我们要实现自己的 HandlerMethodArgumentResolver,我们不妨看下 Spring Web 已有的实现类有哪些

要实现 @ProductId 在 Controller 方法中自动注入值,很简单,实现一个  ProductIdMethodArgumentResolver,同时通过 WebMvcConfigurer 注册 Spring MVC 中去

早先的 Spring 或 Java 版本我们需通过继承 WebMvcConfigurerAdapter 的类来注册 MethodArgumentResolver, 由于 Java 8 支持接口的 default 方法,所以这个 *Adapter 就显得多余,不再被推荐使用。

现在测试

$ curl http://localhost:8080/hello?productId=newbie
productId: newbie
$ curl -H "x-product-id:xyz" http://localhost:8080/hello
productId: xyz
$ curl -H "x-product-id:xyz" http://localhost:8080/hello?productId=newbie
productId: newbie

首先从 queryString 中通过 productId 获得,没有话从 header 中取得, key 是 x-product-id

在 HandlerMethodArgumentResolver 中的 resolveArgument 方法中,我们有 MethodParameter, ModelAndViewContainer, NativeWebRequest, 和 WebDataBinderFactory 参数,所以能够实现更复杂的数据绑定,或者使用现有或自定义的 HttpMessageConverter 把请求数据转换成需要的数据对象。

类别: PlayFramework. 阅读(32). 订阅评论. TrackBack.
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x