SpringBoot Web开发与源码分析
2021/10/15 20:16:31
本文主要是介绍SpringBoot Web开发与源码分析,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
SpringBoot 快速入门
概念性的东西见: https://www.yuque.com/atguigu/springboot/rmxq85
需要环境: jdk8 & 兼容java14, maven 3.3+, idea 219.1.2
maven设置:
去到 maven 的配置文件中修改添加:
<mirrors> <mirror> <id>nexus-aliyun</id> <mirrorOf>central</mirrorOf> <name>Nexus aliyun</name> <url>http://maven.aliyun.com/nexus/content/groups/public</url> </mirror> </mirrors> <profiles> <profile> <id>jdk-1.8</id> <activation> <activeByDefault>true</activeByDefault> <jdk>1.8</jdk> </activation> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion> </properties> </profile> </profiles>
Step 1: 创建 maven 工程
Step 2: 在pom文件引入依赖:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.4.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
step 3: 在 src/main/java 下创建包和主类(也叫主程序应用)
// 告诉 springboot 这是一个springboot 应用 // 主程序应用 @SpringBootApplication public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); } }
step 4: 在另一个包下创建 Controller 并编写业务代码
/* 可以用 RestController 代替 @ResponseBody // 代表这个类返回的是字符串 @Controller */ @RestController public class HelloController { @RequestMapping("/hello") public String handle01() { return "hello SpringBoot"; } }
step 5: 直接运行主应用的 main 方法
step 6: 在 resource 下创建 application.properties 文件, 该文件可以在全局进行配置, 详细见官方文档
step 7: 简化部署
通过加入插件便可以生成jar包, 直接在目标服务器执行即可
(命令行输入: 如 java -jar springboot01-1.0-SNAPSHOT.jar) 注意取消cmd的快速编辑模式
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
在 target/下 SpringBoot 帮我们打包成了 jar 包, 里面有我们运行时所需的第三方 jar 包
SpringBoot自动配置原理
1.0 SpringBoot 基本特点
1.1 依赖管理
依赖管理 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.4.RELEASE</version> </parent> 它的父项目 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.3.4.RELEASE</version> </parent> 几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制, 所以我们无需声明版本号
1、引入依赖默认都可以不写版本
2、引入非版本仲裁的 jar,要写版本号。
自定义修改版本号
1、查看spring-boot-dependencies里面规定当前依赖的版本 2、在当前项目里面重写配置 <properties> <mysql.version>5.1.43</mysql.version> </properties>
Staters
一组依赖的集合描述, 是 maven 的依赖特性
1、见到很多 spring-boot-starter-* : *是某种场景 2、只要引入starter,这个场景的所有常规需要的依赖我们都自动引入 3、SpringBoot所有支持的场景 https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter 4、见到的 *-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器 5、所有场景启动器最底层的依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.3.4.RELEASE</version> <scope>compile</scope> </dependency>
1.2 自动配置
-
自动配置好 Tomcat
- 引入依赖
- 配置 Tomcat
-
自动配置好 SpringMVC
-
引入了 SpringMVC 的全套组件
如何查看 IOC 容器有那些组件:主程序应用下的主方法: public static void main(String[] args) { // 返回 IOC 容器 ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args); // 查看容器组件名 String[] names = run.getBeanDefinitionNames(); for(String s : names) { System.out.println(s); } run.getBean("yang", User.class); // 容器中只有一个组件 单实例 }
-
自动配置好了 SpringMVC 常用组件 (功能)
-
-
自动配置好 Web 常见功能, 如: 字符编码问题
-
默认的包结构
- 主程序所在包及其下面的子包所有组件都会被默认扫描进来
- 无需以前的包扫描配置
- 想要改变扫描路径,@SpringBootApplication(scanBasePackages=“com.yangyu”)
或者 @ComponentScan 指定扫描路径
@SpringBootApplication 等同于 @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan("com.atguigu.boot") // 可以在这里指定扫描路径
- 各种配置拥有默认值
- 默认配置最终都是映射到某个类上,如:MultipartProperties
- 配置文件的值最终会绑定每个类上,这个类会在容器中创建对象
- 按需加载所有自动配置项
- 非常多的 starter
- 只要引入了这个场景, 这个场景的自动配置就会开启
- SpringBoot 所有的自动配置功能都在 spring-boot-autoconfigure 包里面
2.0 容器功能
2.1 组件添加
1. @Configuration
Spring 添加组件的方法: 创建类, 创建 spring 的配置文件, 在 xml 文件中编写<bean>
SpringBoot 添加组件的方法:
-
配置类组件之间无依赖关系用 Lite 模式加速容器启动过程,减少判断
-
配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用 Full 模式
/* 1. @Bean 是给容器注册组件, 默认单实例 2. 配置类本身也是一个组件 3. proxyBeanMethods: 代码 bean 的方法 默认为true Full(proxyBeanMethods = true) (保证每个 @Bean 方法被调用多少次返回的组件都是单实例的) Lite(proxyBeanMethods = false) (每个 @Bean 方法被调用多少次返回的组件都是新创建的, 容器不保存代理对象) 组件依赖必须使用 Full 模式默认。其他默认是 Lite 模式 */ @Configuration(proxyBeanMethods = true) // 告诉 SpringBoot 这是一个配置类 默认单实例 public class MyConfig { // Full: 外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象 @Bean // 给容器添加组件, 以方法名作为组件的 id, 返回类型就是组件类型, 返回的值就是组件在容器中的实例 public User user01() { return new User("01", 20); } @Bean("yang") public User yangyu() { return new User("yangyu", 20); } }
2. @Component @Controller @Service @Repository (适用于标注自己写的类) @Bean (适用于标注第三方包里面的组件)
3. @ComponentScan @Import (快速给容器导入组件)
@Import({User.class, DBHelper.class}) 给容器中自动创建出这两个类型的组件、默认组件的名字就是全类名 @Import({User.class, DBHelper.class}) @Configuration(proxyBeanMethods = false) public class MyConfig { }
ImportSelector: 是一个接口, 需要自定义类进行实现, 需要重写的方法可以返回需要导入的组件的全类名数组, 需要返回哪些类的全类名就通过自己的逻辑去得到全类名然后返回一个全类名字符串数组 注意: 在config类中需要 @import 你实现的这个接口的类, 这样就会自动到容器中生成对应的实例, 你实现的这个接口的类不会在容器中生成实例
ImportBeanDefinitionRegistrar: 也是一个接口, 需要实现. 把所有需要添加到容器的 bean 调用BeanDefinitionRegistry.registerBeanDefinition 手工注册
**FactorBean (Spring 提供): **也是一个接口, 需要实现.
4. @Conditional
条件装配: 满足指定条件则进行组件注入
ConditionOnBean: 当容器中有这个组件时才…
@ConditionalOnBean(name = {"user01", "myConfig"})
2.2 原生配置文件引入
1. @ImportResource
原生xml: resouces/beans.xml
@ImportResource("classpath:beans.xml") public class MyConfig {}
2.3 配置绑定
读取 properties 文件中的内容,并且把它封装到JavaBean中,以供随时使用;
原生java做法:
public class getProperties { public static void main(String[] args) throws FileNotFoundException, IOException { Properties pps = new Properties(); pps.load(new FileInputStream("a.properties")); Enumeration enum1 = pps.propertyNames();//得到配置文件的名字 while(enum1.hasMoreElements()) { String strKey = (String) enum1.nextElement(); String strValue = pps.getProperty(strKey); System.out.println(strKey + "=" + strValue); //封装到JavaBean。 } } }
1. @ConfigurationProperties
xxx.properties:
mycar.aaa=3 mycar.bbb=好
Car类:
@Component // 只有容器中的组件才有SpringBoot提供的强大功能 @ConfigurationProperties(prefix = "mycar") public class Car { private Integer aaa; private String bbb; }
**2. @EnableConfigurationProperties + @ConfigurationProperties **
如果是第三方类, 就在配置类上@EnableConfigurationProperties(Car.class)
- 开启 Car 配置绑定
- 把 Car 这个组件自动注册到容器里
和上一种区别: 要么用 @Component 加入到容器中, 要么用@EnableConfigurationProperties
3.0 自动配置原理入门
3.1 引导加载自动配置类
@SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication{}
1. @SpringBootConfiguration
其中的 @Configuration 代表当前类是一个配置类
2. @Component
指定要扫描哪些包, 参照 Spring 注解
3. EnableAutoConfiguration
@AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration {}
- @AutoConfigurationPackage
@Import(AutoConfigurationPackages.Registrar.class) // 给容器中导入一个组件 public @interface AutoConfigurationPackage {} // 利用Registrar给容器中导入一系列组件 // 将指定的一个包下的所有组件导入进来: MainApplication 所在包下
- @Import({AutoConfigurationImportSelector.class})
1、利用 getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件 2、调用 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类 3、利用工厂加载 Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) 得到所有的组件 4、从 META-INF/spring.factories 位置来加载一个文件。 默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件 spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories 文件里面写死了spring-boot一启动就要给容器中加载的所有配置类
3.2 按需开启自动配置项
虽然我们127个场景的所有自动配置启动的时候默认全部加载。xxxxAutoConfiguration
按照条件装配规则(@Conditional),最终会按需配置。
3.3 源码分析与修改默认配置
@Bean @ConditionalOnBean(MultipartResolver.class) // 容器中有这个类型组件 @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) // 容器中没有这个名字 multipartResolver 的组件 public MultipartResolver multipartResolver(MultipartResolver resolver) { // 给 @Bean 标注的方法传入了对象参数,这个参数的值就会从容器中找。 // SpringMVC multipartResolver。防止有些用户配置的文件上传解析器不符合规范 // Detect if the user has created a MultipartResolver but named it incorrectly return resolver; } 给容器中加入了文件上传解析器;
SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先
@Bean @ConditionalOnMissingBean public CharacterEncodingFilter characterEncodingFilter() {} 如果想自己配: public CharacterEncodingFilter filter(){ return null; }
总结:
-
SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration
-
每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿. xxxProperties和配置文件进行了绑定
-
生效的配置类就会给容器中装配很多组件
-
只要容器中有这些组件,相当于这些功能就有了
-
定制化配置
- 用户直接自己@Bean替换底层的组件
- 用户去看这个组件是获取的配置文件什么值就去修改。
xxxxxAutoConfiguration —> 组件 —> xxxxProperties里面拿值 ----> application.properties
3.4 最佳实践
- 引入场景依赖
https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters - 查看自己配置了哪些 (选做)
- 自己分析源码, 引入场景的自动配置一般都生效了
- application.properties中输入 debug=true 开启自动配置报告 Negative matches (不生效) \ Positive matches (生效)
- 是否需要修改
- 参照文档修改配置项
https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#common-application-properties
自己分析: xxxxProperties绑定了配置文件的哪些 - 自定义加入或替代组件
@Bean @Component - 自定义器 XXXCustomizer
- 参照文档修改配置项
开发技巧
LomBok
简化 JavaBean 开发
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> 并在 idea 搜索安装 lombok 插件
@NoArgsConstructor @AllArgsConstructor @Data @ToString @EqualsAndHashCode ============================================ // 简化日志开发 @Slf4j public class HelloController { @RequestMapping("/hello") public String handle01(){ log.info("请求进来了...."); return "123" } }
dev-tools
热更新 (实则是自动重启)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency>
项目或者页面修改以后:Ctrl+F9
Spring Initailizr
IDEA 快速创建 SpringBoot 应用, 可以勾选你需要的场景, 并自动生成 pom 文件
4.0 配置文件
4.1 properties
4.2 yaml
仍然是一种标记语言
非常适合用来做以数据为中心的配置文件
基本语法:
-
key: value;k v之间有空格
-
大小写敏感
-
使用缩进表示层级关系
-
缩进不允许使用tab,只允许空格
-
缩进的空格数不重要,只要相同层级的元素左对齐即可
-
'#'表示注释
-
字符串无需加引号,如果要加,’'与""表示字符串内容 会被 转义/不转义
数据类型:
-
字面量:单个的、不可再分的值。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
示例:
# yaml表示以上对象 person: userName: zhangsan boss: false birth: 2019/12/12 20:12:33 # private Date birth; age: 18 pet: # private Pet pet name: tomcat weight: 23.4 interests: [篮球,游泳] # private String[] interests; animal: # private List<String> animal; - jerry - mario score: # private Map<String, Object> score; english: first: 30 second: 40 third: 50 math: [131,140,148] chinese: {first: 128,second: 136} salarys: # private Set<Double> salarys; - 9999.98 - 9999.99 allPets: # private Map<String, List<Pet>> allPets; sick: - {name: tom} - {name: jerry,weight: 47} - name: 阿虫 weight: 77.77 health: [{name: mario,weight: 47}]
4.3 配置提示
自定义的类和配置文件绑定一般没有提示
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <!-- 防止打包的时候打进去 --> <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>
Web开发
1.0 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.0 简单功能分析
2.1 静态资源访问
只要静态资源放在类路径下: /static
or /public
or /resources
or /META-INF/resources
访问 : 当前项目根路径/ + 静态资源名
原理: 静态映射/**
请求进来,先去找 Controller 看能不能处理, 不能处理的所有请求又都交给静态资源处理器. 静态资源也找不到则响应 404 页面
改变默认的静态资源地址
spring: mvc: static-path-pattern: /res/** resources: static-locations: [classpath:/haha/]
spring.resources.static-locations 用于告诉Spring Boot应该在何处查找静态资源文件,这是一个列表性的配置,查找文件时会依赖于配置的先后顺序依次进行,默认的官方配置如下:
spring.resources.static-locations=classpath:/static,classpath:/public,classpath:/resources,classpath:/META-INF/resources
为了防止在实际开发中静态资源被拦截器拦截, 会在静态资源路径上加一个前缀, 让拦截器放行指定前缀的路径的所有请求
当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找
webjar
自动映射 /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 不能被默认访问
- controller 能处理 /index
-
spring: # mvc: # static-path-pattern: /res/** 这个会导致welcome page功能失效 resources: static-locations: [classpath:/haha/]
2.3 自定义Favicon
favicon.ico 放在静态资源目录下即可, 且static-path-pattern: /res/** 会导致 welcome page 功能失效
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 {}
-
配置文件的相关属性和xxx进行了绑定。WebMvcProperties==spring.mvc ResourceProperties==spring.resources
@Configuration( proxyBeanMethods = false ) @Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class}) @EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class, WebProperties.class}) @Order(0)
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; } addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/"); addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> { registration.addResourceLocations(this.resourceProperties.getStaticLocations()); if (this.servletContext != null) { ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION); registration.addResourceLocations(resource); } }); }
spring: # mvc: # static-path-pattern: /res/** resources: add-mappings: false 禁用所有静态资源规则
public static class Resources { 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. 欢迎页处理规则
EnableWebMvcConfiguration 类下HandlerMapping:处理器映射. 保存了每一个Handler能处理哪些请求。 @Bean public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping( new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(), this.mvcProperties.getStaticPathPattern()); welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider)); welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations()); return welcomePageHandlerMapping; } // 欢迎页功能写死了 WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) { //要用欢迎页功能,必须是/** if (welcomePage != null && "/**".equals(staticPathPattern)) { logger.info("Adding welcome page: " + welcomePage); setRootViewName("forward:index.html"); } else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) { // 调用Controller /index logger.info("Adding welcome page template: index"); setRootViewName("index"); } }
3.0 请求参数处理
3.1 请求映射
-
xxxMapping
-
Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
-
以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
-
现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
-
核心Filter;HiddenHttpMethodFilter
-
用法: 表单method=post,隐藏域 _method=put
-
SpringBoot中手动开启
mvc: hiddenmethod: filter: enable: true
-
-
扩展:如何把_method 这个名字换成我们自己喜欢的。
-
// 测试代码 //@RequestMapping(value = "/user",method = RequestMethod.GET) @GetMapping("/user") public String getUser(){ return "GET-张三"; } //@RequestMapping(value = "/user",method = RequestMethod.POST) @PostMapping("/user") public String saveUser(){ return "POST-张三"; } //@RequestMapping(value = "/user",method = RequestMethod.PUT) @PutMapping("/user") public String putUser(){ return "PUT-张三"; } //@RequestMapping(value = "/user",method = RequestMethod.DELETE) @DeleteMapping("/user") public String deleteUser(){ return "DELETE-张三"; } @Bean @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false) public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); } //创建配置类并在类中自定义filter @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 重写了getMethod 方法,返回的是传入的值
- 过滤器链放行的时候用 wrapper. 以后的方法调用getMethod 是调用 requesWrapper 的 getMethod
- 请求是否正常,并且是POST
Rest 使用客户端工具,
- 如 PostMan 直接发送 Put, delete 等方式请求,无需 Filter
3.2 请求映射原理
观察 DispatcherServlet 的继承树
SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet 的 doDispatch() 开始
// 找到当前请求使用哪个Handler(Controller的方法)处理 mappedHandler = getHandler(processedRequest); // HandlerMapping:处理器映射。/xxx -> xxxx
RequestMappingHandlerMapping:保存了所有 @RequestMapping 和 handler 的映射规则.
所有的请求映射都在 HandlerMapping 中.
-
SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping . 访问 / 能访问到 index.html;
-
SpringBoot 自动配置了默认的 RequestMappingHandlerMapping
-
请求进来,挨个尝试所有的 HandlerMapping 看是否有被请求的信息
- 如果有就找到这个请求对应的 handler
- 如果没有就找下一个 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; }
- 我们需要一些自定义的映射处理,我们也可以自己给容器中放 HandlerMapping. 自定义 HandlerMapping
3.3 普通参数与基本注解
1. 注解
@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody
@PathVariable, @RequestParam, @RequestBody, @CookieValue, @RequestHeader :
// 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); // 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
@GetMapping("/goto") 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 @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; }
@MatrixVariable 与 UrlPathHelper
/cars/{path}?xxx=xxx&aaa=ccc queryString 查询字符串。@RequestParam;
/cars/sell;low=34;brand=byd,audi,yd ;矩阵变量
在页面开发中, cookie 被禁用, 那么 session 里面的内容如何使用
首先, 我们可以在 session 保存数据, 且每一个人都有一个 jsessionid, 这个jsessionid 会被保存在 cookie 中, 且 cookie 每次请求都会被携带, 所以服务器会通过 cookie 的 jsessionid 来查找
我们可以通过矩阵变量的方式来解决这个问题: url重写: /abc;jsessionid=xxx 把cookie的值使用矩阵变量的方式进行传递
第一个分号前面是路径, 分号后面是矩阵变量, 多个矩阵变量通过分号区分
//1. 语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd //2. SpringBoot 默认是禁用了矩阵变量的功能 // 手动开启: 原理: 对于路径的处理: UrlPathHelper 进行解析. 其中的 removeSemicolonContent (移除分号内容) 用来支持矩阵变量的 //3. 矩阵变量必须有 url 路径变量才能被解析 @GetMapping("/cars/{path}") public Map carsSell(@MatrixVariable("low") Integer low, @MatrixVariable("brand") List<String> brand, @PathVariable("path") String path){ Map<String,Object> map = new HashMap<>(); map.put("low",low); map.put("brand",brand); map.put("path",path); return map; } // /boss/1;age=20/2;age=10 @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; }
手动开启矩阵变量:
@Configuration(proxyBeanMethods = false) public class WebConfig /*implements WebMvcConfigurer*/ { //1、WebMvcConfigurer定制化SpringMVC的功能 @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 不移除;后面的内容。矩阵变量功能就可以生效 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } } }
2. Servlet API
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); }
3. 复杂参数
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)
Model 存的是数据, View 存的是视图
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; }
5. POJO 封装过程
ServletModelAttributeMethodProcessor 参数处理见下文
3.4 参数处理原理
-
HandlerMapping 中找到能处理请求的 Handler(Controller.method())
-
为当前Handler 找一个适配器 HandlerAdapter; RequestMappingHandlerAdapter
-
适配器执行目标方法并确定方法参数的每一个值
1. HandlerAdapter
0 - 支持方法上标注 @RequestMapping
1 - 支持函数式编程的
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目标方法能写多少种参数类型. 取决于参数解析器
参数解析器接口:
- 当前解析器是否支持解析这种参数
- 支持就调用 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; }
6. 遍历找到哪个参数解析器能支持解析这个参数
@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; }
7. 解析这个参数的值
调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可
8. 自定义类型参数 封装 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)); }
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
WebDataBinder :web数据绑定器,将请求参数的值绑定到指定的JavaBean里面
WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中
GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean – Integer)
如: 文件上传: byte – > file
@FunctionalInterface public interface Converter<S, T>
未来我们可以给 WebDataBinder 里面放自己的 Converter;
private static final class StringToNumber<T extends Number> implements Converter<String, T>
自定义 Converter:
//1、WebMvcConfigurer定制化SpringMVC的功能 @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 不移除;后面的内容。矩阵变量功能就可以生效 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new Converter<String, Pet>() { @Override public Pet convert(String source) { // 啊猫,3 if(!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; } }); } }; }
3.5 目标方法执行完成
将所有的数据都放在 ModelAndViewContainer; 包含要去的页面地址View. 还包含Model数据
3.5.1 处理派发结果
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
InternalResourceView: @Override protected void renderMergedOutputModel( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { // Expose the model object as request attributes. exposeModelAsRequestAttributes(model, request); // Expose helpers as request attributes, if any. exposeHelpers(request); // Determine the path for the request dispatcher. String dispatcherPath = prepareForRendering(request, response); // Obtain a RequestDispatcher for the target resource (typically a JSP). RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath); if (rd == null) { throw new ServletException("Could not get RequestDispatcher for [" + getUrl() + "]: Check that the corresponding file exists within your web application archive!"); } // If already included or response already committed, perform include, else forward. if (useInclude(request, response)) { response.setContentType(getContentType()); if (logger.isDebugEnabled()) { logger.debug("Including [" + getUrl() + "]"); } rd.include(request, response); } else { // Note: The forwarded resource is supposed to determine the content type itself. if (logger.isDebugEnabled()) { logger.debug("Forwarding to [" + getUrl() + "]"); } rd.forward(request, response); } }
暴露模型作为请求域属性 // Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception { // model中的所有数据遍历挨个放在请求域中 model.forEach((name, value) -> { if (value != null) { request.setAttribute(name, value); } else { request.removeAttribute(name); } }); }
4.0 数据响应与内容协商
数据响应
- 响应页面
- 响应数据
- JSON
- XML
- xls
- 图片, 音视频
- 自定义协议数据
4.1 响应 JSON
1. jackson.jar + @ResponseBody
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
web 场景自动引入了 json 场景
返回值解析器
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); }
返回值解析器原理
- 返回值处理器判断是否支持这种类型返回值 supportsReturnType
- 返回值处理器调用 handleReturnValue 进行处理
- RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的
- 利用 MessageConverters 进行处理 将数据写为 json
- 内容协商 (浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
- 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
- SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理
- 得到 MappingJackson2HttpMessageConverter 可以将对象写为json
- 利用 MappingJackson2HttpMessageConverter 将对象转为json再写出去
- 利用 MessageConverters 进行处理 将数据写为 json
SpringMVC 支持返回哪些返回值 由 returnValueHandlers 决定
ModelAndView Model View ResponseEntity ResponseBodyEmitter StreamingResponseBody HttpEntity HttpHeaders Callable DeferredResult ListenableFuture CompletionStage WebAsyncTask 有 @ModelAttribute 且为对象类型的 @ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;
HTTPMessageConverter 原理
-
MessageConverter 规范
HttpMessageConverter: 看是否支持将此 Class 类型的对象, 转为MediaType类型的数据例如:Person 对象转为 JSON 或者 JSON 转为 Person
-
默认的 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 转换的)
4.2 内容协商
根据客户端接收能力不同,返回不同媒体类型的数据
1. 引入 xml 依赖
<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency>
2. postman 测试返回 json 和 xml
只需要改变请求头中Accept字段. Http协议中规定的,告诉服务器本客户端可以接收的数据类型: application/xml
3. 开启浏览器参数方式协商功能
spring: contentnegotiation: favor-parameter: true #开启请求参数内容协商模式
http://localhost:8080/test/person?format=json
http://localhost:8080/test/person?format=xml
确定客户端接收什么样的内容类型;
- Parameter策略优先确定是要返回 json 数据 (获取请求头中的format的值)
- 最终进行内容协商返回给客户端 json 即可。
内容协商原理
- 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。调用它进行转化
4.3 自定义 MessageConverter
实现多协议数据兼容。json、xml、x-guigu
-
@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理
-
Processor 处理方法返回值。通过 MessageConverter 处理
-
所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
-
内容协商找到最终的 messageConverter;
SpringMVC的什么功能。一个入口给容器中添加一个 WebMvcConfigurer
@Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new GuiguMessageConverter()); } } } ============ GuiguMessageConverter ============ public class GuiguMessageConverter implements HttpMessageConverter<Person> { @Override public boolean canRead(Class<?> clazz, MediaType mediaType) { return false; } @Override public boolean canWrite(Class<?> clazz, MediaType mediaType) { return clazz.isAssignableFrom(Person.class); } /** * 服务器要统计所有MessageConverter都能写出哪些内容类型 * * application/x-guigu * @return */ @Override public List<MediaType> getSupportedMediaTypes() { return MediaType.parseMediaTypes("application/x-guigu"); } @Override public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { return null; } @Override public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { //自定义协议数据的写出 String data = person.getUserName()+";"+person.getAge()+";"+person.getBirth(); //写出去 OutputStream body = outputMessage.getBody(); body.write(data.getBytes()); } } ================== WebConfig ================= /** * 自定义内容协商策略 * @param configurer */ @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { //Map<String, MediaType> mediaTypes Map<String, MediaType> mediaTypes = new HashMap<>(); mediaTypes.put("json",MediaType.APPLICATION_JSON); mediaTypes.put("xml",MediaType.APPLICATION_XML); mediaTypes.put("gg",MediaType.parseMediaType("application/x-guigu")); //指定支持解析哪些参数对应的哪些媒体类型 ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes); // parameterStrategy.setParameterName("gg"); HeaderContentNegotiationStrategy headeStrategy = new HeaderContentNegotiationStrategy(); configurer.strategies(Arrays.asList(parameterStrategy,headeStrategy)); }
有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。
大家考虑,上述功能除了我们完全自定义外?SpringBoot有没有为我们提供基于配置文件的快速修改媒体类型功能?怎么配置呢?(提示: 参照SpringBoot官方文档web开发内容协商章节)
5.0 视图解析与模板引擎
视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染
5.1 视图解析
1. 视图解析
- 转发
- 重定向
- 自定义视图
1. 视图解析原理流程
1、目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面。包括数据和视图地址
2、任何目标方法执行完成以后都会返回 ModelAndView (数据和视图地址)
3、processDispatchResult() 处理派发结果 (页面该如何响应)
-
1、render(mv, request, response); 进行页面渲染逻辑
-
1、根据一些值(ViewName, )得到 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);
-
-
ThymeleafViewResolver:
-
- 返回值以 forward: 开始: new InternalResourceView(forwardUrl); --> 转发request.getRequestDispatcher(path).forward(request, response);
- 返回值以 redirect: 开始: new RedirectView() --> render就是重定向
-
- 返回值是普通字符串: new ThymeleafView()–> render
自定义视图解析器+自定义视图: 大厂学院
5.2 模板引擎 - Thymeleaf
现代化、服务端Java模板引擎
1. 基本语法
表达式 | 语法 | 用途 |
---|---|---|
变量取值 | ${…} | 获取请求域, session 域, 对象等值 |
选择变量 | *{…} | 获取上下文对象值 |
消息 | #{…} | 获取国际化等值 |
链接 | @{…} | 生成链接 |
片段表达式 | ~{…} | jsp:include 作用, 引入公共页面片段 |
字面量
文本值: ‘one text’ , 'Another one!'
数字: 0 , 34 , 3.0 , 12.3
布尔值: true , false
空值: null
变量: one, two 变量不能有空格
文本操作
字符串拼接: +
变量替换: The name is ${name}
数学运算
+ , - , * , / , %
布尔运算
and, or, !, not
比较运算
比较: > , < , >= , <= ( gt , lt , ge , le **)**等式: == , != ( eq , ne )
条件运算
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
2. 设置属性值 - th:attr
<img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" /> 代替写法 <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
3. 迭代
<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>
4. 条件运算
<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>
5. 属性优先级
不按书写顺序定义优先级, 当同一个标签中有多个th:*属性时,Thymleaf在处理这些属性时也有优先级的问题
Order | Feature | Attributes |
---|---|---|
1 | Fragment inclusion | th:include th:replace |
2 | Fragment iteration | th:each |
3 | Conditional evaluation | th:if th:unless th:switch th:case |
4 | Local variable definition | th:object th:with |
5 | General attribute modification | th:attr th:attrprepend th:attrappend |
6 | Specific attribute modification | th:value th:href th:src … |
7 | Text (tag body modification) | th:text th:utext |
8 | Fragment specification | th:fragment |
9 | Fragment removal | th:remove |
5.3 thymeleaf 使用
1. 引入 starter
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
thymeleaf 自动配置好了
@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、我们只需要直接开发页面
public static final String DEFAULT_PREFIX = "classpath:/templates/"; public static final String DEFAULT_SUFFIX = ".html"; // xxx.html
5.4 构建后台管理系统
1. 创建项目, 把所有静态资源放到 static 文件夹下
2. 路径构建
th:action="@{/login}"
3. 模板抽取
th:insert/replace/include **注意三者区别 用 div 把要抽取的内容包起来 **
<head th:fragment="commonheader"> <!--common--> <link href="css/style.css" rel="stylesheet" th:href="@{/css/style.css}"> <link href="css/style-responsive.css" rel="stylesheet" th:href="@{/css/style-responsive.css}"> <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> <!--[if lt IE 9]> <script src="js/html5shiv.js" th:src="@{/js/html5shiv.js}"></script> <script src="js/respond.min.js" th:src="@{/js/respond.min.js}"></script> <![endif]--> </head> <div id="leftmenu" class="left-side sticky-left-side"> ... </div> ========= main.html ========= <div th:include="common :: commonheader"></div> <!--因为使用 id 声明, 所以用 # 选择器来选择--> <div th:replace="common :: #leftmenu"></div>
4. 页面跳转
@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"; }else { model.addAttribute("msg","账号密码错误"); //回到登录页面 return "login"; } }
5. 数据渲染
@GetMapping("/dynamic_table") public String dynamic_table(Model model){ //表格内容的遍历 List<User> users = Arrays.asList(new User("zhangsan", "123456"), new User("lisi", "123444"), new User("haha", "aaaaa"), new User("hehe ", "aaddd")); model.addAttribute("users",users); return "table/dynamic_table"; }
<thead> <tr> <th>#</th> <th>用户名</th> <th>密码</th> </tr> </thead> <tbody> <tr class="gradeX" th:each="user, userstat:${users}"> <td th:text="${userstat.count}">Trident</td> <td th:text="${user.userName}">Internet</td> <td>[[${user.password}]]</td> </tr> </tbody>
6.0 拦截器
6.1 HandlerInterceptor 接口
- 编写一个拦截器实现 HandlerInterceptor 接口
- 拦截器注册到容器中 (实现 webMvcConfigurer 的 addInterceptors)
- 指定拦截规则 [如果是拦截所有,静态资源也会被拦截]
@Slf4j public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException { String requestURI = request.getRequestURI(); log.info("preHandle 拦截的请求路径是{}",requestURI); // 登录判断 HttpSession session = request.getSession(); Object obj = session.getAttribute("user"); if(obj != null) { return true; } // 请求转发到登录页 request.setAttribute("msg", "请先登录"); request.getRequestDispatcher("/").forward(request, response); return false; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { log.info("postHandle 执行{}",modelAndView); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) { log.info("afterCompletion 执行异常{}",ex); } } ============== AdminWebConfig ============== @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**") .excludePathPatterns("/", "/login", "/css/**", "/fonts/**", "/images/**", "/js/**"); }
6.2 拦截器原理
1、根据当前请求,找到 **HandlerExecutionChain【**可以处理请求的handler以及handler的所有 拦截器】
2、先来顺序执行所有拦截器的 preHandle 方法
- 如果当前拦截器 prehandler 返回为true. 则执行下一个拦截器的preHandle
- 如果当前拦截器返回为 false. 直接倒序执行所有已经执行了的拦截器的 afterCompletion
3、如果任何一个拦截器返回false. 直接跳出不执行目标方法
4、所有拦截器都返回True. 执行目标方法
5、倒序执行所有拦截器的postHandle方法。
6、前面的步骤有任何异常都会直接倒序触发 afterCompletion
7、页面成功渲染完成以后,也会倒序触发 afterCompletion
7.0 文件上传
7.1 表单
<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data"> <div class="form-group"> <label for="exampleInputEmail1">邮箱</label> <input type="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email"> </div> <div class="form-group"> <label for="exampleInputPassword1">名字</label> <input type="text" name="username" class="form-control" id="exampleInputPassword1" placeholder="Password"> </div> <div class="form-group"> <label for="exampleInputFile">头像</label> <input type="file" name="headerImg" id="exampleInputFile"> </div> <div class="form-group"> <label for="exampleInputFile">照片集</label> <input type="file" name="photos" multiple> </div> <button type="submit" class="btn btn-primary">提交</button> </form>
7.2 文件上传代码
/** * 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)); } } } return "main"; }
7.3 自动配置原理
文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties
-
自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
-
原理步骤
-
请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
-
参数解析器 (RequestPartMethodArgumentResolver) 来解析请求中的文件内容封装成MultipartFile
-
**将request中文件信息封装为一个Map;**MultiValueMap<String, MultipartFile>
-
FileCopyUtils: 实现文件流的拷贝
8.0 异常处理
8.1 错误处理
1. 默认规则
-
默认情况下,Spring Boot提供
/error
处理所有错误的映射 -
对于机器客户端,它将生成 JSON 响应,其中包含错误,HTTP 状态和异常消息的详细信息. 对于浏览器客户端,响应一个 whitelabel 错误视图,以 HTML 格式呈现相同的数据
-
要对其进行自定义,添加
View
解析为error
-
要完全替换默认行为,可以实现
ErrorController
并注册该类型的Bean定义,或添加ErrorAttributes
类型的组件以使用现有机制但替换其内容。 -
error/下的4xx,5xx页面会被自动解析;
2. 定制错误处理逻辑 -
自定义错误页
- error/404.html, error/5xx.html; 有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
-
@ControllerAdvice + @ExceptionHandler处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler({ArithmeticException.class,NullPointerException.class}) //处理异常 public String handleArithException(Exception e){ return "login"; //视图地址 } }
-
@ResponseStatus + 自定义异常 ;底层是 ResponseStatusExceptionResolver ,把responsestatus 注解的信息底层调用 response.sendError(statusCode, resolvedReason); tomcat 发送的 /error
@ResponseStatus(value= HttpStatus.FORBIDDEN,reason = "用户数量太多") public class UserTooManyException extends RuntimeException { public UserTooManyException(){} public UserTooManyException(String message){ super(message); } }
-
Spring 底层的异常,如参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常
- response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage())
- 自定义实现 HandlerExceptionResolver 处理异常; 可以作为默认的全局异常处理规则
@Order(value= Ordered.HIGHEST_PRECEDENCE) //优先级,数字越小优先级越高 @Component public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { response.sendError(511,"我喜欢的错误"); // try - catch return new ModelAndView(); } }
-
ErrorViewResolver 实现自定义处理异常
-
response.sendError: error请求就会转给controller
-
你的异常没有任何人能处理。tomcat 底层 response.sendError. error请求就会转给controller
-
basicErrorController 要去的页面地址是 ErrorViewResolver
-
3. 异常处理自动配置原理
- ErrorMvcAutoConfiguration 自动配置异常处理规则
- 容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes
- public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver()
- DefaultErrorAttributes:定义错误页面中可以包含哪些数据
- 容器中的组件:类型:BasicErrorController --> id:basicErrorController(json+白页 适配响应)
- 处理默认 /error 路径的请求;页面响应 new ModelAndView(“error”, model);
- 容器中有组件 View -> id 是error**;(响应默认错误页)**
- **容器中放组件 **BeanNameViewResolver(视图解析器);按照返回的视图名作为组件的 id 去容器中找 View 对象。
- 容器中的组件:**类型:DefaultErrorViewResolver -> id:**conventionErrorViewResolver
- 如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面
- error/404、5xx.html
- 容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes
如果想要返回页面;就会找error视图 (StaticView) (默认是一个白页)
4. 异常处理步骤流程
-
执行目标方法,目标方法运行期间有任何异常都会被catch, 而且标志当前请求结束; 并且异常会被赋值给 dispatchException
-
进入视图解析流程 (页面渲染)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
- mv = processHandlerException; 处理handler发生的异常,处理完成返回ModelAndView;
- 遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】
系统默认的异常解析器
1、DefaultErrorAttributes 先来处理异常. 把异常信息保存到rrequest域,并且返回null
HadnlerExceptionResolverComposite { 0. ExceptionHandlerExceptionResolver 1. ResponseStatusExceptionResolver 2. DefaultHandlerExceptionResolver }
2、默认没有人能处理异常,所以异常会被抛出
-
1、如果没有人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController 处理
-
2、解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析
-
3、默认的 DefaultErrorViewResolver 中的 ErrorViewResolver 把响应状态码作为错误页的地址,error/500.html
-
4、模板引擎最终响应这个页面 error/500.html
9.0 Web原生三大组件注入
9.1 使用 Servlet API
@ServletComponentScan(basePackages = “com.atguigu.admin”) :指定原生Servlet组件都放在那里
@WebServlet(urlPatterns = “/my”):效果:直接响应,没有经过Spring的拦截器
@WebFilter(urlPatterns={"/css/*","/images/*"})
@WebListener
====== Servlet ====== //@WebServlet(urlPatterns = "/my") public class MyServlet extends HttpServlet { // 实现方法 } ====== Filter ====== //@WebFilter(urlPatterns={"/css/*","/images/*"}) // my public class MyFilter implements Filter { // 实现方法 } ====== lisenter ====== //@WebListener public class MySwervletContextListener implements ServletContextListener { // 实现方法 }
建议使用 RegistrationBean 方式
ServletRegistrationBean
, FilterRegistrationBean
, and ServletListenerRegistrationBean
====== MyRegistConfig ====== // (proxyBeanMethods = true):保证依赖的组件始终是单实例的 @Configuration(proxyBeanMethods = true) 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()); 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); } }
扩展:DispatchServlet 如何注册进来
-
容器中自动配置了 DispatcherServlet 属性绑定到 WebMvcProperties;对应的配置文件配置项是 spring.mvc。
-
通过 ServletRegistrationBean<DispatcherServlet> 把 DispatcherServlet 配置进来。
-
默认映射的是 /路径。
Tomcat-Servlet:
多个Servlet都能处理到同一层路径,精确优选原则
10.0 嵌入式Servlet 容器
10.1 切换嵌入式Servlert容器
-
默认支持的webServer
-
Tomcat
,Jetty
, orUndertow
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
, orUndertowServletWebServerFactory
底层直接会有一个自动配置类: ServletWebServerFactoryAutoConfiguration
ServletWebServerFactoryAutoConfiguration
导入了ServletWebServerFactoryConfiguration (配置类)
ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
TomcatServletWebServerFactory 创建出Tomcat服务器并启动; TomcatWebServer 的构造器拥有初始化方法 initialize---this.tomcat.start();
内嵌服务器, 就是手动把启动服务器的代码调用 (tomcat核心jar包存在)
10.2 定制Servlet容器
- 实现 WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>
- 把配置文件的值和
ServletWebServerFactory 进行绑定
- 把配置文件的值和
- 修改配置文件 server.xxx
- 直接自定义 ConfigurableServletWebServerFactory
xxxxxCustomizer:定制化器,可以改变xxxx的默认规则
@Component public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> { @Override public void customize(ConfigurableServletWebServerFactory server) { server.setPort(9000); } }
11.0 定制化原理
定制化方式:
-
修改配置文件;
-
xxxxxCustomizer;
-
编写自定义的配置类 xxxConfiguration;+ @Bean替换、增加容器中默认组件;视图解析器
-
Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能;+ @Bean给容器中再扩展一些组件 (重要)
@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 没有生效
如何分析原理
场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties – 绑定配置文件项
这篇关于SpringBoot Web开发与源码分析的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-10-01基于Python+Vue开发的医院门诊预约挂号系统
- 2024-10-01基于Python+Vue开发的旅游景区管理系统
- 2024-10-01RestfulAPI入门指南:打造简单易懂的API接口
- 2024-10-01初学者指南:了解和使用Server Action
- 2024-10-01Server Component入门指南:搭建与配置详解
- 2024-10-01React 中使用 useRequest 实现数据请求
- 2024-10-01使用 golang 将ETH账户的资产平均分散到其他账户
- 2024-10-01JWT用户校验课程:从入门到实践
- 2024-10-01Server Component课程入门指南
- 2024-09-30Dnd-Kit学习:新手快速入门指南