PRE-HTTP请求 结构
实例
处理函数 处理函数可以通过多种方式定义,其最主要还是通过注解方式,即@RequestMapping
方式。Spring为不同的HTTP方法提供了简化的注解,分别如下:
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping
另外还可以在WebConfig
中手动配置,但这种方法用得不多,这里就不再赘述。
处理函数接受的参数 处理函数由于本质上还是由SpringMVC来调用,因此SpringMVC是可以通过反射获取到特定处理函数的参数列表的。所以SpringMVC提供了灵活的参数选择,可以填写多种参数,具体如下表:
参数
描述
ServerWebExchange
获取一个ServerWebExchange
对象,这个对象是由Spring定义的一个类,其相当于是整个HTTP过程的容器,可以从中获取request,response或者attributes等。
ServerHttpRequest
ServerHttpResponse
获取一个原始的HTTP request或者response对象
WebSession
获取一个session对象。(如果不增加session值,不会新创建session)
java.security.Principal
获取一个Spring Security的Principal对象,实际上就是当前用户认证信息。(但用的不多,需要时再研究)
org.springframework.http.HttpM ethod
获取当前请求的HTTP方法。(既然定义了,肯定是知道方法的,所以用得也不多)
java.util.Locale
获取当前请求的Locale对象,可以通过配置LocaleResolver/LocaleContextResolver
来决定。
java.util.TimeZone
java.time.ZoneId
获取当前request的时区对象,也是由LocaleContextResolver
来决定。
@PathVariable
获取URL模板变量。详细见下
@MatrixVariable
获取在URL中定义的通过分号间隔的键值对变量。详细见下
@RequestParam
获取Servlet request的请求参数。参数值会转换到指定的类型。详细见下 (@RequestParam是可选的,简单类型如果不加,则相当于加上了该注解。
)
@RequestHeader
获取请求头部。头部值会被转换为指定的类型。详细见下
@CookieValue
获取指定cookie值。cookie值会被转换为指定类型。详细见下
@RequestBody
获取HTTP请求的body部分。body部分会通过HttpMessageConverter
实例来转换为方法中给出的目标参数类型。详细见下
HttpEntity<B>
获取请求的header和body。body部分会通过HttpMessageConverter
实例来转换为方法中给出的目标参数类型。
@RequestPart
获取 multipart/form-data
请求中的一个部分。
java.util.Map
org.springframework.ui.Model
org.springframework.ui.ModelMa p
用在模板系统中的注入的变量。(待确定)
@ModelAttribute
通过应用数据绑定和验证访问模型中的现有属性(如果不存在,则实例化)。(待确定)
Errors
BindingResult
获取验证和绑定对象时的错误。(必须在验证过的方法参数之后立即声明Errors或BindingResult参数。)
SessionStatus
class-level@SessionAttributes
UriComponentsBuilder
用于获取相对于当前请求的主机、端口、方案和上下文路径的URL。
@SessionAttribute
用于获取会话属性-与存储在会话中的作为类级别结果的模型属性相反@SessionAttributes
声明。
@RequestAttribute
获取请求的参数。即request的attribute。
其他类型参数
如果这个参数不匹配以上任意的参数。则如果是简单类型,就默认添加@RequestParam
,否则就默认添加@ModelAttribute
。
详细解析 类型转换 从HTTP协议种得到的数据归根到底都是字符串,而有时候我们的处理函数中的参数不仅仅是字符串。这个时候就需要转换器将字符串转换到对应的类型,比如数字类型等。
可以在Webconfig
中添加转换器,如:
1 2 3 4 5 6 7 8 public class WebConfig implements WebMvcConfigurer { @Override public void configureMessageConverters (List<HttpMessageConverter<?>> converters) { WebMvcConfigurer.super .configureMessageConverters(converters); converters.add(new MappingJackson2HttpMessageConverter ()); converters.add(new StringHttpMessageConverter ()); } }
@RequestParam
这个注解是用来将URL上的query部分转换为处理函数的参数。例如URL=http://baidu.com/addUser?user=123&group=1
中的query就是user=123
和group=1
。
那么可以这样获取参数:
1 2 3 4 5 @GetMapping("/addUser") public void addUser (@RequestParam String user, @RequestParam Integer group) { System.out.println(user); System.out.println(group); };
值得注意的是,这个注解是默认添加的,即时不加,也可以获取query的参数。所以下面的代码又是一样的:
1 2 3 4 5 @GetMapping("/addUser") public void addUser (String user, Integer group) { System.out.println(user); System.out.println(group); };
值得注意的是,该注解还可以接受其他参数为集合类型,比如Map,List。
例如:
Map接收 URL:http://localhost:8080/xpan/file/file/compressFile?d=123&dd=ddd
处理函数:
1 2 3 4 @PostMapping("/compressFile") public void compressFile (@RequestParam Map<String, String> res) { System.out.println(res); }
结果:
说明:
用Map接收的时候,不需要在乎参数的名字,因为其名字会作为Map的键。
List接收 URL:http://localhost:8080/xpan/file/file/compressFile?res=123&res=ddd
处理函数:
1 2 3 4 @PostMapping("/compressFile") public void compressFile (@RequestParam List<String> res) { System.out.println(res); }
结果:
说明:
使用List的时候就需要注意,提交的参数名字必须和接收的参数名字一样,或者使用@RequestParam("xxx")
的形式来获取xxx的值。
数组接收 URL:http://localhost:8080/xpan/file/file/compressFile?res=123&res=ddd
处理函数:
1 2 3 4 @PostMapping("/compressFile") public void compressFile (@RequestParam String[] res) { System.out.println(Arrays.toString(res)); }
结果:
说明:
使用数组和List基本一致,就是名字也需要保持一致,或者在注解中自定义。
源码位置在RequestParamMethodArgumentResolver
。待解析。
@RequestBody
这个注解是用来获取HTTP请求的body部分参数(见前面的HTTP结构图)。
并且可以通过converter将其转换为确定的类型。
这个注解也是需要converter来支持特定的request类型,即header中的content-type
属性。即实际上body部分就是一串字符串,我们需要约定特定的格式来进行交互,而这个格式得有一个名字,这个名字就存储在content-type
中。然后就按照这种格式来编码body得内容。
而我们一般使用的是JSON格式进行交互,所以此时的content-type
就为application/json
。
那么就需要一个JSON的转换器,这个转换器spring官方是没有内置的,需要我们自己添加,比如Jackson,可以在Webconfig
中进行配置:
1 2 3 4 5 6 @Override public void configureMessageConverters (List<HttpMessageConverter<?>> converters) { WebMvcConfigurer.super .configureMessageConverters(converters); converters.add(new MappingJackson2HttpMessageConverter ()); converters.add(new StringHttpMessageConverter ()); }
接收JSON字符串 URL:http://localhost:8080/xpan/file/folder/createFolder
body:
1 2 3 4 5 6 7 8 { "folder_name" : "myfolder" , "depth" : 1 , "pid" : 0 , "size" : 0 , "sub_folder_count" : 0 , "sub_file_count" : 0 }
处理函数:
1 2 3 4 @PostMapping("/createFolder") public void createFolder (@RequestBody Folder folder) { System.out.println(folder); }
结果:
1 Folder(id=null, user_id=null, folder_name=myfolder, depth=1, icon=null, pid=0, size=0, sub_folder_count=0, sub_file_count=0, gmt_statistics=null, gmt_create=null, gmt_update=null)
值得注意的是:这里还有一些Jackson的配置,比如默认是可以忽略参数的,即如果一些参数不带也是可以解析的。同样可以设置为全参数,否则报错的模式。
@PathVariable
这个注解可以用于获取URI中的变量,与requestParam
,这里是可以通过自定义的格式获取URI中的某一部分作为路径变量,然后注入到处理方法的参数中。
其中URI的匹配规则如下:
Pattern
Description
Example
?
匹配一个字符
/pages/t?st.html
匹配 /pages/test.html
和/pages/t3st.html
*
匹配0个或多个字符 (在一个路径段中)
/resources/*.png
匹配/resources/file.png
/projects/*/versions
匹配 /projects/spring/versions
但是不匹配/projects/spring/boot/versions
**
匹配直到路径尾部的一个或多个路径段
/resources/**
匹配 /resources/file.png
和 /resources/images/file.png
/resources/**/file.png
是不合法的,因为**只允许在路径尾部。
{name}
匹配一个路径段并且将其捕捉为一个名为name的变量
/projects/{project}/versions
匹配 /projects/spring/versions
并且捕捉为 project=spring
{name:[a-z]+}
匹配[a-z]+
为一个名为name的变量
/projects/{project:[a-z]+}/versions
匹配/projects/spring/versions
但不匹配/projects/spring1/versions
{*path}
匹配直到路径尾部的一个或多个路径段并作为一个名为path的变量
注:
路径段(path segment):指两个反斜杠内的部分。如https://www.baidu.com/xxx/yyy
其中xxx就是一个路径段。
接下来可以在处理方法中的参数中使用@PathVariable
来获取路径变量。
如:
URL:http://localhost:8080/xpan/file/file/compressFile/12345
处理函数:
1 2 3 4 @PostMapping("/compressFile/{userId}") public void compressFile (@PathVariable("userId") Integer userId) { System.out.println(userId); }
结果:
当然也可以用其他更复杂,比如正则表达式来接收路径变量。
@MatrixVariable
矩阵变量(Matrix Variable)其实很简单,就是在路径段中的键值对。
与路径query的作用一致,只是形式不同。矩阵变量是以;
分号作为URL结尾与矩阵变量开始的分隔符(query中是是?
)和不同的矩阵变量之间的分隔符(query中是是&
)。而同一个name的不同值用,分割。同样的,键值之间仍然是以=
作为连接符的。
例如/cars;color=red,green;year=2012
中的矩阵变量为:color=red,green;year=2012
。
与@RequestParam
一致,也支持List和数组接收。
普通参数 URL:http://localhost:8080/xpan/file/file/compressFile/12345;groupId=111;username=hhhhh
处理函数:
1 2 3 4 5 6 @PostMapping("/compressFile/{userId}") public void compressFile (@PathVariable("userId") Integer userId, @MatrixVariable("groupId") String groupId, @MatrixVariable("username") String username) { System.out.println(userId); System.out.println(groupId); System.out.println(username); }
结果:
List接收 URL:http://localhost:8080/xpan/file/file/compressFile/12345;groupId=111;username=hhhhh;color=red,blue
处理函数:
1 2 3 4 5 6 7 @PostMapping("/compressFile/{userId}") public void compressFile (@PathVariable("userId") Integer userId, @MatrixVariable("groupId") String groupId, @MatrixVariable("username") String username, @MatrixVariable("color") List color) { System.out.println(userId); System.out.println(groupId); System.out.println(username); System.out.println(color); }
结果:
1 2 3 4 12345 111 hhhhh [red, blue]
数组接收 URL:http://localhost:8080/xpan/file/file/compressFile/12345;groupId=111;username=hhhhh;color=red,blue
处理函数:
1 2 3 4 5 6 7 @PostMapping("/compressFile/{userId}") public void compressFile (@PathVariable("userId") Integer userId, @MatrixVariable("groupId") String groupId, @MatrixVariable("username") String username, @MatrixVariable("color") String[] color) { System.out.println(userId); System.out.println(groupId); System.out.println(username); System.out.println(Arrays.toString(color)); }
结果:
1 2 3 4 12345 111 hhhhh [red, blue]
Map接收 使用map接收的时候,@MatrixVariable
中就不需要填写参数了。例如:
URL:http://localhost:8080/xpan/file/file/compressFile/12345;groupId=111;username=hhhhh;color=red,blue
处理函数:
1 2 3 4 5 6 @PostMapping("/compressFile/{userId}") public void compressFile (@PathVariable("userId") Integer userId, @MatrixVariable("groupId") String groupId, @MatrixVariable Map matrixVars) { System.out.println(userId); System.out.println(groupId); System.out.println(matrixVars); }
结果:
1 2 3 12345 111 {groupId=111, username=hhhhh, color=red}
这里有两个点值得注意:
矩阵变量可以单独读取和Map读取同时进行,即一个矩阵变量在多个处理方法的参数中。
当使用Map接收的时候,如果不指定泛型类型,则默认为<String, String>
,所以后面color只解析出一个值。
如果要克服也很简单,就是将泛型类型指定为List,如下:
1 2 3 4 5 6 @PostMapping("/compressFile/{userId}") public void compressFile (@PathVariable("userId") Integer userId, @MatrixVariable("groupId") String groupId, @MatrixVariable Map<String, List> matrixVars) { System.out.println(userId); System.out.println(groupId); System.out.println(matrixVars); }
那么就能正常读取结果:
1 2 3 12345 111 {groupId=[111], username=[hhhhh], color=[red, blue]}
矩阵变量与路径参数的匹配 矩阵变量与路径参数是可以匹配的,即如果矩阵变量名字相同,可以通过前置的路径变量进行定位。例如:
URL:http://localhost:8080/xpan/file/file/compressFile/12345;order=111/123;order=222
处理函数:
1 2 3 4 5 6 7 @PostMapping("/compressFile/{userId}/{groupId}") public void compressFile (@PathVariable("userId") Integer userId, @MatrixVariable(name = "order", pathVar = "userId") Integer userOrder, @PathVariable("groupId") Integer groupId, @MatrixVariable(name = "order", pathVar = "groupId") String groupOrder) {System.out.println(userId); System.out.println(userOrder); System.out.println(groupId); System.out.println(groupOrder); }
结果:
这个注解很简单,就是将header中的属性注入到处理函数的参数中。例如:
处理函数:
1 2 3 4 5 @PostMapping("/compressFile") public void compressFile (@RequestHeader("Host") String host, @RequestHeader("Accept") String accept) { System.out.println(host); System.out.println(accept); }
结果:
@CookieValue
这个注解是用来获取cookie的值并注入到处理函数的参数中,例如:
cookie(附在HTTP header中):
Cookie: userId=123
处理函数:
1 2 3 4 @PostMapping("/compressFile") public void compressFile (@CookieValue("userId") Integer userId) { System.out.println(userId); }
结果:
JSON属性分解 自定义注解 Java中注解的定义也很简单,但是需要注意的是需要加上另外两个注解:
Target
:当前注解被用的位置(参数、方法、类等),参数是java.lang.annotation.ElementType
。
Retention
:注解存留的时间(源代码、类、运行时),参数是java.lang.annotation.RetentionPolicy
。
HandlerMethodArgumentResolver HandlerMethodArgumentResolver接口可以用来解析HTTP请求,并且给处理函数的参数进行赋值。相当于自己实现一个参数解析的过程,本来springMVC中是有这一部分的实现,比如@RequestParam
的RequestParamMethodArgumentResolver
。
这个接口也很简单,只含有两个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package org.springframework.web.method.support;import org.springframework.core.MethodParameter;import org.springframework.lang.Nullable;import org.springframework.web.bind.support.WebDataBinderFactory;import org.springframework.web.context.request.NativeWebRequest;public interface HandlerMethodArgumentResolver { boolean supportsParameter (MethodParameter parameter) ; @Nullable Object resolveArgument (MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; }
supportsParameter
:该参数解析器是否支持当前参数。
resolveArgument
:返回解析后的对象,这个对象会直接赋值给当前参数。
自定义解析JSON 利用上述两个知识和Jackson,就可以实现自定义的JSON解析方式。理论上可以分为2步:
定义一个注解,并且Target
为ElementType.PARAMETER
;Retention
为RetentionPolicy.RUNTIME
,即确保整个注解是加在参数上,保留到运行时。
定义一个HandlerMethodArgumentResolver
,support方法查看这个参数是否有上面定义的注解,resolveArgument方法利用Jackson来解析字符串。
一个简单的例子如下,其功能是解析JSON中包含的user_id
并返回:
注解类:
1 2 3 4 @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface UserId {}
UserIdHandlerMethodArgumentResolver
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Component public class UserIdHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { private final String[] userMethodIdExpList = {"uid" , "id" }; @Override public boolean supportsParameter (MethodParameter parameter) { return parameter.hasParameterAnnotation(UserId.class); } @Override public Object resolveArgument (MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { String token = webRequest.getHeader("Authorization" ); Integer id = JWTUtils.getIdFromToken(token.substring(7 )); if (id == null ){ throw new AuthenticationException ("身份认证失败!" ); } Class<?> parameterType = parameter.getParameterType(); if (parameterType == Integer.class || parameterType == int .class){ return id; }else { String requestJsonString = HttpUtils.getRequestJsonString(webRequest.getNativeRequest(HttpServletRequest.class)); Object o = JSONUtils.JSON2Object(requestJsonString, parameterType); for (String s : userMethodIdExpList) { PropertyDescriptor pd = new PropertyDescriptor (s,parameterType); Method writeMethod = pd.getWriteMethod(); writeMethod.invoke(o, id); return o; } throw new ServerException ("UserId注解obj模式下,参数对象必须含有uid/id字段" ); } } }
注:这里面还加入了身份认证和参数类型验证等。
JSON属性分解注入 在面对前端传入的JSON数据,有时候可能并不是系统内定义的实体类,可能只是分开的几个参数,此时一般采用Map来接收,但是这样并不好做验证。所以我就想实现一个JSON属性分解,然后分别注入到处理函数的参数。所以一个简单的实现如下:
注解类:
1 2 3 4 5 6 @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface SingleRequestBody { String value () default "-1" ; }
UserIdHandlerMethodArgumentResolver
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package priv.mw.xpan.resolver;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;import priv.mw.xpan.annotation.SingleRequestBody;import priv.mw.xpan.utils.HttpUtils;import priv.mw.xpan.utils.JSONUtils;import priv.mw.xpan.utils.XPanRequestWrapper;import java.util.HashMap;@Component public class SingleMethodParamResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter (MethodParameter parameter) { return parameter.hasParameterAnnotation(SingleRequestBody.class); } @Override public Object resolveArgument (MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { String requestJsonString = HttpUtils.getRequestJsonString(webRequest.getNativeRequest(XPanRequestWrapper.class)); HashMap<String, String> jsonObject = JSONUtils.JSON2Object(requestJsonString, HashMap.class); SingleRequestBody singleRequestBody = parameter.getParameterAnnotation(SingleRequestBody.class); String value = singleRequestBody.value(); if (!value.equals("-1" )){ return jsonObject.get(value); }else { String parameterName = parameter.getParameterName(); return jsonObject.get(parameterName); } } }
测试:
1 2 3 4 @PostMapping("/renameFile") public void renameFile (@SingleRequestBody String oldName) { System.out.println(oldName); }
完全没问题。
但是在测试如下代码时:
1 2 3 4 5 6 7 @PostMapping("/renameFile") public void renameFile (@SingleRequestBody String oldName,@SingleRequestBody String newName,@SingleRequestBody Integer pId, @UserId Integer userId) { System.out.println(oldName); System.out.println(newName); System.out.println(pId); System.out.println(userId); }
这样就出现问题了,解析第一个参数时没问题,但是在解析第二个参数时就没办法从request中读取body部分了。
经过搜索才发现request的body是一个流,读取一次过后body指针就到最后的位置了,第二次就没办法读取了。
解决request body只能读取一次 这个问题网上也提供了一种比较好的把那份,即使用自定义的HttpServletRequestWrapper
来包装一次request,这个HttpServletRequestWrapper
需要重新定义一下body的读取方式。这里的思路也很简单,就是定义一个私有的body变量。读取的时候,如果已经有了就直接读取了。
下面给出一个示例(来自网上):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 public class XPanRequestWrapper extends HttpServletRequestWrapper { private final byte [] body; public XPanRequestWrapper (HttpServletRequest request) throws IOException { super (request); body = IOUtils.toByteArray(super .getInputStream()); } @Override public BufferedReader getReader () throws IOException { return new BufferedReader (new InputStreamReader (getInputStream())); } @Override public ServletInputStream getInputStream () throws IOException { return new RequestBodyCachingInputStream (body); } private class RequestBodyCachingInputStream extends ServletInputStream { private byte [] body; private int lastIndexRetrieved = -1 ; private ReadListener listener; public RequestBodyCachingInputStream (byte [] body) { this .body = body; } @Override public int read () throws IOException { if (isFinished()) { return -1 ; } int i = body[lastIndexRetrieved + 1 ]; lastIndexRetrieved++; if (isFinished() && listener != null ) { try { listener.onAllDataRead(); } catch (IOException e) { listener.onError(e); throw e; } } return i; } @Override public boolean isFinished () { return lastIndexRetrieved == body.length - 1 ; } @Override public boolean isReady () { return isFinished(); } @Override public void setReadListener (ReadListener listener) { if (listener == null ) { throw new IllegalArgumentException ("listener cann not be null" ); } if (this .listener != null ) { throw new IllegalArgumentException ("listener has been set" ); } this .listener = listener; if (!isFinished()) { try { listener.onAllDataRead(); } catch (IOException e) { listener.onError(e); } } else { try { listener.onAllDataRead(); } catch (IOException e) { listener.onError(e); } } } @Override public int available () throws IOException { return body.length - lastIndexRetrieved - 1 ; } @Override public void close () throws IOException { lastIndexRetrieved = body.length - 1 ; body = null ; } } }
经过以上的步骤,还需要定义一个Filter,用来每次替换request:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Component public class XPanRequestWrapperFilter extends OncePerRequestFilter { @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (!(request instanceof XPanRequestWrapper)){ request = new XPanRequestWrapper (request); } filterChain.doFilter(request, response); } }
HttpServletRequestWrapper无法处理muilti-part文件 解决了上面的问题,又发现上传文件时就无法读取到文件了,这里有一个曲线救国的方法:
就是检测content-type
是multipart/form-data
的时候,就不进行包装。
所以Filter定义需要重新加一段逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Component public class XPanRequestWrapperFilter extends OncePerRequestFilter { @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String contentType = request.getContentType(); String method = "multipart/form-data" ; if (contentType != null && contentType.contains(method)) { request = new StandardServletMultipartResolver ().resolveMultipart(request); }else { if (!(request instanceof XPanRequestWrapper)){ request = new XPanRequestWrapper (request); } } filterChain.doFilter(request, response); } }
等后期研究一下,我觉得还有更好的解决方案,即完善这个HttpServletRequestWrapper
,让它也支持multi-part
文件。