前提:在SpringBoot中导入了web场景包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
文章目录
一、简单静态资源访问
1.静态资源目录
默认配置了这四个文件夹的静态资源访问/static
or /public
or/resources
or/META-INF/resources
只要访问: 当前项目根路径/ + 静态资源名就可以拿到。
原理: 静态映射/**
的所有请求,请求进来先去看Controller能不能处理,如果不能再转到静态资源处理, 最后再转到404页面。
2.静态资源前缀配置
默认无前缀
# 这表示只有静态资源的访问路径为/res/**时,才会处理请求
spring:
mvc:
static-path-pattern: /res/**
配置了之后可以使用: 项目根路径 + static-path-pattern + 资源名 = 静态资源文件夹下寻找
# 还可以配置
spring:
web:
resources:
static-locations: classpath:/haha
# 这样就指定了/haha是静态资源文件夹, 取代了前面四个默认值(因为前四个默认值是一个数组,所以自定义写了值的话,就直接顶替掉了那个数组变量,详见下文源码)
3.欢迎页支持
- 静态资源目录下放一个index.html
- 静态资源目录下放一个favicon.ico
- 能处理/index请求的Controller
4.原理
SpringBoot启动默认加载xxxAutoConfiguration类(自动配置类)
SpringMVC功能的自动配置类WebMVCAutoConfiguration,一系列@Conditional判断之后生效
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {}
那么这个自动配置类给容器中配置了什么?
@Configuration(proxyBeanMethods = false)
@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
@EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class, WebProperties.class})
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {}
它里面有一个内部类WebMvcAutoConfigurationAdapter,与配置文件的相关属性进行了绑定。WebMvcProperties(prefix = "spring.mvc")
、ResourceProperties(prefix = "spring.resources")
、WebProperties(@ConfigurationProperties("spring.web"))
同时该内部类只有一个有参构造器
# 有参构造器所有参数的值都会从容器中确定
# ResourceProperties resourceProperties:获取和spring.resources绑定的所有值的对象
# WebProperties webProperties:获取和spring.mvc绑定的所有值的对象
# ListableBeanFactory beanFactory:Spring的beanFactory
# HttpMessageConverters 找到所有的HttpMessageConverters
# ResourceHandlerRegistrationCustomizer 找到资源处理器的自定义器 <-
# DispatcherServletPath
# ServletRegistrationBean 给应用注册Servlet、Filter等
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider, ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider, ObjectProvider<DispatcherServletPath> dispatcherServletPath, ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = (Resources)(resourceProperties.hasBeenCustomized() ? resourceProperties : webProperties.getResources());
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = (WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer)resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
this.mvcProperties.checkConfiguration();
}
在该内部类中找到静态资源配置的处理器代码:
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// spring.resources.add-mappings=false的话,静态资源就无法访问(默认是true)
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
// 默认注册/webjars/**匹配到classpath:/META-INF/resources/webjars/路径
this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
// 再注册spring.mvc.static-path-pattern匹配到spring.web.resources.static-locations路径的静态资源访问,如果用户配置文件没写也有默认值(/**和四个静态资源文件夹,见下一段代码)
this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
registration.addResourceLocations(new Resource[]{resource});
}
});
}
}
// 节选spring.web.resources.static-locations默认文件夹
public static class Resources {
private static final String[]CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
private String[] staticLocations;
public Resources() {
this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
}
}
同时在内部类中还可以看到欢迎页的处理规则:
HandlerMapping: 处理器映射。保存了每一个Handler能处理哪些请求
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
// 只能找到spring.mvc.static-path-pattern为/**(默认)的欢迎页,如果自己配置了访问前缀,就找不到index.html了
if (welcomePage != null && "/**".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage);
this.setRootViewName("forward:index.html");
} else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
// 如果没有找到符合条件的静态资源index.html,就去Controller找/index
logger.info("Adding welcome page template: index");
this.setRootViewName("index");
}
}
二、请求参数处理
1.REST请求映射
- @xxxMapping
- Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
- 以前: /getUser /deleteUser /editUser /saveUser
- 现在: /user 然后以不同的请求方式-> GET DELETE PUT POST
- 核心Filter HiddenHttpMethodFilter
- 用法: 表单method=post, 隐藏域_method=put
<form action="/user" method="get"> <input type="submit" value="REST-get提交"> </form> <form action="/user" method="post"> <input type="submit" value="REST-post提交"> </form> <form action="/user" method="post"> <input type="hidden" name="_method" value="put"> <input type="submit" value="REST-put提交"> </form> <form action="/user" method="post"> <input type="hidden" name="_method" value="delete"> <input type="submit" value="REST-delete提交"> </form>
@RequestMapping(value = "/user", method = RequestMethod.GET) public String getUser(){ return "GET"; } @RequestMapping(value = "/user", method = RequestMethod.POST) public String saveUser(){ return "SAVE"; } @RequestMapping(value = "/user", method = RequestMethod.PUT) public String putUser(){ return "PUT"; } @RequestMapping(value = "/user", method = RequestMethod.DELETE) public String delUser(){ return "DELETE"; }
// 默认是关闭掉隐藏域过滤器的 @Bean @ConditionalOnMissingBean({HiddenHttpMethodFilter.class}) @ConditionalOnProperty( prefix = "spring.mvc.hiddenmethod.filter", name = {"enabled"}, matchIfMissing = false ) public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); }
然后就可以通过Rest方式进入Controller同一个URL的不同method处理了# 在配置文件中显式开启该过滤器 spring: mvc: hiddenmethod: filter: enabled: true # 开启页面表单的REST, 因为表单只能写POST请求, 客户端发送的请求可以直接是PUT, 则无需开启
Rest原理(表单提交要使用REST时):
- 表单提交会带上
<input type="hidden" name="_method" value="put">
- 请求过来被OrderedHiddenHttpMethodFilter拦截
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { HttpServletRequest requestToUse = request; // 首先必须是POST请求 if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) { // methodParam = "_method";然后拿到隐藏域的_method的值 String paramValue = request.getParameter(this.methodParam); if (StringUtils.hasLength(paramValue)) { // 然后把_method内的单词转为大写 String method = paramValue.toUpperCase(Locale.ENGLISH); // ALLOWED_METHODS = [PUT, DELETE, PATCH]; if (ALLOWED_METHODS.contains(method)) { // 进行了一个包装, 把符合要求的三种扩展请求方式, 包装进原请求 requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method); // 见下段代码 } } } // 最后带着包装后的请求, 执行过滤器链, 完成了REST风格的扩展 filterChain.doFilter((ServletRequest)requestToUse, response); }
public HttpMethodRequestWrapper(HttpServletRequest request, String method) { super(request); this.method = method; }
Rest(使用客户端工具):
- 如PostMan可以直接发送PUT, DELETE等请求, 无需Filter
扩展注解
// @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 "SAVE";
}
// @RequestMapping(value = "/user", method = RequestMethod.PUT)
@PutMapping("/user")
public String putUser() {
return "PUT";
}
// @RequestMapping(value = "/user", method = RequestMethod.DELETE)
@DeleteMapping("/user")
public String delUser() {
return "DELETE";
}
2.请求分发
接下来我们研究doDispatch()方法:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// ...省略若干代码...
// 找到当前请求使用哪个Handler处理(xxController.xxMethod())
mappedHandler = this.getHandler(processedRequest);
// ...省略若干代码...
}
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
// HandlerMappings保存了/url -> xxController.xxMethod() 的映射
if (this.handlerMappings != null) {
Iterator var2 = this.handlerMappings.iterator();
while(var2.hasNext()) {
HandlerMapping mapping = (HandlerMapping)var2.next();
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
默认有5个HandlerMapping, 比如我们熟悉的WelcomePageHandlerMapping内保存了/
路径 -> /index
orclasspath:index.html
的映射。
接下来我们的主角是RequestMappingHandlerMapping,它保存了所有@RequestMapping注解和handler的映射规则(在应用启动过程中,Spring扫描了所有Controller,并且将其信息保存到RequestMappingHandlerMapping中)。
比如我请求的是/user
在匹配过程中, 会先根据路径匹配
然后发现有四个可以匹配到的路径, 再慢慢匹配Method, 直到最后拿到唯一确定的Handler返回给doDispatch()的mappedHandler,如果最终还有多个匹配的,就会报错。
总结
- SpringBoot自动配置欢迎页的HandlerMapping,访问
/
能访问到index.html
或者/index
- SpringBoot自动配置了默认的RequestMappingHandlerMapping
- 请求进来,挨个尝试所有HandlerMapping看是否有请求信息
- 如果有就找到这个请求对应的handler
- 如果没有就下一个HandlerMapping
- 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping
3.普通参数与基本注解
@PathVariable 路径变量
@GetMapping("/car/{id}/window/{brand}")
public String getCar(@PathVariable("id") String id,
@PathVariable("brand") String brand,
@PathVariable Map<String, String> pv){
return id + "\n" + brand + "\n" + pv.toString();
}
http://localhost:8089/car/no1/window/BYD
请求得到 =>
no1 BYD {id=no1, brand=BYD}
@RequestHeader 获取请求头
@GetMapping("/test")
public String getCar(@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String, String> rh){
return userAgent + "\n" + rh.toString();
}
http://localhost:8089/test
请求得到 =>
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36 {host=localhost:8089, connection=keep-alive, pragma=no-cache, cache-control=no-cache, sec-ch-ua=" Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96", sec-ch-ua-mobile=?0, sec-ch-ua-platform="Windows", upgrade-insecure-requests=1, user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36, accept=text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/ *;q=0.8,application/signed-exchange;v=b3;q=0.9, sec-fetch-site=none, sec-fetch-mode=navigate, sec-fetch-user=?1, sec-fetch-dest=document, accept-encoding=gzip, deflate, br, accept-language=zh-CN,zh;q=0.9,en;q=0.8, cookie=Webstorm-66b7c9ab=246565bc-c7a7-4785-ae87-22d92088296e; Idea-ca2b6365=a9f831df-c5d1-4aa9-b53c-a9adebb95a7a}
@RequestParam 获取请求参数
@GetMapping("/test1")
public String test1(@RequestParam("name") String name,
@RequestParam("age") String age,
@RequestParam("hobby") List<String> hobby,
@RequestParam Map<String, String> rp){
return name + " " + age + " " + hobby.toString() + " " + rp.toString();
}
http://localhost:8089/test1?name=Aomrsou&age=23&hobby=篮球&hobby=足球&hobby=羽毛球
请求得到 =>
Aomrsou 23 [篮球, 足球, 羽毛球] {name=Aomrsou, age=23, hobby=篮球}
@CookieValue 获取cookie值
<script type="text/javascript">
document.cookie="name=Aomrsou;path=/"
// 时间可以不要,但路径(path)必须要填写,因为JS的默认路径是当前页,如果不填,此cookie只在当前页面生效!~
</script>
<a href="/test2">getCookie</a>
@GetMapping("/test2")
public String test2(@CookieValue("name") Cookie cookie){
return cookie.getName() + " is " + cookie.getValue();
}
点击a链接
请求得到 =>
name is Aomrsou
@RequestBody 获取请求体[POST请求]
- String接收所有参数(会得到一坨键值对):
<form action="/test3" method="post"> <input type="text" name="name" value="Aomrsou"> <input type="text" name="age" value="23"> <input type="submit" value="提交"> </form>
@PostMapping("/test3") public String test3(@RequestBody String content){ return content; } 提交表单得到 => name=Aomrsou&age=23
- 对象接收所有参数(会自动匹配名字到变量上):
@PostMapping("/test3") public String test3(@RequestBody User user){ return user.toString(); } public class User { private String name; private Integer age; } 用PostMan发送Content type为'application/json'的POST请求,然后拿对象去接收 => User{name='Aomrsou', age=23}
@RequestAttribute 获取请求域中的值
@Controller
public class HiController {
@GetMapping("/goto")
public String go(HttpServletRequest request){
request.setAttribute("msg", "成了");
return "forward:/success"; // 转发到success, 所以还是同一次请求
}
@ResponseBody
@GetMapping("/success")
public String to(@RequestAttribute("msg") String msg,
HttpServletRequest request){
return msg + " " + request.getAttribute("msg");
}
}
http://localhost:8089/goto
请求得到 =>
成了 成了
@MatrixVariable 获取矩阵变量中的值
Spring中默认是关掉了矩阵变量功能,我们需要自己实现,给UrlPathHelper类中的RemoveSemicolonContent(移除分号后的内容)属性设置为false
// 写法1:实现WebMvcConfigurer接口,然后重写configurePathMatch方法
@Configuration(proxyBeanMethods = true)
public class MyConfig implements WebMvcConfigurer{
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}
// 写法2: 直接@Bean给Spring注册一个WebMvcConfigurer实例
@Configuration(proxyBeanMethods = true)
public class MyConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}
}
// 矩阵变量的内容必须写在路径{}内
@GetMapping("/test4/{path}")
public Maptest4(@MatrixVariable("name") String name,
@MatrixVariable("hobby") List<String> hobby,
@PathVariable("path") String path){
Map<String, String> map = new HashMap<>();
map.put("name", name);
map.put("path", path);
map.put("hobby", hobby.toString());
return map;
}
http://localhost:8089/test4/whoami;name=Aomrsou;hobby=eat;hobby=sleep;hobby=play
请求得到 =>
{"path":"whoami","name":"Aomrsou","hobby":"[eat, sleep, play]"}
// 如果有多个同名的矩阵变量,还可以指定pathVar来进行区分
@GetMapping("/test5/{bossId}/{empId}")
public Map test5(@MatrixVariable(value = "age", pathVar = "bossId") String bossAge,
@MatrixVariable(value = "age", pathVar = "empId") String empAge){
Map<String, String> map = new HashMap<>();
map.put("bossAge", bossAge);
map.put("empAge", empAge);
return map;
}
http://localhost:8089/test5/1;age=10/2;age=20
请求得到 =>
{"bossAge":"10","empAge":"20"}