【02】SpringBoot2核心技术-核心功能—文件类型_web开发
2. SpringBoot2核心技术-核心功能
1、文件类型
1.1 properties
同以前的properties用法
1.2 yaml
1.2.1 简介
YAML 是 "YAML Ain't Markup Language"(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)。
非常适合用来做以数据为中心的配置文件(yaml注重的是数据本身,而不是以标记语言为重点)
补充:
XML主要应用在应用程序之间交换结构化的数据,易于机器识别处理,但是自身复杂、冗余的结构不利于阅读;
JSON通常可以用于替代XML,相比于XML,JSON具有小巧和易于阅读的优势,常用在服务器和网页之间传输数据,也用于配置文件;
XML和JSON都具有良好的机器可读性,YAML在易于人阅读上做的更加出色,如今很多配置文件都使用YAML,和Python相似的缩进样式也得到Python使用者的好感(比如我),作为一名网工在很多地方都能看到YAML的身影,比如Ansible(比较火热的自动化工具);
对比:
-
YAML:
user: name: alex location: city: Shanghai country: China roles: - admin - user
-
JSON:
{ "user":{ "name":"alex", "location":{ "city":"Shanghai", "country":"China" }, "roles":[ "admin", "user" ] } }
-
XML:
<?xml version="1.0" encoding="UTF-8" ?> <user> <name>alex</name> <location> <city>Shanghai</city> <country>China</country> </location> <roles>admin</roles> <roles>user</roles> </user>
1.2.2 基本语法
-
key: value;kv之间有空格
-
数组,使用一个短斜线加一个空格表示一个数组项;
-
大小写敏感
-
使用缩进表示层级关系(类似于Python)
-
缩进不允许使用tab,只允许空格
-
缩进的空格数不重要,只要相同层级的元素左对齐即可
-
'#'表示注释
-
字符串无需加引号,如果要加,' '与" "表示字符串内容 会被 转义/不转义
1.2.3 数据类型
- 字面量:单个的、不可再分的值。date、boolean、string、number、null
k: v
- 对象:键值对的集合。map、hash、set、object
行内写法: 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 示例
Person类对象:
@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;
}
Pet类对象:
@Data
public class Pet {
private String name;
private Double weight;
}
在resources文件夹下创建一个application.yaml(.yml)
person:
# 单引号会将 \n作为字符串输出 双引号会将\n 作为换行输出
# 双引号不会转义,单引号会转义
# 转义字符在单引号中无效,在双引号中有效
userName: 'zhangsan \n 李四'
boss: true
birth: 2019/12/9
age: 18
# interests: [篮球,足球]
interests:
- 篮球
- 足球
- 18
animal: [阿猫,阿狗]
# score:
# english: 80
# math: 90
score: {english:80,math:90}
salarys:
- 9999.98
- 9999.99
pet:
name: 阿狗
weight: 99.99
allPets:
sick:
- {name: 阿狗,weight: 99.99}
- name: 阿猫
weight: 88.88
- name: 阿虫
weight: 77.77
health:
- {name: 阿花,weight: 199.99}
- {name: 阿明,weight: 199.99}
user-name: zhangsan
#推荐在yaml文件内设置配置属性(有提示功能)
#spring:
# banner:
# image:
# bitdepth: 4
# cache:
# type: redis
# redis:
# time-to-live: 11000
1.3 配置提示
自定义的类和配置文件绑定时,一般是没有提示的。在yaml文件中,可以发现进行spring的配置绑定时,会有相应提示及缩进。
在pom.xml文件中导入依赖:加入配置处理器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
但是配置处理器只是方便开发人员进行开发,重新打包的时候不要打包进去,毕竟与业务功能无关,打包的时候排除掉这个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>
2、Web开发
2.1 SpringMVC自动配置概览
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)
The auto-configuration adds the following features on top of Spring’s defaults:
-
Inclusion of
ContentNegotiatingViewResolver
andBeanNameViewResolver
beans. -
- 内容协商视图解析器和BeanName视图解析器
-
Support for serving static resources, including support for WebJars (covered later in this document)).
-
- 静态资源(包括webjars)
-
Automatic registration of
Converter
,GenericConverter
, andFormatter
beans. -
- 自动注册
Converter,GenericConverter,Formatter
- 自动注册
-
Support for
HttpMessageConverters
(covered later in this document). -
- 支持
HttpMessageConverters
(后来我们配合内容协商理解原理)
- 支持
-
Automatic registration of
MessageCodesResolver
(covered later in this document). -
- 自动注册
MessageCodesResolver
(国际化用)
- 自动注册
-
Static
index.html
support. -
- 静态index.html 页支持
-
Custom
Favicon
support (covered later in this document). (现在的版本已经取消这条了) -
- 自定义
Favicon
- 自定义
-
Automatic use of a
ConfigurableWebBindingInitializer
bean (covered later in this document). -
- 自动使用
ConfigurableWebBindingInitializer
,(DataBinder负责将请求数据绑定到JavaBean上)
- 自动使用
If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own
@Configuration
class of typeWebMvcConfigurer
but without@EnableWebMvc
.不用@EnableWebMvc注解。使用
**@Configuration**
+**WebMvcConfigurer**
自定义规则
If you want to provide custom instances of
RequestMappingHandlerMapping
,RequestMappingHandlerAdapter
, orExceptionHandlerExceptionResolver
, and still keep the Spring Boot MVC customizations, you can declare a bean of typeWebMvcRegistrations
and use it to provide custom instances of those components.声明
**WebMvcRegistrations**
改变默认底层组件
If you want to take complete control of Spring MVC, you can add your own
@Configuration
annotated with@EnableWebMvc
, or alternatively add your own@Configuration
-annotatedDelegatingWebMvcConfiguration
as described in the Javadoc of@EnableWebMvc
.使用
**@EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC**
2.2 简单功能分析
2.2.1 静态资源访问
1、静态资源目录
只要静态资源放在类路径classpath(即resources文件夹)下: called /static
(or /public
or /resources
or /META-INF/resources
),也就是类路径下这四种命名的文件夹目录下,存放了静态资源。
-
访问 : 当前项目根路径/ + 静态资源名
-
原理: 静态映射 /**
By default, resources are mapped on
/**
(默认,静态资源是映射所有请求的)请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面。
所以,当请求为 /bug.jpg时,若有controller,则会执行controller的相关逻辑,没有才会显示图片。(bug.jpg静态资源存在时)
改变默认的静态资源路径:
spring:
resources:
static-locations: [classpath:/haha/]
2、静态资源访问前缀
默认无前缀
修改静态资源访问前缀:
spring:
mvc:
static-path-pattern: /res/**
当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找
/bug.jpg 变成 /res/bug.jpg
3、Webjars
WebJars是将客户端(浏览器)静态资源(JavaScript,Css等)打成jar包文件,以对资源进行统一依赖管理。WebJars的jar包部署在Maven中央仓库上。
自动映射 /webjars/**
<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.2 欢迎页支持
Spring Boot supports both static and templated welcome pages. It first looks for an
index.html
file in the configured static content locations(静态资源路径下). If one is not found, it then looks for anindex
template(或者是能够处理index请求,跳转到index模板). If either is found, it is automatically used as the welcome page of the application.
-
静态资源路径下存放index.html
-
- 可以配置静态资源路径
- 但是不可以配置静态资源的访问前缀,会否则导致 index.html不能被默认访问
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致welcome page功能失效
resources:
static-locations: [classpath:/haha/]
- controller能处理/index
2.2.3 自定义Favicon
网页小图标
favicon.ico 放在静态资源目录下即可。
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致 Favicon 功能失效
2.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 {}
- 给容器中配了什么:WebMvcProperties.class, WebProperties.class
@Configuration(
proxyBeanMethods = false
)
@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
@EnableConfigurationProperties({WebMvcProperties.class, WebProperties.class})
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {}
-
配置文件的相关属性和xxx进行了绑定。WebMvcPropertiesspring.mvc、WebPropertiesspring.resources
- WebMvcProperties.class
@ConfigurationProperties( prefix = "spring.mvc" ) public class WebMvcProperties {}
- WebProperties.class
@ConfigurationProperties("spring.web") public class WebProperties {}
-
WebMvcAutoConfiguration.class配置类只有一个有参构造器:
//有参构造器所有参数的值都会从容器中确定
//ResourceProperties resourceProperties:获取和spring.resources绑定的所有的值的对象
//WebMvcProperties mvcProperties:获取和spring.mvc绑定的所有的值的对象
//ListableBeanFactory beanFactory:Spring的beanFactory
//HttpMessageConverters:找到所有的HttpMessageConverters
//ResourceHandlerRegistrationCustomizer:找到 资源处理器的自定义器。=========
//DispatcherServletPath
//ServletRegistrationBean 给应用注册Servlet、Filter....
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();
}
- WebMvcAutoConfiguration.class配置类资源处理的默认规则
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});
}
});
}
}
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
this.addResourceHandler(registry, pattern, (registration) -> {
registration.addResourceLocations(locations);
});
}
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, Consumer<ResourceHandlerRegistration> customizer) {
if (!registry.hasMappingForPattern(pattern)) {
ResourceHandlerRegistration registration = registry.addResourceHandler(new String[]{pattern});
customizer.accept(registration);
registration.setCachePeriod(this.getSeconds(this.resourceProperties.getCache().getPeriod()));
registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
this.customizeResourceHandlerRegistration(registration);
}
}
spring:
# mvc:
# static-path-pattern: /res/**
resources:
add-mappings: false 禁用所有静态资源规则
通过这段获取静态资源路径的方法,从而发现默认的静态路径为:"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"
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();
}
- WebMvcAutoConfiguration.class配置类中关于欢迎页的处理规则
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
return welcomePageHandlerMapping;
}
注意其中的this.getWelcomePage()获取欢迎页的方法:
private Resource getWelcomePage() {
String[] var1 = this.resourceProperties.getStaticLocations();
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
String location = var1[var3];
Resource indexHtml = this.getIndexHtml(location);
if (indexHtml != null) {
return indexHtml;
}
}
ServletContext servletContext = this.getServletContext();
if (servletContext != null) {
return this.getIndexHtml((Resource)(new ServletContextResource(servletContext, "/")));
} else {
return null;
}
}
private Resource getIndexHtml(String location) {
return this.getIndexHtml(this.resourceLoader.getResource(location));
}
private Resource getIndexHtml(Resource location) {
try {
Resource resource = location.createRelative("index.html");
if (resource.exists() && resource.getURL() != null) {
return resource;
}
} catch (Exception var3) {
}
return null;
}
2.3 请求参数处理(重点)
2.3.1 请求映射
1、restful风格请求的使用与原理
-
@xxxMapping;
-
Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
-
- 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
- 现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
-
-
核心Filter:HiddenHttpMethodFilter
浏览器form表单只支持GET与POST请求,而DELETE、PUT等method并不支持,spring3.0添加了一个过滤器,可以将这些请求转换为标准的http方法,使得支持GET、POST、PUT与DELETE请求,该过滤器为HiddenHttpMethodFilter。
- 为什么要使用过滤器:因为大多数浏览器的表单提交只支持GET和POST请求,不支持PUT、DELETE等请求
-
-
-
-
用法: 表单method=post,隐藏域 _method=put
在WebMvcAutoConfiguration.class文件中,有一个hiddenHttpMethodFilter()方法,进入该方法之后,可以看见有一个参数_method,通过这个过滤器将PUT、DELETE、PATCH请求进行转换:
public class HiddenHttpMethodFilter extends OncePerRequestFilter { private static final List<String> ALLOWED_METHODS; public static final String DEFAULT_METHOD_PARAM = "_method"; private String methodParam = "_method"; ......... }
正确使用方法:当我们使用post方法时,过滤器才会进行判断_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); }
-
SpringBoot中手动开启
虽然在新版本的SpringBoot里面没有matchIfMissing参数,但默认是false值,也就是没有开启,需要开发人员手动开启才行。
application.yaml中:
spirng: mvc: hiddenmethod: filter: enabled: true
@Bean @ConditionalOnMissingBean({HiddenHttpMethodFilter.class}) @ConditionalOnProperty( prefix = "spring.mvc.hiddenmethod.filter", name = {"enabled"} ) public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); }
-
-
-
- 扩展:如何把_method 这个名字换成我们自己喜欢的。
-
@Configuration(proxyBeanMethods = false) public class WebConfig /*implements WebMvcConfigurer*/ { @Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter(){ HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter(); methodFilter.setMethodParam("_m"); return methodFilter; } }
-
-
控制层写法简洁
-
-
Rest原理(表单提交要使用REST的时候)
- 表单提交会带上_method=PUT
- 请求过来被HiddenHttpMethodFilter拦截
-
请求是否正常,并且是POST
-
-
获取到_method的值。
-
兼容以下请求;PUT.DELETE.PATCH(小写的话,会转换为大写)
-
-
-
原生request(post),包装模式requesWrapper重写了原生request的getMethod方法,返回的是传入的值。
-
过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。
-
-
Rest使用客户端工具,
- 如Android、PostMan能够直接发送Put、Delete等方式请求,无需Filter。所以HiddenHttpMethodFilter是选择性开启,根据客户端种类选择使用,因为springboot大多数是做微服务开发,前后端分离,不做页面,只提供接口返回Json数据,不需要交互页面。
spring: mvc: hiddenmethod: filter: enabled: true #开启页面表单的Rest功能
2.3.2 请求映射原理(重点:MVC映射处理原理)
SpringBoot请求映射的底层还是MVC,所以DispatcherServlet是处理所有请求的开始,关于DispatcherServlet.class的继承树结构:
依照继承关系,层层调用
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 {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
// 找到当前请求使用哪个Handler(Controller的方法)处理
mappedHandler = this.getHandler(processedRequest);
//HandlerMapping:处理器映射。/xxx->>xxxx
- RequestMappingHandlerMapping:上图中表示在handlerMappings里面,下图中能够发现其保存了所有@RequestMapping 和handler的映射规则。
所有的请求映射都在HandlerMapping中
-
SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。
- 也就是说,当有请求 / 的时候,默认会自动跳转index页面
@Bean public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern()); welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider)); welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations()); return welcomePageHandlerMapping; }
- 访问 / 能访问到index.html(handler方法)
-
SpringBoot自动配置了默认的RequestMappingHandlerMapping(在WebMvcAutoConfiguration.class文件中)
@Bean @Primary public RequestMappingHandlerMapping requestMappingHandlerMapping(@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager, @Qualifier("mvcConversionService") FormattingConversionService conversionService, @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) { return super.requestMappingHandlerMapping(contentNegotiationManager, conversionService, resourceUrlProvider); }
-
请求进来,挨个尝试所有的HandlerMapping看是否有请求信息
-
- 如果有就找到这个请求对应的handler
- 如果没有就是下一个 HandlerMapping
- 下面的代码,就是遍历所有的handlermapper进行匹配
-
@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);//找到该请求对应的handler(即,控制器方法) if (handler != null) { return handler; } } } return null; }
-
我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping(保存了哪个请求对应哪个控制器方法来处理)。自定义 HandlerMapping
2.3.3 普通参数与基本注解
注解
1. 常用注解
@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody
- @PathVariable:路径变量,获取请求路径上的指定参数值
- @RequestHeader:获取请求头中指定参数值
- @RequestParam:获取请求参数中的指定值
- @CookieValue:获取cookie中的指定参数值
- @RequestBody:获取请求体中表单数据
- 对于各个注解,可以使用Map<String,String>获取所有的值,例如:
- @PathVariable Map<String,String> pv
- @RequestHeader Map<String,String> header
- @RequestParam Map<String,String> params
@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;
}
}
2. @RequestAttribute
@Controller
public class RequestController {
@GetMapping("/goto") // @RequestMapping("/goto", method = RequestMethod.GET)
public String goToPage(HttpServletRequest request){
request.setAttribute("msg","成功了...");
request.setAttribute("code",200);
return "forward:/success"; //重定向:转发到 /success请求
}
@GetMapping("/params")
public String testParam(Map<String,Object> map,
Model model,
HttpServletRequest request,
HttpServletResponse response){
map.put("hello","world666");
model.addAttribute("world","hello666");
request.setAttribute("message","HelloWorld");
Cookie cookie = new Cookie("c1","v1");
response.addCookie(cookie);
return "forward:/success";
}
@ResponseBody // 将Java类对象转换为字符串
@GetMapping("/success")
public Map success(@RequestAttribute(value = "msg",required = false) String msg,
@RequestAttribute(value = "code",required = false)Integer code,
HttpServletRequest request){
Object msg1 = request.getAttribute("msg");
Map<String,Object> map = new HashMap<>();
Object hello = request.getAttribute("hello");
Object world = request.getAttribute("world");
Object message = request.getAttribute("message");
map.put("reqMethod_msg",msg1);
map.put("annotation_msg",msg);
map.put("hello",hello);
map.put("world",world);
map.put("message",message);
return map;
}
}
3. @MatrixVariable和@UrlPathHelper
- 一个问题:
/cars/{path}?xxx=xxx&aaa=ccc 请求参数的方式,queryString 查询字符串。@RequestParam;
/cars/sell;low=34;brand=byd,audi,yd 矩阵变量的方式
页面开发,cookie禁用了,session里面的内容怎么使用;
session、cookie原理:
session.set(a,b)---> jsessionid(每一个session都有一个id) ---> id保存在cookie ----> 每次发请求携带cookie由于cookie被禁用了,于是无法通过cookie获取到jsessionid,从而无法从session中取值。
解决方案:用矩阵变量的方式带上jsessionid的值,路径重写
url重写:/abc;jsesssionid=xxxx
也就是把cookie的值使用矩阵变量的方式进行传递.
-
矩阵变量的使用:
url中 ; 后面所有的参数都是矩阵变量
, 分隔 或者 使用;让参数多次赋值,从而携带多个参数
<a href="/cars/sell;low=34;brand=byd,audi,yd">@MatrixVariable(矩阵变量)</a> <a href="/cars/sell;low=34;brand=byd;brand=audi;brand=yd">@MatrixVariable(矩阵变量)</a> <!-- url:/boss/1/2 --> <a href="/boss/1;age=20/2;age=10">@MatrixVariable(矩阵变量)/boss/{bossId}/{empId}</a>
-
获取矩阵变量的值
@MatrixVariable:获取矩阵变量的值
@RestController public class ParameterTestController { //1、语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd //2、SpringBoot默认是禁用了矩阵变量的功能,需要在在springboot底层springmvc进行配置 // 手动开启:原理,对于路径的处理。springboot是使用UrlPathHelper类进行url的解析。 // 其中的removeSemicolonContent变量默认为true,也就是移除url中的分号,为了支持矩阵变量的,需要设置为false @GetMapping("/cars/{path}") public Map carsSell(@MatrixVariable("low") Integer low, @MatrixVariable("brand") List<String> brand, @PathVariable("path") String path){ Map<String,Object> map = new HashMap<>(); map.put("low",low); map.put("brand",brand); map.put("path",path);// 测试path是:sell 还是 sell;low=34;brand=byd,audi,yd return map; } }
//3、矩阵变量必须有url路径变量才能被解析(即下文中的 bossId和empId // url路径: /boss/1;age=20/2;age=10 @GetMapping("/boss/{bossId}/{empId}") public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge, @MatrixVariable(value = "age",pathVar = "empId") Integer empAge){ Map<String,Object> map = new HashMap<>(); map.put("bossAge",bossAge); map.put("empAge",empAge); return map; }
- 如何配置开启矩阵变量的使用
springboot默认是将url中的逗号给去除,需要手动在springmvc中进行设置,而在WebMvcAutoConfiguration.class自动配置类中,是通过configurePathMatch()方法中的UrlPathHelper对url处理进行配置的。而UrlPathHelper.class中只有将removeSemicolonContent手动设置为false,才能使用矩阵变量。
removeSemicolonContent:private boolean removeSemicolonContent = true; // 默认为true
/** * Set if ";" (semicolon) content should be stripped from the request URI. * <p>Default is "true". * (设置如果“;” (分号)内容应该从请求 URI 中删除。 * 默认值为“真”) */ public void setRemoveSemicolonContent(boolean removeSemicolonContent) { checkReadOnly(); this.removeSemicolonContent = removeSemicolonContent; }
-
手动配置使用矩阵变量的两种方法
第一种:在配置类中,实现WebMvcConfigurer接口,重写configurePathMatch()方法
@Configuration public class MyConfig implements WebMvcConfigurer { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 移除url中分号:设置为false,不移除;这样,才能从url中取出矩阵变量的值 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } }
第二种:在配置类中,用户使用WebMvcConfigurer定制化SpringMVC的功能,并添加到容器中
@Configuration public class WebConfig /*implements WebMvcConfigurer*/ { // WebMvcConfigurer定制化SpringMVC的功能 @Bean public WebMvcConfigurer webMvcConfigurer(){ @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 不移除;后面的内容。矩阵变量功能就可以生效 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } }
2.3.4 各种类型参数解析处理原理(重点)
1. 处理请求的大致流程(总览)
-
第一步:DispatcherServlet拦截请求,并在doDispatch()方法中进行处理,其中通过HandlerMapping中找到能处理当前请求的Handler(Controller.method(),即控制器方法)
// Determine handler for the current request. 确定处理当前请求的处理器方法 mappedHandler = getHandler(processedRequest);//按顺序尝试所有处理程序映射
-
第二步:为当前 Handler 找一个适配器 HandlerAdapter(是一个接口,实现这个适配器,需要指明这个适配器支持哪种处理器方法,以及如何调用)(因为mvc是通过反射机制使用控制器方法并确定参数的值,这一复杂的过程交由adapter来做)。此外,handler适配器有多种种类(四种,如下),这里是返回RequestMappingHandlerAdapter这一类型的handler适配器。
// Determine handler adapter for the current request. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
-
第三步:最后适配器执行目标方法并确定方法参数的每一个值
// Actually invoke the handler. 真正执行handler处理器方法 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
2. 处理器适配器-HandlerAdapter
一共有四种HandlerAdapter分别处理不同的handler(控制器方法),会进行判断的
0 - RequestMappingHandlerAdapter:支持处理被@RequestMapping注解的方法
1 - HandlerFunctionAdapter:支持函数式编程的方法
....
3. 执行目标方法
调用执行处理器的方法:mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
其中执行处理器方法中,底层待参数确定后,真正执行的方法:mav = invokeHandlerMethod(request, response, handlerMethod);
-
DispatcherServlet
public class DispatcherServlet extends FrameworkServlet { protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ModelAndView mv = null; ... // Determine handler for the current request. 找处理器适配器 mappedHandler = getHandler(processedRequest); if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } // Determine handler adapter for the current request. 为当前请求找处理器适配器 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); ... //本节重点 // Actually invoke the handler. 调用执行处理器的方法 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
-
处理Request请求的处理器适配器方法:HandlerAdapter接口实现类RequestMappingHandlerAdapter(是用来处理被@RequestMapping注解修饰的方法)
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { ... //AbstractHandlerMethodAdapter类的方法,RequestMappingHandlerAdapter继承AbstractHandlerMethodAdapter public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return handleInternal(request, response, (HandlerMethod) handler); } @Override protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ModelAndView mav; //handleInternal的重要部分 mav = invokeHandlerMethod(request, response, handlerMethod);// 底层真正执行处理器方法的方法,解析在下面(涉及到参数解析器和返回值解析器) //... return mav; } }
4. 参数解析器-HandlerMethodArgumentResolver
参数解析器的目的:确定将要执行的目标方法的每一个参数的值是什么
27种控制器参数类型
SpringMVC目标方法能写多少种参数类型,取决于参数解析器argumentResolvers
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {// 参数解析器,解析在下面
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
...
关于参数解析器的取值:方法中参数被注解修饰的27种种类,也就是Spring支持哪些被注解修饰的参数,比如@RequestParam修饰参数,就能被解析
this.argumentResolvers在afterPropertiesSet()方法内初始化:
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
@Nullable
private HandlerMethodArgumentResolverComposite argumentResolvers;
@Override
public void afterPropertiesSet() {
...
if (this.argumentResolvers == null) { //初始化argumentResolvers
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
...
}
//初始化了那26种实现HandlerMethodArgumentResolver接口的参数解析器
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() { // 注意HandlerMethodArgumentResolver
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30);
...
}
HandlerMethodArgumentResolver:
- 当前解析器是否支持解析这种参数
- 支持就调用 resolveArgument
5. 返回值处理器-returnValueHandlers
returnValueHandlers
15种控制器方法的返回值类型
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {// 返回值处理器
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
...
}
this.returnValueHandlers在afterPropertiesSet()方法内初始化:
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
@Nullable
private HandlerMethodReturnValueHandlerComposite returnValueHandlers;
@Override
public void afterPropertiesSet() {
...
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
//初始化了一堆的实现HandlerMethodReturnValueHandler接口的
private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20);
// Single-purpose return value types
handlers.add(new ModelAndViewMethodReturnValueHandler());
handlers.add(new ModelMethodProcessor());
handlers.add(new ViewMethodReturnValueHandler());
.......
}
}
handler有15种返回值类型(控制器方法执行完,return返回的数据类型)
6. 如何确定目标方法每一个参数的值
重点分析ServletInvocableHandlerMethod
的getMethodArgumentValues
方法
============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;
}
// 判断当前方法中的参数是否是25种参数类型中的
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;
}
this.resolvers的类型为HandlerMethodArgumentResolverComposite
两个主要方法:
- 解析参数的值方法
- 判断方法所有参数匹配的参数解析器
public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return getArgumentResolver(parameter) != null;
}
// 方法:解析参数的值
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unsupported parameter type [" +
parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
}
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
// 方法:判断当前参数是由哪个参数解析器解析
@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);//找到了,resolver就缓存起来,方便稍后resolveArgument()方法使用
break;
}
}
}
return result;
}
}
解析参数的值:调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可
7. 总结
一个请求发送到DispatcherServlet后的具体处理流程(重点是对handler控制器方法的处理)。
2.3.5 Servlet API参数解析原理(重点)
WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
-
与上面同,直接跳到参数解析部分,即HandlerMethodArgumentResolverComposite
-
查找支持解析的解析器,使用getArgumentResolver方法:判断当前参数需要由哪个参数解析器解析
- ServletRequestMethodArgumentResolver
-
发现有支持解析的解析器,那么就进入这个参数解析器进行参数解析,能够发现支持Servlet的参数解析如下:
-
进行参数解析,并返回原生Request
2.3.6 复杂参数解析原理(Model、Map原理)(重点)
Map、Model(map、model里面的数据会被放在request的请求域中,相当于使用了request.setAttribute()方法 )、Errors/BindingResult、RedirectAttributes( 重定向携带数据)、ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder
-
给request域中添加数据的三种方法
- map.put("map", "map666");
- model.addAttribute("model", "model666");
- request.setAttribute("request", "request666");
-
选择合适的参数解析器,比如第一个参数是Model时
-
当前需要解析的参数
-
当解析的第一个参数是Map类型时。通过mavContainer.getModel(); ——> 返回BindingAwareModelMap(即是Model,也是Map) ——> 继承于ExtendedModelMap ——> extends ModelMap implements Model
-
当开始解析第二个参数Model类型时。
- 选择ModelMethodProcessor参数解析器
- 开始参数解析
- 解析发现Model也是返回了mavContainer.getModel();
-
解析完所有参数后,args中有四个对象,其中前两个是同一个对象,也就是最开始的Map和Model参数,然后是传入的request和response
-
接下来是如何处理控制器方法返回值,怎么将参数随转发请求发送出去:ServletInvocableHandlerMethod类中的invokeAndHandle方法
-
进入返回值处理方法:handleReturnValue
-
ModelAndViewContainer mavContainer:模型视图容器
-
绑定视图名称
-
Map和Model的数据也在mavContainer中
-
-
接下来就是对mavContainer的相关处理了,返回一个ModelAndView对象
-
对ModelAndView对象处理,渲染视图..... 这块原理在 2.5.1视图解析 有原理解析
-
最重要的一点是在InternalResourceView类中的exposeModelAsRequestAttributes方法
-
将需要添加到请求域中的参数,用model进行遍历添加
-
-
-
总结
Map和Model底层都是相同的对象,即BindingAwareModelMap对象,添加到Map和Model中的数据都会添加到同一个BindingAwareModelMap对象中。
2.3.7 自定义对象参数解析原理(重点)
1、数据绑定功能
我们知道springboot在封装POJO对象的时候,可以进行自动类型转换,可以级联封装,即,数据绑定功能:页面提交的请求数据(GET、POST)都可以和对象属性进行绑定。如下:
/**
* 姓名: <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;
}
表单提交的数据可以直接封装成对象,并可以通过Json数据的格式在页面打印
// 数据绑定
@PostMapping("/saveperson")
Person savePerson(Person person){
return person;
}
测试封装POJO;
<form action="/saveperson" method="post">
姓名: <input name="userName" value="zhangsan"/> <br/>
年龄: <input name="age" value="18"/> <br/>
生日: <input name="birth" value="2019/12/10"/> <br/>
宠物姓名:<input name="pet.name" value="阿猫"/><br/>
宠物年龄:<input name="pet.age" value="5"/>
<input type="submit" value="保存"/>
</form>
2、原理解析
-
如前文一样,直接来看参数解析器这块,自定义对象的参数解析器是由ServletModelAttributeMethodProcessor进行解析处理
public class ServletModelAttributeMethodProcessor extends ModelAttributeMethodProcessor
-
参数解析时:需要判断是不是简单参数类型,也就是判断是不是自定义的对象参数
-
ModelAttributeMethodProcessor中解析参数的方法:resolveArgument
- 在这个例子中,就是创建了一个空的Person对象
-
数据绑定
WebDataBinder web数据绑定器:将请求参数的值绑定到指定的 Java Bean对象中
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
.......
if (bindingResult == null) {
// Bean property binding and validation;
// skipped in case of binding failure on construction.
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); //创建一个web数据绑定器
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();
}
........
}
绑定器绑定数据的原理:使用convertters转换器将请求参数中的数据转成指定的数据类型,并绑定到target中的自定义Java Bean对象中去。
bindRequestParameters(binder, webRequest); // 绑定数据的操作方法
GenericConversionService:在每转换一个参数值的时候,都会去遍历所有转换器converter,查找可以将这个数据类型(request带来的参数类型)进行指定转换的转换器(JavaBean中的属性类型),并进行转换绑定。
3、自定义Convert
-
现在有这样的一个问题,如果按照以下进行封装的话,会报错,提示没有一个convert能够将字符串“啊猫,3”给转成Pet对象中的属性。这就需要我们自定义一个convert转换器按照我们的规则进行转换。
测试封装POJO;
<form action="/saveuser" method="post">
宠物: <input name="pet" value="啊猫,3"/>
<input type="submit" value="保存"/>
</form>
- 解决方案:新增加一个自定义的converter转换器
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {
@Override
public Pet convert(String source) { //source是request传来需要封装绑定的参数
// 啊猫,3
if(!StringUtils.isEmpty(source)){
Pet pet = new Pet();
String[] split = source.split(",");// 分隔
pet.setName(split[0]);
pet.setAge(Integer.parseInt(split[1]));
return pet;
}
return null;
}
});
}
};
}
2.4 数据响应与内容协商
2.4.1 响应JSON
1、jackson.jar + @ResponseBody
- web场景启动器已经引入了Json场景:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.6.1</version>
<scope>compile</scope>
</dependency>
- json环境启动器引入的依赖:jackson.jar
<dependencies>
.......
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.13.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.13.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
<version>2.13.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
-
只需要在方法上标注@ResponseBody,底层就调用对应的HttpMessageConverter消息转换器进行转换,就可以给前端自动返回JSON数据
-
原理解析:
-
在ServletInvocableHandlerMethod类中处理返回值
-
判断当前需要使用哪个返回值处理器进行处理:
this.returnValueHandlers.handleReturnValue(...);
- 先判断是不是异步的返回值
-
返回值处理器判断是否支持这种类型返回值 supportsReturnType
-
RequestResponseBodyMethodProcessor 这个返回值处理器,是可以处理标了@ResponseBody 注解的类的返回值。
-
RequestResponseBodyMethodProcessor返回值处理器中的handleReturnValue方法
@Override public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { mavContainer.setRequestHandled(true); ServletServerHttpRequest inputMessage = createInputMessage(webRequest);// 封装原生request请求,为请求 ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);// 封装原生request请求,为响应 // Try even with null return value. ResponseBodyAdvice could get involved. // 使用消息转换器进行写出操作 writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage); //底层转换JSON的方法 }
-
-
2. 利用 MessageConverters 进行处理,将数据写为JSON
-
接下来的代码涉及到了内容协商
-
内容协商:浏览器默认会以请求头的方式通知服务器,支持接收哪种类型的内容。
-
浏览器能够接收的类型:**acceptableTypes = getAcceptableMediaTypes(request); **下面是某浏览器通过请求头发出的:
-
服务器根据自身的能力,觉得能够产生的数据类型:
List
producibleTypes = getProducibleMediaTypes(request, valueType, targetType); -
最终决定出返回值的数据类型:selectedMediaType = mediaType;
-
遍历所有容器底层的 HttpMessageConverter 消息转换器(将Class类型的对象转换为MediaType类型的数据或者相反,例:Person对象转为JSON,或JSON转Person),判断哪个转换器来处理
所有的HttpMessageConverter:
-
这里是MappingJackson2HttpMessageConverter能够处理JSON的相关转换(利用底层的Jackson的ObjectMapper进行转换的),并写出去
-
2、内容协商(待更新 P39-P42)
根据客户端能力的不同,返回与之相适应的数据。(即,可以根据客户端的不同,返回不同的数据)
-
例子:使用postman携带header请求头中Accept字段的不同参数,服务器会返回不同格式的数据
- 如果当前服务器只能返回Json格式数据的话,而客户端的请求头中Accept字段是 application/xml 的话,会报 406 Not Acceptable 错误
2.5 视图解析与模板引擎
2.5.1 视图解析
1. 视图处理方式
2. 视图解析原理的流程
-
首先在DispatcherServlet中交给对应的处理器适配器方法进行处理,也就是 RequestMappingHandlerAdapter
-
当控制器方法返回 视图名称 字符串的时候,需要相应的返回值处理器:ViewNameMethodReturnValueHandler
-
通过ViewNameMethodReturnValueHandler,在接下来的目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面。包括数据和视图地址(自定义类型的参数也是放在这里面的)
-
其判断是否是重定向视图的方法也很简单,视图名称是不是以 redirect 命名的
-
也能看出ModelAndViewContainer中携带了很多内容(包括往域中添加的数据)
-
-
任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)。
-
然后返回到DispatcherServlet类中进行处理,在doDispatch方法中的 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); 处理派发结果(页面如何响应跳转)
-
首先进行页面渲染:render(mv, request, response); 其中mv是ModelAndView类型,包含视图名称和域中数据
-
根据得到的视图名称获取到一个 View 对象【定义了页面的渲染逻辑,因为有自定义的render()渲染方法】
-
进入 resolveViewName() 视图解析:所有的视图解析器进行遍历,尝试是否能够根据当前返回值得到一个View对象。
- 当前是用ContentNegotiatingViewResolver(内容协商视图解析器)进行处理,通过redirect:/main.html --> Thymeleaf new RedirectView(),得到了一个RedirectView
- ContentNegotiationViewResolver 里面包含了下面四种视图解析器,其内部还是利用下面四种视图解析器得到视图对象。
-
通过resolveViewName()方法获得view视图对象后,调用视图对象自定义的render渲染方法
-
由于本次是一个重定向地址:redirect:/main.html 。因此是RedirectView进行渲染,下面进入RedirectView的render处理:
-
- RedirectView中的方法createTargetUrl:获取目标url地址
protected final String createTargetUrl(Map<String, Object> model, HttpServletRequest request) throws UnsupportedEncodingException { // Prepare target URL. StringBuilder targetUrl = new StringBuilder(); String url = getUrl(); // redirect:/main.html Assert.state(url != null, "'url' not set"); if (this.contextRelative && getUrl().startsWith("/")) { // Do not apply context path to relative URLs. targetUrl.append(getContextPath(request)); } targetUrl.append(getUrl()); // 添加进去 // 下面开始设置编码格式 String enc = this.encodingScheme; if (enc == null) { enc = request.getCharacterEncoding(); } if (enc == null) { enc = WebUtils.DEFAULT_CHARACTER_ENCODING; } if (this.expandUriTemplateVariables && StringUtils.hasText(targetUrl)) { Map<String, String> variables = getCurrentRequestUriVariables(request); targetUrl = replaceUriTemplateVariables(targetUrl.toString(), model, variables, enc); } if (isPropagateQueryProperties()) { appendCurrentQueryParams(targetUrl, request); } if (this.exposeModelAttributes) { //如果有一些重定向的数据,那么会以查询参数的方式拼接到url后面 appendQueryProperties(targetUrl, model, enc); } return targetUrl.toString(); }
-
- sendRedirect:将重定向请求发送回HTTP客户端
-
-
2.5.2 模板引擎-Thymeleaf
Springboot支持多种视图模板引擎,在官方文档starter中可以找到有:
- freemarker
- thymeleaf
- groovy-templates
- mustache
1. Thymeleaf简介
Thymeleaf is a modern server-side Java template engine for both web and standalone environments.
Thymeleaf 是一个现代化的服务器端 Java 模板引擎,用于 web 和独立环境。
Thymeleaf's main goal is to bring elegant natural templates to your development workflow — HTML that can be correctly displayed in browsers and also work as static prototypes, allowing for stronger collaboration in development teams.
Thymeleaf 的主要目标是为开发工作流程带来优雅的自然模板ー HTML,它既可以在浏览器中正确显示,也可以作为静态原型工作,从而加强开发团队之间的协作。
With modules for Spring Framework, a host of integrations with your favourite tools, and the ability to plug in your own functionality, Thymeleaf is ideal for modern-day HTML5 JVM web development — although there is much more it can do.
有了 Spring Framework 的模块、大量与您最喜欢的工具集成的功能,以及插入您自己功能的能力,Thymeleaf 是现代 HTML5 JVM web 开发的理想选择ーー尽管它可以做的还有很多。
2. 基本语法
基本语法跟JSP很像
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. 特殊操作
- 无操作: _
9. 设置属性值-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
10. 迭代
<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>
11. 条件运算
<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>
12. 属性优先级
3. Thymeleaf使用
1. 在pom.xml中引入starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2. thymeleaf的自动配置
由于是通过starter引入的thymeleaf,所以会为我们自动配置好thymeleaf,可以找到一个ThymeleafAutoConfiguration.class,自动配置文件。
@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、我们只需要直接开发页面
从自动配置文件中可以发现,thymeleaf的自动配置属性,是放在ThymeleafProperties.class文件中:我们在配置thymeleaf的属性时,需要加上前缀spring.thymeleaf
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
// thymeleaf的默认前缀
public static final String DEFAULT_PREFIX = "classpath:/templates/";
// thymeleaf的默认后缀
public static final String DEFAULT_SUFFIX = ".html";
...
}
3. 页面开发
-
添加thymeleaf的命名空间,才能使用th:标签
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <title>Title</title> </head> <body> <h1 th:text="${msg}">哈哈</h1> <h2> <a href="www.atguigu.com" th:href="${link}">去B站</a> <br/> <a href="www.atguigu.com" th:href="@{link}">去B站2</a> </h2> </body> </html>
@Controller public class ViewTestController { @RequestMapping("/view") public String viewTest(Model model) { model.addAttribute("link", "http://www.bilibili.com"); model.addAttribute("msg", "viewTest"); return "success"; // 因为使用thymeleaf进行html页面渲染,已经有默认前缀后缀了 } }
-
支持动态渲染(静态网页时,显示静态值,一有新数据,就渲染更新后的值)
-
静态时:查看源码可以发现th标签并没有修改值
-
运行起来之后:由于有最新值,被th修饰的标签的属性值会被替换掉
-
2.5.4 拦截器
拦截器配置流程:
- 1、编写一个拦截器实现HandlerInterceptor接口
- 2、拦截器需要注册到容器中(配置类需要实现WebMvcConfigurer接口,并重写addInterceptors方法)
- 3、指定拦截规则(如果是拦截所有请求的话,一定要记住静态资源也会被拦截)
1. HandlerInterceptor
spring mvc设计了三个拦截位置,分别可以执行相应的操作:
- preHandle:执行控制器方法之前执行一些操作
- postHandle:执行完控制器方法之后执行一些操作
- afterCompletion:页面视图渲染完之后,执行一些操作
如果我们需要自定义拦截器的内容的话,必须需要先实现HandlerInterceptor接口:
登录检查:
- 1.配置好拦截器要拦截哪些请求;
- 2.把这些配置放在容器中
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
/**
* 控制器方法执行之前
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("拦截的请求路径是{}", requestURI);
// 登陆检查逻辑
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
// 拦截器判断当前是否有用户登陆
if(loginUser != null) {
return true; // 放行
} else { // 未登陆
request.setAttribute("msg", "请登陆");
request.getRequestDispatcher("/login").forward(request, response); // 跳转回登录页面
return false; // 拦截住
}
}
/**
* 控制器方法执行完成以后
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
/**
*
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
2. 配置拦截器(注册到容器中)
/*
* 1、编写一个拦截器实现HandlerInterceptor接口
* 2、拦截器需要注册到容器中(配置类需要实现WebMvcConfigurer接口,并重写addInterceptors方法)
* 3、指定拦截规则(如果是拦截所有请求的话,一定要记住静态资源也会被拦截)
* */
// 实现WebMvcConfigurer,底层会将这个自定义配置WebMvcConfigurer进行注册,和自动配置一起生效
@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方法(下面doDispatch方法中的这行applyPreHandle方法)
if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
- 1、如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
应用注册拦截器的 preHandle 方法。
返回:
如果执行链应该继续下一个拦截器或处理程序本身,则为true 。 否则, DispatcherServlet 假设这个拦截器已经处理了响应本身boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { for (int i = 0; i < this.interceptorList.size(); i++) { HandlerInterceptor interceptor = this.interceptorList.get(i); if (!interceptor.preHandle(request, response, this.handler)) { // 如果当前有一个拦截器执行失败 triggerAfterCompletion(request, response, null); return false; // 返回false,if语句判定成功,return,结束,目标方法都不会执行(ha.handle) } this.interceptorIndex = i; // 记录执行到哪个拦截器 } return true; }
- 2、如果当前拦截器返回为false。直接倒序执行所有已经执行了的拦截器的 afterCompletion;
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) { for (int i = this.interceptorIndex; i >= 0; i--) { // 注意倒序执行 HandlerInterceptor interceptor = this.interceptorList.get(i); try { interceptor.afterCompletion(request, response, this.handler, ex); } catch (Throwable ex2) { logger.error("HandlerInterceptor.afterCompletion threw exception", ex2); } } }
- 3、如果任何一个拦截器返回false(执行失败)。直接跳出,不执行目标方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; }
- 4、所有拦截器都返回True。执行目标方法
// Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
- 5、目标方法执行完之后,倒序执行所有拦截器的postHandle方法。
// Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); // 重要,执行所有拦截器的postHandle方法
为什么倒序执行呢?看源码for循环(顺序执行preHandle方法后,倒序执行postHandle方法)
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception { for (int i = this.interceptorList.size() - 1; i >= 0; i--) { HandlerInterceptor interceptor = this.interceptorList.get(i); interceptor.postHandle(request, response, this.handler, mv); } }
- 6、前面的步骤有任何异常都会直接倒序触发 afterCompletion
- 7、页面成功渲染完成以后,也会倒序触发 afterCompletion
-
拦截器执行流程总结:
其中任何一处出现异常,都会立马终于剩下的执行操作,转而直接执行afterCompletion,并结束。
2.5.5 文件上传
1、页面表单
<div class="panel-body">
<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data"> <!--method、enctype固定写法-->
<div class="form-group">
<label for="exampleInputEmail1">邮箱</label>
<input type="email" class="form-control" name="email" id="exampleInputEmail1" placeholder="Enter email">
</div>
<div class="form-group">
<label for="exampleInputPassword1">名字</label>
<input type="text" class="form-control" name="username" id="exampleInputPassword1" placeholder="Name">
</div>
<div class="form-group">
<label for="exampleInputFile">头像</label>
<input type="file" name="headerImg" id="exampleInputFile">
</div>
<div class="form-group">
<label for="exampleInputFile">生活照</label>
<input type="file" name="photos" multiple> <!--当前是一个多文件上传-->
</div>
<div class="checkbox">
<label>
<input type="checkbox"> Check me out
</label>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
</div>
2、控制器方法
/**
* MultipartFile 自动封装上传过来的文件
*/
@PostMapping("/upload")
public String upload(@RequestParam("username")String username,
@RequestParam("email")String email,
@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();
// 使用MultipartFile中封装好了的文件传输方法 —— transferTo方法
headerImg.transferTo(new File("F:\\springbootCache\\"+originalFilename)); // 测试就保存在本地
}
if(photos.length > 0){
for (MultipartFile photo : photos) {
if(!photo.isEmpty()){
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("F:\\springbootCache\\"+originalFilename));
}
}
}
return "main";
}
自动配置中对文件上传的大小有限制,如果用户上传的文件大小过大就会报错,需要重新配置。
MultipartAutoConfiguration.class:@EnableConfigurationProperties(MultipartProperties.class)
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(MultipartProperties.class)
public class MultipartAutoConfiguration {
.......
}
MultipartProperties.class:如果我们需要修改,则在application.properties中使用前缀进行修改spring.servlet.multipart
@ConfigurationProperties(prefix = "spring.servlet.multipart", ignoreUnknownFields = false)
public class MultipartProperties {
/**
* Whether to enable support of multipart uploads.
*/
private boolean enabled = true;
/**
* Intermediate location of uploaded files.
*/
private String location;
/**
* Max file size.
*/
private DataSize maxFileSize = DataSize.ofMegabytes(1);
/**
* Max request size.
*/
private DataSize maxRequestSize = DataSize.ofMegabytes(10);
/**
* Threshold after which files are written to disk.
*/
private DataSize fileSizeThreshold = DataSize.ofBytes(0);
/**
* Whether to resolve the multipart request lazily at the time of file or parameter
* access.
*/
private boolean resolveLazily = false;
...
}
application.properties中修改默认属性值:
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=100MB
3、自动配置原理(文件上传)
文件上传自动配置类【MultipartAutoConfiguration.class】,同时配置属性类【MultipartProperties.class】
-
自动配置好了 StandardServletMultipartResolver【文件上传解析器】
- 如果缺少文件上传解析器MultipartResolver,也会自动生成一个并命名为multipartResolver
@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) @ConditionalOnMissingBean(MultipartResolver.class) public StandardServletMultipartResolver multipartResolver() { // 文件上传解析器 StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver(); multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily()); return multipartResolver; }
-
原理步骤
-
-
请求进来使用文件上传解析器判断(isMultipart方法),是不是一个文件上传请求
所以我们在表单上传的时候一定要写:enctype="multipart/form-data"
@Override public boolean isMultipart(HttpServletRequest request) { return StringUtils.startsWithIgnoreCase(request.getContentType(), (this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/")); }
-
并封装(使用resolveMultipart方法封装,返回MultipartHttpServletRequest)文件上传请求
resolveMultipart方法封装原生请求,并返回StandardMultipartHttpServletRequest
-
我们要使用的请求,已经重新封装成一个StandardMultipartHttpServletRequest标准的文件上传请求
processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request);//因为processedRequest已经通过checkMultipart将原生request请求封装过了,所以表达式成立为true。multipartRequestParsed = true,当前是一个文件上传请求
-
-
-
参数解析器来解析请求中的文件内容封装成MultipartFile
-
-
-
将request中文件信息封装为一个Map;MultiValueMap<String, MultipartFile>
-
MultipartFile中的FileCopyUtils方法,实现文件流的拷贝。
-
2.5.6 异常处理
1、错误处理
- 默认情况下,Spring Boot提供
/error
处理所有错误的映射 - 对于机器客户端(比如Postman),它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
-
要对其进行自定义,添加 View 解析为 error
-
要完全替换默认行为,可以实现
ErrorController
并注册该类型的Bean定义,或添加ErrorAttributes类型的组件
以使用现有机制但替换其内容。 -
静态资源文件夹下的error/中4xx,5xx页面会被自动解析:(默认规则)
2、异常处理自动配置原理
-
ErrorMvcAutoConfiguration 自动配置异常处理规则
-
-
容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes
@Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(); }
-
-
- 客户端收到的错误信息:exception、trace、message、errors、timestamp、status
-
-
@Order(Ordered.HIGHEST_PRECEDENCE) public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered { ... @Override public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE)); if (!options.isIncluded(Include.EXCEPTION)) { errorAttributes.remove("exception"); } if (!options.isIncluded(Include.STACK_TRACE)) { errorAttributes.remove("trace"); } if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) { errorAttributes.remove("message"); } if (!options.isIncluded(Include.BINDING_ERRORS)) { errorAttributes.remove("errors"); } return errorAttributes; } private Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { Map<String, Object> errorAttributes = new LinkedHashMap<>(); errorAttributes.put("timestamp", new Date()); addStatus(errorAttributes, webRequest); addErrorDetails(errorAttributes, webRequest, includeStackTrace); addPath(errorAttributes, webRequest); return errorAttributes; } private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) { Integer status = getAttribute(requestAttributes, RequestDispatcher.ERROR_STATUS_CODE); if (status == null) { errorAttributes.put("status", 999); errorAttributes.put("error", "None"); return; } errorAttributes.put("status", status); try { errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase()); } catch (Exception ex) { // Unable to obtain a reason errorAttributes.put("error", "Http Status " + status); } } ... } }
-
DefaultErrorAttributes:定义错误页面中可以包含哪些数据
-
-
容器中的组件:类型:BasicErrorController --> id:basicErrorController(json+白页 适配响应)
@Bean @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) public BasicErrorController basicErrorController(ErrorAttributes errorAttributes, ObjectProvider<ErrorViewResolver> errorViewResolvers) { return new BasicErrorController(errorAttributes, this.serverProperties.getError(), errorViewResolvers.orderedStream().collect(Collectors.toList())); }
@Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController { private final ErrorProperties errorProperties; ... }
-
-
-
-
-
处理默认 /error 路径的请求,两种响应方法,要么响应ModelAndView返回一个error页面,要么返回ResponseEntity,显示Json字符串;(根据客户端来选择)
-
ErrorMvcAutoConfiguration中的WhitelabelErrorViewConfiguration方法会向容器中添加一个组件 View,id是error,也就是一个叫error的错误页面;(响应默认错误页)
@Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true) @Conditional(ErrorTemplateMissingCondition.class) protected static class WhitelabelErrorViewConfiguration { private final StaticView defaultErrorView = new StaticView(); @Bean(name = "error") @ConditionalOnMissingBean(name = "error") public View defaultErrorView() { return this.defaultErrorView; } // If the user adds @EnableWebMvc then the bean name view resolver from // WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment. @Bean @ConditionalOnMissingBean public BeanNameViewResolver beanNameViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); return resolver; } }
-
-
-
-
-
-
-
容器中放组件 BeanNameViewResolver(视图解析器);按照返回的视图名,作为组件的id去容器中找View对象。(上一点中的WhitelabelErrorViewConfiguration方法中)
-
总结:
如果请求路径为/error,那么来到BasicErrorController中进行处理,如果是需要向客户端显示一个错误页面的话,那么会在errorHtml方法中需要跳转到一个叫做error的视图,此时就会通过BeanNameViewResolver视图解析器按照id去容器中查找error视图对象,最后进行渲染页面。
-
-
-
-
-
容器中的组件:类型:DefaultErrorViewResolver -> id:conventionErrorViewResolver
-
@Bean @ConditionalOnBean(DispatcherServlet.class) @ConditionalOnMissingBean(ErrorViewResolver.class) DefaultErrorViewResolver conventionErrorViewResolver() { return new DefaultErrorViewResolver(this.applicationContext, this.resources); }
-
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { private static final Map<Series, String> SERIES_VIEWS; static { Map<Series, String> views = new EnumMap<>(Series.class); views.put(Series.CLIENT_ERROR, "4xx"); views.put(Series.SERVER_ERROR, "5xx"); SERIES_VIEWS = Collections.unmodifiableMap(views); } ... }
-
-
-
-
-
如果发生错误,会以HTTP的状态码作为视图页地址(viewName)(HttpStatus status),找到真正的页面
-
error/404、5xx.html
这也是为什么在静态资源目录下放状态码的html文件,就能被调用(用状态码当做页面名字的)
-
-
-
-
如果想要返回页面;就会找error视图【StaticView】。(默认是一个白页)
3、异常处理步骤流程
1、执行目标方法( mv = ha.handle() ),目标方法运行期间有任何异常都会被catch、而且标志当前请求结束;并且用 dispatchException 捕获异常
catch (Exception ex) {
dispatchException = ex;
}
2、进入视图解析流程(页面渲染)
进行视图解析processDispatchResult方法中
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
// 该方法会传入一个异常参数dispatchException,如果判断当前有异常,执行下面代码
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception); // 重点
errorView = (mv != null);
}
}
3、mv = processHandlerException();处理handler发生的异常,处理完成返回ModelAndView;
-
1、遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】
// Check registered HandlerExceptionResolvers... ModelAndView exMv = null; if (this.handlerExceptionResolvers != null) { for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {// 进行遍历操作 exMv = resolver.resolveException(request, response, handler, ex); // 调用处理异常的方法,即DefaultErrorAttributes.class的resolveException方法 if (exMv != null) { break; } } }
-
2、系统默认的异常解析器:this.handlerExceptionResolvers的值
exMv = resolver.resolveException(request, response, handler, ex); //处理异常
-
-
1、DefaultErrorAttributes这个类先来处理异常。把异常信息保存到request域,并且返回null;
-
2、默认没有任何人能处理异常,所以异常会被抛出
-
-
-
- 1、如果没有任何能够处理的话,最终底层就会发送 /error 请求,交给底层专门处理error的BasicErrorController处理
- 2、解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。
-
-
-
-
-
- 3、默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html
-
-
-
-
-
- 4、模板引擎最终响应这个页面 error/500.html
-
-
-
4、定制错误处理逻辑
2.5.7 Web原生组件注入(Servlet、Filter、Listener)
1、 使用Servlet、Filter、Listener API
要想使用,必须添加相应的注解
@WebServlet、@WebFilter、@WebListener
-
编写一个servlet,需要继承HttpServlet
@WebServlet(urlPatterns = "/my") // 响应的请求地址 public class MyServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("MyServlet_doGet"); } }
-
需要在spring boot主程序中引入注解@ServletComponentScan
@ServletComponentScan(basePackages = "com.atguigu") // 指定扫描原生servlet的路径;如果不写basePackages,默认是扫描主程序类所在包及内包 @SpringBootApplication public class Boot05WebAdminApplication { public static void main(String[] args) { SpringApplication.run(Boot05WebAdminApplication.class, args); } }
-
效果:直接响应请求,不会被Spring的拦截器拦截
- 原理解读:
1、MyServlet --> /my
2、DispatcherServlet --> /
-
扩展:DispatchServlet 如何注册进来
-
容器中自动配置了 DispatcherServlet 属性绑定到 WebMvcProperties;对应的配置文件配置项前缀是 spring.mvc;即,通过下面的方法对DispatcherServlet进行初始化,绑定配置。
-
(重点)DispatcherServletRegistrationBean继承ServletRegistrationBean
,不仅会导入DispatcherServletConfiguration的组件,还会将DispatcherServlet进行注册,命名为:dispatcherServlet,添加到容器中。 -
而DispatcherServlet的默认映射路径是:/
如果要进行更改,只需要在配置文件properties或者yml文件中:拦截/mvc/路径的请求
spring.mvc.servlet.path=/mvc/
-
-
Tomcat-Servlet
如果多个Servlet都能处理到同一层路径,精确优选原则
A: /my/
B: /my/1
如果是经过spring流程过来的请求,是由DispatcherServlet进行映射,如果是原生Servlet,则进行精确优先原则匹配,选择由谁处理。
-
Filter
@Slf4j @WebFilter(urlPatterns = {"/my"}) // 拦截/my请求 public class MyFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { log.info("MyFilter初始化完成"); } @Override public void destroy() { log.info("MyFilter销毁"); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { log.info("MyFilter工作"); chain.doFilter(request, response); // 把原生的request和response放行 } }
-
Listener
@Slf4j @WebListener public class MyListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { log.info("MyListener 监听到项目初始化完成"); } @Override public void contextDestroyed(ServletContextEvent sce) { log.info("MyListener 监听到项目销毁"); } }
2、使用RegistrationBean
使用spring提供的三个bean类,创建好组件并放入容器中:
-
ServletRegistrationBean,
-
FilterRegistrationBean,
-
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()); // 拦截myServlet()的响应请求路径
// 第二种写法
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
// 设置拦截的请求路径
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
MyListener mySwervletContextListener = new MyListener();
return new ServletListenerRegistrationBean(mySwervletContextListener);
}
}
2.5.8 嵌入式Servlet容器(为什么无需外置Tomcat)
Under the hood, Spring Boot uses a different type of ApplicationContext
for embedded servlet container support. The ServletWebServerApplicationContext
is a special type of WebApplicationContext
that bootstraps itself by searching for a single ServletWebServerFactory
bean. Usually a TomcatServletWebServerFactory
, JettyServletWebServerFactory
, or UndertowServletWebServerFactory
has been auto-configured.
Spring Boot 使用不同类型的 ApplicationContext 来支持嵌入式 servlet 容器。ServletWebServerApplicationContext 是一种特殊类型的 WebApplicationContext,它通过搜索单个 ServletWebServerFactory bean 来引导自身。通常是自动配置的 TomcatServletwebserverfactory、 JettyServletWebServerFactory 或 undertowletwebserverfactory。
1、切换嵌入式Servlet容器
-
SpringBoot应用默认支持的webServer,无需外置一个服务器(比如Tomcat)
-
Tomcat
,Jetty
, orUndertow
(默认启动SpringBoot应用时,是使用Tomcat)ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器
-
切换服务器
<!--排除掉starter-web中的默认服务器Tomcat--> <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> <!--再加上想要导入的web server--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency>
-
原理:(重要)
-
- SpringBoot应用启动发现当前是Web应用。spring-boot-starter-web默认导入了tomcat
- web应用会创建一个web版的IOC容器
ServletWebServerApplicationContext
-
ServletWebServerApplicationContext
启动的时候会寻找ServletWebServerFactory
(Servlet 的web服务器工厂---> Servlet 的WebServer)- SpringBoot底层默认有很多的WebServer工厂;
TomcatServletWebServerFactory
,JettyServletWebServerFactory
, orUndertowServletWebServerFactory
-
-
底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration
-
ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
-
-
-
ServletWebServerFactoryConfiguration 配置类根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory。
也能发现,只有@ConditionalOnClass条件成立的时候,才会使用哪个Web服务器,这个可以是用户自定义根据需求来导入相应的包。
-
TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize---this.tomcat.start();
/** * Create a new {@link TomcatWebServer} instance. * @param tomcat the underlying Tomcat server * @param autoStart if the server should be started * @param shutdown type of shutdown supported by the server */ public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) { Assert.notNull(tomcat, "Tomcat Server must not be null"); this.tomcat = tomcat; this.autoStart = autoStart; this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null; initialize(); }
-
-
-
内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)
TomcatServletWebServerFactory.class
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) { return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown()); } @Override public WebServer getWebServer(ServletContextInitializer... initializers) { if (this.disableMBeanRegistry) { Registry.disableRegistry(); } Tomcat tomcat = new Tomcat(); File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat"); tomcat.setBaseDir(baseDir.getAbsolutePath()); Connector connector = new Connector(this.protocol); connector.setThrowOnFailure(true); tomcat.getService().addConnector(connector); customizeConnector(connector); tomcat.setConnector(connector); tomcat.getHost().setAutoDeploy(false); configureEngine(tomcat.getEngine()); for (Connector additionalConnector : this.additionalTomcatConnectors) { tomcat.getService().addConnector(additionalConnector); } prepareContext(tomcat.getHost(), initializers); return getTomcatWebServer(tomcat); }
-
2、定制Servlet容器
-
通过spring-boot-starter-web查看默认导入的web server
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <version>2.6.1</version> <scope>compile</scope> </dependency>
● 实现 WebServerFactoryCustomizer
-
把配置文件的值和ServletWebServerFactory 进行绑定
@Override public void customize(ConfigurableServletWebServerFactory factory) { PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(this.serverProperties::getPort).to(factory::setPort); map.from(this.serverProperties::getAddress).to(factory::setAddress); map.from(this.serverProperties.getServlet()::getContextPath).to(factory::setContextPath); ...... }
● 修改配置文件 server.xxx(ServerProperties.class)
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
...
}
● 直接自定义 ConfigurableServletWebServerFactory
xxxxxCustomizer:定制化器,可以改变xxxx的默认规则(推荐还是使用通过配置文件进行定制化)
2.5.9 定制化容器的原理
1、定制化容器的常见方法
● 1、(推荐)修改配置文件;
● 2、xxxxxCustomizer;
● 3、(推荐)编写自定义的配置类 xxxConfiguration + @Bean注解,进行替换、增加容器中默认组件,增加的容器会和默认组件一块生效;典型的例子:视图解析器
● 4、Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能;在配置类中,对方法使用@Bean注解,给容器中再扩展一些组件。
例如:
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 会拦截所有请求(包括静态资源的访问)
.excludePathPatterns("/", "/login", "/css/**", "/fonts/**", "/images/**", "/js/**"); // 放行请求(包括对静态资源访问的请求)
}
}
-
@EnableWebMvc + 实现WebMvcConfigurer接口 + 用@Bean添加组件,可以全面接管SpringMVC,所有规则需要全部自己重新配置; 实现定制和扩展功能
-
- 原理
- 1、WebMvcAutoConfiguration.class 是默认的SpringMVC的自动配置功能类。包括:静态资源、欢迎页.....
-
- 2、一旦使用 @EnableWebMvc ,会 @Import(DelegatingWebMvcConfiguration.class)
- 3、DelegatingWebMvcConfiguration 的作用,只保证SpringMVC最基本的使用(有核心组件)
-
-
-
把所有系统中的 WebMvcConfigurer 获取到,并将所有定制的 WebMvcConfigurer 合起来一起配置生效。
-
自动配置了一些非常底层的组件(核心组件)。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取。
-
-
-
-
- public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
-
-
- 4、WebMvcAutoConfiguration.class 里面的配置要能生效,必须满足@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
- 5、因为@EnableWebMvc注解导入了public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport,所以导致了 WebMvcAutoConfiguration 没有生效。(SpringBoot为MVC的自动配置,都没有生效)
2、原理分析套路
场景starter -> xxxxAutoConfiguration -> 导入xxx组件 -> 绑定xxxProperties -> 绑定配置文件项