1、SpringMVC自动配置概览
https://docs.spring.io/spring-boot/docs/current/reference/html/web.html#web.servlet.spring-mvc.auto-configuration
2、简单功能分析
2.1、静态资源访问
2.1.1、静态资源目录
静态资源放在类路径下: /static
or /public
or /resources
or /META-INF/resources
访问:当前项目根路径/ + 静态资源名
原理: 静态映射/**。
请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面。
2.1.2、静态资源访问前缀
默认无前缀
spring:
mvc:
static-path-pattern: /res/**
访问:当前项目根路径/ + res + /静态资源名
2.1.3、静态资源目录
默认是在2.1.1节中的四个目录中,如果我们想改变默认目录:
spring:
web:
resources:
static-locations: [classpath:/haha/]
2.1.4、webjar
https://www.webjars.org/
自动映射 /webjars/**
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.6.0</version>
</dependency>
访问地址:http://localhost:8080/webjars/jquery/3.6.0/jquery.js 后面地址要按照依赖里面的包路径
2.2、欢迎页支持
- 静态资源路径下创建
index.html
- 可以配置静态资源路径
- 但是不可以配置静态资源的访问前缀。否则导致
index.html
不能被默认访问
- controller能处理
/index
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致welcome page功能失效
web:
resources:
static-locations: [classpath:/haha/]
2.3、自定义 Favicon
favicon.ico
放在静态资源目录下即可。名字必须是 favicon.ico
。
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致Favicon功能失效
web:
resources:
static-locations: [classpath:/haha/]
2.4、静态资源配置原理
-
SpringBoot 启动默认加载 xxxAutoConfiguration 类(自动配置类)
-
SpringMVC 功能的自动配置类
WebMvcAutoConfiguration
,生效@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, WebProperties.class}) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware { ......
有
@EnableConfigurationProperties
,说明配置文件的相关属性和xxx进行了绑定。主要有两个:WebMvcProperties 和 WebProperties。
先来看WebMvcProperties:
@ConfigurationProperties( prefix = "spring.mvc" ) public class WebMvcProperties {、 ......
可以看到 WebMvcProperties 和 spring.mvc 进行了绑定。
再来看WebProperties:
@ConfigurationProperties("spring.web") public class WebProperties { ......
可以看到 WebProperties 和 spring.web 进行了绑定。
配置类只有一个有参构造器:
有参构造器中的所有参数都会从容器中确定。
public WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider, ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider, ObjectProvider<DispatcherServletPath> dispatcherServletPath, ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) { this.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(); }
分析其参数:
-
WebProperties webProperties
获取和 spring.web 绑定的所有值的对象
-
WebMvcProperties mvcProperties
获取和 spring.mvc 绑定的所有值的对象
-
ListableBeanFactory beanFactory
Spring 的 beanFactory
-
ObjectProvider<HttpMessageConverters> messageConvertersProvider
找到所有的messageConvertersProvider
-
ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider
找到资源处理器的自定义器
-
ObjectProvider<DispatcherServletPath> dispatcherServletPath
-
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations
给应用注册Servlet、Filter…
资源处理的默认规则:
public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled"); } else { this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/"); 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}); } }); } }
先看源代码,如果
this.resourceProperties.isAddMappings()
是true,才会访问else下边的代码,可以看到是处理静态资源的路径的。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; private boolean addMappings; private boolean customized; private final WebProperties.Resources.Chain chain; private final WebProperties.Resources.Cache cache; public Resources() { this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS; this.addMappings = true; this.customized = false; this.chain = new WebProperties.Resources.Chain(); this.cache = new WebProperties.Resources.Cache(); } ......
默认为true。
spring: # mvc: # static-path-pattern: /res/** web: resources: static-locations: [classpath:/haha/] add-mappings: false #禁用所有静态资源规则
-
3、请求参数处理
3.1、请求映射
3.1.1、Rest使用与原理
-
@xxxMapping;
-
Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
-
以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
-
现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
-
核心Filter;
HiddenHttpMethodFilter
用法: 表单method=post,隐藏域 _method=put
SpringBoot中手动开启
-
在控制器中添加四个请求映射:
@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-张三";
}
在index.html
中创建对应请求的表单:
<h1>测试REST风格</h1>
<form action="/user" method="get">
<input value="REST-GET 提交" type="submit">
</form>
<form action="/user" method="post">
<input value="REST-POST 提交" type="submit">
</form>
<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>
在这里需要配置<input name="_method" type="hidden" value="put">
的原因,查看源码:
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
private static final List<String> ALLOWED_METHODS;
public static final String DEFAULT_METHOD_PARAM = "_method";
private String methodParam = "_method";
......
这里必须有一个"_method"
前缀。而且:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest requestToUse = request;
if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
}
}
}
filterChain.doFilter((ServletRequest)requestToUse, response);
}
必须是POST才可以。
但是经过测试还是无法得到要求。再看查看SpringBoot为我们自动注入的HiddenHttpMethodFilter
过滤器源码:
@Bean
@ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
@ConditionalOnProperty(
prefix = "spring.mvc.hiddenmethod.filter",
name = {"enabled"}
)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
我们没有配置spring.mvc.hiddenmethod.filter
。那么就在application.yaml
中添加配置:
spring:
# mvc:
# static-path-pattern: /res/**
web:
resources:
static-locations: [classpath:/haha/]
add-mappings: true
mvc:
hiddenmethod:
filter:
enabled: true
成功运行!
Rest原理(表单提交要使用REST的时候)
表单提交会带上
_method=PUT
请求过来被
HiddenHttpMethodFilter
拦截请求是否正常,并且是POST
- 获取到
_method
的值- 兼容以下请求:PUT、DELETE、PATCH
- 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值;
- 过滤器链放行的时候用wrapper。以后的方法调用getMethod的调用requesWrapper的。
Rest使用客户端工具:如PostMan直接发送Put、delete等方式请求,无需Filter。
扩展:如何把_method 这个名字换成我们自己喜欢的:
查看源码我们知道:
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
private static final List<String> ALLOWED_METHODS;
public static final String DEFAULT_METHOD_PARAM = "_method";
private String methodParam = "_method";
......
虽然DEFAULT_METHOD_PARAM
是final,但是methodParam
是可以更改的。而且没有HiddenHttpMethodFilter
时,SpringBoot才会给我们自动注入,那么我们就自己配置一个,并且设定methodParam
的值。
@Configuration(proxyBeanMethods = false)
public class WebConfig {
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
hiddenHttpMethodFilter.setMethodParam("_m");
return hiddenHttpMethodFilter;
}
}
3.1.2、请求映射原理
我们知道无论什么请求,都要经过DispatchServlet,而且一定要重写doGet()
和doPost()
方法:
查看继承树,发现DispatcherServlet继承了FrameworkServlet,FrameworkServlet继承了HttpServletBean。在HttpServletBean中并没有发现doGet()
和doPost()
方法,在FrameworkServlet中找到了doGet()
和doPost()
方法。
protected final void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.processRequest(request, response);
}
protected final void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.processRequest(request, response);
}
无论是doGet()
和doPost()
方法,都最终调用了本类的processRequest()
方法。
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = this.buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());
this.initContextHolders(request, localeContext, requestAttributes);
try {
this.doService(request, response);
} catch (IOException | ServletException var16) {
failureCause = var16;
throw var16;
} catch (Throwable var17) {
failureCause = var17;
throw new NestedServletException("Request processing failed", var17);
} finally {
this.resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
this.logResult(request, response, (Throwable)failureCause, asyncManager);
this.publishRequestHandledEvent(request, response, startTime, (Throwable)failureCause);
}
}
前边的都是获取参数配置项,最主要的就是try之后的doService()
方法。
protected abstract void doService(HttpServletRequest request, HttpServletResponse response) throws Exception;
FrameworkServlet类中的doService()
方法是abstract(抽象)类,在FrameworkServlet类中并没有实现。因此我们需要在其继承类中寻找其实现。
在DispatcherServlet中有doService()
方法的实现:
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
this.logRequest(request);
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap();
Enumeration attrNames = request.getAttributeNames();
label116:
while(true) {
String attrName;
do {
if (!attrNames.hasMoreElements()) {
break label116;
}
attrName = (String)attrNames.nextElement();
} while(!this.cleanupAfterInclude && !attrName.startsWith("org.springframework.web.servlet"));
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, this.getThemeSource());
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}
RequestPath previousRequestPath = null;
if (this.parseRequestPath) {
previousRequestPath = (RequestPath)request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
ServletRequestPathUtils.parseAndCache(request);
}
try {
this.doDispatch(request, response);
} finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) {
this.restoreAttributesAfterInclude(request, attributesSnapshot);
}
if (this.parseRequestPath) {
ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);
}
}
}
前边的也是一堆初始化,最重要的也是try之后的一个doDispatch()
方法。也就是说:
SpringMVC功能分析都要从org.springframework.web.servlet.DispatcherServlet
的doDispatch()
开始。
......
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
//找到当前请求使用哪个Handler(Controller)处理
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
......
查看getHandler()
方法:
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
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;
}
其中有一个handlerMappings,查看,有5个:
RequestMappingHandlerMapping 保存了所有 @RequestMapping 和 handler 的映射规则。
那这个是怎么找到映射的呢?
可以看到handlerMappings后边使用了一个迭代,分别拿出这5个映射规则(mapping),一一对应:
总结:
所有的请求映射都在handlerMappings中。
- SpringBoot 自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;
- SpringBoot 自动配置了默认的 RequestMappingHandlerMapping
- 请求进来,挨个尝试所有的 handlerMappings 看是否有请求信息
- 如果有就找到这个请求对应的 handler
- 如果没有就是下一个 handlerMapping
- 我们需要一些自定义的映射处理,我们也可以自己给容器中放handlerMappings。自定义HandlerMapping
3.2、普通参数与基本注解
3.2.1、注解
-
@PathVariable:获取请求路径中的变量
获取路径中的值,赋值给方法中的变量。
<a href="/car/3/owner/lisi">/car/{id}/owner/{username}</a>
// car/id/owner/username @GetMapping("/car/{id}/owner/{username}") public Map<String, Object> getCar(@PathVariable("id") Integer id, @PathVariable("username") String name, @PathVariable Map<String, String> pv){ HashMap<String, Object> map = new HashMap<>(); map.put("id", id); map.put("name",name); map.put("pv", pv); return map; }
-
@RequestHeader:获取请求头
@RestController public class ParameterTestController { // car/id/owner/username @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){ HashMap<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); return map; } }
加入
@RequestHeader("User-Agent") String userAgent
和@RequestHeader Map<String, String> header
: -
@RequestParam:获取请求参数中的值
<a href="/car/3/owner/lisi?age=18&inters=basketball&inters=game">/car/{id}/owner/{username}</a>
我想要获取age、inters中的值。
@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){
加入
@RequestParam("age") Integer age
、@RequestParam("inters") List<String> inters
和@RequestParam Map<String, String> params
。 -
@CookieValue:获取Cookie的值
@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("ol_offset") String ol_offset, @CookieValue("ol_offset") Cookie cookie){
加入
@CookieValue("ol_offset") String ol_offset
和@CookieValue("ol_offset") Cookie cookie
:System.out.println(cookie); //javax.servlet.http.Cookie@701df2b8
-
@RequestBody:获取请求体
我们知道请求体只存在于POST的请求方式。
在
index.html
写一个表单:<form action="/save" method="post"> 测试@RequestBody获取数据<br/> 用户名:<input name="userName" /> <br> 邮箱:<input name="email"> <input type="submit" value="提交"> </form>
重新定义一个控制器方法:
@PostMapping("/save") public Map postMethod(@RequestBody String content){ HashMap<Object, Object> map = new HashMap<>(); map.put("content", content); return map; }
-
@RequestAttribute:获取Request域
一般用来获取Request域中的值,进行域属性共享和传递。
创建一个新的控制器,使用
@Controller
,一般用来实现页面跳转。在这里模拟页面跳转,使用转发。@Controller //普通的控制器,一般用来实现页面跳转 public class RequestController { @GetMapping("/goto") public String goToPage(HttpServletRequest request){ request.setAttribute("msg", "成功了..."); request.setAttribute("code", 200); return "forward:/success"; //转发到 /success请求 } @ResponseBody @GetMapping("/success") public Map success(@RequestAttribute("msg") String msg, @RequestAttribute("code") Integer code, HttpServletRequest request){ Object msg1 = request.getAttribute("msg"); HashMap<Object, Object> map = new HashMap<>(); map.put("requestMethod", msg1); map.put("annotation_msg", msg); return map; } }
在success方法中,使用注解
@RequestAttribute("msg") String msg
和原生的HttpServletRequest request
分别获取request域中的值。 -
@MatrixVariable:获取矩阵变量
路径的了两种写法:
-
/cars/{path}?xxx=xxx&aaa=ccc
称之为queryString,查询字符串
可以使用
@RequestParam
获取。 -
/cars/sell;low=34;brand=byd,audi,yd
称之为矩阵变量
一个面试题:页面开发,如果cookie禁用了,session里边的内容怎么使用?
使用session里边的内容,要使用session.set(a,b),我们要知道jsessionid,而jsessionid是被保存在cookie中,每次发请求时携带。
把cookie禁用了,那么就获取不到session了。
可以使用url重写:
/abc;jsessionid=xxx
。把cookie的值使用矩阵变量的方式进行传递。矩阵变量的语法:
- 矩阵变量需要在SpringBoot中手动开启
- 根据RFC3986的规范,矩阵变量应当绑定在路径变量中
- 若是有多个变量,应当使用英文符号;进行分隔
- 若是一个矩阵变量有多个值,应当使用英文,进行分隔,或者命名多个重复的key即可
在
index.html
中写上测试链接:<a href="/cars/sell;low=34;brand=byd,audi,yd">@MatrixVariable 矩阵变量1 /cars/{path}</a> <a href="/cars/sell;low=34;brand=byd,audi;brand=yd">@MatrixVariable 矩阵变量2</a> <a href="/boss/1;age=20/2;age=10">@MatrixVariable 矩阵变量3 /boss/{bossId}/{empId}</a>
SpringBoot中需要手动开启矩阵变量,在web的配置类中:
@Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); //不移除;后边的内容,矩阵变量就可以生效 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); }
测试第一种情况:
// /cars/sell;low=34;brand=byd,audi,yd @GetMapping("/cars/{path}") public Map carsSell(@MatrixVariable("low") Integer low, @MatrixVariable("brand") List<String> brand, @PathVariable("path") String path){ HashMap<Object, 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){ HashMap<Object, Object> map = new HashMap<>(); map.put("bossAge", bossAge); map.put("empAge", empAge); return map; }
-
@PathVariable
:获取请求路径中的变量
@RequestHeader
:获取请求头
@RequestParam
:获取请求参数
@MatrixVariable
:获取矩阵变量
@CookieValue
:获取cookie
@RequestBody
:获取请求体
各种类型参数解析原理:
在DispathcerServlet的doDispatcher()
方法打上断点,调试运行:
发送localhost:8080/car/3/owner/lisi?age=18&inters=basketball&inters=game
请求。
HandlerMapping中找到能处理请求的Handler(Controller.method())
为当前 Handler 找一个适配器 HandlerAdapter,默认 HandlerAdapter 有四种;
适配器执行目标方法并确定方法参数的每一个值
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);
参数解析器-HandlerMethodArgumentResolver
确定将要执行的目标方法的每一个参数的值是什么
SpringMVC目标方法能写多少种参数类型。取决于参数解析器。
返回值处理器
4、视图解析与模板引擎
4.1、试图解析
…
4.2、模板引擎-Thymeleaf
4.2.1、thymeleaf简介
Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text.
现代化、服务端Java模板引擎
4.2.2、基本语法
表达式:
表达式名字 | 语法 | 用途 |
---|---|---|
变量取值 | ${…} | 获取请求域、session域、对象等值 |
选择变量 | *{…} | 获取上下文对象值 |
消息 | #{…} | 获取国际化等值 |
链接 | @{…} | 生成链接 |
片段表达式 | ~{…} | jsp:include 作用,引入公共页面片段 |
字面值:
- 文本值: ‘one text’ , ‘Another one!’ **,…**数字: 0 , 34 , 3.0 , 12.3 **,…**布尔值: true , false
- 空值: null
- 变量: one,two,… 变量不能有空格
文本操作:
- 字符串拼接: +
- 变量替换: |The name is ${name}|
数学运算:
- 运算符: + , - , * , / , %
布尔运算:
- 运算符: and , or
- 一元运算: ! , not
比较运算:
- 比较: > , < , >= , <= ( gt , lt , ge , le **)**等式: == , != ( eq , ne )
条件运算:
- If-then: (if) ? (then)
- If-then-else: (if) ? (then) : (else)
- Default: (value) ?: (defaultvalue)
特殊操作:
- 无操作: _
设置属性值-th:attr:
-
设置单个值
<form action="subscribe.html" th:attr="action=@{/subscribe}"> <fieldset> <input type="text" name="email" /> <input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/> </fieldset> </form>
-
设置多个值
<img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
-
以上两个的代替写法 th:xxxx
<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/> <form action="subscribe.html" th:action="@{/subscribe}">
迭代:
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
条件运算:
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manager</p>
<p th:case="*">User is some other thing</p>
</div>
4.3、thymeleaf使用
4.3.1、引入Starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
4.3.2、自动配置好了thymeleaf
@EnableConfigurationProperties({ThymeleafProperties.class})
@ConditionalOnClass({TemplateMode.class, SpringTemplateEngine.class})
@AutoConfigureAfter({WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class})
@Import({ReactiveTemplateEngineConfiguration.class, DefaultTemplateEngineConfiguration.class})
public class ThymeleafAutoConfiguration {
......
自动配好的策略:
-
所有thymeleaf的配置值都在 ThymeleafProperties
-
配置好了 SpringTemplateEngine
-
配好了 thymeleafViewResolver
-
我们只需要直接开发页面
@ConfigurationProperties( prefix = "spring.thymeleaf" ) public class ThymeleafProperties { private static final Charset DEFAULT_ENCODING; public static final String DEFAULT_PREFIX = "classpath:/templates/"; public static final String DEFAULT_SUFFIX = ".html"; ......
页面放在
"classpath:/templates/"
类路径下的templates下,后缀都是.html
。在templates目录下新建一个
success.html
:<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1 th:text="${msg}">哈哈</h1> <h2> <a href="www.baidu.com" th:href="${link}">去百度</a> <a href="www.baidu.com" th:href="@{/link}">去百度</a> </h2> </body> </html>
新建一个控制器:
@GetMapping("/baidu") public String baidu(Model model){ //model中的数据会被放在请求域中,request.setAttribute("a", aa) model.addAttribute("msg", "你好,百度"); model.addAttribute("link","http://www.baidu.com"); return "success"; }
4.4、构建后台管理系统
4.4.1、项目创建
thymeleaf、web-starter、devtools、lombok
4.4.2、静态资源处理
自动配置好,我们只需要把所有静态资源放到 static 文件夹下
4.4.3、路径构建
@Controller
public class IndexController {
@GetMapping({"/","/login"})
public String loginPage(){
return "login";
}
@PostMapping("/login")
public String main(String name, String password){
//登录成功,重定向到main页面
return "redirect:/main.html";
}
@GetMapping("/main.html")
public String mainPage(){
//去main页面
return "main";
}
}
重定向到main.html
,解决了刷新表单重复提交的问题。
<form class="form-signin" action="index.html" method="post" th:action="@{/login}">
这样写有一个缺陷,只要输入请求main.html
就可以访问,我们需要登录之后才可以访问:
@Controller
public class IndexController {
@GetMapping({"/","/login"})
public String loginPage(){
return "login";
}
@PostMapping("/login")
public String main(User user, HttpSession session, Model model){
if (!StringUtils.isEmpty(user.getUserName()) && !StringUtils.isEmpty(user.getPassword())){
session.setAttribute("loginUser", user);
//登录成功,重定向到main页面
return "redirect:/main.html";
}else {
model.addAttribute("msg", "账号密码错误");
return "login";
}
}
@GetMapping("/main.html")
public String mainPage(HttpSession session, Model model){
//是否登录成功 拦截器,过滤器
Object loginUser = session.getAttribute("loginUser");
if (loginUser != null){
return "main";
}else {
//回到登录页面
model.addAttribute("msg", "请重新登录");
return "login";
}
}
}
@Data
public class User {
private String userName;
private String password;
}
<input type="text" name="userName" class="form-control" placeholder="用户名" autofocus>
<input type="password" name="password" class="form-control" placeholder="密码">
<a href="#" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<img src="images/photos/user-avatar.png" alt="" />
[[${session.loginUser.userName}]]
<span class="caret"></span>
</a>
模拟实现了登录检查功能。
4.4.4、模板抽取
把页面中公共的部门抽取出来,放在一个commo.html
中。
在其他页面引入即可,例如:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta name="description" content="">
<meta name="author" content="ThemeBucket">
<link rel="shortcut icon" href="#" type="image/png">
<title>Basic Table</title>
<div th:include="common :: commonheader"></div>
</head>
<body class="sticky-header">
<section>
<div th:replace="common :: #leftmenu"></div>
<!-- main content start-->
<div class="main-content">
<div th:replace="common :: headermenu"></div>
<!-- 页面标题开始-->
......
<!-- 页面标题结束-->
......
<!--内容区域开始-->
......
<!--内容区域结束-->
<!--footer start-->
<footer>2014 © AdminEx by ThemeBucket</footer>
<!--footer end--> </div>
<!-- main content end--> </section>
<!-- Placed js at the end of the document so the pages load faster -->
<div th:replace="common :: #commonscript"></div>
</body>
</html>
使用th:include
或者th:replace
。
4.4.5、动态表格
@GetMapping("/dynamic_table")
public String dynamic_table(Model model){
//表格内容的遍历
List<User> users = Arrays.asList(new User("zhangsan", "123456"),
new User("lisi", "123444"),
new User("haha", "aaaaa"),
new User("hehe", "bbbbb"));
model.addAttribute("users", users);
return "table/dynamic_table";
}
在dynamic_table.html
中动态获取表格内容:
<table class="display table table-bordered table-striped" id="dynamic-table">
<thead>
<tr>
<th>#</th>
<th>用户名</th>
<th>密码</th>
</tr>
</thead>
<tbody>
<tr th:each="user,stats:${users}">
<td th:text="${stats.count}">Trident</td>
<td th:text="${user.userName}">Internet</td>
<td th:text="${user.password}">Internet</td>
</tr>
</tbody>
</table>
4.4.6、拦截器
配置拦截器,具体拦截方式:
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginIntercepter())
.addPathPatterns("/**") //静态资源也会拦截
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行的请求
}
}
4.4.7、单文件与多文件的上传
页面:
<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="exampleInputEmail1">邮箱</label>
<input type="email" name="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
</div>
<div class="form-group">
<label for="exampleInputPassword1">名字</label>
<input type="text" name="username" class="form-control" id="exampleInputPassword1" placeholder="Password">
</div>
<div class="form-group">
<label for="exampleInputFile">头像</label>
<input type="file" name="headerImg" id="exampleInputFile">
<p class="help-block">Example block-level help text here.</p>
</div>
<div class="form-group">
<label for="exampleInputFile">生活照</label>
<input type="file" name="photos" multiple>
<p class="help-block">Example block-level help text here.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox"> Check me out
</label>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
设置上传大小限制:
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB
控制器:
/**
* 文件上传测试
*/
@Slf4j
@Controller
public class FormTestController {
@GetMapping("/form_layouts")
public String form_layouts(){
return "form/form_layouts";
}
/**
* MultipartFile 自动封装上传过来的文件
* @param email
* @param username
* @param headerImg
* @param photos
* @return
*/
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos){
log.info("上传的信息:email={}, username={}, headerImg={}, photos={}",
email, username, headerImg.getSize(), photos.length);
return "main";
}
}
上传的信息:email=1448402493@qq.com, username=67465, headerImg=4286, photos=2
把上传的文件进行保存:
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
log.info("上传的信息:email={}, username={}, headerImg={}, photos={}",
email, username, headerImg.getSize(), photos.length);
if (!headerImg.isEmpty()){
//保存到文件服务器,oss服务器
String originalFilename = headerImg.getOriginalFilename();
headerImg.transferTo(new File("G:\\work\\coding\\java\\SpringBoot2\\documents\\"+originalFilename));
}
if (photos.length > 0){
for (MultipartFile photo : photos) {
if (!photo.isEmpty()) {
//保存到文件服务器,oss服务器
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("G:\\work\\coding\\java\\SpringBoot2\\documents\\"+originalFilename));
}
}
}
return "main";
}
5、异常处理
5.1、默认规则
-
默认情况下,Spring Boot 提供
/error
处理所有错误的映射 -
对于机器客户端,它将生成 JSON 响应,其中包含错误,HTTP 状态和异常消息的详细信息。对于浏览器客户端,响应一个 “ whitelabel” 错误视图,以 HTML 格式呈现相同的数据
-
error/
下的4xx,5xx页面会被自动解析;