一、SpringBoot概述
1、能做什么
Spring能力
微服务开发
响应式编程(异步非阻塞)
分布式云开发
web开发
无服务开发(FAAS:函数级开发)
事件驱动(使用数据流利用响应式编程)
批处理开发
Spring生态
web开发
数据访问
安全控制
分布式
消息服务
移动开发
批处理
…
Spring5升级
响应式编程:占用少量线程,使用少量资源就可以处理大量的并发和需要的业务!
内部源码设计:基于Java8的一些新特性,如:接口默认实现(适配器模式)。重新设计源码架构。
2、为什么用
能快速创建出生产级别的Spring应用!
SpringBoot优点
SpringBoot是整合Spring技术栈的一站式框架!
SpringBoot是简化Spring技术栈的快速开发脚手架!
创建独立Spring应用
内嵌web服务器
自动starter依赖,简化构建配置
自动配置Spring以及第三方功能
提供生产级别的监控、健康检查及外部化配置
无代码生成、无需编写XML
SpringBoot缺点
人称版本帝,迭代快,需要时刻关注变化
封装太深,内部原理复杂,不容易精通
3、时代背景
微服务
微服务是一种架构风格
一个应用拆分为一组小型服务
每个服务运行在自己的进程内,也就是可独立部署和升级
服务之间使用轻量级HTTP交互
服务围绕业务功能拆分
可以由全自动部署机制独立部署
去中心化,服务自治。服务可以使用不同的语言、不同的存储技术
分布式
分布式的困难:
远程调用
服务发现
负载均衡
服务容错
配置管理
服务监控
链路追踪
日志管理
任务调度
…
分布式问题解决: SpringBoot + SpringCloud
云原生
原生应用如何上云。 Cloud Native!
上云的困难:
服务自愈
弹性伸缩
服务隔离
自动化部署
灰度发布
流量治理
…
4、SpringBoot特点
依赖管理
父项目做依赖管理:几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制
1 2 3 4 5 6 7 8 9 10 11 12 13 依赖管理!<parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.3.4.RELEASE</version > </parent > 他的父项目中声明了所有可能会用到的包的版本信息!<parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-dependencies</artifactId > <version > 2.3.4.RELEASE</version > </parent >
开发导入starter场景启动器
1 2 3 4 5 6 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > <version > 2.3.4.RELEASE</version > <scope > compile</scope > </dependency >
可以修改默认版本号:
查看spring-boot-dependencies里面规定当前依赖的版本用的 key。
在当前项目里面重写配置
1 2 3 <properties > <mysql.version > 5.1.43</mysql.version > </properties >
无需关注版本号,自动版本仲裁
引入依赖默认都可以不写版本
引入非版本仲裁的 jar ,要写版本号。
自动配置
自动配好Tomcat:引入Tomcat依赖,配置Tomcat
自动配好SpringMVC:引入SpringMVC全套组件,自动配好SpringMVC常用组件(功能)
自动配好Web常见功能:如:字符编码问题,
SpringBoot帮我们配置好了所有web开发的常见场景
默认的包结构
主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来
无需以前的包扫描配置
改变扫描路径@SpringBootApplication(scanBasePackages="com.itnxd")
或者 @ComponentScan
指定扫描路径
各种配置拥有默认值
默认配置最终都是映射到某个类上
配置文件的值最终会绑定每个类上,这个类会在容器中创建对象
按需加载所有自动配置项
非常多的starter:引入了哪些场景这个场景的自动配置才会开启
SpringBoot所有的自动配置功能都在 spring-boot-autoconfigure 包 里面
1 2 3 4 5 6 7 8 9 @SpringBootApplication(scanBasePackages = "com.itnxd") @ComponentScan("com.itnxd") @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan
二、容器功能
1、组件添加
@Configuration
指明是一个配置类!
配置类里面使用@Bean标注在方法上给容器注册组件,默认是单实例的
配置类本身也是组件
proxyBeanMethods:代理bean的方法,@Configuration
的配置属性
Full(proxyBeanMethods = true
):保证每个@Bean方法被调用多少次返回的组件都是单实例的。去容器中找,慢,单实例,默认
Lite(proxyBeanMethods = false
):每个@Bean方法被调用多少次返回的组件都是新创建的。新建一个,快,多实例
建议:
组件依赖(一个组件依赖另一个组件如user依赖cat)必须使用Full模式默认(防止实例不统一)。其他默认是否Lite模式!
配置类组件之间无依赖关系,用Lite模式加速容器启动过程,减少判断
配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @Configuration(proxyBeanMethods = true) public class MyConfig { ... }@SpringBootApplication public class HelloWorldApplication { public static void main (String[] args) { ConfigurableApplicationContext run = SpringApplication.run(HelloWorldApplication.class, args); String[] beanDefinitionNames = run.getBeanDefinitionNames(); for (String beanDefinitionName : beanDefinitionNames) { System.out.println(beanDefinitionName); } User user01 = run.getBean("user01" , User.class); System.out.println(user01); MyConfig bean = run.getBean(MyConfig.class); System.out.println(bean); User user = bean.user01(); User user1 = bean.user01(); System.out.println(user == user1); User user2 = bean.user01(); System.out.println(user2.getCat() == bean.getCat()); } }
@Import
给容器中自动创建出这些个类型的组件(自动调用无参构造)、默认组件的名字就是全类名!
1 2 3 4 5 @Import({User.class, DBHelper.class}) @Configuration(proxyBeanMethods = true) public class MyConfig { ... }
@Bean
给容器添加组件!
1 2 3 4 5 6 7 8 9 10 11 12 @Bean public User user01 () { User zs = new User("zs" , 18 ); zs.setCat(getCat()); return zs; }
@Conditional
条件装配:满足Conditional指定的条件,则进行组件注入!
有如下条件配置注解!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @ConditionalOnBean(name = "tom") @Configuration(proxyBeanMethods = true) public class MyConfig { @ConditionalOnBean(name = "tom") @Bean public User user01 () { User zs = new User("zs" , 18 ); zs.setCat(getCat()); return zs; } ... }
2、原生配置文件引入
@ImportResource:导入旧项目的xml配置文件,任何类上都可以!
1 2 3 4 5 6 @ImportResource("classpath:bean.xml") @Configuration(proxyBeanMethods = true) public class MyConfig { ... }
3、配置绑定
如何使用Java读取到properties文件中的内容,并且把它封装到JavaBean中,以供随时使用!
@ConfigurationProperties
我们之前将配置文件绑定到JavaBean可以这样做:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class getProperties { public static void main (String[] args) throws FileNotFoundException, IOException { Properties pps = new Properties(); pps.load(new FileInputStream("a.properties" )); Enumeration enum1 = pps.propertyNames(); while (enum1.hasMoreElements()) { String strKey = (String) enum1.nextElement(); String strValue = pps.getProperty(strKey); System.out.println(strKey + "=" + strValue); } } }
使用注解@ConfigurationProperties:
1 2 3 4 5 6 7 8 @Component @ConfigurationProperties(prefix = "mycar") public class Car { private String brand; private Integer price; ... }
properties配置文件:
1 2 mycar.brand =BYD mycar.price =100000
两种配置文件绑定方法
实体类添加 @Component 和 @ConfigurationProperties(prefix = "mycar")
启动类或配置类添加 @EnableConfigurationProperties(Car.class)
,实体类添加 @ConfigurationProperties(prefix = "mycar")
第二种适合第三方类使用,毕竟我们不能直接添加@Component注解到该类!
三、自动配置原理
从分析@SpringBootApplication这个启动类注解入手!
1 2 3 4 5 6 7 @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication{}
1、@SpringBootConfiguration
1 2 @Configuration public @interface SpringBootConfiguration {}
@Configuration:代表当前是一个配置类
2、@ComponentScan
指定扫描哪些包,用来扫描注解!
1 2 3 4 5 6 7 8 9 public @interface ComponentScan { @AliasFor("basePackages") String[] value() default {}; @AliasFor("value") String[] basePackages() default {}; ... }
3、@EnableAutoConfiguration
该注解由以下两个注解组成:
1 2 3 4 5 6 7 8 9 @AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration" ; Class<?>[] exclude() default {}; String[] excludeName() default {}; }
@AutoConfigurationPackage
自动配置包,指定了默认的包规则!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Import({Registrar.class}) public @interface AutoConfigurationPackage { String[] basePackages() default {}; Class<?>[] basePackageClasses() default {}; }static class Registrar implements ImportBeanDefinitionRegistrar , DeterminableImports { Registrar() { } public void registerBeanDefinitions (AnnotationMetadata metadata, BeanDefinitionRegistry registry) { AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0 ])); } public Set<Object> determineImports (AnnotationMetadata metadata) { return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata)); } }
@Import
1 2 3 @AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration {
利用 getAutoConfigurationEntry(annotationMetadata);
给容器中批量导入一些组件
调用List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes)
获取到所有需要导入到容器中的配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry (AnnotationMetadata annotationMetadata) { if (!this .isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } else { AnnotationAttributes attributes = this .getAttributes(annotationMetadata); List<String> configurations = this .getCandidateConfigurations(annotationMetadata, attributes); configurations = this .removeDuplicates(configurations); Set<String> exclusions = this .getExclusions(annotationMetadata, attributes); this .checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = this .getConfigurationClassFilter().filter(configurations); this .fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions); } }
configurations有一百多个组件:
利用工厂加载 Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader)
得到所有的组件
从META-INF/spring.factories
位置来加载一个文件。 默认扫描我们当前系统里面所有META-INF/spring.factories
位置的文件
1 2 3 4 5 6 7 8 9 10 public static List<String> loadFactoryNames (Class<?> factoryType, @Nullable ClassLoader classLoader) { ClassLoader classLoaderToUse = classLoader; if (classLoader == null ) { classLoaderToUse = SpringFactoriesLoader.class.getClassLoader(); } String factoryTypeName = factoryType.getName(); return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList()); }
到了这里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) { Map<String, List<String>> result = (Map)cache.get(classLoader); if (result != null ) { return result; } else { HashMap result = new HashMap(); try { Enumeration urls = classLoader.getResources("META-INF/spring.factories" ); while (urls.hasMoreElements()) { URL url = (URL)urls.nextElement(); UrlResource resource = new UrlResource(url); Properties properties = PropertiesLoaderUtils.loadProperties(resource); Iterator var6 = properties.entrySet().iterator(); while (var6.hasNext()) { Entry<?, ?> entry = (Entry)var6.next(); String factoryTypeName = ((String)entry.getKey()).trim(); String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue()); String[] var10 = factoryImplementationNames; int var11 = factoryImplementationNames.length; for (int var12 = 0 ; var12 < var11; ++var12) { String factoryImplementationName = var10[var12]; ((List)result.computeIfAbsent(factoryTypeName, (key) -> { return new ArrayList(); })).add(factoryImplementationName.trim()); } } } result.replaceAll((factoryType, implementations) -> { return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); }); cache.put(classLoader, result); return result; } catch (IOException var14) { throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]" , var14); } } }
spring.factories文件:
spring-boot-autoconfigure-2.3.4.RELEASE.jar
包里面也有 META-INF/spring.factories
文件里面写死了 spring-boot 一启动就要给容器中加载的所有配置类
目前最新版132个自动配置项
按需开启自动配置组件
虽然我们127个场景的所有自动配置启动的时候默认全部加载。
xxxxAutoConfiguration 按照条件装配规则(@Conditional),最终会按需配置。
以aop包为例:
修改默认配置
SpringBoot 规范化命名例子:
给容器中加入了文件上传解析器:
1 2 3 4 5 6 7 8 9 10 11 @Bean @ConditionalOnBean({MultipartResolver.class}) @ConditionalOnMissingBean( name = {"multipartResolver"} // 容器中没有这个名字 multipartResolver 的组件 ) public MultipartResolver multipartResolver (MultipartResolver resolver) { return resolver; }
修改默认配置:
SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先!
找到这些组件,默认就是方法名,因此我们可以在自己的配置类中添加如下即可:
1 2 3 4 5 @Bean public MultipartResolver multipartResolver () { return null ; }
总结
SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration
每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties 里面拿。xxxProperties和配置文件进行了绑定
生效的配置类就会给容器中装配很多组件
只要容器中有这些组件,相当于这些功能就有了
定制化配置
用户直接自己@Bean替换底层的组件
用户去看这个组件是获取的配置文件什么值就去修改。
xxxxxAutoConfiguration —> 导入组件 —> 从xxxxProperties里面拿值 ----> 去application.properties获取
四、开发技巧
1、Lombok
可以简化 JavaBean 开发!同时包含了Slf4j日志框架!
似乎公司一般不使用,需要安装对应的Idea插件!(新版本以及默认安装)
最好的一点是改动JavaBean不用进行其他操作,自动完成!
依赖引入
1 2 3 4 5 <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency >
简化JavaBean
1 2 3 4 5 6 7 8 9 10 11 12 13 @Component @ConfigurationProperties(prefix = "mycar") @Data @ToString @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode public class Car { private String brand; private Integer price; }
简化日志Slf4j
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Slf4j @RestController public class HelloController { @Autowired Car car; @RequestMapping("/car") public Car car () { log.info("请求来了...." ); return car; } }
可以用来热更新,但有时还不如直接重启!
底层有两个类加载器。一个复制加载第三方库,一个负贵加载本地java库。
第三方库是不会变的,也就是重就启动只需要加载本地java类即可,节省了重启时间!
快捷键 CTRL + F9
:文件有变化时候才会生效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-devtools</artifactId > <optional > true</optional > </dependency >
3、Spring Initailizr
Idea提供的快速构建SpringBoot项目初始化向导!
可以根据选择自动引入各种开发场景的starter包,以及一些其他工具等等!
五、配置文件
分为properties和两种配置文件!
1、配置文件分类
2、yaml
简介
YAML 是 “YAML Ain’t Markup Language”(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。
非常适合用来做以数据为中心的配置文件!
基本语法
数据类型
字面量:单个的、不可再分的值。date、boolean、string、number、null
对象:键值对的集合。map、hash、set、object
1 2 3 4 5 6 7 k: {k1: v1 ,k2: v2 ,k3: v3 }k: k1: v1 k2: v2 k3: v3
数组:一组按次序排列的值。array、list、queue
1 2 3 4 5 6 7 k: [v1 ,v2 ,v3 ]k: - v1 - v2 - v3
案例
JavaBean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Data @ToString @Component @ConfigurationProperties(prefix = "person") public class Person { private String userName; private Boolean boss; private Date birth; private Integer age; private Pet pet; private String[] interests; private List<String> animal; private Map<String, Object> score; private Set<Double> salarys; private Map<String, List<Pet>> allPets; }@Data @ToString public class Pet { private String name; private Double weight; }
yaml文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 person: userName: zs boss: true birth: 2021 /10/6 age: 18 interests: - 篮球 - 足球 - 18 animal: [ 猫 ,狗 ] score: { english: 80 , math: 90 } salarys: - 9999.89 - 8832.22 pet: name: 狗狗 weight: 99 allPets: sick: - { name: 狗 , weight: 99 } - name: 猫 weight: 98 health: - { name: 狗 , weight: 99 }
3、配置提示
自定义的类和配置文件绑定一般没有提示,可以添加如下包就会由一些友好的提示!
依赖导入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-configuration-processor</artifactId > <optional > true</optional > </dependency > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > <configuration > <excludes > <exclude > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </exclude > <exclude > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-configuration-processor</artifactId > </exclude > </excludes > </configuration > </plugin > </plugins > </build >
六、Web开发
1、SpringMVC自动配置概览
大多场景我们都无需自定义配置!
内容协商视图解析器和BeanName视图解析器
静态资源(包括webjars)
自动注册 Converter,GenericConverter,Formatter
支持 HttpMessageConverters (后来我们配合内容协商理解原理)
自动注册 MessageCodesResolver (国际化用)
静态index.html 页支持
自定义 Favicon
自动使用 ConfigurableWebBindingInitializer ,(DataBinder负责将请求数据绑定到JavaBean上)
不用@EnableWebMvc注解。使用 @Configuration
+ WebMvcConfigurer
自定义规则
声明 WebMvcRegistrations
改变默认底层组件
使用 @EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC
2、简单功能分析
静态资源访问
只要静态资源放在类路径下: /static
or /public
or /resources
or /META-INF/resources
访问 : 当前项目根路径/ + 静态资源名
原理: 静态映射/**。
请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面!
改变默认的静态资源路径和前缀:
默认无前缀!
1 2 3 4 5 6 7 8 9 10 11 12 13 spring: mvc: static-path-pattern: /res/** web: resources: static-locations: classpath:/haha/
webjar
即将一些第三方其他包打包成了jar供我们导入!
官网:https://www.webjars.org/
自动映射 /webjars/**
1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > org.webjars</groupId > <artifactId > jquery</artifactId > <version > 3.6.0</version > </dependency >
欢迎页支持
即默认的index页面!
静态资源路径下 index.html
可以配置静态资源路径
但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问
自定义 Favicon
favicon.ico:放在静态资源目录下即可。
注意:同样配置静态资源访问前缀会导致失效!无法生效重启项目或rebuild项目并强制刷新浏览器!
静态资源配置原理
1、SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
2、SpringMVC功能的自动配置类 WebMvcAutoConfiguration,生效
1 2 3 4 5 6 7 8 @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 {}
3、给容器中配了什么。
配置文件的相关属性和xxx进行了绑定。WebMvcProperties,spring.mvc 、ResourceProperties,spring.resources
配置类只有一个有参构造器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Configuration(proxyBeanMethods = false) @Import(EnableWebMvcConfiguration.class) @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}public WebMvcAutoConfigurationAdapter (ResourceProperties resourceProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider, ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider, ObjectProvider<DispatcherServletPath> dispatcherServletPath, ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) { this .resourceProperties = resourceProperties; this .mvcProperties = mvcProperties; this .beanFactory = beanFactory; this .messageConvertersProvider = messageConvertersProvider; this .resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable(); this .dispatcherServletPath = dispatcherServletPath; this .servletRegistrations = servletRegistrations; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Override public void addResourceHandlers (ResourceHandlerRegistry registry) { if (!this .resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled" ); return ; } Duration cachePeriod = this .resourceProperties.getCache().getPeriod(); CacheControl cacheControl = this .resourceProperties.getCache().getCachecontrol().toHttpCacheControl(); if (!registry.hasMappingForPattern("/webjars/**" )) { customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**" ) .addResourceLocations("classpath:/META-INF/resources/webjars/" ) .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); } String staticPathPattern = this .mvcProperties.getStaticPathPattern(); if (!registry.hasMappingForPattern(staticPathPattern)) { customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern) .addResourceLocations(getResourceLocations(this .resourceProperties.getStaticLocations())) .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); } }
对应配置:
1 2 3 4 5 spring: web: resources: static-locations: classpath:/haha/ add-mappings: true
5、这里的 this.resourceProperties.getStaticLocations())
默认静态资源路径值
1 2 3 4 5 6 7 8 9 10 11 @ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false) public class ResourceProperties { private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/" , "classpath:/resources/" , "classpath:/static/" , "classpath:/public/" }; private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Bean public WelcomePageHandlerMapping welcomePageHandlerMapping (ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping( new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(), this .mvcProperties.getStaticPathPattern()); welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider)); welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations()); return welcomePageHandlerMapping; } WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) { if (welcomePage.isPresent() && "/**" .equals(staticPathPattern)) { logger.info("Adding welcome page: " + welcomePage.get()); setRootViewName("forward:index.html" ); } else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) { logger.info("Adding welcome page template: index" ); setRootViewName("index" ); } }
3、请求参数处理
请求映射
REST原理
原理(表单提交要使用REST的时候):
表单提交会带上_method=PUT
请求过来被HiddenHttpMethodFilter
拦截
请求是否正常,并且是POST
获取到_method
的值。
兼容以下请求;PUT.DELETE.PATCH
原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值 。
过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。
Rest使用客户端工具,如PostMan直接发送Put、delete等方式请求,无需Filter,不会经过Filter过滤!还是因为表单不支持put,delete请求才进行包装重写!
rest风格手动开启:
1 2 3 4 5 6 spring: mvc: hiddenmethod: filter: enabled: true
SpringBoot的一批新的注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @RestController public class HelloController { @RequestMapping("/mn.jpg") public String hello () { return "aaa" ; } @GetMapping("/user") public String getUser () { return "GET-张三" ; } @PostMapping("/user") public String saveUser () { return "POST-张三" ; } @PutMapping("/user") public String putUser () { return "PUT-张三" ; } @DeleteMapping("/user") public String deleteUser () { return "DELETE-张三" ; } }
修改默认的_method参数名称:
使用@Bean 注入 WebMvcConfigurer
直接实现WebMvcConfigurer接口即可
1 2 3 4 5 6 7 8 9 10 11 @Configuration(proxyBeanMethods = false) public class WebConfig implements WebMvcConfigurer { @Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter () { HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter(); methodFilter.setMethodParam("_m" ); return methodFilter; } }
请求映射原理
SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet ->doDispatch()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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); mappedHandler = getHandler(processedRequest);
getHandler()方法的handlerMappings处理器映射:
所有的请求映射都在HandlerMapping中:
SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping
。访问 /
能访问到index.html;
SpringBoot自动配置了默认的 RequestMappingHandlerMapping
请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
如果有就找到这个请求对应的 handler
如果没有就是下一个 HandlerMapping
RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则。
我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping 。自定义 HandlerMapping
普通参数与基本注解
注解
@PathVariable:路径参数获取,或使用Map<String, String>
@RequestHeader:请求头参数获取,或使用Map<String, String>, MultiValueMap<String, String>, or HttpHeaders
@RequestParam:请求参数获取,或使用Map<String, String> or MultiValueMap<String, String>
@RequestBody:请求体获取(post请求才有请求体,表单提交 )
@CookieValue:cookie获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 @RestController public class ParameterController { @GetMapping("/car/{id}/owner/{username}") public Map<String, Object> getCar (@PathVariable Integer id, @PathVariable String username, @PathVariable Map<String, String> mp, // 请求头参数获取 @RequestHeader("User-Agent") String userAgent, @RequestHeader Map<String, String> header, // 请求参数获取 Map<String, String> or MultiValueMap<String, String> @RequestParam("age") Integer age, @RequestParam("inters") List<String> inters, @RequestParam Map<String, String> params // cookie获取 // @CookieValue("_ga") String _ga, //@CookieValue Cookie cookie, ) { HashMap<String, Object> map = new HashMap<>(); map.put("id" , id); map.put("username" , username); map.put("mp" , mp); map.put("UA" , userAgent); map.put("header" , header); map.put("age" , age); map.put("inters" , inters); map.put("params" , params); return map; } @PostMapping("/save") public Map<String, Object> postMethod (@RequestBody String content) { HashMap<String, Object> map = new HashMap<>(); map.put("content" , content); return map; } }
@RequestAttribute:获取请求域参数,也可以HttpServletRequest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @Controller public class RequestController { @GetMapping("/goto") public String goToPage (HttpServletRequest request) { request.setAttribute("msg" , "成功了..." ); request.setAttribute("code" , 200 ); return "forward:/success" ; } @ResponseBody @GetMapping("/success") public Map<String, Object> success (@RequestAttribute(value = "msg", required = false) String msg, @RequestAttribute(value = "code", required = false) Integer code, // 同一次请求可以直接调用request对象 HttpServletRequest request) { Map<String, Object> map = new HashMap<>(); Object attribute = request.getAttribute("msg" ); map.put("msg" , msg); map.put("code" , code); map.put("request" , attribute); map.put("map" , request.getAttribute("map" )); map.put("model" , request.getAttribute("model" )); map.put("request" , request.getAttribute("request" )); return map; } }
1、语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd
2、SpringBoot 默认是禁用了矩阵变量的功能
3、矩阵变量必须有url路径变量才能被解析,即矩阵变量在url中要{}
括起来
手动开启矩阵变量功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Configuration(proxyBeanMethods = false) public class WebConfig implements WebMvcConfigurer { @Override public void configurePathMatch (PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); urlPathHelper.setRemoveSemicolonContent(false ); configurer.setUrlPathHelper(urlPathHelper); } @Bean public WebMvcConfigurer webMvcConfigurer () { return new WebMvcConfigurer() { @Override public void configurePathMatch (PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); urlPathHelper.setRemoveSemicolonContent(false ); configurer.setUrlPathHelper(urlPathHelper); } }; } }
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @ResponseBody @GetMapping("/cars/{path}") public Map<String, Object> carsSell (@MatrixVariable("low") Integer low, @MatrixVariable("brand") List<String> brand, @PathVariable("path") String path) { Map<String, Object> map = new HashMap<>(); map.put("low" , low); map.put("brand" , brand); map.put("path" , path); return map; }@ResponseBody @GetMapping("/boss/{bossId}/{empId}") public Map boss (@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge, @MatrixVariable(value = "age",pathVar = "empId") Integer empAge) { Map<String,Object> map = new HashMap<>(); map.put("bossAge" ,bossAge); map.put("empAge" ,empAge); return map; }
Servlet API
ServletRequestMethodArgumentResolver 来解析以下的部分参数:
1 WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
复杂参数
Map 、**Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、**Errors/BindingResult、RedirectAttributes( 重定向携带数据) 、ServletResponse(response) 、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder
自定义对象参数
可以自动类型转换与格式化,可以级联封装!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Data public class Person { private String userName; private Integer age; private Date birth; private Pet pet; }@Data public class Pet { private String name; private String age; }
POJO封装过程
ServletModelAttributeMethodProcessor来解析参数!
参数处理原理
1 2 3 4 5 6 7 8 9 10 11 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); mav = invokeHandlerMethod(request, response, handlerMethod); Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
3、参数解析器-HandlerMethodArgumentResolver
确定将要执行的目标方法的每一个参数的值是什么;
SpringMVC目标方法能写多少种参数类型。取决于参数解析器。
当前解析器是否支持解析这种参数
支持就调用 resolveArgument
1 2 Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
1 WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
WebDataBinder:web数据绑定器,将请求参数的值绑定到指定的JavaBean里面
WebDataBinder:利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中
GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean – Integer)
未来我们可以给WebDataBinder里面放自己的Converter;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Configuration(proxyBeanMethods = false) public class WebConfig implements WebMvcConfigurer { @Bean public WebMvcConfigurer webMvcConfigurer () { return new WebMvcConfigurer() { @Override public void addFormatters (FormatterRegistry registry) { registry.addConverter(new Converter<String, Pet>() { @Override public Pet convert (String source) { if (!StringUtils.isEmpty(source)){ Pet pet = new Pet(); String[] split = source.split("," ); pet.setName(split[0 ]); pet.setAge(Integer.parseInt(split[1 ])); return pet; } return null ; } }); } } } }
6、目标方法执行完成
将所有的数据都放在 ModelAndViewContainer ;包含要去的页面地址View。还包含Model数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); renderMergedOutputModel(mergedModel, getRequestToExpose(request), response); 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); } }); }
4、数据响应与内容协商
响应JSON
jackson.jar+@ResponseBody
1 2 3 4 5 6 7 8 9 10 11 12 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > web场景自动引入了json场景<dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-json</artifactId > <version > 2.3.4.RELEASE</version > <scope > compile</scope > </dependency >
返回值解析器原理
返回值处理器判断是否支持这种类型返回值 supportsReturnType
返回值处理器调用 handleReturnValue 进行处理
RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的。
利用 MessageConverters 进行处理将数据写为json
内容协商 (浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?
得到MappingJackson2HttpMessageConverter可以将对象写为json
利用MappingJackson2HttpMessageConverter将对象转为json再写出去。
SpringMVC支持的返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ModelAndView Model View ResponseEntity ResponseBodyEmitter StreamingResponseBody HttpEntity HttpHeaders Callable DeferredResult ListenableFuture CompletionStage WebAsyncTask 有 @ModelAttribute 且为对象类型的 @ResponseBody 注解 ---> RequestResponseBodyMethodProcessor
HTTPMessageConverter原理
MessageConverter规范
HttpMessageConverter: 看是否支持将此 Class类型的对象,转为MediaType类型的数据。
默认的MessageConverter
最终 MappingJackson2HttpMessageConverter
把对象转为JSON(利用底层的jackson的objectMapper转换的)
内容协商
根据客户端接收能力不同,返回不同媒体类型的数据。
引入xml依赖
1 2 3 4 5 <dependency > <groupId > com.fasterxml.jackson.dataformat</groupId > <artifactId > jackson-dataformat-xml</artifactId > </dependency >
postman分别测试返回json和xml
只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型!
开启浏览器参数方式内容协商功能
1 2 3 spring: contentnegotiation: favor-parameter: true
发请求:
http://localhost:8080/test/person?format=json
http://localhost:8080/test/person?format=xml
内容协商原理
1、判断当前响应头中是否已经有确定的媒体类型。MediaType
2、获取客户端(PostMan、浏览器)支持接收的内容类型。获取客户端Accept请求头字段 application/xml
contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略
HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型
3、遍历循环所有当前系统的 MessageConverter ,看谁支持操作这个对象(Person)
4、找到支持操作Person的converter,把converter支持的媒体类型统计出来。
5、客户端需要 application/xml。服务端能力【10种、json、xml】
6、进行内容协商的最佳匹配媒体类型
7、用支持将对象转为最佳匹配媒体类型的converter。调用它进行转化 。
自定义 MessageConverter
实现多协议数据兼容。json、xml、x-nb
流程:
@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理
Processor 处理方法返回值。通过 MessageConverter 处理
所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
内容协商找到最终的 messageConverter;
自定义的NBMessageConverter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class NBMessageConverter implements HttpMessageConverter <Person > { @Override public boolean canRead (Class<?> clazz, MediaType mediaType) { return false ; } @Override public boolean canWrite (Class<?> clazz, MediaType mediaType) { return clazz.isAssignableFrom(Person.class); } @Override public List<MediaType> getSupportedMediaTypes () { return MediaType.parseMediaTypes("application/x-nb" ); } @Override public Person read (Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { return null ; } @Override public void write (Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { String data = person.getUserName() + ";" + person.getAge() + ";" + person.getBirth(); OutputStream body = outputMessage.getBody(); body.write(data.getBytes()); } }
加入容器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Configuration(proxyBeanMethods = false) public class WebConfig implements WebMvcConfigurer { @Bean public WebMvcConfigurer webMvcConfigurer () { return new WebMvcConfigurer() { @Override public void extendMessageConverters (List<HttpMessageConverter<?>> converters) { converters.add(new NBMessageConverter()); } }; } }
测试:
使用postman在请求头添加 Accept=application/x-nb
发送请求测试即可!
基于浏览器参数的内容协商
将自定义的 application/x-nb
绑定为nb
使用浏览器请求参数format传进去!
有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。
测试 :http://localhost:8080/test/person?format=nb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Configuration(proxyBeanMethods = false) public class WebConfig implements WebMvcConfigurer { @Bean public WebMvcConfigurer webMvcConfigurer () { return new WebMvcConfigurer() { @Override public void extendMessageConverters (List<HttpMessageConverter<?>> converters) { converters.add(new NBMessageConverter()); } @Override public void configureContentNegotiation (ContentNegotiationConfigurer configurer) { HashMap<String, MediaType> mediaTypes = new HashMap<>(); mediaTypes.put("json" , MediaType.APPLICATION_JSON); mediaTypes.put("xml" , MediaType.APPLICATION_ATOM_XML); mediaTypes.put("nb" , MediaType.parseMediaType("application/x-nb" )); ParameterContentNegotiationStrategy paramsStrategy = new ParameterContentNegotiationStrategy(mediaTypes); HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy(); configurer.strategies(Arrays.asList(paramsStrategy, headerStrategy )); } }; } }
5、视图解析原理
目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面。包括数据和视图地址
方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer
任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)。
processDispatchResult 处理派发结果(页面改如何响应)
render(mv, request, response); 进行页面渲染逻辑
根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】
所有的视图解析器尝试是否能根据当前返回值得到View对象
得到了 redirect:/main.html --> Thymeleaf new RedirectView()
ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
view.render(mv.getModelInternal(), request, response)
视图对象调用自定义的render进行页面渲染工作
RedirectView 如何渲染【重定向到一个页面】
获取目标url地址
response.sendRedirect(encodedURL);
视图解析:
返回值以 forward: 开始: new InternalResourceView(forwardUrl)
, 转发 request.getRequestDispatcher(path).forward(request, response)
返回值以 redirect: 开始: new RedirectView()
,render就是重定向
返回值是普通字符串: new ThymeleafView()
6、模板引擎Thymeleaf
thymeleaf简介
Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text.
现代化、服务端Java模板引擎
基本语法
表达式:
表达式名字
语法
用途
变量取值
${…}
获取请求域、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)
设置属性值
设置单个值:
1 2 3 4 5 6 <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 >
设置多个值:
1 <img src ="../../images/gtvglogo.png" th:attr ="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
以上两个的代替写法 th:xxxx
:
1 2 <input type ="submit" value ="Subscribe!" th:value ="#{subscribe.submit}" /> <form action ="subscribe.html" th:action ="@{/subscribe}" >
所有h5兼容的标签写法: https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes
迭代
1 2 3 4 5 <tr th:each ="prod : ${prods}" > <td th:text ="${prod.name}" > Onions</td > <td th:text ="${prod.price}" > 2.41</td > <td th:text ="${prod.inStock}? #{true} : #{false}" > yes</td > </tr >
1 2 3 4 5 <tr th:each ="prod,iterStat : ${prods}" th:class ="${iterStat.odd}? 'odd'" > <td th:text ="${prod.name}" > Onions</td > <td th:text ="${prod.price}" > 2.41</td > <td th:text ="${prod.inStock}? #{true} : #{false}" > yes</td > </tr >
条件运算
1 2 3 <a href ="comments.html" th:href ="@{/product/comments(prodId=${prod.id})}" th:if ="${not #lists.isEmpty(prod.comments)}" > view</a >
1 2 3 4 5 <div th:switch ="${user.role}" > <p th:case ="'admin'" > User is an administrator</p > <p th:case ="#{roles.manager}" > User is a manager</p > <p th:case ="*" > User is some other thing</p > </div >
属性优先级
7、Thymeleaf使用
引入Starter
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-thymeleaf</artifactId > </dependency >
自动配置好了thymeleaf
1 2 3 4 5 @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(ThymeleafProperties.class) @ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class }) @AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class }) public class ThymeleafAutoConfiguration { }
自动配好的策略
所有thymeleaf的配置值都在 ThymeleafProperties
配置好了 SpringTemplateEngine
配好了 ThymeleafViewResolver
我们只需要直接开发页面
默认配置好的前后缀:
1 2 3 public static final String DEFAULT_PREFIX = "classpath:/templates/" ;public static final String DEFAULT_SUFFIX = ".html" ;
需引入thymeleaf的名称空间:
1 <html lang ="en" xmlns:th ="http://www.thymeleaf.org" >
$和@
@会自动用地址拼串,即使你加了 全局访问前缀路径,也会自动帮你拼接!
即使添加了访问前缀路径!
1 2 3 server: servlet: context-path: /hello
测试页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <!DOCTYPE html > <html lang ="en" xmlns:th ="http://www.thymeleaf.org" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <h1 th:text ="${msg}" > 哈哈</h1 > <h2 > <a href ="www.baidu.com" th:href ="${link}" > 去百度</a > <a href ="www.baidu.com" th:href ="@{/link}" > 去百度</a > </h2 > </body > </html >
行内写法
th:text="${xxx}"
:标签内写法
[[${xxx}]]
:行内写法,标签外写法
1 2 3 4 5 6 7 <a href ="#" class ="btn btn-default dropdown-toggle" data-toggle ="dropdown" > <img src ="images/photos/user-avatar.png" alt ="" /> [[${session.loginUser.userName}]] <span class ="caret" > </span > </a > <div class ="value" th:text ="${indexCount}" > 230</div >
抽取公共页面
官方文档:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#template-layout
可使用th:fragment
或直接使用id选择器也可以声明为公共页面!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!DOCTYPE html > <html lang ="en" xmlns:th ="http://www.thymeleaf.org" > <head th:fragment ="commonheader" > <link href ="css/style.css" th:href ="@{/css/style.css}" rel ="stylesheet" > <link href ="css/style-responsive.css" th:href ="@{/css/style-responsive.css}" rel ="stylesheet" > </head > <body > <div id ="leftmenu" class ="left-side sticky-left-side" > </div > <div th:fragment ="headermenu" class ="header-section" > </div > <div id ="commonscript" > ...</div > </body > </html >
引用公共页面:
th:insert
th:replace
th:include
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <div th:fragment ="copy" > © 2011 The Good Thymes Virtual Grocery</div > <body > ... <div th:insert ="footer :: copy" > </div > <div th:replace ="footer :: copy" > </div > <div th:include ="footer :: copy" > </div > <div > <footer > © 2011 The Good Thymes Virtual Grocery </footer > </div > <footer > © 2011 The Good Thymes Virtual Grocery </footer > <div > © 2011 The Good Thymes Virtual Grocery </div > <div th:insert ="~{footer :: copy}" > </div > <div th:insert ="footer :: copy" > </div > </body >
测试使用:
1 2 3 4 <div th:include ="common :: commonheader" > </div > <div th:replace ="common :: #leftmenu" > </div > <div th:replace ="common :: headermenu" > </div > <div th:replace ="common :: #commonscript" > </div >
数据渲染
遍历状态:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#keeping-iteration-status
路径参数的获取:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#link-urls
当前标签生成序列:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#numbers
带有参数的处理规则:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#link-urls
th:each="user,stats:${users}"
:遍历状态,user变量后面用逗号隔开的变量stats就是Thymeleaf的状态变量
stats.count:计数,获取当前是第几个
stats.current:获取当前对象
1 2 3 4 5 <tr class ="gradeX" th:each ="user,stats:${users}" > <td th:text ="${stats.count}" > Trident</td > <td th:text ="${user.userName}" > Internet</td > <td > [[${user.password}]]</td > </tr >
${#numbers.sequence(1,page.pages)}
:Thymeleaf的工具类用来生成一个序列,可以将当前标签按照规则重复生成!
1 2 3 4 5 6 7 8 9 10 11 12 <a th:href ="@{/user/delete/{id}(id=${user.id},pn=${page.current})}" type ="button" > 删除</a > <li th:class ="${num==page.current?'active':''}" th:each ="num:${#numbers.sequence(1,page.pages)}" > <a th:href ="@{/dynamic_table(pn=${num})}" > [[${num}]]</a > </li > <li class ="next" > <a th:href ="@{/dynamic_table(pn=${page.current+1})}" > Next → </a > </li >
8、拦截器
HandlerInterceptor 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 @Slf4j public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("拦截的路径是{}" , request.getRequestURI()); HttpSession session = request.getSession(); Object loginUser = session.getAttribute("loginUser" ); if (loginUser != null ){ return true ; } request.setAttribute("msg" , "请先登录!" ); request.getRequestDispatcher("/" ).forward(request, response); return false ; } @Override public void postHandle (HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerInterceptor.super .postHandle(request, response, handler, modelAndView); } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { HandlerInterceptor.super .afterCompletion(request, response, handler, ex); } }
配置拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Configuration public class AdminConfig implements WebMvcConfigurer { @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**" ) .excludePathPatterns("/" , "/login" , "/css/**" , "/js/**" , "/fonts/**" , "/images/**" , "/aa/**" ); } }
拦截器原理
根据当前请求,找到HandlerExecutionChain 【可以处理请求的handler以及handler的所有 拦截器】
先来顺序执行 所有拦截器的 preHandle方法
如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
如果当前拦截器返回为false。直接倒序执行 所有已经执行了的拦截器的 afterCompletion;
如果任何一个拦截器返回false。直接跳出不执行目标方法
所有拦截器都返回True。执行目标方法
倒序 执行所有拦截器的postHandle方法。
前面的步骤有任何异常都会直接倒序触发 afterCompletion
页面成功渲染完成以后,也会倒序 触发 afterCompletion
如下图:
9、文件上传
页面表单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <form role ="form" th:action ="@{/upload}" method ="post" enctype ="multipart/form-data" > <div class ="form-group" > <label for ="exampleInputEmail1" > 邮箱</label > <input type ="email" name ="email" class ="form-control" id ="exampleInputEmail1" placeholder ="Enter email" > </div > <div class ="form-group" > <label for ="exampleInputPassword1" > 名字</label > <input type ="text" name ="username" class ="form-control" id ="exampleInputPassword1" placeholder ="username" > </div > <div class ="form-group" > <label for ="exampleInputFile" > 头像</label > <input type ="file" name ="headerImg" id ="exampleInputFile" > </div > <div class ="form-group" > <label for ="exampleInputFile" > 生活照</label > <input type ="file" name ="photos" multiple > </div > <div class ="checkbox" > <label > <input type ="checkbox" > Check me out </label > </div > <button type ="submit" class ="btn btn-primary" > 提交</button > </form >
文件上传代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @Slf4j @Controller public class FormController { @GetMapping("/form_layouts") public String form_layouts () { return "form/form_layouts" ; } @PostMapping("/upload") public String upload (@RequestParam("email") String email, @RequestParam("username") String username, // @RequestPart 获取表单文件 @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()){ String originalFilename = headerImg.getOriginalFilename(); headerImg.transferTo(new File("C:\\Users\\15890\\Desktop\\" +originalFilename)); } if (photos.length > 0 ){ for (MultipartFile photo : photos) { if (!photo.isEmpty()){ String originalFilename = photo.getOriginalFilename(); photo.transferTo(new File("C:\\Users\\15890\\Desktop\\" +originalFilename)); } } } return "index" ; } }
文件上传配置
1 2 3 4 5 spring: servlet: multipart: max-file-size: 10MB max-request-size: 100MB
自动配置原理
文件上传自动配置类-MultipartAutoConfiguration -MultipartProperties
自动配置好了 StandardServletMultipartResolver 【文件上传解析器 】
原理步骤:
1、请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
2、参数解析器来解析请求中的文件内容封装成MultipartFile
3、将request中文件信息封装为一个Map;MultiValueMap<String, MultipartFile>
FileCopyUtils。实现文件流的拷贝,底层就是该工具类实现的!
10、异常/错误 处理
默认规则
默认情况下,Spring Boot提供**/error**处理所有错误的映射
对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。
对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
机器客户端JSON响应:
浏览器的白页:
要完全替换默认行为,可以实现 ErrorController 并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。
error/下的4xx,5xx页面会被自动解析;
异常处理自动配置原理
ErrorMvcAutoConfiguration 自动配置异常处理规则
容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
DefaultErrorAttributes:定义错误页面中可以包含哪些数据 。
容器中的组件:类型:BasicErrorController --> id:basicErrorController(json+白页 适配响应 )
处理默认 /error 路径的请求;页面响应 new ModelAndView("error", model);
容器中有组件 View->id 是error;(响应默认错误页 )
容器中放组件 BeanNameViewResolver (视图解析器 );按照返回的视图名作为组件的id去容器中找View对象。
容器中的组件:类型:DefaultErrorViewResolver -> id:conventionErrorViewResolver
如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面
error/404、5xx.html
如果想要返回页面;就会找error视图【StaticView 】。(默认是一个白页)
小总结:
如果想向错误页面保存数据(Request域)则修改DefaultErrorAttributes
如果想修改默认的错误页json或白页就去修改BasicErrorController
如果想修改错误页面的文件位置则去修改BeanNameViewResolver
异常处理步骤流程
1、执行目标方法,目标方法运行期间有任何异常都会被catch 、而且标志当前请求结束;并且用 dispatchException 来封装
2、进入视图解析流程(页面渲染) processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException)
3、mv = processHandlerException
,处理handler发生的异常,处理完成返回ModelAndView ;
1、遍历所有的 handlerExceptionResolvers ,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】
2、系统默认的 异常解析器:
1、DefaultErrorAttributes先来处理异常。把异常信息保存到request域 ,并且返回null;
2、默认没有任何人能处理异常,则将异常抛出
1、如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理
2、解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。
3、默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址 ,error/500.html
4、模板引擎最终响应 这个页面 error/500.html
发生错误放到request域的数据
status
error
trace
message
path
postman返回的json数据:
1 2 3 4 5 6 7 8 { "timestamp" : "2021-10-08T11:44:47.853+00:00" , "status" : 500 , "error" : "Internal Server Error" , "trace" : "java.lang.ArithmeticException: / by zer" , "message" : "/ by zero" , "path" : "/basic_table" }
错误页面获取数据:
1 2 3 4 5 6 7 8 <section class ="error-wrapper text-center" > <h1 > <img alt ="" src ="images/500-error.png" > </h1 > <h2 > OOOPS!!!</h2 > <h3 th:text ="${message}" > Something went wrong.</h3 > <h3 th:text ="${error}" > Something went wrong.</h3 > <p class ="nrml-txt" th:text ="${trace}" > Why not try refreshing you page? Or you can <a href ="#" > contact our support</a > if the problem persists.</p > <a class ="back-btn" href ="index.html" th:text ="${status}" > Back To Home</a > </section >
定制错误处理逻辑
自定义错误页
error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页!
1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping(value = {"/basic_table", "/basic_table.html"}) public String basicTable (@RequestParam("a") int a) { int i = 10 /0 ; return "table/basic_table" ; }
@ControllerAdvice+@ExceptionHandler
处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Slf4j @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler({ArithmeticException.class, NullPointerException.class, MissingServletRequestParameterException.class}) public String handleArithException (Exception e) { log.error("异常是: {}" , e); return "login" ; } }
@ResponseStatus+自定义异常
底层是 ResponseStatusExceptionResolver ,把responsestatus注解的信息组装成ModelAndView返回,底层调用 response.sendError(statusCode, resolvedReason)
相当于调用tomcat发送的 /error !
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "用户数量太多!") public class UserTooManyException extends RuntimeException { public UserTooManyException () { } public UserTooManyException (String msg) { super (msg); } }
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @GetMapping(value = {"/dynamic_table", "/dynamic_table.html"}) public String dynamicTable (Model model) { List<User> users = Arrays.asList(new User("牛逼" , "123456" ), new User("张三" , "89032" ), new User("李四" , "93993" )); model.addAttribute("users" , users); if (users.size() > 2 ){ throw new UserTooManyException("用户太多了!" ); } return "table/dynamic_table1" ; }
Spring底层的异常
如参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常。
没人处理tomcat发送 /error请求,SpringBoot会处理,要是SpringBoot也不处理则会返回tomcat的默认错误蓝白页!
自定义异常解析器
自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Order(value = Ordered.HIGHEST_PRECEDENCE) @Component public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { response.sendError(520 , "我喜欢的错误!" ); } catch (IOException e) { e.printStackTrace(); } return new ModelAndView(); } }
11、Web原生组件注入
使用Servlet API
SpringBoot为了兼容旧系统的Servlet!
@ServletComponentScan(basePackages = “com.itnxd.admin” ) :指定原生Servlet组件都放在那里
@WebServlet(urlPatterns = “/my” ):效果:直接响应,没有经过Spring的拦截器
@WebFilter(urlPatterns={“/css/*” ,“/images/*” })
@WebListener
主应用开启扫描Servlet包:
1 2 3 4 5 6 7 8 9 10 11 @ServletComponentScan(basePackages = "com.itnxd.helloworld4") @SpringBootApplication public class HelloWorld4Application { public static void main (String[] args) { SpringApplication.run(HelloWorld4Application.class, args); } }
WebServlet:
1 2 3 4 5 6 7 8 @WebServlet(urlPatterns = "/my") public class MyServlet extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("666" ); } }
WebFilter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Slf4j @WebFilter(urlPatterns = {"/my", "/css/*", "/images/*"}) public class MyFilter implements Filter { @Override public void init (FilterConfig filterConfig) throws ServletException { log.info("filter初始化!" ); } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { log.info("filter工作!" ); filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy () { log.info("filter销毁!" ); } }
WebListener:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Slf4j @WebListener public class MyListener implements ServletContextListener { @Override public void contextInitialized (ServletContextEvent sce) { log.info("Listener监听到项目初始化完成!" ); } @Override public void contextDestroyed (ServletContextEvent sce) { log.info("Listener监听到项目销毁!" ); } }
使用RegistrationBean
ServletRegistrationBean
FilterRegistrationBean
ServletListenerRegistrationBean
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Configuration(proxyBeanMethods = true) public class MyRegistConfig { @Bean public ServletRegistrationBean myServlet () { MyServlet myServlet = new MyServlet(); return new ServletRegistrationBean(myServlet, "/my" , "/my2" ); } @Bean public FilterRegistrationBean myFilter () { MyFilter myFilter = new MyFilter(); FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter); filterRegistrationBean.setUrlPatterns(Arrays.asList("/my" , "/my2" , "/css/*" )); return filterRegistrationBean; } @Bean public ServletListenerRegistrationBean myListener () { MyListener myListener = new MyListener(); return new ServletListenerRegistrationBean(myListener); } }
不经过spring拦截器?
多个Servlet都能处理到同一层路径,精确优选原则!
DispatchServlet 如何注册进来:
1 2 3 4 spring: mvc: servlet: path: /
原生Servlet默认使我们自己定义的,例如:/my
因此:精确匹配的话,默认是到不了DispatcherServlet 的!
12、嵌入式Servlet容器
原理
SpringBoot 应用启动发现当前是Web应用。web场景包-导入tomcat
web应用会创建一个web版的ioc容器 ServletWebServerApplicationContext
·ServletWebServerApplicationContext· 启动的时候寻找 ServletWebServerFactory
(Servlet 的web服务器工厂,Servlet 的web服务器 )
SpringBoot底层默认有很多的WebServer工厂;TomcatServletWebServerFactory, JettyServletWebServerFactory, UndertowServletWebServerFactory
底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration
ServletWebServerFactoryAutoConfiguration 导入了 ServletWebServerFactoryConfiguration (配置类 )
ServletWebServerFactoryConfiguration 配置类动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包 ),容器中就有 TomcatServletWebServerFactory
TomcatServletWebServerFactory 创建出 Tomcat 服务器并启动;TomcatWebServer 的构造器拥有初始化方法 initialize—this.tomcat.start();
内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)
切换嵌入式Servlet容器
默认支持的webServer:Tomcat, Jetty, or Undertow
ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器
切换服务器
先手动排除默认的tomcat依赖,在手动添加其他服务器依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <exclusions > <exclusion > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-tomcat</artifactId > </exclusion > </exclusions > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-undertow</artifactId > </dependency >
定制Servlet容器
1、实现 WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>
把配置文件的值和 ServletWebServerFactory 进行绑定
修改配置文件 server.xxx (xxx表示服务器名)
2、直接自定义 ConfigurableServletWebServerFactory
xxxxxCustomizer:定制化器 ,可以改变xxxx的默认规则!
1 2 3 4 5 6 7 8 9 @Component public class CustomizationBean implements WebServerFactoryCustomizer <ConfigurableServletWebServerFactory > { @Override public void customize (ConfigurableServletWebServerFactory server) { server.setPort(9000 ); } }
13、定制化原理
定制化的常见方式
修改配置文件;
xxxxxCustomizer,实现定制化器接口
编写自定义的配置类 xxxConfiguration + @Bean替换、增加容器中默认组件;视图解析器
Web应用 编写一个配置类实现 WebMvcConfigurer
即可定制化web功能;+ @Bean给容器中再扩展一些组件
EnableWebMvc + WebMvcConfigurer @Bean
可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能
@EnableWebMvc开启手动定制,则springmvc底层实现好的组件将全部失效,只能自己自定义!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @EnableWebMvc @Configuration public class AdminConfig implements WebMvcConfigurer { @Override public void addResourceHandlers (ResourceHandlerRegistry registry) { registry.addResourceHandler("/aa/**" ) .addResourceLocations("classpath:/static" ); } }
@EnableWebMvc 原理
1、WebMvcAutoConfiguration 默认的SpringMVC的自动配置功能类。静态资源、欢迎页…
2、一旦使用 @EnableWebMvc 。会 @Import(DelegatingWebMvcConfiguration.class)
3、DelegatingWebMvcConfiguration 的作用,只保证SpringMVC最基本的使用
把所有系统中的 WebMvcConfigurer 拿过来。所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效
自动配置了一些非常底层的组件 。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取
4、WebMvcAutoConfiguration 里面的配置要能生效 必须 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
5、@EnableWebMvc 导致了 WebMvcAutoConfiguration 没有生效。
原理分析套路
导入场景starter
xxxxAutoConfiguration
导入xxx组件
绑定xxxProperties
绑定配置文件项
七、数据访问
1、整合JDBC
导入JDBC场景
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-jdbc</artifactId > </dependency >
自动导入的东西:
导入数据库驱动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <properties > <java.version > 1.8</java.version > </properties > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency >
分析自动配置
DataSourceAutoConfiguration : 数据源的自动配置
修改数据源相关的配置:spring.datasource
数据库连接池的配置,是自己容器中没有DataSource才自动配置的
底层配置好的连接池是:HikariDataSource
DataSourceTransactionManagerAutoConfiguration: 事务管理器 的自动配置
JdbcTemplateAutoConfiguration: JdbcTemplate 的自动配置,可以来对数据库进行crud
可以修改这个配置项 @ConfigurationProperties(prefix = “spring.jdbc ”) 来修改JdbcTemplate
JndiDataSourceAutoConfiguration: jndi 的自动配置
XADataSourceAutoConfiguration: 分布式事务相关 的
修改配置项
1 2 3 4 5 6 7 8 9 spring: datasource: url: jdbc:mysql://localhost:3306/book username: root password: xxxxx driver-class-name: com.mysql.cj.jdbc.Driver jdbc: template: query-timeout: 3
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Slf4j @SpringBootTest class HelloWorld4ApplicationTests { @Autowired JdbcTemplate jdbcTemplate; @Test void contextLoads () { Long num = jdbcTemplate.queryForObject("select count(*) from t_book" , Long.class); System.out.println(num); } }
2、整合Druid
druid官方github地址:https://github.com/alibaba/druid
整合第三方技术的两种方式:
自定义方式
导入依赖:
1 2 3 4 5 6 <dependency > <groupId > com.alibaba</groupId > <artifactId > druid</artifactId > <version > 1.1.17</version > </dependency >
配置数据源:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Deprecated @Configuration public class MyDataSourceConfig { @Bean @ConfigurationProperties("spring.datasource") public DataSource dataSource () throws SQLException { DruidDataSource druidDataSource = new DruidDataSource(); return druidDataSource; } }
指定配置文件前缀可直接在配置文件中找:
1 2 3 4 5 6 spring: datasource: url: jdbc:mysql://localhost:3306/book username: root password: xxxxx driver-class-name: com.mysql.cj.jdbc.Driver
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Slf4j @SpringBootTest class HelloWorld4ApplicationTests { @Autowired JdbcTemplate jdbcTemplate; @Autowired DataSource dataSource; @Test void contextLoads () { Long num = jdbcTemplate.queryForObject("select count(*) from t_book" , Long.class); System.out.println(num); log.info("数据源类型是:{}" , dataSource.getClass()); } }
StatViewServlet
StatViewServlet的用途包括:
提供监控信息展示的html页面
提供监控信息的JSON API
旧版的xml配置:
1 2 3 4 5 6 7 8 <servlet > <servlet-name > DruidStatView</servlet-name > <servlet-class > com.alibaba.druid.support.http.StatViewServlet</servlet-class > </servlet > <servlet-mapping > <servlet-name > DruidStatView</servlet-name > <url-pattern > /druid/*</url-pattern > </servlet-mapping >
SpringBoot配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Deprecated @Configuration public class MyDataSourceConfig { @Bean public ServletRegistrationBean statViewServlet () { StatViewServlet statViewServlet = new StatViewServlet(); ServletRegistrationBean<StatViewServlet> servletRegistrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*" ); servletRegistrationBean.addInitParameter("loginUsername" , "admin" ); servletRegistrationBean.addInitParameter("loginPassword" , "admin" ); return servletRegistrationBean; } }
开启 StatFilter和wallfilter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Deprecated @Configuration public class MyDataSourceConfig { @Bean @ConfigurationProperties("spring.datasource") public DataSource dataSource () throws SQLException { DruidDataSource druidDataSource = new DruidDataSource(); druidDataSource.setFilters("stat, wall" ); return druidDataSource; } }
或直接在配置文件里写:
1 2 3 4 5 6 7 8 9 spring: datasource: url: jdbc:mysql://localhost:3306/book username: root password: n158903258 driver-class-name: com.mysql.cj.jdbc.Driver filters: stat,wall
访问地址: http://localhost:8080/druid
StatFilter
用于统计监控信息;如SQL监控、URI监控!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Deprecated @Configuration public class MyDataSourceConfig { @Bean public FilterRegistrationBean webStatFilter () { WebStatFilter webStatFilter = new WebStatFilter(); FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean(webStatFilter, statViewServlet()); filterRegistrationBean.setUrlPatterns(Arrays.asList("/*" )); filterRegistrationBean.addInitParameter("exclusions" , "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" ); return filterRegistrationBean; } }
Starter方式
引入依赖:
1 2 3 4 5 6 <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.1.17</version > </dependency >
分析自动配置
扩展配置项 spring.datasource.druid
导入了一些组件:
DruidSpringAopConfiguration.class , 监控SpringBean的;配置项:spring.datasource.druid.aop-patterns
DruidStatViewServletConfiguration.class , 监控页的配置:spring.datasource.druid.stat-view-servlet;默认开启
DruidWebStatFilterConfiguration.class , web监控配置;spring.datasource.druid.web-stat-filter;默认开启
DruidFilterConfiguration.class }) 所有Druid自己filter的配置
DruidFilterConfiguration的组件:
1 2 3 4 5 6 7 8 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" ;
系统中所有filter:
别名
Filter类名
default
com.alibaba.druid.filter.stat.StatFilter
stat
com.alibaba.druid.filter.stat.StatFilter
mergeStat
com.alibaba.druid.filter.stat.MergeStatFilter
encoding
com.alibaba.druid.filter.encoding.EncodingConvertFilter
log4j
com.alibaba.druid.filter.logging.Log4jFilter
log4j2
com.alibaba.druid.filter.logging.Log4j2Filter
slf4j
com.alibaba.druid.filter.logging.Slf4jLogFilter
commonlogging
com.alibaba.druid.filter.logging.CommonsLogFilter
配置文件
SpringBoot配置示例:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
配置项列表:https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 spring: datasource: url: jdbc:mysql://localhost:3306/book username: root password: xxxx druid: filters: stat,wall stat-view-servlet: enabled: true login-password: admin login-username: admin reset-enable: false web-stat-filter: enabled: true url-pattern: /* exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' filter: stat: enabled: true slow-sql-millis: 1000 log-slow-sql: true wall: enabled: true config: update-allow: true drop-table-allow: false
3、整合MyBatis
官方地址:https://github.com/mybatis
starter:
SpringBoot官方的Starter:spring-boot-starter-*
第三方的: *-spring-boot-starter
导入依赖
1 2 3 4 5 6 <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 2.1.4</version > </dependency >
自动导入的东西:
配置模式
全局配置文件
SqlSessionFactory: 自动配置好了
SqlSession:自动配置了 SqlSessionTemplate 组合了SqlSession
@Import(AutoConfiguredMapperScannerRegistrar .class );
Mapper: 只要我们写的操作MyBatis的接口标注了 @Mapper 就会被自动扫描进来
1 2 3 4 5 6 @EnableConfigurationProperties(MybatisProperties.class) : MyBatis配置项绑定类。@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class }) public class MybatisAutoConfiguration {}@ConfigurationProperties(prefix = "mybatis") public class MybatisProperties
配置文件:
mybatis.configuration下面的所有,就是相当于改mybatis全局配置文件中的值!
1 2 3 4 5 6 mybatis: mapper-locations: classpath:mybatis/mapper/*.xml configuration: map-underscore-to-camel-case: true
测试:
AccountMapper.xml文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.itnxd.helloworld4.mapper.AccountMapper" > <select id ="getAcct" resultType ="com.itnxd.helloworld4.bean.Account" > select * from account_tbl where id = #{id} </select > </mapper >
Mapper层、Service层、Controller层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Mapper public interface AccountMapper { public Account getAcct (Long id) ; }@Service public class AccountService { @Autowired AccountMapper accountMapper; public Account getAcctById (Long id) { return accountMapper.getAcct(id); } }@Autowired AccountService accountService;@ResponseBody @GetMapping("/acct") public Account getById (@RequestParam("id") Long id) { return accountService.getAcctById(id); }
小总结:
注解配置混合模式
简单sql标注注解,复杂sql还是写xml映射文件!
1 2 3 4 5 6 7 8 9 10 11 @Mapper public interface CityMapper { @Select("select * from city where id=#{id}") public City getById (Long id) ; @Insert("insert into city(`name`,`state`,`country`) values(#{name}, #{state}, #{country})") @Options(useGeneratedKeys = true, keyProperty = "id") public void insert (City city) ; }
复杂写映射文件:
1 2 3 <insert id ="insert" useGeneratedKeys ="true" keyProperty ="id" > insert into city(`name`,`state`,`country`) values(#{name}, #{state}, #{country})</insert >
启动类标注@MapperScan指定mapper扫描,即可不用每个mapper上单独标注注解:
1 2 3 4 5 6 7 8 9 10 @MapperScan("com.itnxd.helloworld4.mapper") @SpringBootApplication public class HelloWorld4Application { public static void main (String[] args) { SpringApplication.run(HelloWorld4Application.class, args); } }
小总结:
4、整合Mybatis-Plus
导入依赖
1 2 3 4 5 6 <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.4.1</version > </dependency >
自动配置
MybatisPlusAutoConfiguration 配置类,MybatisPlusProperties 配置项绑定。**mybatis-plus:xxx **就是对mybatis-plus的定制
SqlSessionFactory 自动配置好。底层是容器中默认的数据源
mapperLocations 自动配置好的。有默认值。classpath*:/mapper/**/*.xml
任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件,放在 mapper下。
容器中也自动配置好了 SqlSessionTemplate
@Mapper 标注的接口也会被自动扫描;建议直接 @MapperScan(“com.itnxd.admin.mapper”) 批量扫描
只需要我们的Mapper继承 BaseMapper 就可以拥有crud能力
测试
前提:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public interface UsersMapper extends BaseMapper <Users > { }public interface UsersService extends IService <Users > { }@Service public class UsersServiceImpl extends ServiceImpl <UsersMapper , Users > implements UsersService { }
测试:
1 2 3 4 5 6 7 8 @Autowired UsersMapper usersMapper; @Test void testUserMapper () { Users user = usersMapper.selectById(1 ); log.info("用户信息:{}" , user); }
分页测试
向request域保存page对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @GetMapping(value = {"/dynamic_table", "/dynamic_table.html"}) public String dynamicTable (@RequestParam(value = "pn", defaultValue = "1") Integer pn, Model model) { Page<Users> usersPage = new Page<>(pn, 2 ); Page<Users> page = usersService.page(usersPage, null ); model.addAttribute("page" , page); return "table/dynamic_table1" ; }
开启MP分页插件使分页生效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Configuration public class MyBatisConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(); paginationInnerInterceptor.setOverflow(true ); paginationInnerInterceptor.setMaxLimit(500L ); interceptor.addInnerInterceptor(paginationInnerInterceptor); return interceptor; } }
html页面获取数据:
用到了Thymeleaf的用法,详见注释!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <table class ="display table table-bordered table-striped" id ="dynamic-table" > <thead > <tr > <th > #</th > <th > id</th > <th > name</th > <th > age</th > <th > email</th > <th > 操作</th > </tr > </thead > <tbody > <tr class ="gradeX" th:each ="user,stat:${page.records}" > <td th:text ="${stat.count}" > </td > <td th:text ="${user.id}" > </td > <td th:text ="${user.name}" > </td > <td th:text ="${user.age}" > </td > <td > [[${user.email}]]</td > <td > <a th:href ="@{/user/delete/{id}(id=${user.id},pn=${page.current})}" class ="btn btn-danger btn-sm" type ="button" > 删除</a > </td > </tr > </tbody > </table > <div class ="row-fluid" > <div class ="span6" > <div class ="dataTables_info" id ="hidden-table-info_info" > 当前第 [[${page.current}]] 页 总计 [[${page.pages}]] 页 共 [[${page.total}]] 条记录 </div > </div > <div class ="span6" > <div class ="dataTables_paginate paging_bootstrap pagination" > <ul > <li class ="prev disabled" > <a href ="#" > ← Previous</a > </li > <li th:class ="${num==page.current?'active':''}" th:each ="num:${#numbers.sequence(1,page.pages)}" > <a th:href ="@{/dynamic_table(pn=${num})}" > [[${num}]]</a > </li > <li class ="next" > <a th:href ="@{/dynamic_table(pn=${page.current+1})}" > Next → </a > </li > </ul > </div > </div > </div >
重定向携带参数:
其实就是将参数拼接到url最后进行携带!
1 2 3 4 5 6 7 8 9 10 @GetMapping("/user/delete/{id}") public String deleteUser (@PathVariable("id") Long id, @RequestParam(value = "pn", defaultValue = "1") Integer pn, // 重定向携带参数 RedirectAttributes redirectAttributes) { usersService.removeById(id); redirectAttributes.addAttribute("pn" , pn); return "redirect:/dynamic_table" ; }
5、整合Redis
导入依赖
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
自动导入的东西:
自动配置
RedisAutoConfiguration 自动配置类。RedisProperties 属性类 --> spring.redis.xxx 是对redis的配置
连接工厂是准备好的。Lettuce ConnectionConfiguration(默认 )、Jedis ConnectionConfiguration
自动注入了RedisTemplate <Object , Object >
自动注入了StringRedisTemplate;k:v都是String
底层只要我们使用 StringRedisTemplate、RedisTemplate就可以操作redis
导入Jedis
默认是Lettuce,导入后就有了两个,可以通过配置文件指定选择哪个!
1 2 3 4 5 <dependency > <groupId > redis.clients</groupId > <artifactId > jedis</artifactId > </dependency >
配置文件
1 2 3 4 5 6 7 8 spring: redis: url: redis://82.156.11.189:6379 client-type: jedis jedis: pool: max-idle: 10 max-active: 10
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Autowired StringRedisTemplate stringRedisTemplate;@Autowired RedisConnectionFactory redisConnectionFactory;@Test void testRedis () { ValueOperations<String, String> operations = stringRedisTemplate.opsForValue(); operations.set("hello" , "world" ); String hello = operations.get("hello" ); System.out.println(hello); System.out.println(redisConnectionFactory.getClass()); }
计数器案例
添加拦截器拦截url地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Component public class RedisUrlCountInterceptor implements HandlerInterceptor { @Autowired StringRedisTemplate redisTemplate; @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestURL = request.getRequestURI(); redisTemplate.opsForValue().increment(requestURL); return true ; } }
将拦截器加入容器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Configuration public class AdminConfig implements WebMvcConfigurer { @Autowired RedisUrlCountInterceptor redisUrlCountInterceptor; @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**" ) .excludePathPatterns("/" , "/login" , "/css/**" , "/js/**" , "/fonts/**" , "/images/**" , "/aa/**" ); registry.addInterceptor(redisUrlCountInterceptor) .addPathPatterns("/**" ) .excludePathPatterns("/" , "/login" , "/css/**" , "/js/**" , "/fonts/**" , "/images/**" , "/aa/**" ); } }
Controller保存数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Autowired StringRedisTemplate redisTemplate;@GetMapping("/index.html") public String indexPage (HttpSession session, Model model) { ValueOperations<String, String> operations = redisTemplate.opsForValue(); String s = operations.get("/index.html" ); String s1 = operations.get("/query" ); model.addAttribute("indexCount" , s); model.addAttribute("queryCount" , s1); return "index" ; }
八、单元测试
1、JUnit5 的变化
Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库
作为最新版本的JUnit框架,JUnit5与之前版本的Junit框架有很大的不同。由三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform : Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入 。
JUnit Jupiter : JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部 包含了一个测试引擎 ,用于在Junit Platform上运行。
JUnit Vintage : 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎。
注意:
SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能 @Test)
1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > org.junit.vintage</groupId > <artifactId > junit-vintage-engine</artifactId > <scope > test</scope > <exclusions > <exclusion > <groupId > org.hamcrest</groupId > <artifactId > hamcrest-core</artifactId > </exclusion > </exclusions > </dependency >
依赖导入,默认已经导入:
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency >
以前:
@SpringBootTest + @RunWith(SpringTest.class)
SpringBoot整合Junit以后:
编写测试方法:@Test标注(注意需要使用junit5版本的注解)
Junit类具有Spring的功能,@Autowired、比如 @Transactional 标注测试方法,测试完成后自动回滚
2、JUnit5常用注解
JUnit5的注解与JUnit4的注解有所变化:https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations
**@Test :**表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
**@ParameterizedTest :**表示方法是参数化测试
**@RepeatedTest :**表示方法可重复执行
**@DisplayName :**为测试类或者测试方法设置展示名称
**@BeforeEach :**表示在每个单元测试之前执行
**@AfterEach :**表示在每个单元测试之后执行
**@BeforeAll :**表示在所有单元测试之前执行
**@AfterAll :**表示在所有单元测试之后执行
**@Tag :**表示单元测试类别,类似于JUnit4中的@Categories
**@Disabled :**表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
**@Timeout :**表示测试方法运行如果超过了指定时间将会返回错误
**@ExtendWith :**为测试类或测试方法提供扩展类引用
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 @DisplayName("Junit5功能测试!") @SpringBootTest public class Junit5Test { @DisplayName("测试DisplayName 注解!") @Test void testDisplayName () { System.out.println("==========1==========" ); } @Disabled @DisplayName("测试2!") @Test void testDisplayName2 () { System.out.println("=========2===========" ); } @BeforeEach void testBeforeEach () { System.out.println("============beforeEach=======" ); } @AfterEach void testAfterEach () { System.out.println("============afterEach============" ); } @BeforeAll static void testBeforeAll () { System.out.println("============BeforeAll ==============" ); } @AfterAll static void testAfterAll () { System.out.println("============afterAll ==============" ); } @Timeout(value = 500, unit = TimeUnit.MILLISECONDS) @Test void testTimeout () throws InterruptedException { Thread.sleep(500 ); } @RepeatedTest(5) void testRepeat () { System.out.println("hhhhhhhhh" ); } @ParameterizedTest() @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" }) void palindromes (String candidate) { System.out.println(candidate); } }
3、断言(assertions)
断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法 。
检查业务逻辑返回的数据是否合理。
所有的测试运行结束以后,会有一个详细的测试报告;
简单断言
用来对单个值进行简单的验证。
方法
说明
assertEquals
判断两个对象或两个原始类型是否相等
assertNotEquals
判断两个对象或两个原始类型是否不相等
assertSame
判断两个对象引用是否指向同一个对象
assertNotSame
判断两个对象引用是否指向不同的对象
assertTrue
判断给定的布尔值是否为 true
assertFalse
判断给定的布尔值是否为 false
assertNull
判断给定的对象引用是否为 null
assertNotNull
判断给定的对象引用是否不为 null
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @DisplayName("断言测试!") public class AssertTest { int cal (int a, int b) { return a + b; } @DisplayName("简单断言测试!") @Test void testSimple () { int c = cal(2 , 3 ); assertEquals(5 , c, "计算失败!" ); Object o1 = new Object(); Object o2 = new Object(); assertSame(o1, o2, "对象不一致!" ); } }
数组断言
通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等!
1 2 3 4 5 @DisplayName("数组断言测试!") @Test void testArray () { assertArrayEquals(new int [] {1 , 2 }, new int [] {1 , 2 }); }
组合断言
assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言!
1 2 3 4 5 6 7 8 9 @Test @DisplayName("assert all") public void all () { assertAll("Math" , () -> assertEquals(2 , 1 + 1 , "失败" ), () -> assertTrue(1 > 0 , "失败" ) ); }
异常断言
在JUnit4时期,想要测试方法的异常情况时,需要用**@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式 Assertions.assertThrows()** ,配合函数式编程就可以进行使用。
1 2 3 4 5 6 7 8 @Test @DisplayName("异常测试") public void exceptionTest () { ArithmeticException exception = Assertions.assertThrows( ArithmeticException.class, () -> System.out.println(1 % 0 )); }
超时断言
Junit5还提供了Assertions.assertTimeout() 为测试方法设置了超时时间!
1 2 3 4 5 6 @Test @DisplayName("超时测试") public void timeoutTest () { Assertions.assertTimeout(Duration.ofMillis(1000 ), () -> Thread.sleep(500 )); }
快速失败
通过 fail 方法直接使得测试失败!
1 2 3 4 5 @Test @DisplayName("fail") public void shouldFail () { fail("This should fail" ); }
4、前置条件(assumptions)
JUnit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。
assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。
assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。
assume 出现错误就跳过了,进行maven clean+test时显示的是ignore而不是error,
assert错误就是错误!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private final String environment = "DEV" ;@Test @DisplayName("simple") public void simpleAssume () { assumeTrue(Objects.equals(this .environment, "jjj" )); assumeFalse(() -> Objects.equals(this .environment, "PROD" )); }@Test @DisplayName("assume then do") public void assumeThenDo () { assumingThat( Objects.equals(this .environment, "DEV" ), () -> System.out.println("In DEV" ) ); }
5、嵌套测试
JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 @DisplayName("嵌套测试!") public 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()); } } } }
6、参数化测试
参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。
利用**@ValueSource**等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
@ValueSource : 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
@NullSource : 表示为参数化测试提供一个null的入参
@EnumSource : 表示为参数化测试提供一个枚举入参
@CsvFileSource :表示读取指定CSV文件内容作为参数化测试入参
@MethodSource :表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。
如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider 接口,任何外部文件都可以作为它的入参。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @ParameterizedTest @ValueSource(strings = {"one", "two", "three"}) @DisplayName("参数化测试1") public void parameterizedTest1 (String string) { System.out.println(string); Assertions.assertTrue(StringUtils.isNotBlank(string)); }@ParameterizedTest @MethodSource("method") @DisplayName("方法来源参数") public void testWithExplicitLocalMethodSource (String name) { System.out.println(name); Assertions.assertNotNull(name); }static Stream<String> method () { return Stream.of("apple" , "banana" ); }
7、迁移指南
Junit4迁移到Junit5:
注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中。
把@Before 和@After 替换成@BeforeEach 和@AfterEach。
把@BeforeClass 和@AfterClass 替换成@BeforeAll 和@AfterAll。
把@Ignore 替换成@Disabled。
把@Category 替换成@Tag。
把@RunWith、@Rule 和@ClassRule 替换成@ExtendWith。
九、指标监控
1、SpringBoot Actuator
未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
导入依赖
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency >
自动导入的东西:
版本差异
配置文件
HTTP:默认只暴露health Endpoint,即web方式暴露!
JMX :默认暴露所有Endpoint,即电脑cmd输入jconsole即可打开一个JDK自带的控制台,可以监控所有信息!
1 2 3 4 5 6 management: endpoints: enabled-by-default: true web: exposure: include: '*'
访问测试
访问地址:http://localhost:8080/actuator/**
返回的结果都是JSON格式!推荐使用浏览器插件格式化JSON数据!
暴露所有监控信息为HTTP!
2、Actuator Endpoint
最常使用的端点
ID
描述
auditevents
暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件
。
beans
显示应用程序中所有Spring Bean的完整列表。
caches
暴露可用的缓存。
conditions
显示自动配置的所有条件信息,包括匹配或不匹配的原因。
configprops
显示所有@ConfigurationProperties
。
env
暴露Spring的属性ConfigurableEnvironment
flyway
显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway
组件。
health
显示应用程序运行状况信息。
httptrace
显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository
组件。
info
显示应用程序信息。
integrationgraph
显示Spring integrationgraph
。需要依赖spring-integration-core
。
loggers
显示和修改应用程序中日志的配置。
liquibase
显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase
组件。
metrics
显示当前应用程序的“指标”信息。
mappings
显示所有@RequestMapping
路径列表。
scheduledtasks
显示应用程序中的计划任务。
sessions
允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于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
。
最常用的Endpoint:
Health:健康状况
Metrics:运行时指标
Loggers:日志记录
Health Endpoint
健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。
重要的几点:全部健康才健康!
设置某个endpoint,开启详细信息:
1 2 3 4 5 6 7 8 9 management: endpoints: enabled-by-default: true web: exposure: include: '*' endpoint: health: show-details: always
Metrics Endpoint
提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到;
访问查看:http://localhost:8080/actuator/metrics/jvm.gc.pause,后面跟上下面的项即可!
管理Endpoints
开启与禁用Endpoints:
默认所有的Endpoint除过shutdown都是开启的。
需要开启或者禁用某个Endpoint。配置模式为 management.endpoint.endpointName.enabled = true
jmx和http方式都会受到总开关的控制!
1 2 3 management: endpoints: enabled-by-default: true
或者禁用所有的Endpoint然后手动开启指定的Endpoint
1 2 3 4 5 6 7 8 management: endpoints: enabled-by-default: false endpoint: beans: enabled: true health: enabled: true
3、定制 Endpoint
定制 Health 信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Component public class MyHealthIndicator extends AbstractHealthIndicator { @Override protected void doHealthCheck (Health.Builder builder) throws Exception { HashMap<String, Object> map = new HashMap<>(); if (true ){ builder.status(Status.UP); map.put("count" , 5 ); map.put("ms" , 100 ); }else { builder.status(Status.DOWN); map.put("error" , "连接超时" ); map.put("ms" , 3000 ); } builder.withDetail("code" , 110 ).withDetails(map); } }
定制info信息
编写配置文件
1 2 3 4 5 6 info: appName: boot-admin version: 2.0 .1 mavenProjectName: @project.artifactId@ mavenProjectVersion: @project.version@
编写InfoContributor
1 2 3 4 5 6 7 8 9 10 11 @Component public class AppInfoContributor implements InfoContributor { @Override public void contribute (Info.Builder builder) { builder.withDetail("msg" ,"你好" ) .withDetail("hello" , "哈哈哈" ) .withDetails(Collections.singletonMap("world" , 666 )); } }
测试 :http://localhost:8080/actuator/info
定制Metrics信息
SpringBoot默认支持
JVM metrics, report utilization of:
Various memory and buffer pools
Statistics related to garbage collection
Threads utilization
Number of classes loaded/unloaded
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
增加定制Metrics
调用一次saveCity方法,设置的指标值myservice.method.running.counter
就会加一!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Service public class CityService { Counter counter; public CityService (MeterRegistry meterRegistry) { counter = meterRegistry.counter("myservice.method.running.counter" ); } public void saveCity (City city) { counter.increment(); cityMapper.insert(city); } }
定制Endpoint
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Component @Endpoint(id = "myService") public class MyServiceEndPoint { @ReadOperation public Map getDockerInfo () { return Collections.singletonMap("info" ,"docker started..." ); } @WriteOperation private void restartDocker () { System.out.println("docker restarted...." ); } }
4、微服务可视化监控
官方地址:https://github.com/codecentric/spring-boot-admin
快速开启:https://codecentric.github.io/spring-boot-admin/2.5.1/#getting-started
准备 Admin Server
新建一个module,用来配置admin server!
导入依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <dependencies > <dependency > <groupId > de.codecentric</groupId > <artifactId > spring-boot-admin-starter-server</artifactId > <version > 2.5.1</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > </dependencies >
主配置类开启AdminServer:
1 2 3 4 5 6 7 8 9 @EnableAdminServer @SpringBootApplication public class HelloWorld5Application { public static void main (String[] args) { SpringApplication.run(HelloWorld5Application.class, args); } }
指定一个不同端口:
启动即可!
项目中引用AdminServer
引入监控依赖:
1 2 3 4 5 6 7 8 <dependency > <groupId > de.codecentric</groupId > <artifactId > spring-boot-admin-starter-client</artifactId > <version > 2.5.1</version > </dependency >
配置文件配置监控地址:
1 2 3 4 5 6 spring.boot.admin.client.url =http://localhost:8889 spring.boot.admin.client.instance.prefer-ip =true spring.application.name =HelloWorld4 management.endpoints.web.exposure.include =*
十、原理解析
1、profile功能
为了方便多环境适配,springboot简化了profile功能!
application-profile功能
application.properties:
1 2 person.name =张三111 person.age =2222
application-prod.yml:
application-test.yml:
1 2 3 person: name: test-李四 age: 45
测试: 返回张三111
1 2 3 4 5 6 7 8 9 10 11 @RestController public class HelloController { @Value("${person.name:李四}") @GetMapping("/hello") public String hello () { return name; } }
可以在application.properties中激活指定环境:
1 2 spring.profiles.active =test
@Profile条件装配功能
可以标注在方法上也可以标注在类上,用来在指定环境下生效的条件装配功能!
二者都实现了Person接口,可以使用profile注解标注指定什么环境下有效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public interface Person { }@Profile({"prod", "default"}) @Data @Component @ConfigurationProperties(prefix = "person") public class Boss implements Person { private String name; private Integer age; }@Profile("test") @Data @Component @ConfigurationProperties(prefix = "person") public class Worker implements Person { private String name; private Integer age; }
application.properties文件来指定生效配置:
1 spring.profiles.active =test
测试:
1 2 3 4 5 6 7 8 9 10 11 @RestController public class HelloController { @Autowired private Person person; @GetMapping("/person") public String person () { return person.getClass().toString(); } }
profile分组
可以将各种配置文件都通过profile分组写进来,显示指明使用哪个组,一个组可以有多个配置文件,相当于分文件分类配置,清晰明了!
1 2 3 4 5 6 7 spring.profiles.active =myprod spring.profiles.group.myprod[0] =ppd spring.profiles.group.myprod[1] =prod spring.profiles.group.mytest[0] =test
2、外部化配置
https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config
外部配置源
Java属性文件
YAML文件
环境变量
命令行参数
value注解获取系统环境变量:
1 2 3 @Value("${CLASSPATH}") private String msg;
启动类获取系统环境变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @SpringBootApplication public class HelloWorld6Application { public static void main (String[] args) { ConfigurableApplicationContext run = SpringApplication.run(HelloWorld6Application.class, args); ConfigurableEnvironment environment = run.getEnvironment(); Map<String, Object> systemEnvironment = environment.getSystemEnvironment(); MutablePropertySources propertySources = environment.getPropertySources(); System.out.println(systemEnvironment); System.out.println(propertySources); } }
配置文件查找位置
classpath 根路径
classpath 根路径下config目录
jar包当前目录
jar包当前目录的config目录
jar包当前目录/config子目录的直接子目录
后面覆盖前面!
配置文件加载顺序
当前jar包内部的application.properties和application.yml
当前jar包内部的application-{profile}.properties 和 application-{profile}.yml
引用的外部jar包的application.properties和application.yml
引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml
后面覆盖前面!
指定环境优先,外部优先,后面的可以覆盖前面的同名配置项!
3、自定义starter
Starter结构图
引入starter — xxxAutoConfiguration — 容器中放入组件 ---- 绑定xxxProperties ---- 配置项
创建 Starter Maven Module
用来让人引用
引用autoconfigure包实现功能
全部完成后使用Maven的生命周期中的 clean + install 进行打包并安装到本地Maven仓库即可!
1 2 3 4 5 6 7 8 9 10 11 12 13 <groupId > com.itnxd</groupId > <artifactId > itnxd-hello-spring-boot-starter</artifactId > <version > 1.0-SNAPSHOT</version > <dependencies > <dependency > <groupId > com.itnxd</groupId > <artifactId > itnxd-hello-spring-boot-starter-autoconfigure</artifactId > <version > 0.0.1-SNAPSHOT</version > </dependency > </dependencies >
下方全部配置完成,使用Maven的生命周期中的 clean + install 进行打包并安装到本地Maven仓库即可!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <groupId > com.itnxd</groupId > <artifactId > itnxd-hello-spring-boot-starter-autoconfigure</artifactId > <version > 0.0.1-SNAPSHOT</version > <name > itnxd-hello-spring-boot-starter-autoconfigure</name > <description > itnxd-hello-spring-boot-starter-autoconfigure</description > <properties > <java.version > 1.8</java.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > </dependency > </dependencies >
HelloService:
不要放在容器中,要使用自动配置类来控制!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class HelloService { @Autowired HelloServiceProperties helloServiceProperties; public String sayHello (String name) { return helloServiceProperties.getPrefix() + name + helloServiceProperties.getSuffix(); } }
HelloServiceProperties:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @ConfigurationProperties(prefix = "itnxd.hello") public class HelloServiceProperties { private String prefix; private String suffix; public String getPrefix () { return prefix; } public void setPrefix (String prefix) { this .prefix = prefix; } public String getSuffix () { return suffix; } public void setSuffix (String suffix) { this .suffix = suffix; } }
HelloServiceAutoConfiguration:
自动配置类!我们自己的项目没有实现HelloService的话,自动配置类才会向容器中添加默认的HelloService!
1 2 3 4 5 6 7 8 9 10 11 @Configuration @EnableConfigurationProperties(HelloServiceProperties.class) public class HelloServiceAutoConfiguration { @ConditionalOnMissingBean(HelloService.class) @Bean public HelloService helloService () { return new HelloService(); } }
resources/META-INF/spring.factories:
上面配置的自动配置类默认不会生效,需要在spring.factories指定开启的自动配置类才行!
1 2 3 # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.itnxd.hello.auto.HelloServiceAutoConfiguration
创建测试 Module
导入自定义的依赖:
1 2 3 4 5 <dependency > <groupId > com.itnxd</groupId > <artifactId > itnxd-hello-spring-boot-starter</artifactId > <version > 1.0-SNAPSHOT</version > </dependency >
自定义的配置前缀:
1 2 itnxd.hello.prefix =hh itnxd.hello.suffix =zz
Controller测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 @RestController public class HelloController { @Autowired HelloService helloService; @GetMapping("/hello") public String hello () { String h = helloService.sayHello("张三" ); return h; } }
若我们自己实现了HelloService,自动配置类会发现,则不会放入默认的HelloService:
1 2 3 4 5 6 7 8 9 10 11 @Configuration public class MyConfig { @Bean public HelloService helloService () { HelloService helloService = new HelloService(); return helloService; } }
4、SpringBoot原理
SpringBoot启动过程
创建 SpringApplication
保存一些信息。
判定当前应用的类型。ClassUtils。Servlet
bootstrappers:初始启动引导器 (List<Bootstrapper>
):去spring.factories 文件中找 org.springframework.boot.Bootstrapper
找 ApplicationContextInitializer ;去spring.factories 找 ApplicationContextInitializer
找 ApplicationListener ;应用监听器。去spring.factories 找 ApplicationListener
运行 SpringApplication
StopWatch
记录应用的启动时间
**创建引导上下文(Context环境)**createBootstrapContext()
获取到所有之前的 bootstrappers 挨个执行 intitialize() 来完成对引导启动器上下文环境设置
让当前应用进入headless模式 。java.awt.headless
获取所有 RunListener(运行监听器) 【为了方便所有Listener进行事件感知】
getSpringFactoriesInstances 去spring.factories 找 SpringApplicationRunListener.
遍历 SpringApplicationRunListener 调用 starting 方法
相当于通知所有感兴趣系统正在启动过程的人,项目正在 starting。
保存命令行参数;ApplicationArguments
准备环境 prepareEnvironment();
返回或者创建基础环境信息对象 。StandardServletEnvironment
配置 环境信息对象。读取所有的配置源的配置属性值。
绑定环境信息
监听器 调用 listener.environmentPrepared();通知所有的监听器当前环境准备完成
创建IOC容器 (createApplicationContext())
根据项目类型(Servlet)创建容器,
当前会创建 AnnotationConfigServletWebServerApplicationContext
准备ApplicationContext IOC容器的基本信息 prepareContext()
保存环境信息
IOC容器的后置处理流程。
应用初始化器 ;applyInitializers;
遍历所有的 ApplicationContextInitializer 。调用 initialize。来对ioc容器进行初始化扩展功能
遍历所有的 listener 调用 contextPrepared 。EventPublishRunListenr;通知所有的监听器contextPrepared
所有的监听器调用 contextLoaded。通知所有的监听器 contextLoaded;
刷新IOC容器 。refreshContext。创建容器中的所有组件(Spring注解)
容器刷新完成后工作 afterRefresh
所有监听器调用 listeners.started(context); 通知所有的监听器 started
调用所有runners ;callRunners()
获取容器中的 ApplicationRunner
获取容器中的 CommandLineRunner
合并所有runner并且按照@Order进行排序
遍历所有的runner。调用 run 方法
如果以上有异常,调用Listener 的 failed
调用所有监听器的 running 方法 listeners.running(context); 通知所有的监听器 running
running如果有问题。继续通知 failed 。调用所有 Listener 的 failed;通知所有的监听器 failed
自定义五大组件
ApplicationContextInitializer:
1 2 3 4 5 6 7 8 public class MyApplicationContextInitializer implements ApplicationContextInitializer { @Override public void initialize (ConfigurableApplicationContext applicationContext) { System.out.println("MyApplicationContextInitializer的initialize方法" ); } }
SpringApplicationRunListener:
应用启动到终止的各个状态!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class MySpringApplicationRunListener implements SpringApplicationRunListener { private SpringApplication springApplication; public MySpringApplicationRunListener (SpringApplication application, String[] args) { this .springApplication = application; } @Override public void starting (ConfigurableBootstrapContext bootstrapContext) { System.out.println("MySpringApplicationRunListener---------starting" ); } @Override public void environmentPrepared (ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) { System.out.println("MySpringApplicationRunListener---------environmentPrepared" ); } @Override public void contextPrepared (ConfigurableApplicationContext context) { System.out.println("MySpringApplicationRunListener---------contextPrepared" ); } @Override public void contextLoaded (ConfigurableApplicationContext context) { System.out.println("MySpringApplicationRunListener---------contextLoaded" ); } @Override public void started (ConfigurableApplicationContext context) { System.out.println("MySpringApplicationRunListener---------started" ); } @Override public void running (ConfigurableApplicationContext context) { System.out.println("MySpringApplicationRunListener---------running" ); } @Override public void failed (ConfigurableApplicationContext context, Throwable exception) { System.out.println("MySpringApplicationRunListener---------failed" ); } }
ApplicationListener:
1 2 3 4 5 6 7 public class MyApplicationListener implements ApplicationListener { @Override public void onApplicationEvent (ApplicationEvent event) { System.out.println("MyApplicationListener的onApplicationEvent" ); } }
ApplicationRunner:
1 2 3 4 5 6 7 8 @Order(1) @Component public class MyApplicationRunner implements ApplicationRunner { @Override public void run (ApplicationArguments args) throws Exception { System.out.println("MyApplicationRunner===============run" ); } }
CommandLineRunner:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Order(2) @Component public class MyCommandLineRunner implements CommandLineRunner { @Override public void run (String... args) throws Exception { System.out.println("MyCommandLineRunner===============run" ); } }
前三大组件需要在spring.factories指定:
1 2 3 4 5 6 7 8 9 10 11 # Initializers org.springframework.context.ApplicationContextInitializer=\ com.itnxd.boot.listener.MyApplicationContextInitializer # Application Listeners org.springframework.context.ApplicationListener=\ com.itnxd.boot.listener.MyApplicationListener # Application RunListener org.springframework.boot.SpringApplicationRunListener=\ com.itnxd.boot.listener.MySpringApplicationRunListener
效果: