笔记环境:
<mirror>
<id>aliyunmavenid>
<mirrorOf>*mirrorOf>
<name>阿里云公共仓库name>
<url>https://maven.aliyun.com/repository/publicurl>
mirror>
<profile>
<id>jdk-15id>
<activation>
<activeByDefault>trueactiveByDefault>
<jdk>15jdk>
activation>
<properties>
<maven.compiler.source>15maven.compiler.source>
<maven.compiler.target>15maven.compiler.target> <maven.compiler.compilerVersion>15maven.compiler.compilerVersion>
properties>
profile>
引入SpringBoot父工程
添加starter起步依赖 org.springframework.boot.spirng-boot-starter-web
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<version>2.4.2version>
dependency>
/**
* 主程序类
* @SpringBootApplication:这是一个SpringBoot应用程序
*/
@SpringBootApplication
public class SpringBootApplicationStarter {
public static void main(String[] args) {
SpringApplication.run(SpringBootApplicationStarter.class,args);
}
}
添加SpringBoot Maven Plugin
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
server.port=8888
依靠Maven等构建工具的依赖管理
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.4.2version>
parent>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.4.2version>
parent>
开发导入starter场景起步依赖。场景启动器
见到很多 spring-boot-starter-*
; *就某种场景
只要引入starter,这个场景所有常规需要的依赖我们都会自动导入。
SpringBoot所有的Starters。
还可以自定义starter。但是官方不推荐自定义的starter使用 spring-boot-starter-*
, 推荐使用 *-spring-boot-starter
所有的场景启动器最底层的依赖都是
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.2</version>
</dependency>
无需关注版本号,父工程集成依赖中已经指定了版本号。版本仲裁。
可以修改版本号。Maven子工程可以覆盖version或properties中的version属性
内嵌Tomcat、Jetty等服务器,默认使用Tomcat服务器。
引入Tomcat依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-tomcatartifactId>
<version>2.4.2version>
<scope>compilescope>
dependency>
自动配置SpringMVC
自动配置Web常见功能,如:字符编码问题 (SpringMVC中的CharacterEncodingFileter)
默认统一的包结构
主程序所在包,及其子包下的组件都会被扫描到。无需包扫描配置
默认 componet-scan base-package="主程序所在包"
指定组件扫描的包:
第一种方式:@SpringBootApplication(scanBasePackages = "com.yyl.application")
//第二种扫描包的方式
@SpringBootApplication() //是一个合成注解,等同于下面的三个注解。
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan() //指定组件扫描的包
各种配置拥有默认值
Multipart|Server|* + Properties
类中自动配置项按需加载
org.springframewor.boot.auutoconfigure.*.*
中都是实现自动配置功能的类。…
基本使用
Full模式与Lite模式
示例 @Configuration(proxyBeanMethods = true)
@Configuration(proxyBeanMethods = false)
最佳实战
配置 类组件之间无依赖关系用Lite模式加速容器的启动过程,减少判断
配置类组件之间有依赖关系,方法被调用会得到容器中的存在的单实例组件,用Full模式
/**
* 1、@Configuration注解修饰的类在IOC中也是一个Bean,一个组件*
*
* 2、配置类里面使用@Bean标注在方法上给容器注册组件,默认也是单实例。
*
* 3. Spring5.2 之后新增的ProxyBeanMethod(代理Bean方法)属性, 默认为true
* true : 手动调用该类类中的方法返回的对象保持单例singleton
* false : 手动调用该类中的方法返回的对象原型模式,prototype、
* 从容器中getBean()获得的对象仍旧是singleton, 保持单例。
* proxyBeanMethods=true,该bean会被代理模式代理 MyConfig$$EnhancerBySpringCGLIB$$4da09b2a@1e1e9ef3
*
* Full(proxyBeanMethods = true) 全模式 每次执行方法都会去IOC中这个Bean,使Bean保持单例
* Lite(proxyBeanMethods = false) 轻量模式 每次执行方法直接返回一个新的对象
*
* */
导入,导入指定类型的组件
@Import({
*.class, *.class})
//可以在任意类上使用
//导入, 自动创建出该类型的组件、默认组件的名字就是全类名
条件装配:满足Conditional指定的条件,则被标记的组件进行注入。
可以标注在 类 和方法上。
@Conditional注解下有很多子注解,分别表示不同的条件
例如**@ConditionalOnBean(name = "user01")
: 容器中存在某个Bean时,才将被标记的组件初始化进容器中。**
@ImportResource("classpath:beans.xml")
导入资源, 使用Spring原生方式,将XML配置文件进行导入,并解析。
@ConfigurationProperties
通过配置绑定,可以将SpringBoot配置文件中自定义的**
**属性注入进类的属性中。
只有容器中的Bean,才能拥有SpringBoot各种注解的功能。所以被**@ConfigurationProperties
**标注的类必须存在容器中。
@EnableConfigurationProperties
+ @ConfigurationProperties
@EnableConfigurationProperties(Car.class)
用于将Car类注册进容器中;用于开启Car类的配置绑定功能
该注解标注的类也必须在容器中。
@Component
+ @ConfigurationProperties
@Component
将该类注册进容器中, 容器中的Bean可以使用 @ConfigurationProperties
的功能。(必须开启组件扫描功能。)
@ConfigurationPropertiesScan
+ @ConfigurationProperties
@ConfigurationPropertiesScan
内部封装了@EnableConfigurationProperties
。扫描使用了配置绑定的类,并将其注册进容器中,并开启配置绑定。使用@ConfigurationPropertiesScan
的类必须在容器中存在才能使用此功能。
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}
@SpringBootConfiguration
@Configuration //当前类是一个配置类。
public @interface SpringBootConfiguration {
}
@EnableAutoConfiguration
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}
@AutoConfigurationPackage
自动配置包?指定了默认的包规则。
@Import(AutoConfigurationPackages.Registrar.class)//给容器导入一个组件
public @interface AutoConfigurationPackage {
}
利用Registrar向容器中批量注册一系列组件。
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImports(metadata));
}
}
AnnotationMetadata : 被标注的类(启动类)的注解元信息。其中包含了该类的包名,将报名转换成数组,然后使用register方法批量向容器中注册该包下的组件。
@Import(AutoConfigurationImportSelector.class)
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
1、利用getAutoConfigurationEntry(annotationMetadata); 给容器中批量导入一些组件。
2、List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); 获取到所有需要导入到容器中的配置类。(下图130个配置类,不是所有的配置类都需要注册进容器中。而是根据实际需要。)
3、利用工厂加载Map<String, List<String>> loadSpringFactories(ClassLoader classLoader);从资源文件中得到所有组件。
4、从"META-INF/spring.factories"位置来加载一个文件,默认扫描当前系统所有该位置的文件。(不同的jar中可有这个文件。)
核心:"spring-boot-autoconfigure-2.4.2.jar/META-INF/spring.factories" ,包含了 Auto Configure
"spring-boot-autoconfigure-2.4.2.jar/META-INF/spring.factories"配置文件中写死了,SpringBoot一启动就要加载的所有配置类(只加载其中所需要的)。
@ComponentSCan
组件扫描器。
虽然130个场景的所有自动配置启动的时候默认全部加载。
按照**@Conditional
**条件装配规则,最终会按需配置。
如果容器中的指定类型的组件不是某个名字,则将这个组件以新的名字注册一遍。
@Bean
@ConditionalOnBean(MultipartResolver.class)
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)//容器中没有这个名字 multipartResolver
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// 给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找。
// SpringMVC multipartResolver,防止组件名称不规范的。 multipartResolver必须是这个id,
// 而容器中有着类型的组件,但是名字不是multipartResolver,就会自动从容器中找到这个类传入方法参 数中,然后返回出去,以新的名字命名。
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}
给容器中加入了文件上传解析器;
CharacterEncodingFilter字符编码过滤器会自动被配置。
@ConditionalOnClass(CharacterEncodingFilter.class)
@ConditionalOnProperty(prefix = "server.servlet.encoding", value = "enabled", matchIfMissing = true)
// matchIfMissing=true , 如果没有配置,默认为true,即,开启状态。 开启状态就会自动配置下面的Bean。
public class HttpEncodingAutoConfiguration {
@Bean //注册一个Bean
@ConditionalOnMissingBean //当容器中没有CharacterEncodingFilter类时,创建该Bean。
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(Encoding.Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(Encoding.Type.RESPONSE));
return filter;
}
给容器中加入CharacterEncodingFilter字符编码过滤器。
SpringBoot默认会在底层默认配置好所有组件,但是用户自己配置了以用户的优先。
@Bean
public CharacterEncodingFilter characterEncodingFilter() {}
总结:
debug=true
开启自动配置报告,默认关闭。
简化JavaBean开发
1. 引入依赖
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
<version>version>
dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
安装Lombok插件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NmS9t7Ut-1612143666389)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210120142359217.png)]
SpringInitailizr
创建好了项目结构
POM自动引入了依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
dependency>
导入后,默认启用。(构建项目就可以更新代码效果)Ctrl + F9 。
严格意义上说,devtools只是一个热重启,而不是一个热部署。
可以使用JRebel
插件实现热部署功能。
同以前的properties语法
非常适合用来做以数据为中心的配置文件。
##### 2.1.2.1 yaml 基本语法
字面量: 单个的、不可再分的值。date、boolean、string、number、null
k: v
对象:键值对的集合。map、hash、set、object
行内写法:k: {
k1:v1,k2:v2}
# 或
k:
k1: v1
k2: v2
数组:一组按次序排列的值。array、list、queue
行内写法: k: [v1,v2,v3]
# 或者
k:
- v1
- v2
- v3
2.1.2.3 示例
@Data
@AllArgsConstructor
@NoArgsConstructor
@ConfigurationProperties("com.yyl.springbootstudy.springinitializr.bean.person")
public class Person {
private String name;
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> salary;
private Map<String, List<Pet>> allPets;
}
com:
yyl:
springbootstudy:
springinitializr:
bean:
person:
name: "\n张三" # 双引号\n会转义成换行符, 单引号会作为字符串。
boss: true
birth: 2001/06/03
age: 19
pet:
name: 二哈
type: 哈士奇
interests: 打代码,听音乐,看电影
animal: 阿毛,阿狗
score: {
english: 60分,math: 80分,chinese: 100分}
salary:
- 7000.0
- 10000.0
all-pets:
cat:
- {
name: 小白,type: 银渐层}
- name: 小银
type: 土猫
daog:
- {
name: 二哈,type: 哈士奇}
- name: 毛毛
type: 金毛
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eBvM88T0-1612143666393)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210120191746442.png)]
当使用了配置绑定后
解决方案,加入下面的依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
<scope>providedscope>
dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<exclude>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(SpringBoot为SpringMVC提供了自动配置,在大多数应用程序中都能很好地工作。)
The auto-configuration adds the following features on top of Spring’s defaults:(自动配置在Spring的默认配置之上添加了以下特性)
ContentNegotiatingViewResolver
and BeanNameViewResolver
beans.
Converter
, GenericConverter
, and Formatter
beans.
Converter
转换器, GenericConverter
通用转换器, Formatter
格式器HttpMessageConverters
(covered later in this document).
HttpMessageConverters
http消息转换器(配合内容协商理解原理)MessageCodesResolver
(covered later in this document).
MessageCodesResolver
(i18n国际化用)index.html
support.
Favicon
support
Favicon
ConfigurableWebBindingInitializer
bean (covered later in this document).
ConfigurableWebBindingIntiializer
,(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
.使用
@Configuration
+WebMvcConfigurer
自定义(拦截器, 格式化器, 视图控制器, 和其他特性)规则。但不使用@EnableWebMVC
注解
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.提供自定义的
RequestMappingHandlerMapping
,RequestMappingHandlerAdapter
, orExceptionHandlerExceptionResolver
,可以用WebMvcRegistrations
声明自定义这些组件。声明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
静态资源目录
只要静态资源放在类路径下: /static
or /public
or /resources
or /META-INF/resources
(静态资源目录有不同的优先级)
访问:当前项目根路径/ + 静态资源名
原理:静态映射,默认映射的Url:/** 2
请求进来,处理器映射器会先去找处理器(Controller),如果映射器找不到处理器,不能处理的请求会交给静态资源处理器,静态资源也找不到就会404
静态资源访问前缀
spring:
mvc:
static-path-pattern: /res/**
项目名 + static-path-pattern + 静态资源名 = 按照优先级3在不同的静态资源文件夹下寻找静态资源。
指定静态资源目录
web:
resources:
# 替换默认的静态资源目录
static-locations: [classpath:/haha/]
WebJars
前端资源的jar包形式
<dependency>
<groupId>org.webjarsgroupId>
<artifactId>jqueryartifactId>
<version>3.5.1version>
dependency>
可以看到jquery静态资源在resources/webjars下存放,resources目录是默认的静态资源的目录之一,所以可以通过url直接进行静态资源访问。URL:主机/static-path-pattern/webjars/jquery/3.5.1/jquery.js
SpringBoot 访问项目根路径,默认访问静态资源下的index.html
controller能处理/index
并且只支持**.html** 3
private Resource getIndexHtml(Resource location) {
try {
// WebMvcAutoConfiguration 自动配置时,只定义了扫描了index.html文件。
Resource resource = location.createRelative("index.html");
if (resource.exists() && (resource.getURL() != null)) {
return resource;
}
}
catch (Exception ex) {
}
return null;
}
spring:
# mvc:
# static-path-pattern: /res/** # 会导致默认访问index.html页面失效。
web:
resources:
# 替换默认的静态资源目录
static-locations: [classpath:/haha/]
favicon
支持spring:
mvc:
static-path-pattern: /res/** # 会导致默认访问index.html页面 和 favicon.ico图标失效。
SpringBoot启动默认加载 xxxAutoConfiguration 类[^xxxAutoConfiguration ]
SpringMVC 功能的自动配置类 : WebMvcAutoConfiguration
1
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({
Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({
DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
// ......
}
给容器中配了什么?
// 内部类, @Configuration:也是一个配置类。
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({
WebMvcProperties.class,
org.springframework.boot.autoconfigure.web.ResourceProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
..}
拓展知识点:一个配置类中只有一个有参构造器
//该类只有一个有参构造方法。
// ResourceProperties resourcesProperties 获取和 spring.resources绑定的配置类对象
// WebMvcProperties mvcProperties 获取和 spring.mvc绑定的配置类对象。
// WebProperties WebProperties 获取和 spring.web绑定的配置类对象
// ListableBeanFactory beanFactory : Spring的BeanFactory Bean工厂,IOC容器。
// ObjectProvider messageConvertersProvider : 找到所有的HttpMessageConverters
// resourceHandlerRegistrationCustomizerProvider : 找到资源处理器的自定义器
// dispatcherServletPath : DispatcherServlet的映射路径
// servletRegistrations : 给应用注册原生Servlet
public WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory,
//Spring的BeanFactory Bean工厂,IOC容器。
ObjectProvider<HttpMessageConverters> messageConvertersProvider,
//找到所有的HttpMessageConverters
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
//资源处理器自定义器
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
//DispatcherServlet映射路径
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
//Servlet注册Bean, 可以用来注册原生Servlet
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
this.mvcProperties.checkConfiguration();
}
静态资源处理的默认规则
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
super.addResourceHandlers(registry);
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
ServletContext servletContext = getServletContext();
addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
// classpath:/META-INF/resources/webjars/下的资源 映射到 /webjars/** 请求规则中
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (servletContext != null) {
registration.addResourceLocations(new ServletContextResource(servletContext, SERVLET_LOCATION));
}
});
# 从上面的规则来看,可以发现 addMappings 可以控制开关所有静态资源的默认配置规则。
spring:
web:
resources:
add-mappings: true # false 禁用所有静态资源默认规则
cache:
period: 1100 # 浏览器中请求缓存的存活时间,默认单位/s
// resourceProperties.getStaticLocations() 返回了静态资源默认的4个位置
public static class Resources {
// 静态资源默认的4个位置
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;
index欢迎页面的处理规则 5
HandlerMapping6 :处理器映射。保存了每一个Handler能处理哪些请求(HandlerMapping通过url找到相应的处理方法)
@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) {
// 这里写死了,静态资源目录/index.html存在 并且 是默认(/)的静态资源前缀(spring.web.static-path-pattern),才会请求转发到这个index.html中。
if (welcomePage != null && "/".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage);
setRootViewName(“forward:index.html”);
}
//这里判断了 templates模板目录下是否有index,如果有,跳转到模板引擎中的index,如果有index模板,就转发到模板index中。
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
logger.info(“Adding welcome page template: index”);
setRootViewName(“index”);
}
}
// =========================================
private boolean welcomeTemplateExists(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext) {
return templateAvailabilityProviders.getProvider(“index”, applicationContext) != null;
}
[^模板引擎]:SpringBoot支持的所有模板引擎---> 下图所示↓
模板引擎:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4bbR4YgW-1612143666395)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210121133753729.png)](使用模板引擎的前提是导入了模板引擎的jar包。)
不同的模板引擎的后缀可能不同,所以根据**index**来寻找模板文件。
#### 2.2.3 请求参数处理原理
##### 2.2.3.1 请求映射
###### 2.2.3.1.1RESTFul的使用和原理
- @xxxMapping;
- RESTFul风格支持*(使用HTTP请求方式动词(method=GET\DELETE\PUT\POST)来表示对资源的操作*
- 传统风格:/getUser、/deleteUser、/updateUser、 /saveUser
- ResetFul风格:/user GET-获取、 DELETE-删除、 PUT-修改、 POST-保存
- from表单的method值支持GET和POST,设置成其他的请求方式,将会默认为GET。
- 核心Filter; **`HiddenHttpMethodFilter`** [^HiddenHttpMethodFilter]
- 用法:表单**method=post**,附带参数:**_mehod=put**
- RESTFul风格原理
- 原理概述:
- 表单提交会带上**_mehod=put**
- 请求过来被HiddenHttpMethodFilter拦截
- 请求是否正常,并且是POST
- 获取到**_mehod**参数的值
- 兼容以下请求:PUT。DELETE。PATCH
- 原生`Request(Post)`,包装模式`RequestWrapper`重写了`getMethod`方法和`method`属性,返回的`_method`的值
- 过滤器链放行的时候用`RequestWrapper`放行。以后的方法调用`getMethod`是调用`RequestWrapper`中重写过的方法。
- `spring.mvc.hiddenmethod.filter` 如果没有配置enabled, 默认为关闭。
- ~~~java
@Bean
// 容器中没有这个类,就注册这个类
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
// spring.mvc.hiddenmethod.filter 如果没有配置enabled, 默认为关闭。
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
~~~
- ~~~java
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
private static final List ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(
HttpMethod.PUT.name(),HttpMethod.DELETE.name(),HttpMethod.PATCH.name()
));
/** 默认请求参数名. */
public static final String DEFAULT_METHOD_PARAM = "_method";
private String methodParam = DEFAULT_METHOD_PARAM;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//原生Request
HttpServletRequest requestToUse = request;
//请求必须是POST方式
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
// 获取到_mehtod参数值
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
// 转换大写
String method = paramValue.toUpperCase(Locale.ENGLISH);
// ALLOWED_METHODS支持的method中有该请求的method
if (ALLOWED_METHODS.contains(method)) {
// 对原生Request进行包装,重写method属性和getMethod()方法
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}
filterChain.doFilter(requestToUse, response);
}
}
~~~
- 开启RESTFul风格HTTP请求
- ~~~properties
# 开启RESTFul风格过滤器 HiddenHTTPMethodFilter
spring.mvc.hiddenmethod.filter.enabled=true
~~~
- ~~~html
~~~
- Rest使用客户端工具
- PostMan、PostAPI等
- 直接发送PUT,DELETE,等方式请求,无序Filter。(因为HTML仅支持Post和Get)
- 自定义methodParam
- ~~~java
@Bean
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
hiddenHttpMethodFilter.setMethodParam("_m");
return hiddenHttpMethodFilter;
}
~~~
###### 2.2.3.1.2 请求映射原理
SpringMVC的原理分析都从**org.springframework.web.servlet.DispatcherServlet#doDispatch**开始
~~~~java
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
// 找到当前请求使用哪个Handler(Controller的方法)处理。
mappedHandler = getHandler(processedRequest);
HandlerMapping:处理器映射。/uri ->> 某个处理程序
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jBQwGTkA-1612143666397)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210121194918339.png)]
所有的请求映射都在HandlerMapping中。
SpringBoot自动配置欢迎页的HandlerMapping 。访问“/”就能访问index.html 5
SpringBoot自动配置了默认的 7
请求进来,遍历所有的HandlerMapping看看是否有请求信息。
需要自定义映射处理,可以在容器中注册一个自己的6,实现HandlerMapping接口即可。
@PathVariable(路径变量)
@RequestHeader(获取请求头)
@RequestParam(获取请求参数)
@CookieValue(获取Cookie值)
@RequestAttribute(获取request域属性)
@RequestBody(获取请求体)
@MatrixVariable(矩阵变量)
@PathVariable
If the method parameter is {@link java.util.Map Map
如果方法有@PathVariable Map
参数,则将使用所有路径变量名称和值来填充映射。
@RequestHeader
If the method parameter is {@link java.util.Map Map
如果方法有@RequestHeader Map
或者 @RequestHeader MultiValueMap
或者 @RequestHeader HttpHeaders
参数,则将使用所有请求头名称的值来填充映射。
@RequestParam
If the method parameter is {@link java.util.Map Map
如果方法有 @RequestParam Map
@CookieValue
The method parameter may be declared as type {@link javax.servlet.http.Cookie} * or as cookie value type (String, int, etc.).
方法参数可以声明为类型@CookieValue Cookie cookie 或 cookie值类型(字符串,整数等)。
@GetMapping("/car/{id}/owner/{name}")
public Map<String,Object> getCar(@PathVariable Integer id, @PathVariable("name") String username,
@PathVariable Map<String,String> pathVariableMap,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader String cookie,
@RequestHeader Map<String,String> requestHeaderMap,
@RequestHeader MultiValueMap<String,String> requestHeaderMultiValueMap,
@RequestHeader HttpHeaders httpHeaders,
Integer age,
@RequestParam("inters") List<String> interests,
@RequestParam MultiValueMap<String,String> requestParamMultiValueMap,
@CookieValue("Idea-ae83a0fc") String ideaCookie,
@CookieValue("Webstorm-1a43536c") Cookie webStormCookie){
HashMap<String, Object> map = new HashMap<>();
/*@PathVariable*/
map.put("id",id);
map.put("name",username);
// 如果方法有@PathVariable Map 参数,则将使用所有**路径变量名称和值**来填充映射。
map.put("pathVariableMap",pathVariableMap);
/*@RequestHeader*/
map.put("userAgent",userAgent);
map.put("cookie",cookie);
//如果方法有@RequestHeader Map 或者 @RequestHeader MultiValueMap 或者 @RequestHeader HttpHeaders 参数,则将使用所**有请求头名称的值**来填充映射。
map.put("requestHeaderMap",requestHeaderMap);
map.put("httpHeaders",httpHeaders);
map.put("requestHeaderMultiValueMap",requestHeaderMultiValueMap);
/*@RequestParam*/
map.put("age",age);
map.put("inters", interests);
//如果方法有@RequestParam Map 或者 @RequestParam MultiValueMap,则将使用所**有请求参数名的值**来填充映射。
map.put("requestParamMultiValueMap",requestParamMultiValueMap);
/*@CookieValue*/
// 方法参数可以声明为类型@CookieValue Cookie cookie 或 cookie值类型(字符串,整数等)
map.put("ideaCookie",ideaCookie);
map.put("webStormCookie",webStormCookie);
return map;
}
@RequestBody
@PostMapping("/save")
public Map<String,Object> postMethod(@RequestBody String postContent){
HashMap<String, Object> map = new HashMap<>();
map.put("postContent",postContent);
return map;
}
{
"postContent": "userName=lisi&email=y31367208351111%40gmail.com"}
@RequestAttribute
@RequestMapping("/goto")
public String gotoPage(HttpServletRequest request){
request.setAttribute("msg","成功了");
request.setAttribute("code",200);
return "forward:/success";
}
@RequestMapping("/success")
@ResponseBody
public Map<String,Object> success(@RequestAttribute("msg") String message,
@RequestAttribute Integer code,
HttpServletRequest request){
HashMap<String, Object> map = new HashMap<>();
map.put("annotation_msg",message);
map.put("annotation_code",code);
map.put("reqObject_msg",request.getAttribute("msg"));
map.put("reqObject_code",request.getAttribute("code"));
return map;
}
@MatrixVariable
11 /cars/{path}?xxx=xxx&aaa=ccc queryString 查询字符转。@RequestParam;
/cars/path;low=34;brand=byd,audi,yd : 矩阵变量
页面开发,cookie禁用了,session里面的内容怎么使用:
session.set(a,b) ---> JSessionId ---> cookie ----> 每次发请求携带。
url重写: /abc;jesssionid=xxxx 把cookie的值使用矩阵变量的方式进行传递。
/boss/1/2 : 1号boss下面的第2个员工 /boss/1;age=20/2;age=20 : 1号且age=20的boss下面的第2个且age=20的员工
1、语法 cars/sell;low=34;brand=byd,audi,yd
2、直接访问,报错没有找到矩阵变量。原因:SpringBoot默认是禁用了矩阵变量的功能。
手动开启:原理:对于路径的处理,都是由UrlPathHelper进行解析。
-UrlPathHelper.removeSemicolonContent(删除分号内容) 支持矩阵变量,默认为true。
-UrlPathHelper的创建在WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#configurePathMatch中
3、矩阵变量需要放在路径变量中,UrlPageHelper会对路径变量进行解析。否则报404
Controller:
@GetMapping("/cars/{path}")
@ResponseBody
public Map<String,Object> carsSell(@MatrixVariable(value = "low") Integer low,
@MatrixVariable List<String> brand,
//可以不获取这个path
@PathVariable 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}/{employeeId}")
@ResponseBody
public Map<String,Object> boss(@MatrixVariable(value = "age", pathVar = "bossId") Integer bossAge,
@MatrixVariable(value = "age", pathVar = "employeeId") Integer employeeAge,
@PathVariable String bossId,
@PathVariable String employeeId){
Map<String,Object> map = new HashMap<>();
map.put("bossAge",bossAge);
map.put("bossId",bossId);
map.put("employeeId",employeeId);
map.put("employeeAge",employeeAge);
return map;
}
方式1:
@Configuration
public class OpenMatrixVariableConfig implements WebMvcConfigurer {
/**
* 开启@MatrixVariable 矩阵变量功能, 须实现WebMvcConfigurer接口
* WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter implements WebMvcConfigurer
* */
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
//默认底层中设置了true。该值默认是false
urlPathHelper.setAlwaysUseFullPath(true);
configurer.setUrlPathHelper(urlPathHelper);
}
}
方式2:
/**
* 开启@MatrixVariable 矩阵变量功能。
* 重新注册了一个WebMvcConfigurer类。
* @return 一个自定义个WebMvcConfigurer(WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter)
*/
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
urlPathHelper.setAlwaysUseFullPath(true);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}
方式3(推荐):
/**
* 开启@MatrixVariable 矩阵变量功能。
* 从容器中拿到已经存在UrlPathHelper,然后更改removeSemicolonContent属性
* @return 再默认的基础上修改后的UrlPathHelper
*/
@Bean
public UrlPathHelper urlPathHelper(UrlPathHelper urlPathHelper){
urlPathHelper.setRemoveSemicolonContent(false);
return urlPathHelper;
}
WebRequest、ServletRequest、MultipartRequest、HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
12
ServletRequest方法参数解析器,用来解析目标方法中Servlet原生API,
//支持以下xxx.class所有API
@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) && !parameter.hasParameterAnnotations()) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}
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()
可以自动类型转换与格式化,可以级联封装。
13
/**
* 姓名:
* 年龄:
* 生日:
* 宠物姓名:
* 宠物年靓:
*/
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}
@Data
class Pet {
private String name;
private String age;
}
WebDataBinder
binder = binderFactory.createBinder(webRequest, attribute, name);
** 14 :web数据绑定器,将请求参数的值绑定到指定的JavaBean里面**
WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中
GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean – Integer)
可以在WebDataBinder中放自定义的Converter;
@FunctionalInterface
//S : source 原类型
//T : target 目标类型
public interface Converter
class StringToNumber implements Converter
/**
* Add {@link Converter Converters} and {@link Formatter Formatters} in addition to the ones
* registered by default.
* 除了默认注册的之外,还添加Converter 和 Formatters,供数据绑定器使用。
*/
default void WebMvcConfigurer#addFormatters(FormatterRegistry registry) {
}
Spring中如果有自定义的组件,会使用自定义的组件。
/*
* 自定义Converter注册方式1:
* 在容器中注册一个Converter的实现类,底层初始化时,会自动扫描到所有的Converter,并添加进Formatters。
* */
@Bean
public Converter<String,Date> MyStringToDateConverterBean(){
return new Converter<String, Date>() {
@SneakyThrows({
java.text.ParseException.class})
@Override
public Date convert(String source) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd");
return dateFormat.parse(source);
}
};
}
/*
* 自定义Converter注册方式2:
* 从容器中直接获取FormatterRegistry;通过FormatterRegistry向格式化器中注册自定义Converter
* */
//@Bean
public FormatterRegistry formatterRegistry(FormatterRegistry formatterRegistry){
formatterRegistry.addConverter(new Converter<String, Date>() {
@SneakyThrows({
java.text.ParseException.class})
@Override
public Date convert(String source) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd");
return dateFormat.parse(source);
}
});
return formatterRegistry;
}
/*
* 自定义Converter注册方式3:
* 间接获取FormatterRegistry;通过实现WebMvcConfigurer接口的方法,
* 底层调用该实现类的addFormatters时,通过FormatterRegistry向格式化器中注册自定义Converter
* */
//@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Date>() {
@SneakyThrows({
java.text.ParseException.class})
@Override
public Date convert(String source) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd");
return dateFormat.parse(source);
}
});
}
};
}
}
/*
* 自定义Converter注册方式4:
* 间接获取FormatterRegistry;通过实现WebMvcConfigurer接口的方法,
* 底层调用该实现类的addFormatters时,通过FormatterRegistry向格式化器中注册自定义Converter
* */
//@Component
class MyWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Date>() {
@SneakyThrows({
java.text.ParseException.class})
@Override
public Date convert(String source) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd");
return dateFormat.parse(source);
}
});
}
}
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss:ssss", timezone = "GMT+8")
private Date birth;
[^@RequestMappingHanderAdapter]
[^@HandlerFunctionAdapter]
// Actually invoke the handler.
//DispatcherServlet#doDispatcher
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
//DispatcherServlet#doDispatcher --> AbstractHandlerMethodAdapter#handle --> RequestMappingHandlerAdapter#handleInternal
// 执行目标方法
mav = invokeHandlerMethod(request, response, handlerMethod);
//RequestMappingHandlerAdapter#invokeHandlerMethod
invocableMethod.invokeAndHandle(webRequest, mavContainer);
//真正执行目标方法
//ServletInvocableHandlerMethod#invokeAndHandle
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//获取方法参数的值
//InvocableHandlerMethod#invokeForRequest
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
如何获取目标方法的参数
===============================getMethodArgumentValues=====================================
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;
}
确定将要执行的目标方法的每一个参数值是什么
SpringMVC目标方法能写多少种参数类型,取决于参数解析器。
参数解析器会将请求中的数据注入到目标方法相应的参数中
解析出目标方法中用到的参数注解和相应的解析器。
处理方法参数解析器18
supportsParameter判断当前解析器是否支持处理当前参数
如果支持,就调用resolveArgument进行解析。
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
--->
render(mv, request, response);
--->
view.render(mv.getModelInternal(), request, response);
--->
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
--->
// Expose the model object as request attributes.
// 暴露model 作为 请求属性
exposeModelAsRequestAttributes(model, request);
protected void exposeModelAsRequestAttributes(Map<String, Object> model,
HttpServletRequest request) throws Exception {
model.forEach((name, value) -> {
if (value != null) {
request.setAttribute(name, value);
}
else {
request.removeAttribute(name);
}
});
}
graph LR
A(数据响应) ---> B[响应页面]
A --> C[响应数据]
C --> D[JSON]
C --> E[XML]
C --> F[xls]
C --> G[图片音视频]
C --> H[自定义协议数据]
jackson.jar + (19 或 20)
web起步依赖中集成了Jackson依赖。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jsonartifactId>
<version>2.4.2version>
<scope>compilescope>
dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z7FPtOlE-1612143666399)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210123173842535.png)]
jackson 支持xml功能模块的依赖需要手动导入
<dependency>
<groupId>com.fasterxml.jackson.dataformatgroupId>
<artifactId>jackson-dataformat-xmlartifactId>
dependency>
源码分析流程21
protected <T> void AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse)(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Object body; Class<?> valueType; Type targetType;
if (value instanceof CharSequence) {
body = value.toString();
valueType = String.class;
targetType = String.class;
}
else {
body = value;
valueType = getReturnValueType(body, returnType);
targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
}
if (isResourceType(value, returnType)) {
/*返回值类型是资源类型,进行的一些操作。*/}
/*内容协商部分*/
//媒体类型,内容协商。
MediaType selectedMediaType = null;
//从前面包装res的outputMessage中获取前面已经决定的响应内容类型
MediaType contentType = outputMessage.getHeaders().getContentType();
//判断当前响应头中是否已经有确定可用的媒体类型。MediaType
boolean isContentTypePreset = contentType != null && contentType.isConcrete();
if (isContentTypePreset) {
//内容类型有效就把媒体类型设置成内容类型。
selectedMediaType = contentType;
}else {
//内容类型无效,开始确定相应内容类型。
//从包装req的对象中取出原req
HttpServletRequest request = inputMessage.getServletRequest();
//从req中获取浏览器能接受的数据类型(获取req请求头中Accept字段)
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
//根据返回值类型获取服务器能产出的媒体类型。(所有HttpMessageConverter中找到能处理的,然后用这个能处理的converter调用getSupportedMediaTypes(),返回所有自身能处理的mediaTypes)
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
//最终决定返回值内容类型的集合
List<MediaType> mediaTypesToUse = new ArrayList<>();
//最佳匹配:从浏览器能接受的类型,和服务器能生产出的内容类型中,
//匹配出客户端想要的媒体类型,有没有在服务器能产出的媒体类型中存在。
//(求两个集合的合集)
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
//如果有匹配成功的媒体类型,存放在可用媒体类型中。
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
//将所有可用的媒体类型,按质量权重进行排序,降序排列。
MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
//遍历可用媒体类型
for (MediaType mediaType : mediaTypesToUse) {
//判断媒体类型是不是具体的类型, 不是通配符类型。
if (mediaType.isConcrete()) {
//质量权重 + 具体类型 最佳匹配的一个设置为选中的媒体类型。
selectedMediaType = mediaType;
break;
}
else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
}
if (selectedMediaType != null) {
//移除选中媒体类型中的 “q”权重标志符值。
selectedMediaType = selectedMediaType.removeQualityValue();
/*MessageConverter 核心部分*/
//遍历容器中所有HttpMessageConverter,看谁支持操作这个返回值(Person,将Person转为selectedMediaType:application/xml类型)
for (HttpMessageConverter<?> converter : this.messageConverters) {
// 判断是否能强转成 通用消息转换器,不能就赋值null
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?(GenericHttpMessageConverter<?>) converter : null);
// 当前converter是否能canWrite进行写操作
if (genericConverter != null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) {
//body还是Person对象
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
//在响应头部添加 Content-Disposition(下载文件时需要)。内部自动判断。
addContentDispositionHeader(inputMessage, outputMessage);
//调用HttpMessageConverter接口的write方法,进行写操作
if (genericConverter != null) {
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
}else {
//写操作完毕后,res中出现了Content-Type=application:json
//write方法中最终调用writeInternal(......objectWriter.writeValue(generator, value);调用Jackson将对象写成JSON......),
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
return;
}
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S50c4mAw-1612143666402)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210123190527721.png)]
根据上面跟踪 this.returnValueHandlers 可以发现一共有15种返回值处理器。
ModelAndView
Model
View
ResposeBody
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DefreedResult
AsyncTask
ModelAttribute
RequestResponseBody
@ResponseBody
RequestResponseBodyMethodProcessor
处理器,进行处理。ViewName
Map
ModelAttribute
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dUlBePMJ-1612143666403)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210123194319585.png)]
返回值处理器先判断是否支持当前返回值,支持就返回,不支持就继续用下一个返回值。
如果支持,就调用handleReturnValue
进行处理
使用 RequestResponseBodyMethodProcessor
处理器处理标注@ResponseBody的返回值
例:返回值处理器之一:ModelAndViewMethodReturnValueHandler
的两个实现方法
@Override
public boolean supportsReturnType(MethodParameter returnType) {
//返回目标方法返回值 是否ModelAndView类型
return ModelAndView.class.isAssignableFrom(returnType.getParameterType());
}
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
//设置请求已经处理
mavContainer.setRequestHandled(true);
//对req,res进行包装
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
//使用MessageConverter消息转换器进行写出。
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
处理返回值内部实现原理
@Override
public void HandlerMethodReturnValueHandlerComposite#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);
}
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
//返回值处理器先判断是否支持当前返回值,支持就返回,不支持就继续用下一个返回值处理器进行判断。
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
HttpMessageConverter
运行原理 23从req中获取浏览器能接受的数据类型(获取req请求头中Accept字段);根据返回值类型和能接受的类型,获取能产出的媒体类型。
SpringMVC会挨个遍历容器中所有HttpMessageConverter
,看谁支持操作这个返回值(Person,将Person转为selectedMediaType类型)
找到[^ MappingJackson2HttpMessageConverter] 可以处理@RepsonseBody返回值,将对象写为JSON
最终将Person对象转换为JSON字符串。放入res响应体中,并设置Content-Type
1. HttpMessageConverter
规范
23 : 看是否支持将此Class类型的对象,转为MediaType类型的数据。
例子:Person转Json。或者 JSON转Person
系统中默认的HttpMessageConverter
小例子:24
查看该类的supports方法可以看到支持返回值为Resource接口的类型。
@Override
protected boolean supports(Class<?> clazz) {
return Resource.class.isAssignableFrom(clazz);
}
25 返回值类型可以用来做文件下载。
HttpMessageConverter
支持的媒体类型
@Override
public List<MediaType> getSupportedMediaTypes() {
return Collections.unmodifiableList(this.supportedMediaTypes);
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aw3P432y-1612143666404)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210123211417943.png)]
26
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lMmVscU1-1612143666405)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210123201727227.png)]
根据客户端接收能力,服务器的生产能力不同,返回不同媒体类型。
只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器当前客户端可以接收的数据类型。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3yibH1Wf-1612143666405)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210124114151933.png)]
判断当前响应头中是否已经有确定的媒体类型。MediaType
获取客户端(PostMan、Browser)支持接收的内容类型。(获取客户端Accept请求字段)【application/xml】【application/json】
遍历循环容器中所有的MessageConverter,看谁支持操作这个对象(Person,将Person转为MediaType媒体类型)
找到支持操作这个对象的Converter,吧Converter支持的媒体类型统计出来。
客户端要【application/xml】。服务端能生产【json、xml】
进行内容协商的最佳匹配媒体类型
用 支持 将对象转为 最佳匹配媒体类型 的Converter。调用它进行转化。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-svQOqrqS-1612143666406)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210124134118524.png)]
导入了jackson处理xml的包,xml的Converter就会自动添加进Converters
WebMvcConfigurationSupport#jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
//如果系统中存在处理xml的包,就会将Converter添加进去。
if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
为了方便内容协商,开启基于请求参数的内容协商功能。
# 开启浏览器参数方式内容协商功能
spring.mvc.contentnegotiation.favor-parameter=true
# 指定内容协商参数名, 默认 format
spring.mvc.contentnegotiation.parameter-name=f
发起请求:
http://localhost:8080/test/person?format=xml
http://localhost:8080/test/person?format=json
27
spring:
mvc:
contentnegotiation:
favor-parameter: true #开启浏览器参数方式内容协商功能
parameter-name: f # 指定内容协商参数名, 默认 format
media-types: {
app: application/x-app, x-app: application/x-app} # 参数内容协商策略中添加自定义的媒体类型映射。
或
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
MediaType mediaType = MediaType.parseMediaType("application/x-app");
configurer.mediaType("app",mediaType);
configurer.mediaType("x-app", mediaType);
/*
// 这种方式导致默认ContentHeaderNegotiationStrategy请求头内容协商策略不可用,
// 因为configurer.strategies(Arrays.asList(pcns)); 直接将传入的list覆盖掉了内容协商策略的list,而传入的list中只有一个自定义的。
HashMap map = new HashMap<>();
ParameterContentNegotiationStrategy pcns = new ParameterContentNegotiationStrategy(map);
configurer.strategies(Arrays.asList(pcns));
*/
}
};
}
添加之后,可以看到ParameterContentNegotiationStrategy
中出现了我们自定义的类型映射。
内容协商 + HttpMessageConverter
实现协议数据兼容。Json、Xml、。。。
SpringMVC中想自定义配置或修改一些东西,都只有一个入口:WebMmvConfiguer接口。只需要在配置类中给容器添加一个它的实现类,就可以在容器中添加自定义的组件。
/*
注册自定义HttpMessageConverter方式1:
通过SpringMVC自定义配置的入口进行配置。
*/
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer(){
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new AppHttpMessageConverter());
}
};
}
/*
注册自定义HttpMessageConverter方式2:
直接返回自定义的Http消息转换器,容器底层会自动扫描HttpMessageConverter的实现类。
小细节:该种方式注册HttpMessageConverter会导致该converter出现在所有converters的第一个位置,
底层第一次调用canWrite(clazz,null)遍历converters找到服务器(根据能否处理clazz数据)能生产的media,
第二次调用canWrite(clazz,selectedMediaType)遍历converters确定哪个可以将clazz数据转为selectedMediaType媒体类型,
所以第二次调用canWrite时,要判断好selectedMediaType类型(客户端想要的类型之一)。
如果不判断,就会导致客户端本没有发送自定义类型的accept,却收到了自定义类型的数据。
因为位于第一个位置的自定义HttpMessageConverter只判断了可以处理这个对象,而没有判断是否是客户端想要的类型,直接将其转换。
*/
@Bean
public AppHttpMessageConverter appHttpMessageConverter(){
return new AppHttpMessageConverter();}
/*
* 自定义Http消息转换器
* 经过内容协商,Person类型数据转换成 自定义x-app类型数据。
* */
public class AppHttpMessageConverter implements HttpMessageConverter<Person> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;}
// 底层会调用canWrite(clazz, null) 判断是否可以写,
// 如果可以重写则,调用getSupportedMediaType获取当前HttpMessageConverter支持的所有Media。
//然后再调用canWrite(clazz, selectedMediaType)判断是否可以clazz数据转换为selectedMediaType数据类型。
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
boolean assignableFrom = Person.class.isAssignableFrom(clazz);
if (mediaType == null){
return assignableFrom;
}else {
List<MediaType> supportedMediaTypes = getSupportedMediaTypes();
for (MediaType s : supportedMediaTypes) {
return s.toString().equals(mediaType.toString()) && assignableFrom;
}
}
return false;
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-app");
}
@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() + ";" + person.getPet().getName() + ";" + person.getPet().getAge();
OutputStream body = outputMessage.getBody();
body.write(data.getBytes());
body.flush();
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mhlacu9C-1612143666408)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210124203339766.png)]
图中可以看到内容协商策略的27中,默认支持的mediaTypes只有两个xml和json,不支持其他的参数协商策略。
视图解析:SpringBot默认不支持JSP,需要引入第三方模板引擎技术实现页面渲染。
视图解析
[^模板引擎]
Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text.
现代化、服务端Java模板引擎
因为是服务端的Java模板引擎,所有有以下优缺点:
表达式名字 | 语法 | 用途 |
---|---|---|
变量取值 | ${…} | 获取请求域、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) |
特殊操作 | |
---|---|
无操作 | _ |
设置单个值
<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 scr="../../img/logo.png" th:attr="scr=@{/images/logo.png},title=#{logo},alt=#{alt}">
th:attr的代替写法 th:xxxx
<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">
所有h5兼容的标签写法
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onionstd>
<td th:text="${prod.price}">2.41td>
<td th:text="${prod.inStock ? #{true} : #{false}}">yestd>
tr>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({
TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({
WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {
}
自动配好的策略:
private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
<html lang="en" xmlns:th="http://www.thymeleaf.org">
目标方法处理的过程中,所有数据都会被放在ModelAndViewContainer里面。包裹数据和视图地址
方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer
任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)
processDispatcherResult 处理派发结果(页面该如何响应)
1、render(mv, request, response); 进行页面渲染逻辑
视图解析:
/**
* 登录检查
* 1、配置好拦截器要拦截哪些请求
* 2、把这些配置放在容器中
*/
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
/**
* 目标方法执行之前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("preHandle拦截的请求路径是{}",requestURI);
//登录检查逻辑
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
//放行
return true;
}
//拦截住。未登录。跳转到登录页
request.setAttribute("msg","请先登录");
// re.sendRedirect("/");
request.getRequestDispatcher("/").forward(request,response);
return false;
}
/**
* 目标方法执行完成以后
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行{}",modelAndView);
}
/**
* 页面渲染以后
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行异常{}",ex);
}
}
/**
* 1、编写一个拦截器实现HandlerInterceptor接口
* 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
* 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
*/
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //所有请求都被拦截包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行的请求
}
}
1、根据当前请求,找到**HandlerExecutionChain【**可以处理请求的handler以及handler的所有 拦截器】
2、先来顺序执行 所有拦截器的 preHandle方法
3、如果任何一个拦截器返回false。直接跳出不执行目标方法
4、所有拦截器都返回True。执行目标方法
5、倒序执行所有拦截器的postHandle方法。
6、前面的步骤有任何异常都会直接倒序触发 afterCompletion
7、页面成功渲染完成以后,也会倒序触发 afterCompletion
<form method="post" action="/upload" enctype="multipart/form-data">
<input type="file" name="file"><br>
<input type="submit" value="提交">
form>
/**
* 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";
}
文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties
自动配置好了 StandardServletMultipartResolver【文件上传解析器】
默认解析器名称multipartResolver
public static final String MULTIPART_RESOLVER_BEAN_NAME = "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)并封装(resolveMultipart,返回MUltipartHttpServletRequest)文件上传请求
@Override
public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
}
@Override
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}
最后底层会使用FileCopyUtils实现文件流的拷贝。
默认情况下,SpringBoot提供 /error
处理所有错误的映射
对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个 “whitelabel“ 错误视图,以HTML格式呈现相同的数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DeEWoWyT-1612143666414)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210127113124494.png)]
要对其进行自定义,添加View
解析为error
要完全替换默认行为,可以实现ErrorController
并注册该类型的Bean定义,或添加ErrorAttributes类型的组件
以使用现有机制替换其内容。
error/404.html
error/5xx.html
;精确匹配优先,没有匹配成功触发白页# 错误页包含异常对象
server.error.include-exception=true
# 错误页包含异常信息。 Exception.getMessage()
server.error.include-message=always
# 错误页不显示堆栈信息。
server.error.include-stacktrace=never
ErrorMVCAutoConfiguration29
绑定了ServerProperties配置文件【prefix = “server”】
注册组件 类型:DefaultErrorAttributes —> id:errorAttributes
定义错误页面中可以包含哪些数据。
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
注册组件 类型:BasicErrorController—> id:basicErrorController
@Controller
//动态取值--》${配置属性值:默认值}
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController
处理默认/error 路径的请求;页面响应 new ModelAndView(“error”, model);
容器中组件 View -> id是error(响应默认错误页:whitelabel白页)
容器中放组件 BeanNameViewResolver(Bean名称视图解析器);按照返回的视图名和组件的id进行映射,去容器中找View对象。
注册组件 类型:DefaultErrorResolver —> id:conventionErrorViewResolver
执行目标方法,目标方法运行期间有任何异常都会被catch,而且标识当前请求结束,并且用 dispatchException接收
进入视图解析流程(页面渲染?)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
mv = mv = processHandlerException(request, response, handler, exception); 处理handler发生的异常,处理完成返回ModelAndView
遍历所有的HandlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】
系统默认的:
3028[^ExceptionHandlerExceptionResovler]
DefaultErrorAttributes先来处理异常。把异常信息保存到req域,并且返回null;
ExceptionHandlerExceptionResovler 是用来处理@ExceptionHandler标注的方法
31
没有任何HandlerExceptionResolver能处理当前目标方法异常,异常再次被抛出(触发拦截器Completion方法)
graph TD
A(BasicErrorController) --> B[RequestMapping响应HTML]
A(BasicErrorController) --> C[RequestMapping响应JSON]
B --> D[req中拿到之前底层已经存放的status]
D --> E[req中拿到之前底层已经存放的错误信息model]
E --> F[遍历找到可以处理的视图解析器]
F --> G[找到视图解析器:req rep status model解析]
G --> H[status拼接'.html' 生成ViewName]
H --> I[判断各个目录是否有可用的View模板]
I ---> J[有:封装返回ModelAndView]
I --> P[没有:通过错误系列4xx或5xx拼接'.html',生成ViewName]
P --> I
J --> Q[ModelAndView != null:返回]
J --> K[ModelAndView == null:new ModelAndView 'error', model,使用自动配置类中注册的类型:View,id:error 的组件,'白页']
K --> O[配置类中注册的BeanNameViewResolver视图名称解析器进行解析id为'error'的视图]
C --> L[req中拿到之前底层已经存放的status]
L --> M[req中拿到之前底层已经存放的错误信息model]
M --> N[封装ResponseEntity返回]
/**
* 定义在Controller中,只处理当前Controller中发生的异常。
* 定义在@ControllerAdvice中,处理全局Controller中发生的异常
* 局部ExceptionHandler优先级高于Advice中的ExceptionHandler。(AOP切面机制)
*
*
* @ @ExceptionHandler(Class[] value) 定义自定义异常处理器
* value值定义可以处理目标方法发生的指定异常,留空默认为全部异常。
* @param req 原生req
* @param rep 原生 rep
* @param ex 目标方法发生的异常
* @param model Model参数方式转发携带数据
* @return 可以是ModelAndView String Model Json等。
*/
@ExceptionHandler({
ArithmeticException.class, NullPointerException.class})
public String exceptionHandle(HttpServletRequest req, HttpServletResponse rep, Exception ex, Model model){
System.out.println(req);
System.out.println(rep);
model.addAttribute("ex",ex);
return "error/myerrorpage";
}
/*
* @ControllerAdvice() Controller切面
* 继承@Component注解,当前类是一个组件。
* 作用:注解参数决定当前类作为哪些Controller的切面,
* 底层使用AOP机制将当前类中的方法织入指定的Controller中。
* */
@ControllerAdvice(basePackages = {
"com.yyl.springbootstudy.errorhandler.controller"})
public class ExceptionController {
@ExceptionHandler({
ArithmeticException.class, NullPointerException.class})
public String exceptionHandle(HttpServletRequest req, HttpServletResponse rep, Exception ex, Model model){
}
}
/**
* 验证@ResponseStatus注解的异常
* 没有异常解析器时,会使用@ReponseStatus中设置的statusCode和reason信息跳转error页(whiteLabel)
*/
@ResponseStatus(code = HttpStatus.FORBIDDEN, reason = "用户异常的帅")
/*
* @Order()提升当前异常解析器的优先级:
* 默认会将自定义HandlerExceptionResolver加入resolvers最后一位,它前面有
* 1.DefaultErrorAttribute。 将ex放入req,返回null
* 2.ExceptionHandlerExceptionResolver。 处理标有@ExceptionHandler注解的
* 3.ResponseStatus。 处理标有@ResponseStatus注解的异常类的
* 4.DefaultHandlerExceptionResolver。 SpringMVC底层解析器的
* 5.自定义HandlerExceptionResolver
* 前面的如果能处理掉发生的异常,后面的异常解析器就不会进行判断了,所以自定义的异常解析器需要添加到list的第一个,
* 如果自定义的解决不了这个异常,就会继续向后寻找能处理的异常解析器。
* */
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
public class CustomExceptionResolver implements HandlerExceptionResolver
可以使用@WebServlet
,@WebFilter
,@WebListener
注册三大组件,但是必须开启@ServletComponentScan
, Servlet组件扫描器注解。
**@ServletComponentScan(basePackages=“xxx.xxx”):**默认扫描主启动类下的包及其子包,basePackages手动指定要扫描的包。标注在主启动类上(容器中的Bean才能使用Spring提供的强大注解功能)
WebServlet("/myservlet")
public class MyServlet extends HttpServlet
@WebListener
public class MyListener implements ServletContextListener
@WebFilter(urlPatterns = "/*")
public class MyFilter extends HttpFilter
@ServletComponentScan
@SpringBootApplication
public class ServletComponentsApplication
//保证当前配置类中的组件都是单实例
//Servlet都是单实例,多线程
@Configuration(proxyBeanMethods = true)
public class ServletComponentConfig {
@Bean
public ServletRegistrationBean<HttpServlet> servletRegistrationBean(){
ServletRegistrationBean<HttpServlet> httpServletServletRegistrationBean = new ServletRegistrationBean<>(new HttpServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("@Bean + ServletRegistrationBean 注册Servlet原生组件");
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().append("@Bean + ServletRegistrationBean 注册Servlet原生组件");
}
});
httpServletServletRegistrationBean.addUrlMappings("/myservlet2");
return httpServletServletRegistrationBean;
}
@Bean
public FilterRegistrationBean<Filter> filterFilterRegistrationBean(){
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>((request, response, chain) -> {
System.out.println("@Bean + FilterRegistrationBean 注册Servlet原生组件");
chain.doFilter(request, response);
});
filterFilterRegistrationBean.addUrlPatterns("/*");
return filterFilterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean<ServletContextListener> servletContextListenerServletListenerRegistrationBean(){
return new ServletListenerRegistrationBean<ServletContextListener>(new ServletContextListener() {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("@Bean + ServletListenerRegistrationBean 注册Servlet原生组件");
}
});
}
}
HandlerInterceptor:属于Dispatcher中的拦截器,在请求没有进入Dispatcher时,是不能够使用拦截器的。
Tomcat服务器对多个Servlet可处理同一层的Url的规则是 精确匹配优先
默认支持的webServer
Tomcat
,Jetty
,UndertowServletWebServerApplicationContext
容器启动寻找ServletWebServerFactory 并引导创建服务器切换服务器
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-tomcatartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-undertowartifactId>
dependency>
原理
SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
web应用会创建一个web版的ioc容器 ServletWebServerApplicationContext
ServletWebServerApplicationContext
启动的时候寻找 ServletWebServerFactory
(ServletWeb服务器工厂 --》 Servlet的web服务器)
SpringBoot底层默认有很多的WebServer工厂;
TomcatServerWebServerFactory
,JettyServletWebServerFactory
,UndertowServletWebServerFactory
底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration
ServletWebServerFactoryConfiguration(配置类)
ServletWebServerFactoryConfiguration
根据@Conditional动态判断系统中到底导入了哪个Web服务器的包。(默认是web-stater-tomcat包,tomcat的jar包),容器中就有 TomcatServletWebServerFactory TomcatServlet服务器工厂
TomcatServletWebServerFactory
创建出Tomcat服务器并启动;TomcatWebServer的构造器有一个初始化方法initializeServletWebServerFactory
进行绑定xxxxCustomizer:定制化器,可以改变xxxx的默认规则
@EnableWebMvc
32 + WebMvcConfigurer – @Bean 可以全面接管 SpringMVC,所有规则全部自己重新配置;实现定制和拓展功能
场景Starter - xxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties(绑定配置文件项)
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jdbcartifactId>
dependency>
数据库驱动?
为什么导入JDBC场景,官方不导入驱动?官方不知道我们接下来要操作什么数据库。
默认版本:<mysql.version>8.0.22mysql.version>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
想要修改版本
1、直接依赖引入具体版本(Maven的就近依赖原则)
2、重新声明版本(Maven的属性就近原则)
<properties>
<mysql.version>5.1.49mysql.version>
properties>
自动配置的类
33
修改数据源相关的配置:spring.datasource
数据库连接池的配置,是自己容器中没有DataSource才自动配置的。
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({
DataSource.class, XADataSource.class })
@Import({
DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {
}
底层配置好的默认连接池是:HikariDataSource
@ConditionalOnProperty(name = "spring.datasource.type", havingValue ="com.zaxxer.hikari.HikariDataSource",matchIfMissing = true)
34
35
36
37
修改配置项
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///bootstudy?useSSL=false
username: root
password: root
# type: com.zaxxer.hikari.HikariDataSource 默认HikariDataSource数据源
jdbc:
template:
query-timeout: 10 #配置JDBCTemplate的请求超时时间。
测试
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void testQuery(){
int select_eno_from_emp = jdbcTemplate.queryForList("select eno from emp").size();
log.info("查询出记录数:{}",select_eno_from_emp);
}
https://github.com/alibaba/druid
(Hikari在市面上可能是性能最好的一款产品,但实际开发中,企业更喜欢使用阿里的Druid数据源,因为Druid有着自己的一套解决方案,比如全方位监控,防止sql注入攻击…)
整合第三方技术的两种方式
依赖:
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.4version>
dependency>
配置文件声明式:
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///bootstudy?useSSL=false
username: root
password: root
# type: com.zaxxer.hikari.HikariDataSource 默认HikariDataSource数据源
type: com.alibaba.druid.pool.DruidDataSource
@Configuration 编码式
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///bootstudy?useSSL=false
username: root
password: root
@Bean
//将标注方法返回的对象与配置文件进行配置绑定。
@ConfigurationProperties("spring.datasource")
/*
* HikariDataSource的配置中有注解@ConditionalOnMissingBean(DataSource.class),
* 表示容器中没由有DataSource时,才创建HikariDataSource对象
* */
public DataSource druidDatasource(){
return new DruidDataSource();
}
引入Druid-starter
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.2.4version>
dependency>
分析自动配置类
拓展配置项:spring.datasource.druid
DruidSpringAopConfiguration 监控SpringBean的,配置项:spring.datasource.druid.aop-patterns
DruidStatViewServletConfiguration;监控页的配置:spring.datasource.druid.stat-view-servlet
DruidWebStatFilterConfiguration;web监控配置:spring.datasource.druid.web-stat-filter
DruidFilterConfiguration;所有Druid自己filter的配置。
private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat";
private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config";
private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding";
private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j";
private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j";
private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2";
private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log";
private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";
private static final String FILTER_WALL_CONFIG_PREFIX = FILTER_WALL_PREFIX + ".config";
配置实例
druid:
filters: stat,wall,log4j # 底层开启功能 stat(SQL监控) wall(防火墙),log4j(日志输出)
stat-view-servlet: # 配置监控页功能
enabled: true
url-pattern: /druid/*
login-username: admin
login-password: admin
reset-enable: false # 是否允许重置已经监控到的数据
web-stat-filter: # 监控web
enabled: true
url-pattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
filter: # 对filters中每个filter的详细设置
stat: # 对filters中stat的详细设置
enabled: true
slow-sql-millis: 1000
log-slow-sql: true
wall:
enabled: true
config:
drop-table-allow: false
https://github.com/mybatis
spring官方的starter:spring-boot-starter-*
第三方:*-spring-boot-starter
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ccYwy9OE-1612143666418)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210129171744858.png)]
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({
SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({
DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {
@ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX)
public class MybatisProperties {
public static final String MYBATIS_PREFIX = "mybatis";
可以修改配置文件中mybatis开始的所有;
mybatis:
config-location: classpath:mybatis/mybatis.xml # 全局配置文件位置
mapper-locations: classpath:mybatis/mapper/*.xml # Mapper配置文件位置。
# config-location 不能和 configuration同时存在。否则报错。因为两个功能都是mybatis功能的配置。
# mybatis配置文件中的配置都可以在boot配置文件的mybatis.configuration.xxx中配置
# configuration:
# use-column-label: true # 开启驼峰命名
# 可以不写mybatis的全局配置文件,所有配置都可以在mybatis.configuration.xxx中配置。
配置 private Configuration configuration; myabtis.configuration下面的所有,相当于修改mybatis全局配置文件中的值。
@Mapper
public interface EmpMapper{
@Select("select * from emp")
@Options(Options可以设置在xml标签中的一些功能,比如 useGeneratedKey = true, keyProperty = "id")
Emp selectEmp();
@insert [+ @Optinos]
@Delete [+ @Options]
@Update [+ @Options]
}
MyBatis-Plus(简称MP)是一个MyBatis的增强工具,在MyBatis的基础上制作增强,不做改变,为简化开发,提高效率而生。
建议安装MyBatisX插件
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.2version>
dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qbKbW0ax-1612143666418)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210129185550200.png)]
MyBatisPlusAutoConfiguration
配置类,MyBatisPlusProperties配置项绑定。mybatis-plus.xxxx
自动配置好了哪些东西?
SqlSessionFactory
,底层是容器中的数据源mapperLocations
,默认值:classpath*:/mapper/**/*.xml
;classpath任意路径下的mapper文件夹中,任意路径下的任意xml文件(建议所有的mapper文件都放在mapper路径下)SqlSessionTemplate
@Mapper
标注的接口自动被扫描 @MapperScan // 使用后Mapper接口可以不添@Mapper注解。 MyBatisPlus中不用再次声明,否则就会有两个MapperScan,造成invalid bound not found仅需要XXXMapper entends BaseMapper 就可以拥有crud能力。
Mapper接口 extends BaseMapper
只需要继承BaseMapper就可以拥有很多基本的CURD方法
/** MyBatis-Plus BaseMapper */
@Mapper
public interface PlusEmpMapper extends BaseMapper<Emp> {
}
Service接口 extends IService
只需要继承Iservice就可以拥有很多基本的CURD方法
public interface PlusEmpService extends IService<Emp> {
}
Service实现类 extends ServiceImpl<
只需要继承 ServiceImpl<
@Service
public class PlusEmpServiceImp extends ServiceImpl<PlusEmpMapper, Emp> implements PlusEmpService {
}
MyBatisPlus分页插件
/**
* MyBatisPlus 分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
// paginationInterceptor.setLimit(500);
// 开启 count 的 join 优化,只针对部分 left join
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setOverflow(true);
paginationInnerInterceptor.setMaxLimit(100L);
paginationInnerInterceptor.setOptimizeJoin(true);
mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
return mybatisPlusInterceptor;
}
MybatisPlusInterceptor
: @since 3.4.0 新更新的插件
PaginationInterceptor
:@deprecated 3.4.0 please use {@link MybatisPlusInterceptor} {@link PaginationInnerInterceptor}
Controller
@Autowired
PlusEmpService plusEmpService;
/**
* 查询所有数据
* */
@GetMapping("plus/t_emp")
public String t_emp(Model model){
List<Emp> emps = plusEmpService.list();
model.addAttribute("emps",emps);
return "emp";
}
/**
* 分页查询数据
* */
@GetMapping("plus/t_emp_page")
public String t_emp_page(Model model, @RequestParam(defaultValue = "1") Integer page){
Page<Emp> empPage = new Page<>(page, 2);
Page<Emp> emps = plusEmpService.page(empPage);
System.out.println("emps.getRecords() = " + emps.getRecords());
System.out.println("emps.getCountId() = " + emps.getCountId());
System.out.println("emps.getCurrent() = " + emps.getCurrent());
System.out.println("emps.getMaxLimit() = " + emps.getMaxLimit());
System.out.println("emps.getOrders() = " + emps.getOrders());
System.out.println("emps.getTotal() = " + emps.getTotal());
System.out.println("emps.getSize() = " + emps.getSize());
System.out.println("emps.getPages() = " + emps.getPages());
model.addAttribute("emps",emps);
return "emp_page";
}
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
RedisAutoConfiguration
自动配置类。**RedisProperties
**属性类 --> spring.redis
LettuceConnectionFactory
lettuce连接工厂是准备好的。如果使用的是Jedis客户端,那么底层就会创建Jedis连接工厂。RedisTemplate
:自动注入了RedisTemplate,支持操作Redis的**StringTemplate
:自动注入了StringTemplate。k,v都是StringLettuce: 支持Java操作Redis的一款客户端。(还有Jedis,Redisson)。
spring:
redis:
host: localhost
port: 6379
@GetMapping("/redis/set/{key}/{value}")
@ResponseBody
public String redisSetKey(@PathVariable String key, @PathVariable String value){
redisTemplate.opsForValue().set(key,value);
return Objects.requireNonNull(redisTemplate.getConnectionFactory()).getClass().toString();
}
@GetMapping("/redis/get/{key}")
public String redisGetKey(@PathVariable String key){
return redisTemplate.opsForValue().get(key);
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!--不排除Lettuce则默认使用Lettuce,
默认使用Lettuce可以手动指定
redis.client.type: jedis
来切换Redis的客户端。
-->
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
spring:
redis:
host: localhost
port: 6379
# 切换Redis客户端
client-type: jedis
@WebFilter(urlPatterns = "/*")
public class RedisLookCountFilter implements Filter {
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
this.redisTemplate.opsForValue().increment(((HttpServletRequest) request).getRequestURI());
chain.doFilter(request,response);
}
}
@SpringBootApplication
@ServletComponentScan
public class DatasourceAutoConfigurationApplication
SpringBoot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库。
作为最新版本的JUnit框架,JUnit5与之前版本的JUnit框架有很大的不同。由三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform:是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。
JUnit Jupiter:提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于JUnit Platform上运行。
JUnit Vintage:由于JUnit已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,JUnit4.x的测试引擎。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DfIzGClc-1612143666420)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210130150616217.png)]
SpringBoot 2.4 以上版本移除了默认对Vintage的依赖。如需兼容JUnit4在JUnit Platform上运行,自行引入Vintage
JUnit5的注解于JUnit4的注解有所变化
https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotaions
注解 | 作用 |
---|---|
@Test | 表述方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将有Jupiter提供额外测试。 |
@ParameterizedTest | 表示方法是参数化测试 |
@RepeatedTest | 重复执行测试方法 |
@DisplayName | 为测试类或者测试方法设置展示名称(为测试方法设置测试名称) |
@BeforeEach | 在每个单元测试之前执行 |
@AfterEach | 在每个单元测试只有执行 |
@BeforeAll | 在所有单元测试之前执行 |
@AfterAll | 在所有测试单元测试之后执行 |
@Tag | 单元测试类别,类似JUnit4中的@Categories |
@Disable | 测试类或测试方法不执行,类似于JUnit4中的@Ignore |
@Timeout | 测试方法运行如果超过了指定事件将会返回错误 |
@ExtendWith | 为测试类或测试方法提供拓展类引用,类似于JUnit4中的@RunWith |
断言(爱色色如同时)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是org.junit.jupiter.api.Assertions的静态方法。JUnit5内置的断言可以分成如下几个类别:
检查业务逻辑返回的数据是否合理。
所有测试运行结束后,会有一个详细的测试报告。
测试时,import static org.junit.jupiter.api.Assertions.* ,可以直接使用方法。不需要通过类名调用。
用单个值进行简单的验证。
方法 | 说明 |
---|---|
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个对象引用是否指向不同的对象 |
assertTrue | 判断给定的布尔值应用是否为true |
assertFalse | 判断给定的布尔值应用是否为false |
assertNull | 判断给定的对象是否为null |
assertNotNull | 判断给定的对象是否不为NUll |
通过assertArrayEquals方法来判断两个对象或原始类型的数组是否相等。
assertArrayEquals(new int[]{
1,2}, new int[]{
1,2}) ;
assertAll()方法接受多个org.junit.jupiter.api.Executable函数式接口的实例作为要验证的锻压你,可以通过lambda表达式很容易提供这些断言。
assertAll("Math",
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0));
在JUnit4时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.asserThrows(),配合函数式编程就可以进行使用。
断定一定会出现异常
ArithmeticException exception = Assertions.assertThrows(
ArithmeticException.class,
() -> {
int i = 10 / 0;},
"没有输出这段话表示业务逻辑成功抛出了一个数学异常。"
)
**Assertions.assertTimeout()**测试方法执行时间是否超时。
如果测试方法执行时间超过1s将会异常
assertTimeOut(Duration.ofMillis(1000), () -> Thread.sleep(500))
通过 fail方法直接使得测试失败
fail("this should fail")
JUnit5中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行跳过。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。
assumeTrue(false, 结果不是true);
sout("11111");
当assumeTrue断言表达式结果不为true时,会自动中止并跳过该测试方法。
Junit5可以通过java中的内部类和@Nested注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和 @AfterEach 注解,而且嵌套的层次没有限制。
内层类中的测试方法可以驱动外层类中的@BeforeEach等,外层无法驱动内层
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
参数化测试时JUnit5很重要的新特性,它使得用不同的参数多次运行测试成为了可能,也为单元测试带来了许多遍历。
利用@ValueSource等注解,指定入参,可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就为新增一个单元测试,省去了很多冗余代码。
注解 | 作用 |
---|---|
@ValueSource | 为参数化测试指定入参来源,支持8大基础类以及String类型,Class类型 |
@NullSource | 为参数测试提供一个null值入参 |
@EnumSource | 为参数化测试提供一个枚举入参 |
@CSVFileSource | 读取指定CSV文件内容作为参数化测试入参 |
@MethodSource | 读取指定方法的返回值违参数化测试入参(注意方法返回需要是一个流Stream.of) |
当然如果参数化测试仅仅只能做到指定普通的入参还达不到让人惊艳的地步。让人整整感受到它的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON文件甚至方法的返回值也可以入参。只需要去时间ArgumentsProvider接口,任何外部文件都可以作为它的参数入参。
@ParameterizedTest
@ValueSource(ints = {
1, 2, 3 })
void testWithValueSource(int argument) {
assertTrue(argument > 0 && argument < 4);
}
@ParameterizedTest
@MethodSource("stringProvider")
void testWithExplicitLocalMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> stringProvider() {
return Stream.of("apple", "banana");
}
@ParameterizedTest
@NullSource
@EmptySource
@ValueSource(strings = {
" ", " ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
assertTrue(text == null || text.trim().isEmpty());
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {
" ", " ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
assertTrue(text == null || text.trim().isEmpty());
}
//...
Actuator【 /'æktjʊeɪtə/】
未来每个微服务在云上部署以后,需要对其进行监控、追中、审计、控制等。SpringBoot就抽取了Actuator长江,是的每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gv5jorIi-1612143666421)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210130173149020.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ogHoIokB-1612143666423)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210130173355164.png)]
# management 是所有actuator的配置
# management.endpoint.端点名.xxx 对某个端点的具体配置
management:
endpoints:
enabled-by-default: true #暴露所有端点信息
web:
exposure:
include: '*' # 以Web方式暴露
测试访问
spring-boot-admin
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-esCMdEgI-1612143666425)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210131122841338.png)]
<dependency>
<groupId>de.codecentricgroupId>
<artifactId>spring-boot-admin-starter-serverartifactId>
<version>2.3.1version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
server.port=8888 #避免和admin server 端口重复
@EnableAdminServer
@SpringBootApplication
public class SpringBootAdminApplication{
....}
<dependency>
<groupId>de.codecentricgroupId>
<artifactId>spring-boot-admin-starter-clientartifactId>
<version>2.3.1version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
//引入了security后,需要做以下配置,使actuator endpoint 可以被访问到。
//如果没有引入security,则不需要配置。
@Configuration
public static class SecurityPermitAllConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll()
.and().csrf().disable();
}
}
server.port=8080 #避免和admin server 端口重复
spring.boot.admin.client.url=http://localhost:8888 # adminServer的地址
spring.boot.admin.client.instance.prefer-ip=true # 实例地址优先使用ip,否则为主机名。 默认优先主机名。
management.endpoints.web.exposure.include='*' #以WebHttp方式暴露所有endpoints端点。
最常用的Endpoint
Endpoint | 描述 |
---|---|
auditevents | 暴露当前应用程序的审核时间信息。需要一个AuditEventRepository |
beans | 显示应用程序中所有SpringBean的完整列表。 |
caches | 暴露可用的缓存。 |
conditions | 显示自动配置的所有条件信息,包括匹配或不匹配的原因。 |
configprops | 显示所有@ConfigurationProperties |
env | 暴露Spring的属性ConfigurableEnvironment |
flyway | 显示已应用的所有Flyway 数据库迁移。需要一个或多个Flyway 组件 |
health | 显示应用程序运行状况信息 |
httptrace | 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HTTPTraceRepository 。 |
info | 显示应用程序信息 |
intergrationgraph | 显示Spring integrationgraph 。需要依赖spring-integration-core 。 |
loggers | 显示和修改应用程序中日志的配置 |
liquibase | 显示以应用的所有LiquiBase数据库迁移。需要一个或多个。 |
metrics | 显示所有@RequestMapping 路径列表。 |
mappings | 显示所有@RequestMapping 路径列表 |
scheduledtasks | 显示应用程序中的计划任务。 |
sessions | 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用SpringSession的基于Servlet的Web应用程序。 |
shutdown | 是应用程序正常关闭,默认禁用 |
startup | 显示由ApplicationStartup 收集的启动步骤数据。需要使用SpringApplication 进行配置BufferingApplicationStartup 。 |
threaddump | 执行线程转储。 |
如果您的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:
ID | 描述 |
---|---|
heapdump |
返回hprof 堆转储文件。 |
jolokia |
通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core 。 |
logfile |
返回日志文件的内容(如果已设置logging.file.name 或logging.file.path 属性)。支持使用HTTPRange 标头来检索部分日志文件的内容。 |
prometheus |
以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus 。 |
健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。
重要的几点:
management:
endpoint:
health:
show-details: always
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wTqAfzJC-1612143666426)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210130192555788.png)]
提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VmiZIQhK-1612143666427)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210130192736315.png)]
management:
endpoint:
beans:
enabled: true
management:
endpoints:
enabled-by-default: false
endpoint:
beans:
enabled: true
health:
enabled: true
支持的暴露方式
ID | JMX | Web |
---|---|---|
auditevents |
Yes | No |
beans |
Yes | No |
caches |
Yes | No |
conditions |
Yes | No |
configprops |
Yes | No |
env |
Yes | No |
flyway |
Yes | No |
health |
Yes | Yes |
heapdump |
N/A | No |
httptrace |
Yes | No |
info |
Yes | Yes |
integrationgraph |
Yes | No |
jolokia |
N/A | No |
logfile |
N/A | No |
loggers |
Yes | No |
liquibase |
Yes | No |
metrics |
Yes | No |
mappings |
Yes | No |
prometheus |
N/A | No |
scheduledtasks |
Yes | No |
sessions |
Yes | No |
shutdown |
Yes | No |
startup |
Yes | No |
threaddump |
Yes | No |
定制健康信息需要自定义健康信息检查类实现HealthIndicator
健康指标接口。
public abstract class AbstractHealthIndicator implements HealthIndicator
也可以继承AbstractHealthIndicator
类来实现健康检查功能。
@Component
public class MyHealthIndicator implements HealthIndicator {
@Override
public Health health() {
int errorCode = check(); // perform some specific health check
if (errorCode != 0) {
return Health.down().withDetail("Error Code", errorCode).build();
}
return Health.up().build();
}
}
构建Health
Health build = Health.down()
.withDetail("msg", "error service")
.withDetail("code", "500")
.withException(new RuntimeException())
.build();
@Component
public class MyComponentHealthIndicator extends AbstractHealthIndicator {
/**
* 真实的检查健康信息方法
* @param builder
* @throws Exception
*/
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
//mongodb 获取连接测试
HashMap<String, Object> map = new HashMap<>();
if (1 == 1){
//真实逻辑判断
// builder.up();//健康
builder.status(Status.UP);
map.put("count", 1);
map.put("ms:",100);
}else {
// builder.down();//不健康
builder.status(Status.OUT_OF_SERVICE);
map.put("err", "连接超时");
}
//返回健康信息的详细信息
builder.withDetail("code",100).withDetails(map);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5icig3uA-1612143666429)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210131101024640.png)]
常用两种方式:
# 根节点是info, 不是management.enpoints.info
# 根节点info 用于指定application的应用信息。
info:
appName: boot-admin
version: 2.0.1
mavenProjectName: @project.artifactId@ #@以获取maven的pom文件中的标签值@
mavenProjectVersion: @project.version@
mavenProjectModelVersion: @project.modelVersion@
@Component
public class AppInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("msg", "你好").
withDetail("hello", "info world")
.withDetails(Collections.singletonMap("你好","info 世界"));
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ejug1qnd-1612143666430)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210131102849207.png)]
JVM metrics, report utilization of:
CPU metrics
File descriptor metrics
Kafka consumer and producer metrics
Log4j2 metrics: record the number of events logged to Log4j2 at each level
Logback metrics: record the number of events logged to Logback at each level
Uptime metrics: report a gauge for uptime and a fixed gauge representing the application’s absolute start time
Tomcat metrics (server.tomcat.mbeanregistry.enabled
must be set to true
for all Tomcat metrics to be registered)
Spring Integration metrics
/*
* 定制指标信息
* */
Counter counter;
public PlusController(MeterRegistry meterRegistry) {
counter = meterRegistry.counter("plusController.t_emp_page.counter");
}
/**
* 分页查询数据
* */
@GetMapping("plus/t_emp_page")
public String t_emp_page(Model model, @RequestParam(defaultValue = "1") Integer page){
counter.increment(); //指标信息,指标自增一。
Page<Emp> empPage = new Page<>(page, 2);
Page<Emp> emps = plusEmpService.page(empPage);
model.addAttribute("emps",emps);
return "emp_page";
}
或
@Configuration
public class MyMetricsConfig {
@Bean
MeterBinder queueSize(Queue queue){
return (registry) -> Gauge.builder("queueSize",queue::size).register(registry);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-10S2b58F-1612143666431)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210131105136908.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dgZ8uIxc-1612143666433)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210131110853369.png)]
@Component
@Endpoint(id = "dockerContainer")
public class DockerEndpoint {
@ReadOperation
public Map<String,Object> getDockerInfo(){
return Collections.singletonMap("info","docker started...");
}
@WriteOperation
public void restartDocker(){
System.out.println("docker restarted...");
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yAQanpBa-1612143666434)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210131112607927.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TBSW9fYm-1612143666434)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210131112645278.png)]
场景:开发ReadinessEndpoint
来管理程序是否就绪,或者LivenessEndpoint
来管理是否存活;
当然,这个也可以直接使用
https://docs.spring.io/spring-boot/docs/current/reference/html/pproduction-ready-features.html#production-ready-kubernetes-probes
为了方便多环境适配,SpringBoot简化了profile功能。
# application.properties
server.port=8080
peroson.name=lisi
spring.profiles.active=prod # 指定激活的环境。默认配置文件和指定配置文件都会生效。
#application-prod.properties
server.prot=9999
#application-test.properties
server.prot=7777
#最终效果服务器在9999端口启动。且person.name=lisi
@Profile(“xxx”):如果当前是xxx环境激活,才会执行执行Spring一系列相关操作。
@Configuration(proxyBeanMethods = false)
//如果是生产环境,才会执行Spring一系列相关操作。注入组件
@Profile("production")
public class ProductionConfiguration {
@Bean
// ...
}
//如果是生产环境,才会执行Spring一些列相关操作。配置绑定
@Profile("production")
@Component
@ConfigurationProperties("person")
public class Person{
...}
//如果是测试环境,才会执行Spring一些列相关操作。配置绑定
@Profile("test")
@Component
@ConfigurationProperties("person")
public class Person{
...}
#application.properties
spring.profiles.group.production[0]=proddb
spring.profiles.group.production[1]=prodmq
使用:--spring.profiles.active=production #激活production,会将application.proddb和application.prodmq两个配置文件同时激活。
https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config
SpringApplication.setDefaultProperties
).@PropertySource
annotations on your @Configuration
classes. Please note that such property sources are not added to the Environment
until the application context is being refreshed. This is too late to configure certain properties such as logging.*
and spring.main.*
which are read before refresh begins.application.properties
files)RandomValuePropertySource
that has properties only in random.*
.System.getProperties()
).java:comp/env
.ServletContext
init parameters.ServletConfig
init parameters.SPRING_APPLICATION_JSON
(inline JSON embedded in an environment variable or system property).properties
attribute on your tests. Available on @SpringBootTest
and the test annotations for testing a particular slice of your application.@TestPropertySource
annotations on your tests.$HOME/.config/spring-boot
directory when devtools is active.常用:Java属性文件、YAML文件、环境变量、命令行参数
- The classpath root
- The classpath
/config
package- The current directory
- The
/config
subdirectory in the current directory- Immediate child directories of the
/config
subdirectory列表按加载顺序排序,后面的会覆盖前面的配置。
列表按加载顺序排序,后面的会覆盖前面的配置。
starter-pom引入 autoconfigurer包
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-om41ribj-1612143666436)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210131141605601.png)]
autoconfigurer 包中配置使用 META-INF/spring.factories 中 EnableAutoConfiguration 的值,使得项目启动加载指定自动配置类
编写自动配置类 xxxAutoConfiguration -> xxxxProperties
引入Starter — xxxAutoConfiguration — 容器中放入组件 — 绑定xxxProperties — 配置项。
yyl-hello-spring-boot-starter(启动器)
<dependency>
<groupId>com.yyl.bootstudygroupId>
<artifactId>hello-spring-boot-starter-autoconfigurationartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
yyl-hello-spring-boot-starter-autoconfiguration(自动配置模块)
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
dependency>
dependencies>
#spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.yyl.bootstudy.hellospringbootstarterautoconfiguration.HelloSpringBootAutoConfiguration
@EnableConfigurationProperties(HelloProperties.class)
@Configuration
public class HelloSpringBootAutoConfiguration {
@Bean
@ConditionalOnMissingBean(HelloService.class)
public HelloService helloService(){
return new HelloService();
}
}
/*
* 此处不标注@Service,因为不知道是否要将该类注册进容器。
* */
public class HelloService {
@Autowired
HelloProperties helloProperties;
public String sayHello(){
return helloProperties.getPrefix() + ": 张三666" + helloProperties.getSuffix();
}
}
@ConfigurationProperties("custom.hello")
public class HelloProperties {
private String prefix;
private String suffix;
//getter and setter .....
}
<dependency>
<groupId>com.yyl.bootstudygroupId>
<artifactId>hello-spring-boot-starterartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
custom:
hello:
prefix: 老师
suffix: ☺
@Controller
public class HelloController {
@Autowired
HelloService helloService;
@GetMapping("say")
@ResponseBody
public String say(){
return helloService.sayHello();
}
}
Spring原理【Spring注解】、SpringMVC原理、自动配置原理、SpringBoot原理
https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-application-events-and-listeners
ApplicationContextInitializer
public class MyApplicationContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
System.out.println("ApplicationContextInitializer....initialize");
}
}
Applicationlistener
public class MyApplicationListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println("MyApplicationListener....onApplicationEvent");
}
}
SpringApplicationRunListener
public class MySpringApplicationRunListener implements SpringApplicationRunListener {
public MySpringApplicationRunListener(SpringApplication application, String[] args) {
}
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
System.out.println("MyApplicationRunner....starting");
}
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
System.out.println("MyApplicationRunner....environmentPrepared");
}
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("MyApplicationRunner....contextPrepared");
}
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
System.out.println("MyApplicationRunner....contextLoaded");
}
@Override
public void started(ConfigurableApplicationContext context) {
System.out.println("MyApplicationRunner....started");
}
@Override
public void running(ConfigurableApplicationContext context) {
System.out.println("MyApplicationRunner....running");
}
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.out.println("MyApplicationRunner....failed");
}
}
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("MyApplicationRunner....run");
}
}
@Component
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("MyCommandLineRunner...run");
}
}
学习产出:
9
nts-and-listeners
ApplicationContextInitializer
public class MyApplicationContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
System.out.println("ApplicationContextInitializer....initialize");
}
}
Applicationlistener
public class MyApplicationListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println("MyApplicationListener....onApplicationEvent");
}
}
SpringApplicationRunListener
public class MySpringApplicationRunListener implements SpringApplicationRunListener {
public MySpringApplicationRunListener(SpringApplication application, String[] args) {
}
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
System.out.println("MyApplicationRunner....starting");
}
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
System.out.println("MyApplicationRunner....environmentPrepared");
}
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("MyApplicationRunner....contextPrepared");
}
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
System.out.println("MyApplicationRunner....contextLoaded");
}
@Override
public void started(ConfigurableApplicationContext context) {
System.out.println("MyApplicationRunner....started");
}
@Override
public void running(ConfigurableApplicationContext context) {
System.out.println("MyApplicationRunner....running");
}
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.out.println("MyApplicationRunner....failed");
}
}
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("MyApplicationRunner....run");
}
}
@Component
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("MyCommandLineRunner...run");
}
}
学习产出:
SpringMVC自动配置类 ↩︎ ↩︎
默认无前缀 ↩︎
优先级:SpringBoot底层在扫描4个默认静态资源目录时,会按照顺序遍历4个目录。 ↩︎ ↩︎
与application配置文件进行ConfigurationProperties配置绑定的类 ↩︎
欢迎页处理器映射器,将访问应用的根路径“/”映射到静态资源下的index页面 ↩︎ ↩︎ ↩︎
接口;处理器映射器;用于将url映射到处理程序上。 ↩︎ ↩︎
请求映射处理器映射器;保了所有 @RequestMapping
和 handler
的映射规则。 ↩︎ ↩︎
Bean名称路径处理程序映射器 ↩︎
反应式编程是一种编程范例,它是功能性的,基于事件的,非阻塞的,异步的,并且以数据流处理为中心。 术语反应式来自以下事实:我们对诸如鼠标单击或 I / O 事件之类的更改做出反应。 ↩︎ ↩︎
简单路径处理器映射器 ↩︎
路径匹配帮助器。 (包含解析MatrixVariable矩阵变量等功能) ↩︎
ServletRequest方法参数解析器,用来解析目标方法中Servlet原生API ↩︎
处理目标方法的自定义复杂类型参数,支持级联属性。 ↩︎
web数据绑定器;它里面的 Converters 将请求数据转成指定的数据类型,然后将值绑定到JavaBean中。 ↩︎
格式化器注册中心,可以用来注册自定义的Converter等。也可以用ConverterRegistry,ConverterRegistry是FormatterRegistry的父接口。 ↩︎ ↩︎ ↩︎
SpringMVC自动配置类的接口。
[^WebMvcProperties ]:WebMvcProperties 和 spring.mvc前缀 进行配置绑定。
[^ResourcesProperties ]:ResourcesProperties 和 spring.resources前缀 进行配置绑定。
[^WebProperties ]:WebProperties 和 spring.web前缀 进行配置绑定。 ↩︎
处理器适配器 ↩︎
处理方法参数解析器;supportsParameter方法判断当前解析器是否支持解析这种参数-支持就调用resolveArgument进行解析。 ↩︎ ↩︎
标注方法的返回值作为响应体,常用来返回JSON数据。 ↩︎ ↩︎ ↩︎
标注类中所有处理请求方法的返回值都作为响应体;等价于方法上标注38 + 19;常用来返回JSON数据。 ↩︎
HttpMessageConverter 和 内容协商底层源码 ↩︎
方法返回值处理器接口 ↩︎
Http消息转换器。write方法将输出数据转换器MediaType内容协商后的媒体(内容)类型。
[^ MappingJackson2HttpMessageConverter]:HttpMessageConverter的实现类,内部调用Jackson,支持将对象转换为JSON对象。 ↩︎ ↩︎ ↩︎
资源类型返回值类型HTTP消息转换器 ↩︎ ↩︎ ↩︎
系统文件资源,Resource接口的实现类。24支持的返回值类型,可以用来下载文件。 ↩︎
浏览器会将希望得到的数据类型存放在请求头中 ↩︎
参数内容协商策略。支持的mediaTypes只有两个xml和json ↩︎ ↩︎
处理SpringMVC底层异常的异常解析器 ↩︎ ↩︎
自动配置异常处理默认规则 ↩︎
处理标注有@ResponseStatus注解的异常类的异常解析器 ↩︎
标注的方法发生异常,将会被ExceptionHandlerExceptionResovler进行异常处理。 ↩︎
全面接管SpringMVC,静态资源,视图解析器,欢迎页…的自动配置全部失效,所有规则需要自己全部重新配置 ↩︎
数据源的自动配置类 ↩︎
事务管理器的自动配置类 ↩︎
JdbcTemplate组件的自动配置类,Spring提供的,可以对数据库进行crud ↩︎
Jndi技术数据源自动配置类;web容器配置数据源,比如Tomcat的Jndi技术。 ↩︎
分布式事务相关的。 ↩︎
请求映射;标注方法为处理请求的方法。 标注类为类中请求方法的路径前缀。 ↩︎ ↩︎