04、配置文件(了解)
1、文件类型
1.1、properties
同以前的properties用法
1.2、yaml
1.2.1、简介
YAML 是 “YAML Ain’t Markup Language”(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML
的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。
非常适合用来做以数据为中心的配置文件
1.2.2、基本语法
key: value;kv之间有空格
大小写敏感
使用缩进表示层级关系
缩进不允许使用tab,只允许空格
缩进的空格数不重要,只要相同层级的元素左对齐即可
'#‘表示注释
字符串无需加引号,如果要加,’'与""表示字符串内容 会被 转义/不转义
1.2.3、数据类型
字面量:单个的、不可再分的值。date、boolean、string、number、null
k: v
对象:键值对的集合。map、hash、set、
行内写法: k: {k1:v1,k2:v2,k3:v3}
#或
k:
k1: v1
k2: v2
k3: v3
● 数组:一组按次序排列的值。array、list、queue
行内写法: k: [v1,v2,v3]
#或者
k:
- v1
- v2
- v3
1.2.4、示例
@Data
public class Person {
private String userName;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] interests;
private List<String> animal;
private Map<String, Object> score;
private Set<Double> salarys;
private Map<String, List<Pet>> allPets;
}
@Data
public class Pet {
private String name;
private Double weight;
}
# yaml表示以上对象
person:
userName: zhangsan
boss: false
birth: 2019/12/12 20:12:33
age: 18
pet:
name: tomcat
weight: 23.4
interests: [篮球,游泳]
animal:
- jerry
- mario
score:
english:
first: 30
second: 40
third: 50
math: [131,140,148]
chinese: {first: 128,second: 136}
salarys: [3999,4999.98,5999.99]
allPets:
sick:
- {name: tom}
- {name: jerry,weight: 47}
health: [{name: mario,weight: 47}]
2、配置yaml语言打字提示
自定义的类和配置文件绑定一般没有提示。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
以下配置是不把yml配置文件打包到jar
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
05、Web开发
技术点
1、SpringMVC自动配置概览
官方可找
2、简单功能分析
2.1、静态资源访问
1、静态资源目录
只要静态资源放在类路径(resources)下: /static or /public or /resources or/META-INF/resources 就可访问
访问路径 : 当前项目根路径/ + 静态资源名
http://localhost:8080/xxx.png
原理: 默认静态映射/**。
请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面
一般我们都是改变默认的静态资源路径:
在yml文件中
spring:
mvc:
static-path-pattern: /res/**
#也可以指定获取静态资源的地址 也可用数组的格式[classpath:/photo]
resources:
static-locations: [classpath:/photo]
当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找
http://localhost:8080/res/xxx.png
2、webjar(了解)
比如JQuery标签,我们现在就可以通过pom.xml的形式导入到项目中,也可以通过页面访问到资源。
自动映射的默认路径 /webjars/**
https://www.webjars.org/
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>
访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js
后面地址要按照依赖里面的包路径
2.2、欢迎页支持
● 静态资源路径下 index.html
-
可以配置静态资源路径
-
但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致welcome page功能失效,这个功能不常用,主要是使用静态资源的访问前缀
resources:
static-locations: [classpath:/haha/]
● controller能处理/index
2.3、自定义 Favicon
作用:
网站的小图标
=
favicon.ico 放在静态资源目录下即可。
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致 Favicon 功能失效
2.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 })
public class WebMvcAutoConfiguration {}
- 给容器中配了什么。
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}
- 配置文件的相关属性和xxx进行了绑定。WebMvcPropertiesspring.mvc、ResourcePropertiesspring.resources
1、配置类只有一个有参构造器
下面这段源码是配置类如何在容器中拿到组件。
//有参构造器所有参数的值都会从容器中确定
//ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象
//WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象
//ListableBeanFactory beanFactory Spring的beanFactory
//HttpMessageConverters 找到所有的HttpMessageConverters
//ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。=========
//DispatcherServletPath
//ServletRegistrationBean 给应用注册Servlet、Filter....
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
}
2、资源处理的默认规则
以下处理资源的规则
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
//webjars的规则
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
//
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
spring:
# mvc:
# static-path-pattern: /res/**
resources:
add-mappings: false 禁用所有静态资源规则
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/].
*/
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
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, Optional<Resource> welcomePage, String staticPathPattern) {
if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
//要用欢迎页功能,必须是/**
logger.info("Adding welcome page: " + welcomePage.get());
setRootViewName("forward:index.html");
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
// 调用Controller /index
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
4、favicon
当配置了静态资源的前缀,那么我们配置的网站小图标也找不到
3、请求参数处理
0、请求映射
1、rest使用与原理(以下研究的是页面表单的提交,如果是postman等直接就可以发送put和delete请求)
@xxxMapping; Restful风格支持(使用HTTP请求方式动词来表示对资源的操作)
以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
现在: /user > GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
源码核心Filter;HiddenHttpMethodFilter
用法: 1、如果前台表单提交,需要执行method=post或get直接使用,但是使用put和delete另外添加输入框 name内容是 _method,value是put或delete ,然后隐藏输入框type=hidden
2、SpringBoot中手动开启
扩展:如何把_method 这个名字换成我们自己喜欢的。
@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-张三";
}
//springboot源码
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
//自定义filter 修改页面表单使用resultful方式发送请求时,name传的固定值(_method)。
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}
我们通过配置类给创建出组件并给MethodParam属性set值 框架为我们提供了set方法,那么框架就不会生成默认的组件
2、请求映射原理
handlerMapping负责找到controller中哪个方法可以处理请求后,需要确定参数再利用反射执行方法
dispatcherServlet类的继承树,和关键的方法处理请求
所有的方法进到服务端都会先进到doGet,或doPost。如进入到doGet的方法,通过继承和实现会在dispatcherServlet类中doDispatch方法中进行实现。
从上面分析得知,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 = this.getHandler(processedRequest);
//HandlerMapping:处理器映射。/xxx->>xxxx
RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则,里面装了我们写的controller的路径映射。
所有的请求映射都在HandlerMapping中。
- SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。这就是为什么在页面我们没配置,访问 /也能访问到index.html;
1.SpringBoot自动配置了默认 的 RequestMappingHandlerMapping
2.请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
- 如果有就找到这个请求对应的handler
- 如果没有就是下一个 HandlerMapping
3.如果我们需要一些自定义的映射处理,我们也可以自己给容器中放
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;
}
1、普通参数与基本注解(接收参数的注解,重要)
1.1、注解:
@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody
4种前台页面传参到controller演示
@PathVariable
//@PathVariable("name") String name, @PathVariable("id") String id 作用等于@PathVariable Map<String, String> car
@RestController
@RequestMapping("/carController")
public class ParameterController {
@GetMapping("/owner/{name}/car/{id}")
public Map<String, Object> getCar(@PathVariable("name") String name, @PathVariable("id") String id,
@PathVariable Map<String, String> car){
Map<String, Object> objectObjectHashMap = new HashMap<>();
objectObjectHashMap.put("name",name);
objectObjectHashMap.put("id",id);
objectObjectHashMap.put("car",car);
return objectObjectHashMap;
}
}
以上是@PatVariable注解演示。
//@RequestHeader获取请求头信息
//@RequestHeader获取请求头信息
@RestController
@RequestMapping("/carController")
public class ParameterController {
@GetMapping("/owner/{name}/car/{id}")
public Map<String, Object> getCar(@RequestHeader("User-Agent") String hander, //发送请求的浏览器的名字为User-Agentde 请求头信息
@RequestHeader Map<String, String> allHeader){//拿到所有请求头信息,用Map接收
Map<String, Object> objectObjectHashMap = new HashMap<>();
objectObjectHashMap.put("hander",hander);
objectObjectHashMap.put("allHeader",allHeader);
return objectObjectHashMap;
}
}
@RequestParam 接收页面出的参数
//@RequestParam 接收页面出的参数
@RestController
@RequestMapping("/carController")
public class ParameterController {
@GetMapping("/param")
public Map<String, Object> getCar(@RequestParam("name") Integer name,
@RequestParam("inters") List<String> inters,//拿到inters中的两个参数
@RequestParam Map<String, String> allHeader){//拿到所有的参数用Map接收
Map<String, Object> objectObjectHashMap = new HashMap<>();
objectObjectHashMap.put("name",name);
objectObjectHashMap.put("inters",inters);
objectObjectHashMap.put("allHeader",allHeader);
return objectObjectHashMap;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="/carController/param?name=zhangsan&inters=basketball&inters=game">超链接</a>
</body>
</html>
页面返回值
获取cookie的名字和全部信息 如下代码
@CookieValue("_ga") String _ga, 拿到Cookie中名字是_ga的值
@CookieValue("_ga") Cookie cookie 拿到整个Cookie的参数
@RequestBody 获取请求体的信息
注意,只有post请求才有请求体,所以使用于post请求,如表单提交。
@PostMapping("/save")
public Map<String, Object> save(@RequestBody String content){
Map<String, Object> objectObjectHashMap = new HashMap<>();
objectObjectHashMap.put("content",content);
return objectObjectHashMap;
}
<form action="/carController/save" method="post">
用户名<input type="text" name="username">
密码<input type="text" name="password">
<input type="submit" value="提交">
</form>
页面提交
controller接收参数,并返回结果
@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;
}
}
@RequestAttribute获取域对象中的值
也是两种方式,一种是通过@RequestAttribute注解,一种是通过原生的HttpServletequest的request域对象。
矩阵变量
@MatrixVariable
有一个面试题,也是矩阵变量的应用:比如说在web开发中,cookie被禁用,那么客户端怎么把会话信息传给服务端。
:分析,我们知道cookie的信息是保存在客户端的,而session中的用户信息是通过JsessionID放到cookie中一起发送到服务端,所以当cookie被禁用,我们就需要通过在路径上传值的方式传到服务端。
:解决,就是怎么在controller中拿到矩阵变量的值。
//1、语法: 页面请求路径是这样的:/cars/sell;low=34;brand=byd,audi,yd
请求路径中";"前是访问路径,后是携带的变量,多个变量以";"分割,一个变量有多个值又以","分割。所以说路径和值是一个整体放在URL中。
//2、SpringBoot默认是禁用了矩阵变量的功能
// 手动开启:原理。对于路径的处理spring boot用UrlPathHelper这个类进行解析。而这个类中removeSemicolonContent属性(默认是移除分号内容,也就是不支持矩阵变量),我们让程序支持矩阵变量,通过这个属性即可。
//3、矩阵变量必须有url路径变量才能被解析
@RestController
@RequestMapping("/carController")
public class ParameterController {
@GetMapping("/{path}")//{path}里的path是我们自定义的值
public Map<String, Object> carsSell(@MatrixVariable Integer price,
@MatrixVariable List<String> brand,
@PathVariable String path){
Map<String, Object> map = new HashMap<>();
map.put("price",price);
map.put("brand",brand);
map.put("path",path);
return map;
}
}
开启矩阵变量支持
通过我们手动配置类去替代spring boot默认,通过配置这个接口WebMvcConfigurer,然后通过这个接口的实现类PathMatchConfigurer,new这个属性urlPathHelper,把urlPathHelper里的属性换配置。
@Configuration
public class WebConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}
}
前台页面
<a href="/carController/carsSell;price=20;brand=byd,audi,bwm">矩阵变量</a>
注意:
矩阵变量是放在路径url上的,所以在服务端路径方式是这样的。
@RequestMapping("/carController")/@GetMapping("/{path}")
我们可以看通过路径变量{path}
里面的矩阵变量是什么样的
特殊形况,一个路径(boss)上有两个路径变量,两个路径变量(1和2)上有相同的矩阵变量(age)的名字,那么在controller中怎么接收
<a href="/boss/1;age=20/2;age=10">矩阵变量</a>
@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;
}
1.2、Servlet API:
了解原生HttpServletRequest request的原理,它是如何在域对象中处理数据的
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);
}
1.3、复杂参数:
重要:Map和Model接收的参数会放到请求域中,在返回页面渲染之前。
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(); 获取到值的
1.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
2、POJO封装过程
页面传的参数是怎么直接封装到类里面的,就是通过这个类实现的ServletModelAttributeMethodProcessor
3、参数处理原理
首先handlerMapping找到controller中哪个方法能处理请求后,需要HandlerAdapter适配器解析方法上的参数,再通过反射调用目标方法,以下是HandlerMapping的处理细节
dispatcherServlet类的继承树,和关键的方法处理请求
所有的方法进到服务端都会先进到doGet,或doPost。如进入到doGet的方法,通过继承和实现会在dispatcherServlet类中doDispatch方法中进行实现。
从上面分析得知,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 = this.getHandler(processedRequest);
//HandlerMapping:处理器映射。/xxx->>xxxx
RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则,里面装了我们写的controller的路径映射。
所有的请求映射都在HandlerMapping中。
- SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。这就是为什么在页面我们没配置,访问 /也能访问到index.html;
1.SpringBoot自动配置了默认 的 RequestMappingHandlerMapping
2.请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
- 如果有就找到这个请求对应的handler
- 如果没有就是下一个 HandlerMapping
以下是HandlerAdapter是配置的处理 核心
HandlerMapping中找到能处理请求的Handler(Controller的方法) 为当前Handler 找一个适配器HandlerAdapter; 1、RequestMappingHandlerAdapter 适配器通过反射执行目标方法,在执行目标方法前需要确定使用的参数解析器和确定返回值的返回值处理器才会执行目标方法。Adapter适配器中有参数解析器和返回值处理器,在Adapter适配器中如何确定目标方法上的参数和执行完目标方法后的返回值处理?3、就是通过参数解析器argumentResolver(共26个)如下小节,4、接下来由返回值处理器returnValueHandler(15种)进行处理。5、在执行目标方法时拿到请求发过来的参数并调用相应的参数解析器处理好,最后再利用反射调用目标方法
1、HandlerAdapter
上图第一个HandlerAdapter 0位置是 - 支持方法上标注@RequestMapping的controller方法,也就是可以适配 RequestMappingHandlerMapping映射处理器。
第一个位置1是 - 支持函数式编程的 xxxxxx(了解,也就是说函数式的controller方法用函数式的Adaper适配器处理)
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、参数解析器-HandlerMethodArgumentResolver
确定将要执行的目标方法的每一个参数的值是什么; SpringMVC目标方法能写多少种参数类型。取决于参数解析器。
参数解析器是怎么处理参数的?
参数解析器是一个接口,接口中有这两个方法,如果supportsParameter方法支持这种方法,就用resolveArgument方法进行解析
当前解析器是否支持解析这种参数 支持就调用 resolveArgument
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;
}
5.1、挨个判断所有参数解析器哪个支持解析这个参数
挨个遍历参数解析器 就是说该参数上标注了该解析器能处理的注解类型(如@RequestParam),就用该注解的参数解析器获取参数的值
@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 这个参数处理器支持
是否为简单类型。首先判断参数是否被注解修饰,判断接收的参数是否为简单参数,如下源码就是判断是否为简单参数,不是简单参数就处理。
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));
}
接下来是如何封装
会给我们new一个空的POJO(person)对象,然后用web数据绑定器将参数绑定到POJO对象,循环100多个绑定器看哪一个能把前台
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
WebDataBinder :web数据绑定器,将请求参数的值绑定到指定的JavaBean里面
WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中,如下图
GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean -- Integer)
byte -- > file
@FunctionalInterfacepublic interface Converter<S, T>
@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;
}
4、数据响应与内容协商
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.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
给前端自动返回json数据;
1.1.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);
}
RequestResponseBodyMethodProcessor
@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.1.2、返回值解析器原理
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
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有 @ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;
1.3、HTTPMessageConverter原理
1.3.1、MessageConverter规范
HttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。
例子:Person对象转为JSON。或者 JSON转为Person
1.3.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转换的)
2、内容协商
根据客户端接收能力不同,返回不同媒体类型的数据。
1、引入xml依赖
为了测试jackson返回支持xml类型
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
2、利用postman分别测试返回json和xml
只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。
3、开启浏览器参数方式内容协商功能
为了方便使用浏览器和服务端的内容协商,开启基于请求参数的内容协商功能。
spring:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
通过浏览器发请求:
http://localhost:8080/test/person?format=json
http://localhost:8080/test/person?format=xml
这时就会多一个支持xml的Manager管理器
确定客户端接收什么样的内容类型;
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就会自动进来
源代码如下,通过类工具ClassUtils.isPresent()判断如果导入这个包,就添加jackson处理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
流程
就是把自定义的MesageConverter添加到容器中
实现多协议数据兼容。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) {
}
}
}
创建converter
把自定义的converter添加到配置类中
通过自定义在浏览器通过路径的方式实现内容协商
之前的内容管理器只有两种策略,通过我们自定义配置,增加一种方式
注意:有时需要我们自定义给系统添加配置,有可能系统默认的一些配置就会生效,那么就需要我们通过看源码查找。以上自定义只是适配参数的(浏览器方式请求),那么就没有添加请求头的(Accept),通过请求头告诉服务端我能接收返回数据的方式就不好使了,所以可以再添加适配请求头的
测试 浏览器访问,就会以分号分隔每一个值,
总结:浏览器传了gg那么通过我们自定义的manager管理器,服务器遍历所有的管理器就能找到支持 application/x-guigu的媒体类型进行处理。以上就是内容协商完原理,之后在通过我们自定义的converter处理x-guigu进行读、写,响应给浏览器。
5、视图解析与模板引擎
视图解析:SpringBoot默认不支持 JSP(原因是SpringBoot打jar包,属于压缩包。jsp就不能在压缩包内解析),所以实现页面渲染,需要引入第三方模板引擎技术实现。
1、什么是视图解析,服务端响应给页面的方式
1、视图解析原理流程
1、目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面。包括数据和视图地址
2、方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer
3、任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)。
4、processDispatchResult 处理派发结果(页面改如何响应)
● 1、render(mv, request, response); 进行页面渲染逻辑
○ 1、根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】
■ 1、所有的视图解析器尝试是否能根据当前返回值得到View对象
■ 2、得到了 redirect:/main.html --> Thymeleaf new RedirectView()
■ 3、ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
■ 4、view.render(mv.getModelInternal(), request, response); 视图对象调用自定义的render进行页面渲染工作
● RedirectView 如何渲染【重定向到一个页面】
● 1、获取目标url地址
● 2、response.sendRedirect(encodedURL);
视图解析:
○ 返回值以 forward: 开始: new InternalResourceView(forwardUrl); --> 转发request.getRequestDispatcher(path).forward(request, response);
○ 返回值以 redirect: 开始: new RedirectView() --》 render就是重定向
○ 返回值是普通字符串: new ThymeleafView()—>
自定义视图解析器+自定义视图; 大厂学院。
2、模板引擎-Thymeleaf
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模板引擎,支持于后端
2、基本语法
1、表达式
表达式名字 语法 用途获取值
变量取值 ${...} 获取请求域、session域、对象等值
选择变量 *{...} 获取上下文对象值
消息 #{...} 获取国际化等值
链接 @{...} 生成链接
片段表达式 ~{...} jsp:include 作用,引入公共页面片段
2、字面量
文本值: 'one text' , 'Another one!' ,…数字: 0 , 34 , 3.0 , 12.3 ,…布尔值: true , false
空值: null
变量: one,two,.... 变量不能有空格
3、文本操作
字符串拼接: +
变量替换: |The name is ${name}|
4、数学运算
运算符: + , - , * , / , %
5、布尔运算
运算符: and , or
一元运算: ! , not
6、比较运算
比较: > , < , >= , <= ( gt , lt , ge , le )等式: == , != ( eq , ne )
7、条件运算
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
8、特殊操作
无操作: _
3、设置属性值-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}">
所有h5兼容的标签写法
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes
4、迭代
<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>
5、条件运算
<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>
6、属性优先级
3、thymeleaf使用
1、引入Starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
为什么我们导入Starter即可使用,因为spring boot自动配置好了thymeleaf(了解)
ThymeleafAutoConfigration
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration { }
自动配好的策略
1、所有thymeleaf的配置值都在 ThymeleafProperties
2、配置好了 SpringTemplateEngine
3、配好了 ThymeleafViewResolver
4、我们只需要直接开发页面
#主要的配置就这两个,识别在工程的resources下templates目录下,放页面
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html"; //所有的Thymeleaf默认跳转xxx.html
3、页面开发
<!--加入名称空间 xmlns:th="http://www.thymeleaf.org-->
<!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.atguigu.com" th:href="${link}">去百度</a> <br/>
<a href="www.atguigu.com" th:href="@{link}">去百度2</a>
</h2>
</body>
</html>
4、构建后台管理系统
1、项目创建 用到的组件
thymeleaf、web-starter、devtools、lombok
2、静态资源处理
自动配置好,我们只需要把所有静态资源放到 static 文件夹下
3、路径构建 修改表单提交的访问路径
th:action="@{/login}"
问题:post请求刷新页面会重复提交
请求登录页默认返回是通过转发方式,那么每次请求都是/login页面,如果使用重定向,那么刷新就会访问第二次页面。
处理后
问题:如果直接在页面访问http://localhost:8889/login那么可以直接访问首页
我们需要进行判断只有在登录页面登录过的用户才能直接访问首页。
这只是一个简单的判断,进入后台的所有页面都应该进行判断用户有没有登陆。我们不挨个给每一个页面都进行手动判断,那么后面就需要使用拦截器。
给页面添加一个提示
登录成功的用户应该在页面显示用户信息,比如用户名那么我们可以使用thymeleaf的行内标签[[…]]
4、模板抽取
我们现在要把所有table表格有关的页面抽取出来
访问这几个页面的controller
这个页面的左侧导航栏和上方的都是一样的样式,我们需要抽取
我们将表格的前端代码都抽取到这个公共的页面,然后通过Thymeleaf进行动态绑定
使用id或thymeleaf的th:fragment标签,相当于做个标记在抽取时进行定位元素
使用抽取的css样式 通过Thymeleaf表达式
<!--一共有三种方式-->th:insert/replace/include
成功 查看
使用抽取的js代码
basic页面引入抽取代码
左侧菜单栏的页面访问的路径进行抽取
用户在首页点击退出应该跳转到登录页
做表格
Thymeleaf遍历出来的对象有一个status对象,里面有对应的索引index是从0开始,count是在1开始
5、页面跳转
Java复制代码 @PostMapping("/login")
public String main(User user, HttpSession session, Model model){
if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){
//把登陆成功的用户保存起来
session.setAttribute("loginUser",user);
//登录成功重定向到main.html; 重定向防止表单重复提交
return "redirect:/main.html";
}else {
model.addAttribute("msg","账号密码错误");
//回到登录页面
return "login";
}
}
6、拦截器
1、HandlerInterceptor 接口
拦截器的底层是HandlerInterceptor 接口实现。需要实现这个接口
拦截器可处理这三个阶段:发送一个请求。1、在HandlerMapping处理请求后(也就是找到对应controller执行方法前),对应拦截器底层HandlerInterceptor 接口的PreHandler方法,也就是前置拦截。2、在HandlerMapping处理完方法后到返回给页面之前,对应postHandler方法,也就是后置拦截。3、在页面渲染完后拦截器进行处理就是afterCompletion方法,多用于清理资源
/**
* 需求:登录检查功能,写一拦截器需要实现HandlerInterceptor 接口
* 1、配置好拦截器要拦截哪些请求
* 2、把这些配置放在容器中
*/
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
/**
* 目标方法执行之前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//打印提示的日志
String requestURI = request.getRequestURI();
log.info("preHandle拦截的请求路径是{}",requestURI);
//登录检查逻辑 检查用户是否登录过
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
//放行
return true;
}
//拦截住。未登录的用户,跳转到登录页
request.setAttribute("msg","请先登录");
// re.sendRedirect("/");重定向的话拿不到request域中的错误提示信息
request.getRequestDispatcher("/").forward(request,response);
return false;
}
/**
* 目标方法执行完成以后
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行{}",modelAndView);
}
/**
* 页面渲染以后
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行异常{}",ex);
}
}
2、配置拦截器(改底层源码)
/**
拦截器使用步骤
* 1、经过上一步,编写一个拦截器实现HandlerInterceptor接口
* 2、写个配置类把拦截器注册到容器中(所有配置webMvc的配置类都实现WebMvcConfigurer接口,配置拦截器生效利用addInterceptors方法)
* 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
*/
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // /**所有请求都被拦截包括静态资源,所以下一行配置放行
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行的请求,登录页不需要拦截
}
}
3、拦截器原理
1、根据当前请求,首先找到HandlerExecutionChain执行链,里面有【可以处理请求的handler以及handler的所有拦截器】
2、先来顺序执行 所有拦截器的 preHandle方法
● 1、如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
● 2、如果当前拦截器返回为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion方法,这个方法用于清理资源;有三个拦截器,第一和第二都放行了,但是第三个拦截了,那么会先执行第二个拦截器放行的afterCompletion方法,然后再执行第一个afterCompletion方法,第三个拦截器就不会执行afterCompletion方法。
3、如果任何一个拦截器返回false。直接跳出不执行目标方法
4、我们现在演示的是所有拦截器都返回True。就会执行目标方法
5、接下来倒序执行所有拦截器的postHandle方法。
如果前面的步骤有任何异常都会直接倒序触发 afterCompletion
6、页面成功渲染完成以后,也会倒序触发 afterCompletion
7、文件上传
1、页面表单
<form method="post" action="/upload" enctype="multipart/form-data">
<input type="file" name="file"><br>
<input type="submit" value="提交">
</form>
演示
下图代码multiple可以上传多个照片
注意上图的from表单需要配置post请求,路径和开启文件上传功能
2、文件上传代码
@RequestPart获取页面上传的资源,
MultipartFile transferTo :该类下的方法负责写到服务器
/**
* 文件上传类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) 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("H:\\cache\\"+originalFilename));
}
if(photos.length > 0){
for (MultipartFile photo : photos) {
if(!photo.isEmpty()){
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("H:\\cache\\"+originalFilename));
}
}
}
//执行成功后返回到主页面 Thymeleaf自动配置前缀和后缀,所以return直接写文件名
return "main";
}
问题 :文件上传都是框架为我们配制好的,如果图片的大小超过默认配置会跑错
我们需要手动配置,在这个配置类中有前缀名供我们在手动在yaml中配置,属性则在MultipartPerojperties类中由@EnableConfigrationPropetrties注解中
3、自动配置原理
文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties
● 已经给我们自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
● 原理步骤
○ 1、请求进来使用文件上传解析器判断这个请求是一般的请求还是文件上传请求(isMultipart)并把原生的request请求封装成文件上传的请求类型(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
○ 2、遍历哪个参数解析器来解析请求中的参数(文件内容)封装成MultipartFile
○ 3、将request中文件信息封装为一个Map;MultiValueMap<String, MultipartFile>
FileCopyUtils。实现文件流的拷贝
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos)
8、异常处理
1、错误处理
1、默认规则
● 默认情况下,Spring Boot提供/error处理所有错误的映射,出现错误信息就转到/error
● 对于机器客户端,/error将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息,比如postman。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据.如postman
浏览器错误
发生异常展示展示我们自定义的页面
● 要对其进行自定义异常,添加View解析为error
● 要完全替换默认的异常信息行为,可以实现 ErrorController 并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。
● 可以在templates或static路径下新建路径,error/下的4xx,5xx页面就会被自动解析;
2、自定义错误处理逻辑(推荐)@ExceptionHandler
● 自定义错误页
○ error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
● @ControllerAdvice+@ExceptionHandler处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的
● @ResponseStatus+自定义异常 ;底层是 ResponseStatusExceptionResolver ,把responsestatus注解的信息底层调用 response.sendError(statusCode, resolvedReason);tomcat发送的/error
● Spring底层的异常,如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常。
○ response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
● 自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则
● ErrorViewResolver 实现自定义处理异常;
○ response.sendError 。error请求就会转给controller
○ 你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给controller
○ basicErrorController 要去的页面地址是 ErrorViewResolver ;
1.@ExceptionHandler通过注解(给底层添加配置)
自定义异常(重要)
@Slf4j
@ControllerAdvice //ControllerAdvie是增强的controller,可实现全局处理异常
public class GlobaExceptionHandler {
@ExceptionHandler({ArithmeticException.class, NullPointerException.class})//选择处理异常的类型
public String handArithException(Exception e){//捕获到的异常会被e自动封装
log.error("异常是",e);
return "login";//异常处理返回的是一个MoelAndView 。没用到异常信息我们这里返回view的路径了
}
}
为什么自定义的异常类可以处理出现的异常呢,就是因为在遍历所有的异常处理类ExceptionResolver时,识别哪个方法写了@ExceptionHandler这个注解的ExceptionResolver会生效,然后返回一个字符串
2.@ResponseStatus
自定义一个类跑错,设置http状态码和跑错信息
结果
原理也是通过异常处理器,拿到@ResponseStatus的信息组装成ModelAndView返回
3、异常处理自动配置原理
● ErrorMvcAutoConfiguration 自动配置异常处理规则
○ 容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes 场景:如果我们觉得DefaultErrorAttributes定义错误页面数据不够那么使用DefaultErrorAttributes
■ public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
■ DefaultErrorAttributes:定义错误页面中可以包含哪些数据。
○ 容器中的组件:类型:BasicErrorController --> 如果不想页面返回的json或白页就选择这个 id:basicErrorController(json+白页 适配响应)
■ 处理默认 /error 路径的请求;页面响应 new ModelAndView("error", model);
■ 容器中有组件 View->id是error;(响应默认错误页)
■ 容器中放组件 BeanNameViewResolver(视图解析器);按照返回的视图名作为组件的id去容器中找View对象。
○ 容器中的组件:类型:DefaultErrorViewResolver -> id:conventionErrorViewResolver
■ 如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面
■ error/404、5xx.html
如果想要返回页面;就会找error视图【StaticView】。(默认是一个白页)
4、异常处理步骤流程
演示过程模拟出错数学异常
4.1、执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束;并且用 dispatchException
4.2、进入视图解析流程(页面渲染?)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
4.3、mv = processHandlerException;处理handler发生的异常,处理完成返回ModelAndView;
● 4.3.1、遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】(重要)
● 4.3.2、系统默认的 异常处理解析器;
4.3.2.1、DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,
并且返回null;
4.3.2.2、默认没有任何人能处理异常,所以异常会被抛出
■ 4.3.2.2.1、如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的
BasicErrorController处理
■ 4.3.2.2.2、解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。
■ 3、默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html
■ 4、模板引擎Thymeleaf最终响应这个页面 error/500.html
总结 :出现异常首先会用系统的默认错误解析器,但是以上没有能处理数学异常的处理器,所以只是保存了异常的信息,下次调用出错直接能找到。接下来就发送/error路劲返回错误信息页面
9、Web原生组件注入(Servlet、Filter、Listener)
1、使用Servlet API
@ServletComponentScan(basePackages = “com.atguigu.admin”) :识别到原生Servlet组件都放在那里,默认不识别需要手动配置一下
@ServletComponentScan
@SpringBootApplication
public class Boot05WebAdminApplication {
public static void main(String[] args) {
SpringApplication.run(Boot05WebAdminApplication.class, args);
}
}
@WebServlet(urlPatterns = “/my”):效果:直接响应,没有经过Spring的拦截器?
@WebServlet(urlPatterns = "/my")//访问路径
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("666");
}
}
@WebFilter(urlPatterns={"/css/","/images/"})
比如我们过滤静态页面
@Slf4j
@WebFilter(urlPatterns = {"/css/*","/images/*"})
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("过滤器初始化");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("过滤器正在工作");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
log.info("过滤器销毁");
}
}
@WebListener
比如监听当前项目启动
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MyServletContextListener监听当前项目正在初始化");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("项目已销毁");
}
}
为什么@WebServlet(urlPatterns = “/my”):效果:直接响应,没有经过Spring的拦截器?
扩展:DispatchServlet 如何注册进来
● 容器中自动配置了 DispatcherServlet 属性绑定到 WebMvcProperties;对应的配置文件配置项是 spring.mvc。可通过配置项在application.properties中配置
● 通过 ServletRegistrationBean 把 DispatcherServlet 配置进来。
● 默认映射的是 / 路径。
原生的Tomcat-Servlet;
多个Servlet都能处理到同一层路径,精确优选原则 如果访问路径是/my/1那么就访问ServletB,访问路径是/my/2就由ServletA来处理
ServletA: /my/
ServletB: /my/1
由此可知我们访问http://localhost:8889/my就由原生的Servlet处理,DispatcharServlet就没处理,所以sprin框架的用户身份验证的拦截器就不好使
2、使用RegistrationBean(推荐这种方式注入容器)
ServletRegistrationBean, FilterRegistrationBean, and ServletListenerRegistrationBean
@Configuration
public class MyRegistConfig {
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet,"/my","/my02");
}
@Bean
public FilterRegistrationBean myFilter(){
MyFilter myFilter = new MyFilter();
// return new FilterRegistrationBean(myFilter,myServlet()); 拦截Servlet规定的/my,/my02路径
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
MySwervletContextListener mySwervletContextListener = new MySwervletContextListener();
return new ServletListenerRegistrationBean(mySwervletContextListener);
}
}
10、嵌入式Servlet容器
1、切换嵌入式Servlet容器
● 默认支持的webServer
○ Tomcat, Jetty, or Undertow
○ ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器
● 切换服务器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
● 原理
○ SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
○ web应用会创建一个web版的ioc容器 ServletWebServerApplicationContext
○ ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory(Servlet 的web服务器工厂---> Servlet 的web服务器)
○ SpringBoot底层默认有很多的WebServer工厂;TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory
○ 底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration
○ ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
○ ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
○ TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize---this.tomcat.start();
○ 内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)
●
2、定制Servlet容器
● 实现 WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>
○ 把配置文件的值和ServletWebServerFactory 进行绑定
● 修改配置文件 server.xxx
● 直接自定义 ConfigurableServletWebServerFactory
xxxxxCustomizer:定制化器,可以改变xxxx的默认规则
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;
@Component
public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
@Override
public void customize(ConfigurableServletWebServerFactory server) {
server.setPort(9000);
}
}
11、定制化原理
1、定制化的常见方式
我们需要修改spring boot底层的配置有这几种方式:
● 修改配置文件;
● 1、xxxxxCustomizer;(了解)
● 2、(了解)编写自定义的配置类 xxxConfiguration;+ @Bean替换、增加容器中默认组件;如增加视图解析器 、异常处理解析器
●3、(重要,记住这个方式) Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能;+ @Bean可以给容器中再扩展一些组件
如:xxxxxCustomizer
@Configuration
public class AdminWebConfig implements WebMvcConfigurer
● @EnableWebMvc + WebMvcConfigurer —— @Bean 可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能,一但用这个注解,默认的组件都会不好使,比如静态资源访问不到
○ 原理
○ 1、WebMvcAutoConfiguration 默认的SpringMVC的自动配置功能类。静态资源、欢迎页.....
○ 2、一旦使用 @EnableWebMvc 、。会 @Import(DelegatingWebMvcConfiguration.class)
○ 3、DelegatingWebMvcConfiguration 的 作用,只保证SpringMVC最基本的使用
■ 把所有系统中的 WebMvcConfigurer 拿过来。所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效
■ 自动配置了一些非常底层的组件。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取
■ public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
○ 4、WebMvcAutoConfiguration 里面的配置要能生效 必须 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
○ 5、@EnableWebMvc 导致了 WebMvcAutoConfiguration 没有生效。
● ... ...
2、原理分析套路
场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties – 绑定配置文件项