3.请求参数处理
3.1请求映射
3.1.1rest使用与原理
- @xxxMapping;
- Rest风格支持(使用HTTP请求方式动词来表示对资源的操作) 就是说/实体类名。通过不同的请求方式(动词)来进行操作
- 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
- 现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
- 核心Filter;HiddenHttpMethodFilter
- 用法: 表单method=post,隐藏域 _method=put
- SpringBoot中手动开启
- 扩展:如何把_method 这个名字换成我们自己喜欢的。
- 其实在前端页面上DELETE请求和PUT请求会直接默认走get请求
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser(){
return "GET-张三";
}
@RequestMapping(value = "/user",method = RequestMethod.POST)
public String saveUser(){
return "POST-张三";
}
@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String putUser(){
return "PUT-张三";
}
@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser(){
return "DELETE-张三";
}
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
//自定义filter
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}
源码里是这么说的,你想提交DELETE或者PUT,只有表单是POST方式时,才把表单里真正提交的请求参数提交
<form action="/user"method="post">
<input name="_method"type="hidden"value="DELETE"/>
<input value="REST-DELETE"type="submit"/>
</form>
<form action="/user"method="post">
<input name="_method"type="hidden"value="PUT/>
<input value="REsT-PUT提交"type="submit"/>
</form>
Rest原理(表单提交要使用REST的时候)
- 表单提交会带上_method=PUT
- 请求过来被HiddenHttpMethodFilter拦截
- 请求是否正常,并且是POST
- 获取到_method的值。
- 兼容以下请求;PUT.DELETE.PATCH
- 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。
- 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。
Rest使用客户端工具,
- 如PostMan直接发送Put、delete等方式请求,无需Filter。
选择性开启
spring:
mvc:
hiddenmethod:
filter:
enabled: true #开启页面表单的Rest功能
3.1.2请求映射原理
SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet-->doDispatch()
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 找到当前请求使用哪个Handler(Controller的方法)处理
mappedHandler = getHandler(processedRequest);
//HandlerMapping:处理器映射。/xxx->>xxxx
RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则。
所有的请求映射都在HandlerMapping中。
- SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;
- SpringBoot自动配置了默认 的 RequestMappingHandlerMapping
- 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
- 如果有就找到这个请求对应的handler
- 如果没有就是下一个 HandlerMapping
- 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping。自定义 HandlerMapping
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
3.2普通参数与基本注解
3.2.1注解
@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody
举例
@RequestMapping("/Cyb_Dog.jpg")
public String hello(@RequestParam("username") String name){
return "aaa";
}
把请求参数为username映射给name
@RestController
public class ParameterTestController {
// car/2/owner/zhangsan
@GetMapping("/car/{id}/owner/{username}")
public Map<String,Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String name,
@PathVariable Map<String,String> pv,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String,String> header,
@RequestParam("age") Integer age,
@RequestParam("inters") List<String> inters,
@RequestParam Map<String,String> params,
@CookieValue("_ga") String _ga,
@CookieValue("_ga") Cookie cookie){
Map<String,Object> map = new HashMap<>();
// map.put("id",id);
// map.put("name",name);
// map.put("pv",pv);
// map.put("userAgent",userAgent);
// map.put("headers",header);
map.put("age",age);
map.put("inters",inters);
map.put("params",params);
map.put("_ga",_ga);
System.out.println(cookie.getName()+"===>"+cookie.getValue());
return map;
}
@PostMapping("/save")
public Map postMethod(@RequestBody String content){
Map<String,Object> map = new HashMap<>();
map.put("content",content);
return map;
}
//1、语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd
//2、SpringBoot默认是禁用了矩阵变量的功能
// 手动开启:原理。对于路径的处理。UrlPathHelper进行解析。
// removeSemicolonContent(移除分号内容)支持矩阵变量的
//3、矩阵变量必须有url路径变量才能被解析
@GetMapping("/cars/{path}")
public Map carsSell(@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brand,
@PathVariable("path") String path){
Map<String,Object> map = new HashMap<>();
map.put("low",low);
map.put("brand",brand);
map.put("path",path);
return map;
}
// /boss/1;age=20/2;age=10
@GetMapping("/boss/{bossId}/{empId}")
public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
@MatrixVariable(value = "age",pathVar = "empId") Integer empAge){
Map<String,Object> map = new HashMap<>();
map.put("bossAge",bossAge);
map.put("empAge",empAge);
return map;
}
}
矩阵变量
有这样一个面试题,页面开发,如果把cookie 禁用掉,session 里面的内容怎么找到
- cookie 和 session 的机制就是,session里面保存一个k-v的值,然后每个人都有一个jsessionid,这个jsessionid 会被保存在cookie里面,每次用户发送请求cookie 都会携带jsessionid ,如果把cookie 禁用掉,我们要获取session 的k获取v ,肯定要通过jsessionid来拿到这个session对象,但是jsession有保存在cookie 所以无法从session对象的v
- 这里我们可以使用 矩阵变量来获取 session 如 /user;jessinotallow=xxx 用过url 重写,把cookie的值使用矩阵变量的方式进行传递
前端请求
<a href="/cars/sell;low=34;brand=byd,audi,yd">矩阵变量</a>
后端controller
@GetMapping("/cars/{path}")
public Map carsSell(@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brand,
@PathVariable("path") String path){
Map<String,Object> map = new HashMap<>();
map.put("low",low);
map.put("brand",brand);
map.put("path",path);
return map;
}
这样请求是报错的,因为SpringBoot 默认是禁用了矩阵变量的功能,我们需要手动开启,这就需要我们定制化springboot的组件
下面为其源码
WebMvcAutoConfiguration 类的 configurePathMatch方法内部有这样一个处理
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
if (this.mvcProperties.getPathmatch()
.getMatchingStrategy() == WebMvcProperties.MatchingStrategy.PATH_PATTERN_PARSER) {
configurer.setPatternParser(new PathPatternParser());
}
configurer.setUseSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseSuffixPattern());
configurer.setUseRegisteredSuffixPatternMatch(
this.mvcProperties.getPathmatch().isUseRegisteredSuffixPattern());
this.dispatcherServletPath.ifAvailable((dispatcherPath) -> {
String servletUrlMapping = dispatcherPath.getServletUrlMapping();
if (servletUrlMapping.equals("/") && singleDispatcherServlet()) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setAlwaysUseFullPath(true);
configurer.setUrlPathHelper(urlPathHelper);
}
});
}
- UrlPathHelper urlPathHelper = new UrlPathHelper(); 创建了一个UrlPathHelper 进行解析 其中在 UrlPathHelper类中有这样一个属性 removeSemicolonContent 默认为true 这个属性的意思就是 要不要移除分号内容
- 所以我们要自定义有以下2种写法 1是实现 WebMvcConfigurer 的 configurePathMatch 2是 @Bean 的方式给容器放入一个WebMvcConfigurer
新增配置类并修改控制层
@Configuration(proxyBeanMethods = false) //每次从容器中获取bean 是否为同一个 如果为false 则每次获取的实例都是不同的
public class MyConfiguration implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
//不移除举证变量后面的内容
urlPathHelper.setRemoveSemicolonContent(false);
urlPathHelper.setAlwaysUseFullPath(true);
configurer.setUrlPathHelper(urlPathHelper);
}
}
@GetMapping(value = "/cars/{path}")
public String cars(@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brands,
@PathVariable("path") String path) {
return "low = " + low + "--- brands=" + brands + "----path=" + path;
}
3.2.2Servlet API
WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
ServletRequestMethodArgumentResolver 以上的部分参数
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
Principal.class.isAssignableFrom(paramType) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}
3.2.3复杂参数
Map、Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、Errors/BindingResult、RedirectAttributes( 重定向携带数据)、ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder
Map<String,Object> map, Model model, HttpServletRequest request 都是可以给request域中放数据,
request.getAttribute();
Map、Model类型的参数,会返回 mavContainer.getModel();---> BindingAwareModelMap 是Model 也是Map
mavContainer.getModel(); 获取到值的
3.2.4自定义对象参数
可以自动类型转换与格式化,可以级联封装。
/**
* 姓名: <input name="userName"/> <br/>
* 年龄: <input name="age"/> <br/>
* 生日: <input name="birth"/> <br/>
* 宠物姓名:<input name="pet.name"/><br/>
* 宠物年龄:<input name="pet.age"/>
*/
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}
@Data
public class Pet {
private String name;
private String age;
}
result
3.3POJO封装过程
- ServletModelAttributeMethodProcessor
3.4参数处理原理
- HandlerMapping中找到能处理请求的Handler(Controller.method())
- 为当前Handler 找一个适配器 HandlerAdapter; RequestMappingHandlerAdapter
- 适配器执行目标方法并确定方法参数的每一个值
3.4.1HandlerAdapter
0 - 支持方法上标注@RequestMapping
1 - 支持函数式编程的
xxxxxx其他的adapter
3.4.2执行目标方法
// Actually invoke the handler.
//DispatcherServlet -- doDispatch
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
mav = invokeHandlerMethod(request, response, handlerMethod); //执行目标方法
//ServletInvocableHandlerMethod
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//获取方法的参数值
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
3.4.3参数解析器-HandlerMethodArgumentResolver
确定将要执行的目标方法的每一个参数的值是什么;
SpringMVC目标方法能写多少种参数类型。取决于参数解析器。
- 当前解析器是否支持解析这种参数
- 支持就调用 resolveArgument
3.4.4返回值处理器
3.4.5如何确定目标方法每一个参数的值
============InvocableHandlerMethod==========================
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}
挨个判断所有参数解析器那个支持解析这个参数
可以看到里面是一个增强for循环,进行每一个参数解析器的匹配
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
解析这个参数的值
调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可
自定义类型参数 封装POJO
ServletModelAttributeMethodProcessor 这个参数处理器支持是否为简单类型。
public static boolean isSimpleValueType(Class<?> type) {
return (Void.class != type && void.class != type &&
(ClassUtils.isPrimitiveOrWrapper(type) ||
Enum.class.isAssignableFrom(type) ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type) ||
Temporal.class.isAssignableFrom(type) ||
URI.class == type ||
URL.class == type ||
Locale.class == type ||
Class.class == type));
}
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
}
else {
// Create attribute instance
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
if (isBindExceptionRequired(parameter)) {
// No BindingResult parameter -> fail with BindException
throw ex;
}
// Otherwise, expose null/empty value and associated BindingResult
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
}
bindingResult = ex.getBindingResult();
}
}
if (bindingResult == null) {
// Bean property binding and validation;
// skipped in case of binding failure on construction.
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
// Add resolved attribute and BindingResult at the end of the model
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}
WebDataBinder binder = binderFactory.createBinder(webRequest,attribute,name);
WebDataBinder:web数据绑定器,将请求参数的值绑定到指定的JavaBean里面
WebDataBinder利用它里面的Converters将请求数据转成指定的数据类型。再次封装到JavaBeant中
GenericConversionService:在设置每一个值的时候,找它里面的所有converteri那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean-Integer) byte-->file
3.4.6目标方法执行完成
将所有的数据都放在 ModelAndViewContainer;包含要去的页面地址View。还包含Model数据。
3.4.7处理派发结果
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
InternalResourceView:
@Override
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
// Expose helpers as request attributes, if any.
exposeHelpers(request);
// Determine the path for the request dispatcher.
String dispatcherPath = prepareForRendering(request, response);
// Obtain a RequestDispatcher for the target resource (typically a JSP).
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
"]: Check that the corresponding file exists within your web application archive!");
}
// If already included or response already committed, perform include, else forward.
if (useInclude(request, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including [" + getUrl() + "]");
}
rd.include(request, response);
}
else {
// Note: The forwarded resource is supposed to determine the content type itself.
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to [" + getUrl() + "]");
}
rd.forward(request, response);
}
}
暴露模型作为请求域属性
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
protected void exposeModelAsRequestAttributes(Map<String, Object> model,
HttpServletRequest request) throws Exception {
//model中的所有数据遍历挨个放在请求域中
model.forEach((name, value) -> {
if (value != null) {
request.setAttribute(name, value);
}
else {
request.removeAttribute(name);
}
});
}