5、Web开发
1、简单功能分析
1.1、静态资源访问
####1、静态资源目录
只要静态资源放在类路径下:/static(or/public or /resources or /META-INF/resources )
访问:当前项目跟路径/ + 静态资源名称
原理:静态映射/***
- 请求进来,先找Controller看能不能处理,不能处理的所有请求又都交给静态资源处理器。
#添加新的静态资源文件夹
spring:
web:
resources:
static-locations: [classpath:/aaa/]
####2、静态资源访问前缀
默认无前缀
spring:
mvc:
static-path-pattern: /res/**
####3、webjar
自动映射
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>
访问地址:http://127.0.0.1:8080/webjars/jquery/3.5.1/jquery.min.js
1.2、欢迎页支持
- 静态资源路径下 index.html
- 可以配置静态资源路径,但是不可以配置静态资源的访问前缀,否则导致index.html不能被默认访问
- controller处理 /index
1.3、自定义Favicon
favicon.ico放在静态资源目录下即可
1.4、静态资源配置原理
- SpringBoot启动时加载xxxAutoConfiguration
- SpringMvc的WebMvcAutoConfiguration,并且生效
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
- 给容器中配置了什么
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class,
org.springframework.boot.autoconfigure.web.ResourceProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
- 配置文件的相关属性和xxx进行绑定:WebMvcProperties==spring.mvc、ResourceProperties==spring.resources
扩展:
####1、如果配置类只有一个有参构造器
//有参构造器所有参数的值都会从容器中确定
//WebProperties webProperties 获取spring.web配置
//WebMvcProperties mvcProperties 获取spring.mvc配置
//ListableBeanFactory beanFactory 获取spring的beanFactory
//HttpMessageConverters 找到所有的HttpMessageConverters
//ResourceHandlerRegistrationCustomizer 找到所有的资源处理器的自定义器
//DispatcherServletPath
//ServletRegistrationBean
public WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
this.mvcProperties.checkConfiguration();
}
2、资源处理的默认规则
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
super.addResourceHandlers(registry);
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
ServletContext servletContext = getServletContext();
addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (servletContext != null) {
registration.addResourceLocations(new ServletContextResource(servletContext, SERVLET_LOCATION));
}
});
}
spring:
# mvc:
# static-path-pattern: /res/**
web:
resources:
static-locations: [classpath:/aaa/]
add-mappings: false #false时可以禁用掉静态资源
3、欢迎页处理规则
//HandlerMapping 处理器映射,保存了每一个Handler处理哪些请求
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
//要用欢迎页功能必须使用/**
if (welcomePage != null && "/**".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage);
setRootViewName("forward:index.html");
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
##2、请求参数处理
####0、请求映射
#####1、REST使用与原理
- xxxMaping
- Rest风格支持(使用Http请求方式动词来表示对资源的操作)
- 以前:/getUser 获取用户 /deleteUser删除用户 /editUser修改用户 /saveUser保存用户
- 现在:/user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
- 核心Filtter:HiddenHttpMethodFilter
- 用法:表单method=post,隐藏域_method=post
- springboot中需要手动开启
新建测试表单
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dalaoyang</title>
<link rel="icon" href="/static/favicon.ico" type="image/x-icon" />
<script src="/webjars/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
测试REST风格:
<form action="/user" method="get">
<input value="REST-GET 提交" type="submit">
</form>
<form action="/user" method="post">
<input value="REST-PORT 提交" type="submit">
</form>
<form action="/user" method="post">
<input type="hidden" name="_method" value="put"/>
<input value="REST-PUT 提交" type="submit">
</form>
<form action="/user" method="post">
<input type="hidden" name="_method" value="delete"/>
<input value="REST-DELETE 提交" type="submit">
</form>
</body>
</html>
//新建请求
@RestController
public class HelloController {
//@RequestMapping(value = "/user", method = RequestMethod.GET)
@GetMapping("/user")
public String getUser() {
return "GET-张三";
}
//@RequestMapping(value = "/user", method = RequestMethod.POST)
@PostMapping("/user")
public String saveUser() {
return "POST-张三";
}
//@RequestMapping(value = "/user", method = RequestMethod.PUT)
@PutMapping("/user")
public String putUser() {
return "PUT-张三";
}
//@RequestMapping(value = "/user", method = RequestMethod.DELETE)
@DeleteMapping("/user")
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();
}
spring:
# mvc:
# static-path-pattern: /res/**
mvc:
hiddenmethod:
filter:
enabled: true
RESR原理(表单提交要使用REST风格的时候)
- 表单提交要带上 _method 参数
- 请求过来被OrderedHiddenHttpMethodFilter拦截
- 请求是否正常,并且是POST
- 获取 _method 的值
- 兼容以下请求:PUT,DELETE,PATCH
- 原生Requert(post),包装模式 requestWrapper重写了getMethod方法,返回的是传入的值
- 过滤器链放行的时候用wrapper,以后的方法调用getMethod是调用requestWrapper的
- 请求是否正常,并且是POST
Rest使用客户端工具发送
- 如postman直接发送put、delete 等方式,无需Filter
#####2、请求映射原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1du0GGai-1614085517841)(images/请求映射原理.png)]
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 处理器映射
RequestMappingHandlerMapping:保存了所有的@RequestMapping 和 handler的映射规则
所有的请求映射都在HandlerMapping中
- SpringBoot自动配置欢迎页的HandlerMapping,访问/能访问到index.html
- 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息
- 如果有就找到这个请求对应的handler
- 如果没有就是下一个HandlerMapping
- 如果我们需要一些自定义的映射处理,我们也可以自己给容器中方HandlerMapping
1、普通参数与基本注解
-
注解:
@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody
新建测试html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dalaoyang</title>
<link rel="icon" href="/static/favicon.ico" type="image/x-icon" />
<script src="/webjars/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
测试REST风格:
<form action="/user" method="get">
<input value="REST-GET 提交" type="submit">
</form>
<form action="/user" method="post">
<input value="REST-PORT 提交" type="submit">
</form>
<form action="/user" method="post">
<input type="hidden" name="_method" value="put"/>
<input value="REST-PUT 提交" type="submit">
</form>
<form action="/user" method="post">
<input type="hidden" name="_method" value="delete"/>
<input value="REST-DELETE 提交" type="submit">
</form>
测试基本注解:
<ul>
<a href="/car/3/owner/lisi?age=18&inters=basketball&inters=game">/car/{id}/owner/{username}</a>
<li>@PathVariable(路径变量)</li>
<li>@RequestHeader(获取请求头)</li>
<li>@RequestParam(获取请求参数)</li>
<li>@CookieValue(获取cookie值)</li>
<li>@RequestBody(获取请求体[POST])</li>
<li>@RequestAttrbuilt(获取request域属性)</li>
<li>@MatrixVariable(矩阵变量)</li>
</ul>
<br/>
<form action="/save" method="post">
测试@RequestBody获取数据<br/>
用户名:<input name="userName"><br/>
邮箱:<input name="email"/><br/>
<input type="submit" value="提交">
</form>
/cars/{path}?xxx=xxx&aaa=ccc queryString 查询字符串。@RequestParam:<br/>
/cars/{path;low=34;brand=byd,audi,yd} ; 矩阵变量<br/>
页面开发,cookie禁用了,session里面的内容怎么使用:
session.set(a, b)--->jsessionid--->cookie--->每次发送请求携带
url重写:/abc;jsessionid=xxxx 把cookie的值使用矩阵变量的方式进行传递
<a href="/cars/sell;low=34;brand=byd,audi,yd">/cars/sell;low=34;brand=byd,audi,yd</a><br/>
<a href="/cars/sell;low=34;brand=byd;brand=audi;brand=yd">/cars/sell;low=34;brand=byd;brand=audi;brand=yd</a><br/>
<a href="/boss/1;age=20/2;age=10">/boss/{bossId}/{empId}</a>
<ul>
<li>矩阵变量需要在SpringBoot中手动开启</li>
<li>根据RFC3986的规范,矩阵变量应绑定在路径变量中!</li>
<li>若是有多个矩阵变量,应当使用英文符号;进行分隔</li>
<li>若是一个矩阵变量有多个值,应当使用英文逗号,进行分隔,或者命名多个重复的Key即可</li>
<li>如:/cars/sell;low=34;brand=byd,audi,yd</li>
</ul>
</body>
</html>
//新建测试类1
package com.sflyy.controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.Cookie;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* TODO
*
* @author asus
* @version 1.0
* @date 2021/1/31 21:39
*/
@RestController
public class ParameterTestController {
@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> param,
@CookieValue("cmcc_order_login") Cookie cookie) {
Map<String, Object> map = new HashMap<>(1);
// map.put("pv", pv);
// map.put("id", id);
// map.put("name", name);
// map.put("userAgent", userAgent);
// map.put("header", header);
map.put("age", age);
map.put("inters", inters);
map.put("param", param);
map.put("cookie", cookie);
return map;
}
@PostMapping("/save")
public Map postMethod(@RequestBody String content) {
Map<String, Object> map = new HashMap<>(1);
map.put("content", content);
return map;
}
}
//新建测试类2
package com.sflyy.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* TODO
*
* @author asus
* @version 1.0
* @date 2021/1/31 23:05
*/
@Controller
public class RequestController {
@GetMapping("/goto")
public String goToPage(HttpServletRequest request) {
request.setAttribute("msg", "成功了...");
request.setAttribute("code", "200");
return "forward:success";
}
@ResponseBody
@GetMapping("/success")
public Map success(@RequestAttribute("msg") String msg,
@RequestAttribute("code") String code,
HttpServletRequest request) {
Object msg1 = request.getAttribute("msg");
Map<String, Object> map = new HashMap<>(1);
map.put("reqMethod_msg", msg1);
map.put("annotaion_msg", msg);
return map;
}
//1、语法:cars/sell;low=34;brand=byd,audi,yd
//2、SpringBoot默认是禁用掉矩阵变量的需要手动开启(新建WebConfig配置类)
@ResponseBody
@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
@ResponseBody
@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;
}
}
//新建webMvcConfigurer配置类
package com.sflyy.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.util.UrlPathHelper;
/**
* TODO
*
* @author asus
* @version 1.0
* @date 2021/2/1 21:20
*/
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {//矩阵变量
UrlPathHelper urlPathHelper = new UrlPathHelper();
//不移除;后面的内容,矩阵变量功能可以实现
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
// @Bean
// public WebMvcConfigurer webMvcConfigurer() {
// return new WebMvcConfigurer() {
// @Override
// public void configurePathMatch(PathMatchConfigurer configurer) {
// UrlPathHelper urlPathHelper = new UrlPathHelper();
// urlPathHelper.setRemoveSemicolonContent(false);
// configurer.setUrlPathHelper(urlPathHelper);
// }
// };
// }
}
-
Servlet API:
WebRequest、ServletRequest、MultipartRequest、HttpSession、PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
-
复杂参数
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.setAttribute();获取
Map、Model 类型的参数会返回 mavContainer.getModel() --> BindingAwareModelMap 是 Model也是Map
- 对象
2、参数处理原理
- HandlerMapping中找到能处理请求的Handler(Controller.method)
- 为当前Handler找一个适配器:RequestMappingHandlerMapping
1、HandlerAdapter
0-支持方法上标注RequestMapping
1-支持函数式编程的
2、执行目标方法
// Actually invoke the handler.
//DispatcherServlet-->doDispatch
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
//RequestMappingHandlerAdapter-->handleInternal
mav = invokeHandlerMethod(request, response, handlerMethod);//执行目标方法
//ServletInvocableHandlerMethod-->invokeAndHandle
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//获取方法的参数值
//InvocableHandlerMethod-->invokeForRequest
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
3、参数解析器
确定将要执行的目标方法的每一个参数的值是什么
SpringMVC目标方法能写多少种参数类型取决于参数解析器
- 当前解析器是否支持解析这种参数
- 支持就调用resolveArgument
4、返回值处理器
5、如何确定目标方法每一个参数的值
//InvocableHandlerMethod-->getMethodArgumentValues
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;
}
5.1挨个判断所有参数哪个支持解析这个参数
@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;
}
5.2、解析这个参数的值
调用 HandlerMethodArgumentResolver 的 resolveArgument
5.3、自定义类型参数 封装POJO
ServletModelAttributeMethodProcessor这个参数处理器支持
是否为简单类型
//org.springframework.beans.BeanUtils
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));
}
//org.springframework.web.method.annotation.ModelAttributeMethodProcessor
@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();
}
else {
attribute = ex.getTarget();
}
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 将请求参数转出指定的数据类型,再次封装到JavaBean里面
5.4、自定义参数格式化解析器
测试封装POJO:
<form action="/saveUser" method="post">
姓名:<input name="userName" value="zhangsan"/><br/>
年龄:<input name="age" value="18"/><br/>
生日:<input name="birth" value="2019/12/10"/><br/>
<!--宠物姓名:<input name="pet.name" value="阿猫"/><br/>
宠物年龄:<input name="pet.age" value="5"/><br/>-->
宠物:<input name="pet" value="阿猫,3"/><br/>
<input type="submit" value="保存">
</form>
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {
@Override
public Pet convert(String source) {
//阿猫,3
if (!ObjectUtils.isEmpty(source)) {
Pet pet = new Pet();
String[] split = source.split(",");
pet.setName(split[0]);
pet.setAge(Integer.valueOf(split[1]));
return pet;
}
return null;
}
});
}
};
}
6、目标方法执行完成
将所有的数据都放在ModelAndViewContainer;包含要去的页面地址View,还包含Model数据
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);
}
});
}
3、数据响应与内容协商
1、响应json
1.1、jackson.jar+@ResponseBody
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
web场景自动引入了json场景
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.4.2</version>
<scope>compile</scope>
</dependency>
//测试浏览器访问 http://localhost:8080//test/person
//返回:{"userName":"张三","age":28,"birth":"2021-02-18T14:27:44.999+00:00","pet":null}
@ResponseBody
@GetMapping("/test/person")
public Person getPerson() {
Person person = new Person();
person.setAge(28);
person.setBirth(new Date());
person.setUserName("张三");
return person;
}
1、返回值解析器
try {
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
//使用消息转换器进行写出操作
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
1、返回值处理器判断是否支持这种类型的返回值supportsReturnType
2、返回值处理器调用handleReturnValue进行处理
3、RequestResponseBodyMethodProcessor可以处理返回值标注了@ResponseBody注解的。
- 1、利用MessageConverters进行处理,将数据写为json
- 1、内容协商(浏览器默认会以请求头的方式告诉服务器它能接收什么样的内容类型)
- 2、服务器最终根据自身能力,决定服务器能生产出什么样内容类型的数据。
- 3、SpringMVC会挨个遍历所有容器底层的HttpMessageConverter,看谁能处理?
- 1、得到MappingJackson2HttpMessageConverter可以将对象写为JSON
- 2、利用MappingJackson2HttpMessageConverter将对象转为JSON再写出去
1.2、SpringMVC到底支持哪些返回值
ModelAndView
Model
View
ResponseEntity
ResponseBodyEmitter
StreamingResponseBody
ResponseEntity
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有@ModelAttribute注解
@ResponseBody注解 ---> RequestResponseBodyMethodProcessor
1.3、HttpMessageConverter原理
#####1、MessageConverter规范
HttpMessageConverter:看是否支持将此Class类型的对象,转为 MediaType 类型的数据
例子:Person转为json,或者JSON转为Person
#####2、默认的MessageConverter
0-只支持byte类型
1-String
2-String
3-Resource
4-ResourceRegion
5-DOMSource.class、SAXSource.class、StAXSource.class、StreamSource.class、Source.class
6-MultiValueMap
7-true
8-true
9-支持注解方式xml处理的
最终MappingJackson2HttpMessageConverter把对象转为JSON(利用底层的jackson的objectMapper转换)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ko1mWxcN-1614085517899)(images/outputMessage.png)]
3、内容协商
根据客户端接收能力不同,返回不同媒体类型的数据。
1、引入xml依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
####2、postman分别测试返回json和xml
只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。
####3、开启浏览器参数方式内容协商功能
为了方便内容协商,开启基于请求参数的内容协商功能。
spring:
mvc:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
发请求: http://localhost:8080/test/person?format=json
http://localhost:8080/test/person?format=xml
确定客户端接收什么样的内容类型;
1、Parameter策略优先确定是要返回json数据(获取请求头中的format的值)
2、最终进行内容协商返回给客户端json即可。
4、内容协商原理
-
1、判断当前响应头中是否已经有确定的媒体类型。MediaType
-
2、获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段)【application/xml】
-
- contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略
-
- **HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型 **
-
3、遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)
-
4、找到支持操作Person的converter,把converter支持的媒体类型统计出来。
-
5、客户端需要【application/xml】。服务端能力【10种、json、xml】
-
-
6、进行内容协商的最佳匹配媒体类型
-
7、用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。
导入了jackson处理xml的包,xml的converter就会自动进来
WebMvcConfigurationSupport
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
5、自定义 MessageConverter
实现多协议数据兼容。json、xml、x-guigu
0、@ResponseBody 响应数据出去 调用 **RequestResponseBodyMethodProcessor **处理
1、Processor 处理方法返回值。通过 **MessageConverter **处理
2、所有 **MessageConverter **合起来可以支持各种媒体类型数据的操作(读、写)
3、内容协商找到最终的 messageConverter;
SpringMVC的什么功能。一个入口给容器中添加一个 WebMvcConfigurer
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
}
}
有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。