# springboot **Repository Path**: OHUHO/springboot ## Basic Information - **Project Name**: springboot - **Description**: SpringBoot2 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2022-10-13 - **Last Updated**: 2022-12-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SpringBoot # 一、SpringBoot2核心技术 — 基础入门 ## 1、Spring与SpringBoot ### 1.1、Spring能做什么 #### 1.1.1、Spring的能力 | ![image-20221013195418921](https://cdn.jsdelivr.net/gh/a-jingchao/picture-bed/BlogImages/202210131954113.png) | | :----------------------------------------------------------: | #### 1.1.2、Spring的生态 https://spring.io/projects/spring-boot - web开发 - 数据访问 - 安全控制 - 分布式 - 消息服务 - 移动开发 - 批处理 #### 1.1.3、Spring5重大升级 ##### ① 响应式编程 ##### ② 内部源码设计 基于Java8的一些新特性,如:接口默认实现。重新设计源码架构。 ### 1.2、为什么使用SpringBoot 能快速创建出生产级别的Spring应用 #### 1.2.1、SpringBoot的优点 - 创建独立Spring应用 - 内嵌web服务器 - 自动starter依赖,简化构建配置 - 自动装配Spring以及第三方功能 - 提供生产级别的监控、健康检查及外部化配置 - 无代码生产,无需编写XML > SpringBoot是整合Spring技术栈的一站式框架 > > SpringBoot是简化Spring技术栈的快速开发脚手架 #### 1.2.2、SpringBoot缺点 - 迭代快,需要时刻关注变化 - 封装太深,内部原理复杂,不容易精通 ### 1.3、时代背景 #### 1.3.1、微服务 - 微服务是一种架构风格 - 一个应用拆分为一组小型服务 - 每个服务运行在自己的进程内,也就是可以独立部署和升级 - 服务之间使用轻量级HTTP交互 - 服务围绕业务功能拆分 - 可以由全自动部署机制独立部署 - 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术 #### 1.3.2、分布式 **分布式的困难** - 远程调用 - 服务发现 - 负载均衡 - 服务容错 - 配置管理 - 服务监控 - 链路追踪 - 日志管理 - 任务调度 - …… **分布式的解决** - SpringBoot + SpringCloud | ![image-20221013202100111](https://cdn.jsdelivr.net/gh/a-jingchao/picture-bed/BlogImages/202210132021163.png) | | :----------------------------------------------------------: | #### 1.3.3、云原生 原生应用如何上云。Cloud Native **上云的困难** - 服务自愈 - 弹性伸缩 - 服务隔离 - 自动化部署 - 灰度发布 - 流量治理 - …… **上云的解决** ### 1.4、如何学习SpringBoot [**官方文档**](https://spring.io/projects/spring-boot#learn) [**查看新版本特性**](https://github.com/spring-projects/spring-boot/wiki#release-notes) ## 2、SpringBoot2入门 ### 2.1、系统要求 - Java 8 + - Maven 3.5 + - IDEA 2019.1.2 ### 2.2、HelloWorld #### 2.2.1、创建Maven工程 #### 2.2.2、引入依赖 ```xml org.springframework.boot spring-boot-starter-parent 2.7.4 org.springframework.boot spring-boot-starter-web ``` #### 2.2.3、创建主程序 ```java @SpringBootApplication public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); } } ``` #### 2.2.4、编写业务 ```java @RestController public class HelloController { @RequestMapping("/hello") public String handle01(){ return "Hello, SpringBoot!"; } } ``` #### 2.2.5、测试 直接运行main方法 #### 2.2.6、简化配置 **application.properties** ```properties server.port=8888 ``` #### 2.2.7、简化部署 ```xml org.springframework.boot spring-boot-maven-plugin ``` 项目打包成 jar 包,直接在目标服务器执行即可。 ## 3、了解自动配置原理 ### 3.1、SpringBoot特点 #### 3.1.1、依赖管理 - 父项目做依赖管理 ```xml 依赖管理 org.springframework.boot spring-boot-starter-parent 2.7.4 他的父项目 org.springframework.boot spring-boot-dependencies 2.7.4 几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制 ``` - 开发导入starter场景启动器 > 1. 见到很多 spring-boot-starter-* : *就某种场景 > 2. 只要引入starter,这个场景的所有常规需要的依赖我们都自动引入 > 3. SpringBoot所有支持的场景 > https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter > 4. 见到的 *-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器。 > 5. 所有场景启动器最底层的依赖 ```xml org.springframework.boot spring-boot-starter 2.7.4 compile ``` - 无需关注版本号,自动版本号仲裁 > 1. 引入依赖默认都可以不写版本 > 2. 引入非版本仲裁的jar,要写版本号 - 可以修改默认版本号 > 1. 查看spring-boot-dependencies里面规定当前依赖的版本 用的 key。 > > 2. 在当前 项目里面重写配置 > > ```xml > > 5.1.43 > > ``` #### 3.1.2、自动配置 - 自动配置Tomcat - 引入Tomcat依赖 - 配置Tomcat ```xml org.springframework.boot spring-boot-starter-tomcat 2.7.4 compile ``` - 自动配置好SpringMVC - 引入SpringMVC全套组件 - 自动配置好SpringMVC常用组件 - 自动配置好Web常见功能,如:字符编码问题 - SpringBoot帮助我们配置好了所有Web开发常见场景 - 默认的包结构 - 主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来 - 无需以前的包扫描配置 - 想要改变扫描路径 @SpringBootApplication(scanBasePackages=**"com.jignchao"**) 或者 @ComponentScan指定扫描路径 > @SpringBootApplication 等同于 > @SpringBootConfiguration > @EnableAutoConfiguration > @ComponentScan("com.jingchao.boot") - 各种配置拥有默认值 - 默认配置最终都是映射到每个类上,如MultipartProperties - 配置文件的值最终会绑定到每个类上,这个类会在容器中创建对象 - 按需加载所有自动配置项 - 非常多的starter - 引入了那些场景这个场景的自动配置才会开启 - SpringBoot所有的自动配置功能都在 spring-boot-autoconfigure 包里面 - …… ### 3.2、容器功能 #### 3.2.1、组件添加 ##### ① @Configuration - 基本使用 - Full模式与Lite模式 - 示例 - 最佳实战 - 配置类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断 - 配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式 > 1. 配置类里面使用@Bean标注在方法上面给容器注册组件,默认也是单实例的 > 2. 配置类本身也是组件 > 3. proxyBeanMethods:代理bean的方法 > - Full(proxyBeanMethods = true)每个@Bean方法调用多少次返回的组件都是单实例的 > - Lite(proxyBeanMethods = false)每个@Bean方法被调用多少次返回的组件都是新创建的 > - 注意:组件依赖必须使用Full模式(默认),其他情况Lite模式 ```java @Configuration(proxyBeanMethods = false) public class MyConfig { @Bean public User user01(){ User zhangsan = new User("zhangsan", 18); // user 组件依赖 Pet 组件 zhangsan.setPet(tomcatPet()); return zhangsan; } @Bean("tom") public Pet tomcatPet(){ return new Pet("tomcat"); } } ``` ```java @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan("com.jingchao.boot") public class MainApplication { public static void main(String[] args) { // 返回我们的IOC容器 ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args); // 查看容器里面的组件 String[] names = run.getBeanDefinitionNames(); for (String name : names) { System.out.println(name); } // 从容器中获取组件 Pet tom01 = run.getBean("tom", Pet.class); Pet tom02 = run.getBean("tom", Pet.class); System.out.println(tom01 == tom02); // MyConfig bean = run.getBean(MyConfig.class); System.out.println(bean); User user01 = bean.user01(); User user02 = bean.user01(); System.out.println(user02 == user01); User user011 = run.getBean("user01", User.class); Pet tom = run.getBean("tom", Pet.class); System.out.println(user011.getPet() == tom); } } ``` ##### ② @Bean、@Component、@Controller、@Service、@Repository ##### ③ @ComponentScan、@Import > @Import 给容器中自动创建组件,默认组件名字是全类名 ```java @Import({User.class, DBHelper.class}) @Configuration(proxyBeanMethods = false) public class MyConfig(){ } ``` ##### ④ @Conditional 条件装配:满足Conditional指定的条件,则进行组件注入 eg:@ConditionalOnBean ```java @Configuration(proxyBeanMethods = false) public class MyConfig { @ConditionalOnBean(name = "tom") @Bean public User user01(){ User zhangsan = new User("zhangsan", 18); // user 组件依赖 Pet 组件 zhangsan.setPet(tomcatPet()); return zhangsan; } // @Bean("tom") public Pet tomcatPet(){ return new Pet("tomcat"); } } ``` ```java @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan("com.jingchao.boot") public class MainApplication { public static void main(String[] args) { // 返回我们的IOC容器 ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args); // 查看容器里面的组件 String[] names = run.getBeanDefinitionNames(); for (String name : names) { System.out.println(name); } boolean tom = run.containsBean("tom"); System.out.println("tom = " + tom); // false boolean user01 = run.containsBean("user01"); System.out.println("user01 = " + user01); // false } } ``` #### 3.2.2、原生配置文件引入 ##### ① @ImportResource ```xml ``` > 直接将 xml 中的组件迁移成注解的方式 ```java @ImportResource("classpath:bean.xml") public class MyConfig{} ``` ```java boolean j = run.containsBean("j"); System.out.println(j); //true boolean c = run.containsBean("c"); System.out.println(c); //true ``` #### 3.2.3、配置绑定 使用 Java 读取到properties文件中的内容,并且把它封装到 JavaBean中哦,以供随时使用 ```java public class getProperties { public static void main(String[] args) throws FileNotFoundException, IOException { Properties pps = new Properties(); pps.load(new FileInputStream("a.properties")); Enumeration enum1 = pps.propertyNames();//得到配置文件的名字 while(enum1.hasMoreElements()) { String strKey = (String) enum1.nextElement(); String strValue = pps.getProperty(strKey); System.out.println(strKey + "=" + strValue); //封装到JavaBean } } } ``` ##### ① @ConfigurationProperties ##### ② @EnableConfigurationProperties + @ConfigurationProperties ```java @EnableConfigurationProperties(Car.class) //1、开启Car配置绑定功能 //2、把这个Car这个组件自动注册到容器中 public class MyConfig { } ``` ```java @ConfigurationProperties(prefix = "mycar") public class Car { ...... } ``` ##### ③ @Component + @ConfigurationProperties ```java @Component @ConfigurationProperties(prefix = "mycar") public class Car { private String brand; private Integer price; public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } public Integer getPrice() { return price; } public void setPrice(Integer price) { this.price = price; } @Override public String toString() { return "Car{" + "brand='" + brand + '\'' + ", price=" + price + '}'; } } ``` ### 3.3、自动配置原理入门 ```java @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan( excludeFilters = {@Filter( type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class} ), @Filter( type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class} )} ) public @interface SpringBootApplication { } ``` #### 3.3.1、引导加载自动配置类 ##### ① @SpringBootConfiguration > @Configuration:代表当前是一个配置类 ##### ② @ComponentScan > 指定扫描那些组件 ##### ③ @EnableConfiguration ```java @AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration { } ``` **@AutoConfigurationPackage** > 自动配置包:指定默认的包规则 ```java @Import({AutoConfigurationPackages.Registrar.class}) public @interface AutoConfigurationPackage { } /** * 利用Registrar给容器中导入一系列组件 * 将MainApplication所在包下的所有组件导入进来, */ ``` **@Import({AutoConfigurationImportSelector.class})** > 1. 利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件 > 2. 调用List configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类 > 3. 利用工厂加载 Map> loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件 > 4. 从META-INF/spring.factories位置来加载一个文件。 > 默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件 > spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories #### 3.3.2、按需开启自动配置项 > 虽然我们127个场景的所有自动配置启动的时候默认全部加载。xxxxAutoConfiguration > 按照条件装配规则(@Conditional),最终会按需配置。 #### 3.3.3、修改默认配置 ```java @Bean @ConditionalOnBean(MultipartResolver.class) //容器中有这个类型组件 @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) //容器中没有这个名字 multipartResolver 的组件 public MultipartResolver multipartResolver(MultipartResolver resolver) { // 给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找。 // SpringMVC multipartResolver。防止有些用户配置的文件上传解析器不符合规范 // Detect if the user has created a MultipartResolver but named it incorrectly return resolver; } // 给容器中加入了文件上传解析器; ``` SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先 ```java @Bean @ConditionalOnMissingBean public CharacterEncodingFilter characterEncodingFilter() { } ``` **总结:** - SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration - 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定 - 生效的配置类就会给容器中装配很多组件 - 只要容器中有这些组件,相当于这些功能就有了 - 定制化配置 - - 用户直接自己@Bean替换底层的组件 - 用户去看这个组件是获取的配置文件什么值就去修改。 **xxxxxAutoConfiguration --> 组件 --> xxxxProperties里面拿值 --> application.properties** #### 3.3.4、最佳实战 - 引入场景依赖 - https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter - 查看自动配置了哪些(选做) - 自己分析,引入场景对应的自动配置一般都生效了 - 配置文件中 debug=true 开启自动配置报告。Negative(不生效)\Positive(生效) - 是否需要修改 - 参照文档修改配置项 - https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#common-application-properties - 自己分析。xxxxProperties绑定了配置文件的哪些。 - 自定义加入或者替换组件 - @Bean、@Component、…… - 自定义器 **XXXXXCustomizer**; - ...... ## 4、开发小技巧 ### 4.1、Lombok ```xml org.projectlombok lombok ``` > **简化JavaBean开发** ### 4.2、dev-tools ```xml org.springframework.boot spring-boot-devtools true ``` > **项目或者页面修改以后:Ctrl+F9** ### 4.3、Spring Initailizr(项目初始化向导) …… # 二、SpringBoot2核心技术 — 核心功能 | ![image-20221015101549003](https://cdn.jsdelivr.net/gh/a-jingchao/picture-bed/BlogImages/202210151015066.png) | | :----------------------------------------------------------: | ## 1、配置文件 ### 1.1、文件类型 #### 1.1.1、properties …… #### 1.1.2、yaml ##### ① 简介 YAML 是 "YAML Ain't Markup Language"(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)。 非常适合用做以数据为中心的配置文件 ##### ② 基本语法 - key: value;kv之间有空格 - 大小写敏感 - 使用缩进表示层级关系 - 缩进不允许使用tab,只允许空格 - 缩进的空格数不重要,只要相同层级的元素左对齐即可 - '#'表示注释 - 字符串无需加引号,如果要加,''与""表示字符串内容 会被 转义/不转义 ##### ③ 数据类型 - 字面量:单个的、不可再分的值。date、boolean、string、number、null ```yaml k:v ``` - 对象:键值对的集合。map、hash、set、object ```yaml 行内写法: k: {k1:v1,k2:v2,k3:v3} #或 k: k1: v1 k2: v2 k3: v3 ``` - 数组:一组按次序排列的值。array、list、queue ```yaml 行内写法: k: [v1,v2,v3] #或者 k: - v1 - v2 - v3 ``` ##### ④ 示例 ```java @Data @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 animal; private Map score; private Set salary; private Map> allPets; } ``` ```java @Data public class Pet { private String name; private Double weight; } ``` ```yaml person: userName: zhangsan boss: false birth: 2019/12/12 20:12:33 age: 18 pet: name: tomcat weight: 23.4 # interests: [唱,条,rap,篮球] interests: - 唱 - 跳 - rap - 篮球 animal: - jerry - mario score: english: first: 30 second: 40 third: 50 math: [131,140,148] chinese: {first: 128,second: 136} salary: [3999,4999.98,5999.99] allPets: sick: - {name: tom} - {name: jerry,weight: 47} health: [{name: mario,weight: 47}] ``` ### 1.2、配置提示 ```xml org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-maven-plugin org.springframework.boot spring-boot-configuration-processor ``` ## 2、Web开发 | ![image-20221015101907540](https://cdn.jsdelivr.net/gh/a-jingchao/picture-bed/BlogImages/202210151019611.png) | | :----------------------------------------------------------: | ### 2.1、SpringMVC自动化配置概述 Spring Boot provides auto-configuration for Spring MVC that **works well with most applications.(大多场景我们都无需自定义配置)** The auto-configuration adds the following features on top of Spring’s defaults: - Inclusion of `ContentNegotiatingViewResolver` and `BeanNameViewResolver` beans. - - 内容协商视图解析器和BeanName视图解析器 - Support for serving static resources, including support for WebJars (covered [later in this document](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-spring-mvc-static-content))). - - 静态资源(包括webjars) - Automatic registration of `Converter`, `GenericConverter`, and `Formatter` beans. - - 自动注册 `Converter,GenericConverter,Formatter ` - Support for `HttpMessageConverters` (covered [later in this document](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-spring-mvc-message-converters)). - - 支持 `HttpMessageConverters` (后来我们配合内容协商理解原理) - Automatic registration of `MessageCodesResolver` (covered [later in this document](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-spring-message-codes)). - - 自动注册 `MessageCodesResolver` (国际化用) - Static `index.html` support. - - 静态index.html 页支持 - Custom `Favicon` support (covered [later in this document](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-spring-mvc-favicon)). - - 自定义 `Favicon` - Automatic use of a `ConfigurableWebBindingInitializer` bean (covered [later in this document](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-spring-mvc-web-binding-initializer)). - - 自动使用 `ConfigurableWebBindingInitializer` ,(DataBinder负责将请求数据绑定到JavaBean上) > If you want to keep those Spring Boot MVC customizations and make more [MVC customizations](https://docs.spring.io/spring/docs/5.2.9.RELEASE/spring-framework-reference/web.html#mvc) (interceptors, formatters, view controllers, and other features), you can add your own `@Configuration` class of type `WebMvcConfigurer` but **without** `@EnableWebMvc`. **不用@EnableWebMvc注解。使用 @Configuration + WebMvcConfigurer 自定义规则** > If you want to provide custom instances of `RequestMappingHandlerMapping`, `RequestMappingHandlerAdapter`, or `ExceptionHandlerExceptionResolver`, and still keep the Spring Boot MVC customizations, you can declare a bean of type `WebMvcRegistrations` and use it to provide custom instances of those components. **声明 WebMvcRegistrations 改变默认底层组件** > If you want to take complete control of Spring MVC, you can add your own `@Configuration` annotated with `@EnableWebMvc`, or alternatively add your own `@Configuration`-annotated `DelegatingWebMvcConfiguration` as described in the Javadoc of `@EnableWebMvc`. **使用 @EnableWebMvc + @Configuration + DelegatingWebMvcConfiguration 全面接管SpringMVC** ### 2.2、简单功能分析 #### 2.2.1、静态资源访问 ##### ① 静态资源目录 静态资源目录:`/static` (or `/public` or `/resources` or `/META-INF/resources`) **访问:当前项目的根路径/ + 静态资源名** 原理:静态映射 /** 请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面 修改默认的静态资源路径 ```yaml spring: mvc: static-path-pattern: /resources/** web: resources: static-locations: [classpath:/haha/] ``` ##### ② 静态资源访问前缀 ```yaml spring: mvc: static-path-pattern: "/resources/**" ``` **访问:当前项目 + static-path-pattern + 静态资源名** ##### ③ webjar 自动映射 `/webjars/**` https://www.webjars.org/ ```xml org.webjars jquery 3.5.1 ``` 访问地址:[http://localhost:8080/webjars/**jquery/3.5.1/jquery.js**](http://localhost:8080/webjars/jquery/3.5.1/jquery.js) 后面地址要按照依赖里面的包路径 #### 2.2.2、欢迎页支持 - 静态资源路径下 index.html - 可以配置静态资源路径 - 但是不能配置静态资源的访问前缀。否则导致 index.html 页面不能被默认访问 ```yaml spring: # mvc: # static-path-pattern: /resources/** web: resources: static-locations: [classpath:/haha/] ``` - controller 处理 /index #### 2.2.3、自定义Favicon favicon.ico放在静态资源目录下 #### 2.2.4、静态资源配置原理 - SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类) - SpringMVC功能的自动配置类 WebMvcAutoConfiguration,生效 ```java @AutoConfiguration( after = {DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class} ) @ConditionalOnWebApplication( type = Type.SERVLET ) @ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class}) @ConditionalOnMissingBean({WebMvcConfigurationSupport.class}) @AutoConfigureOrder(-2147483638) public class WebMvcAutoConfiguration { } ``` - 给容器中配置了什么 ```java @Configuration( proxyBeanMethods = false ) @Import({EnableWebMvcConfiguration.class}) @EnableConfigurationProperties({WebMvcProperties.class, WebProperties.class}) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware { } ``` - 配置文件的相关属性和xxx进行绑定 WebMvcProperties==**spring.mvc** WebProperties==**spring.web** ##### ① 资源处理的默认规则 ```java public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled"); } else { this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/"); this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> { registration.addResourceLocations(this.resourceProperties.getStaticLocations()); if (this.servletContext != null) { ServletContextResource resource = new ServletContextResource(this.servletContext, "/"); registration.addResourceLocations(new Resource[]{resource}); } }); } } ``` ```yaml spring: web: resources: add-mappings: false # 禁用所有静态资源 ``` ```java @ConfigurationProperties("spring.web") public class WebProperties { public static class Resources { private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"}; private String[] staticLocations; private boolean addMappings; private boolean customized; private final Chain chain; private final Cache cache; public Resources() { this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS; this.addMappings = true; this.customized = false; this.chain = new Chain(); this.cache = new Cache(); } } } ``` ##### ② 欢迎页的处理规则 ```java @Bean public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern()); welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider)); welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations()); return welcomePageHandlerMapping; } ``` ```java WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) { if (welcomePage != null && "/**".equals(staticPathPattern)) { logger.info("Adding welcome page: " + welcomePage); this.setRootViewName("forward:index.html"); } else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) { logger.info("Adding welcome page template: index"); this.setRootViewName("index"); } } ``` ##### ③ favicon ### 2.3、请求参数处理 #### 2.3.1、请求映射 ##### ① rest使用与原理 - @xxxMapping; - Rest风格支持(*使用**HTTP**请求方式动词来表示对资源的操作*) - *以前:**/getUser* *获取用户* */deleteUser* *删除用户* */editUser* *修改用户* */saveUser* *保存用户* - *现在: /user* *GET-**获取用户* *DELETE-**删除用户* *PUT-**修改用户* *POST-**保存用户* - 核心Filter;HiddenHttpMethodFilter - 用法: 表单method=post,隐藏域 _method=put - SpringBoot中手动开启 - 扩展:如何把_method 这个名字换成我们自己喜欢的 ```java @RequestMapping(value = "/user",method = RequestMethod.GET) public String getUser(){ return "GET-张三"; } @RequestMapping(value = "/user",method = RequestMethod.POST) public String saveUser(){ return "POST-张三"; } @RequestMapping(value = "/user",method = RequestMethod.PUT) public String putUser(){ return "PUT-张三"; } @RequestMapping(value = "/user",method = RequestMethod.DELETE) public String deleteUser(){ return "DELETE-张三"; } ``` ```java @Bean @ConditionalOnMissingBean({HiddenHttpMethodFilter.class}) @ConditionalOnProperty( prefix = "spring.mvc.hiddenmethod.filter", name = {"enabled"} ) public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); } ``` - 自定义filter ```java @Configuration(proxyBeanMethods = false) public class WebConfig { @Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter(){ HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter(); methodFilter.setMethodParam("_m"); return methodFilter; } } ``` Rest原理(表单提交要使用REST的时候) - 表单提交会带上**_method=PUT** - **请求过来被**HiddenHttpMethodFilter拦截 - 请求是否正常,并且是POST - 获取到**_method**的值。 - 兼容以下请求;**PUT**、**DELETE**、**PATCH** - **原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。** - **过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。** Rest使用客户端工具 - 例如 Postman 直接发送 put、delete请求方式时,无需Filter ```yaml spring: mvc: hiddenmethod: filter: enabled: true #开启页面表单的Rest功能 ``` ##### ② 请求映射原理 | ![image-20221015190303331](https://cdn.jsdelivr.net/gh/a-jingchao/picture-bed/BlogImages/202210151903428.png) | | :----------------------------------------------------------: | SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet —> doDispatch() ```java protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { try { ModelAndView mv = null; Exception dispatchException = null; try { processedRequest = this.checkMultipart(request); multipartRequestParsed = processedRequest != request; mappedHandler = this.getHandler(processedRequest); if (mappedHandler == null) { this.noHandlerFound(processedRequest, response); return; } HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); String method = request.getMethod(); boolean isGet = HttpMethod.GET.matches(method); if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) { return; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } this.applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception var20) { dispatchException = var20; } catch (Throwable var21) { dispatchException = new NestedServletException("Handler dispatch failed", var21); } this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException); } catch (Exception var22) { this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22); } catch (Throwable var23) { this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23)); } } finally { if (asyncManager.isConcurrentHandlingStarted()) { if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else if (multipartRequestParsed) { this.cleanupMultipart(processedRequest); } } } ``` **RequestMappingHandlerMapping**:保存了所有@RequestMapping 和handler的映射规则。 **所有的请求映射都在HandlerMapping中** - SpringBoot自动装配欢迎页的 WelcomePageHandlerMapping 。访问 / 能访问到index.html - SpringBoot自动配置了默认 的 RequestMappingHandlerMapping - pringBoot自动配置了默认 的 RequestMappingHandlerMapping - 如果有就找到这个请求对应的handler - 如果没有就是下一个 HandlerMapping - 我们需要一些自定义的映射处理,我们也可以自己给容器中放 **HandlerMapping** ```java protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { if (this.handlerMappings != null) { Iterator var2 = this.handlerMappings.iterator(); while(var2.hasNext()) { HandlerMapping mapping = (HandlerMapping)var2.next(); HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null) { return handler; } } } return null; } ``` #### 2.3.2、普通参数与基本注解 ##### ① 注解 @PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody ```java @RestController public class ParameterTestController { // car/2/owner/zhangsan @GetMapping("/car/{id}/owner/{username}") public Map getCar(@PathVariable("id") Integer id, @PathVariable("username") String name, @PathVariable Map pv, @RequestHeader("User-Agent") String userAgent, @RequestHeader Map header, @RequestParam("age") Integer age, @RequestParam("inters") List inters, @RequestParam Map params, @CookieValue("_ga") String _ga, @CookieValue("_ga") Cookie cookie){ Map map = new HashMap<>(); // map.put("id",id); // map.put("name",name); // map.put("pv",pv); // map.put("userAgent",userAgent); // map.put("headers",header); map.put("age",age); map.put("inters",inters); map.put("params",params); map.put("_ga",_ga); System.out.println(cookie.getName()+"===>"+cookie.getValue()); return map; } @PostMapping("/save") public Map postMethod(@RequestBody String content){ Map map = new HashMap<>(); map.put("content",content); return map; } //1、语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd //2、SpringBoot默认是禁用了矩阵变量的功能 // 手动开启:原理。对于路径的处理。UrlPathHelper进行解析。 // removeSemicolonContent(移除分号内容)支持矩阵变量的 //3、矩阵变量必须有url路径变量才能被解析 @GetMapping("/cars/{path}") public Map carsSell(@MatrixVariable("low") Integer low, @MatrixVariable("brand") List brand, @PathVariable("path") String path){ Map map = new HashMap<>(); map.put("low",low); map.put("brand",brand); map.put("path",path); return map; } // /boss/1;age=20/2;age=10 @GetMapping("/boss/{bossId}/{empId}") public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge, @MatrixVariable(value = "age",pathVar = "empId") Integer empAge){ Map map = new HashMap<>(); map.put("bossAge",bossAge); map.put("empAge",empAge); return map; } } ``` ##### ② Servlet API WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId > **ServletRequestMethodArgumentResolver 以上的部分参数** ```java @Override public boolean supportsParameter(MethodParameter parameter) { Class paramType = parameter.getParameterType(); return (WebRequest.class.isAssignableFrom(paramType) || ServletRequest.class.isAssignableFrom(paramType) || MultipartRequest.class.isAssignableFrom(paramType) || HttpSession.class.isAssignableFrom(paramType) || (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) || Principal.class.isAssignableFrom(paramType) || InputStream.class.isAssignableFrom(paramType) || Reader.class.isAssignableFrom(paramType) || HttpMethod.class == paramType || Locale.class == paramType || TimeZone.class == paramType || ZoneId.class == paramType); } ``` ##### ③ 复杂参数 **Map**、**Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、**Errors/BindingResult、**RedirectAttributes( 重定向携带数据)**、**ServletResponse(response)**、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder > Map map, Model model, HttpServletRequest request 都是可以给request域中放数据 > request.getAttribute() 进行获取 **Map、Model类型的参数**,会返回 mavContainer.getModel()---> BindingAwareModelMap 是Model 也是Map **mavContainer**.getModel(); 获取到值的 ##### ④ 自定义对象参数 ```java @Data public class Person { private String userName; private Integer age; private Date birth; private Pet pet; } ``` ```java @Data public class Pet { private String name; private Integer age; } ``` #### 2.3.3、POJO封装过程 - **ServletModelAttributeMethodProcessor** #### 2.3.4、参数处理原理 - HandlerMapping 中找到能够处理请求的Handler (Controller.method()) - 为当前的 Handler 找到一个适配器 HandlerAdapter; RequestMappingHandlerAdapter - 适配器执行目标方法并确定方法参数的每一个值 ##### ① HandlerAdapter | ![image-20221016134444976](https://cdn.jsdelivr.net/gh/a-jingchao/picture-bed/BlogImages/202210161344110.png) | | :----------------------------------------------------------: | 0 — 支持方法上面标注 @RequestMapping 1 — 支持函数式编程 …… ##### ② 执行目标方法 ```java //DispatcherServlet -- doDispatch mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); ``` ```java mav = invokeHandlerMethod(request, response, handlerMethod); //执行目标方法 //ServletInvocableHandlerMethod Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); //获取方法的参数值 Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); ``` ##### ③ 参数解析器 HandlerMethodArgumentResolver > 确定将要执行的目标方法的每一个参数的值是什么 > > SpringMVC目标方法能写多少种参数类型。取决于参数解析器 - 当前解析器是否支持解析这种参数 - 支持就才调用 resolveArgument ##### ④ 返回值处理器 ##### ⑤ 如何确定目标方法每一个参数的值 ```java // InvocableHandlerMethod protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { MethodParameter[] parameters = this.getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } else { Object[] args = new Object[parameters.length]; for(int i = 0; i < parameters.length; ++i) { MethodParameter parameter = parameters[i]; parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); args[i] = findProvidedArgument(parameter, providedArgs); if (args[i] == null) { if (!this.resolvers.supportsParameter(parameter)) { throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver")); } try { args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); } catch (Exception var10) { if (logger.isDebugEnabled()) { String exMsg = var10.getMessage(); if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) { logger.debug(formatArgumentError(parameter, exMsg)); } } throw var10; } } } return args; } } ``` **挨个判断所有参数解析器那个支持解析这个参数** ```java @Nullable private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter); if (result == null) { Iterator var3 = this.argumentResolvers.iterator(); while(var3.hasNext()) { HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next(); if (resolver.supportsParameter(parameter)) { result = resolver; this.argumentResolverCache.put(parameter, resolver); break; } } } return result; } ``` **解析这个参数的值** > 调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可 **自定义类型参数 封装POJO** ServletModelAttributeMethodProcessor 这个参数处理器支持 是否为简单类型 ```java // ModelAttributeMethodProcessor public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(ModelAttribute.class) || this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType()); } ``` ```java // BeanUtils public static boolean isSimpleProperty(Class type) { Assert.notNull(type, "'type' must not be null"); return isSimpleValueType(type) || type.isArray() && isSimpleValueType(type.getComponentType()); } public static boolean isSimpleValueType(Class type) { return Void.class != type && Void.TYPE != type && (ClassUtils.isPrimitiveOrWrapper(type) || Enum.class.isAssignableFrom(type) || CharSequence.class.isAssignableFrom(type) || Number.class.isAssignableFrom(type) || Date.class.isAssignableFrom(type) || Temporal.class.isAssignableFrom(type) || URI.class == type || URL.class == type || Locale.class == type || Class.class == type); } ``` ```java @Nullable public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer"); Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory"); String name = ModelFactory.getNameForParameter(parameter); ModelAttribute ann = (ModelAttribute)parameter.getParameterAnnotation(ModelAttribute.class); if (ann != null) { mavContainer.setBinding(name, ann.binding()); } Object attribute = null; BindingResult bindingResult = null; if (mavContainer.containsAttribute(name)) { attribute = mavContainer.getModel().get(name); } else { try { attribute = this.createAttribute(name, parameter, binderFactory, webRequest); } catch (BindException var10) { if (this.isBindExceptionRequired(parameter)) { throw var10; } if (parameter.getParameterType() == Optional.class) { attribute = Optional.empty(); } else { attribute = var10.getTarget(); } bindingResult = var10.getBindingResult(); } } if (bindingResult == null) { WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); if (binder.getTarget() != null) { if (!mavContainer.isBindingDisabled(name)) { this.bindRequestParameters(binder, webRequest); } this.validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) { throw new BindException(binder.getBindingResult()); } } if (!parameter.getParameterType().isInstance(attribute)) { attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter); } bindingResult = binder.getBindingResult(); } Map bindingResultModel = bindingResult.getModel(); mavContainer.removeAttributes(bindingResultModel); mavContainer.addAllAttributes(bindingResultModel); return attribute; } ``` **WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);** **WebDataBinder :web数据绑定器,将请求参数的值绑定到指定的JavaBean里面** **WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中** **GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean -- Integer)** **byte -- > file** 未来我们可以给WebDataBinder里面放自己的Converter; **private static final class** StringToNumber **implements** Converter **自定义Converter** ```java @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 不移除分号后面的内容,矩阵变量才能生效 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new Converter() { @Override public Pet convert(String source) { if (!StringUtils.isEmpty(source)){ String[] split = source.split(","); Pet pet = new Pet(); pet.setName(split[0]); pet.setAge(Integer.parseInt(split[1])); return pet; } return null; } }); } }; } ``` ##### ⑥ 目标方法执行完成 将所有的数据都放在 **ModelAndViewContainer**;包含要去的页面地址View。还包含Model数据 ##### ⑦ 处理派发结果 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); renderMergedOutputModel(mergedModel, getRequestToExpose(request), response); ```java // InternalResourceView protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { this.exposeModelAsRequestAttributes(model, request); this.exposeHelpers(request); String dispatcherPath = this.prepareForRendering(request, response); RequestDispatcher rd = this.getRequestDispatcher(request, dispatcherPath); if (rd == null) { throw new ServletException("Could not get RequestDispatcher for [" + this.getUrl() + "]: Check that the corresponding file exists within your web application archive!"); } else { if (this.useInclude(request, response)) { response.setContentType(this.getContentType()); if (this.logger.isDebugEnabled()) { this.logger.debug("Including [" + this.getUrl() + "]"); } rd.include(request, response); } else { if (this.logger.isDebugEnabled()) { this.logger.debug("Forwarding to [" + this.getUrl() + "]"); } rd.forward(request, response); } } } ``` > 暴露模型作为请求域属性 > exposeModelAsRequestAttributes(model, request); ```java // AbstractView protected void exposeModelAsRequestAttributes(Map model, HttpServletRequest request) throws Exception { model.forEach((name, value) -> { if (value != null) { request.setAttribute(name, value); } else { request.removeAttribute(name); } }); } ``` ### 2.4、数据响应与内容协商 | ![image-20221016205521606](https://cdn.jsdelivr.net/gh/a-jingchao/picture-bed/BlogImages/202210162055753.png) | | :----------------------------------------------------------: | #### 2.4.1、响应JSON ##### ① jackson.jar + @ResponseBody ```xml org.springframework.boot spring-boot-starter-web ``` 给前端自动返回 json **a、返回值解析器** 源码太难了…… **b、返回值解析器原理** 1. 返回值处理器是否支持这种类型返回值 supportReturnType 2. 返回值处理器调用 handleReturnValue 进行处理 3. RequestResponseBodyMethodProcessor 可以处理返回值标了 @Response 注解的 1. 利用 MessageConverters 进行处理将数据写为json 2. 服务器最终根据自己自身的能力,决定服务器能生产出什么样的内容类型的数据 3. SpringMVC 会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理? 1. 得到 MappingJackson2HttpMessageConverter 可以将对象写为 json 2. 利用 MappingJackson2HttpMessageConverter 将对象转为 json 再写出去 ##### ② SpringMVC到底支持那些返回值 > ModelAndView > Model > View > ResponseEntity > ResponseBodyEmitter > StreamingResponseBody > HttpEntity > HttpHeaders > Callable > DeferredResult > ListenableFuture > CompletionStage > WebAsyncTask > 有 @ModelAttribute 且为对象类型的 > @ResponseBody 注解 ---> RequestResponseBodyMethodProcessor; ##### ③ HttpMessageConverter原理 **a、MessageConverter规范** HttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。 例子:Person对象转为JSON。或者 JSON转为Person **b、默认的MessageConvverter** …… #### 2.4.2、内容协商 根据客户端接收能力不同,返回不同媒体类型的数据 ##### ① 引入xml依赖 ```xml com.fasterxml.jackson.dataformat jackson-dataformat-xml ``` ##### ② postman分别测试返回json和xml 只需要改变请求头中方中的 Accept 字段,Http协议中规定的,告诉服务器本客户端可以接收的数据类型 | ![image-20221017085436689](https://cdn.jsdelivr.net/gh/a-jingchao/picture-bed/BlogImages/202210170854889.png) | | :----------------------------------------------------------: | ##### ③ 开启浏览器参数方式内容协商功能 为了方便内容协商,开启基于请求参数的内容协商协议 ```yaml spring: contentnegotiation: favor-parameter: true #开启请求参数内容协商模式 ``` 发请求: http://localhost:8080/test/person?format=json http://localhost:8080/test/person?format=xml 确定客户端接收什么类型的内容类型 1. Parameter 策略优先确定是要返回 json 数据(获取请求头中的 format 的值) 2. 最终进行内容协商返回给客户端 json 即可 ##### ④ 内容协商原理 1. 判断当前响应头中是否有确定的媒体类型 MediaType 2. 获取客户端(Postman、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段)【application/xml】 - contentNegotationMannager 内容协商管理器 默认使用基于请求头的策略 - HeaderConventNegotationStrategy 确定客户端可以接受的内容类型 3. 遍历循环所有当前系统的 MessageConverter 看谁支持操作这个对象(Person) 4. 找到支持操作 Person 的converter 把 converter 支持的媒体类型统计出来 5. 客户端需要【application/xml】服务器能力【10种,json、xml】 6. 进行内容协商的最佳匹配媒体类型 7. 用支持将对象转换为最佳媒体类型的 converter,调用它进行转换 ##### ⑤ 自定义MessageConverter 实现多协议数据兼容 json、xml、x-jingchao 1. @ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 2. Processor 处理方法返回值 通过 MessageConverter 处理 3. 所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写) 4. 内容协商找到最终的 messageConverter SpringMVC 的什么功能 一个入口给容器中添加一个 WebMvcConfigurer ```java @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void extendMessageConverters(List> converters) { converters.add(new JingChaoMessageConverter()); } } } ``` > **有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。** > > **大家考虑,上述功能除了我们完全自定义外?SpringBoot有没有为我们提供基于配置文件的快速修改媒体类型功能?怎么配置呢?【提示:参照SpringBoot官方文档web开发内容协商章节】** ### 2.5、视图解析与模板引擎 视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染 #### 2.5.1、视图解析 | ![image-20221018085600613](https://cdn.jsdelivr.net/gh/a-jingchao/picture-bed/BlogImages/202210180856764.png) | | :----------------------------------------------------------: | ##### ① 视图解析原理流程 1. 目标方法处理的过程中,所有的数据都会被放在 ModelAndViewContainer 里面,包括数据和视图地址 2. 方法的参数是一个自定义类型对象(从请求参数中确定的)。把他重新放在 ModelAndViewContainer 3. 任何目标方法执行完成以后都会返回 ModelAndVIew(数据和视图地址) 4. processDispatchResult 处理派发结果(页面该如何响应) 1. #### 2.5.2、模板引擎-Thymeleaf ##### ① thymeleaf简介 现代化、服务端 Java 模板引擎 ##### ② 基本语法 1. 表达式 | 名字 | 语法 | 描述 | | :--------: | :----: | :--------------------------------: | | 变量取值 | ${...} | 获取请求域、session域、对象等值 | | 选择变量 | *{...} | 获取上下文对象值 | | 消息 | #{...} | 获取国际化等值 | | 链接 | @{...} | 生成链接 | | 片段表达式 | ~{...} | jsp:include 作用,引入公共页面片段 | 2. 字面量 - 文本值:'one text','Another one!',…… - 数字:0,44,3.14,…… - 布尔值:false、true - 空值:null - 变量:one,two,……,变量不能有空格 3. 文本操作 - 字符串拼接:+ - 变量替换**|the name is ${name}|** 4. 数学运算 - 运算符:+、-、*、/、% 5. 布尔运算 - 运算符:and、or - 一元运算:!、not 6. 比较运算 - 比较:>、<、>=、<=(gt、lt、ge、le) - 等式:==、!=(eq、ne) 7. 条件运算 - if-then:(if) ? (then) - if-then-else:(if) ? (then) : (else) - Default:(value) ?: (defaultvalue) 8. 特殊运算 - 无操作:_ ##### ③ 设置属性值 1. 设置单个值 ```html
``` 2. 设置多个值 ```html ``` ##### ④ 迭代 ```html Onions 2.41 yes ``` ```html Onions 2.41 yes ``` ##### ⑤ 条件运算 ```html view ``` ```html

User is an administrator

User is a manager

User is some other thing

``` ##### ⑥ 属性优先级 #### 2.5.3、thymeleaf 使用 ##### ① 引入 starter ```xml org.springframework.boot spring-boot-starter-thymeleaf ``` ##### ② 自动配置好了 thymeleaf ```java @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(ThymeleafProperties.class) @ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class }) @AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class }) public class ThymeleafAutoConfiguration { } ``` 自动配置的策略 1. 所有 thyemleaf 的配置值都在 ThymeleafProperties 2. 配置好了 SpringTemplateEngine 3. 配好了 ThymeleafViewResolver 4. 我们只需要直接开发页面 ```java public static final String DEFAULT_PREFIX = "classpath:/templates/"; public static final String DEFAULT_SUFFIX = ".html"; //xxx.html ``` ##### ③ 页面开发 ```html Hello

Hello页面

去百度
``` #### 2.5.4、构建后台管理系统 ##### ① 创建项目 thymeleaf、web-starter、devtools、lombok ##### ② 静态资源管理 自动配置好,将静态资源放到 static 文件夹xia ##### ③ 路径构建 th:action="@{/login}" ##### ④ 模板抽取 th:insert/replace/include ##### ⑤ 页面跳转 ```java @PostMapping("/login") public String main(User user, HttpSession session, Model model){ if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){ //把登陆成功的用户保存起来 session.setAttribute("loginUser",user); //登录成功重定向到main.html; 重定向防止表单重复提交 return "redirect:/main.html"; }else { model.addAttribute("msg","账号密码错误"); //回到登录页面 return "login"; } } ``` ##### ⑥ 数据渲染 ```java @GetMapping("/dynamic_table") public String dynamic_table(Model model){ //表格内容的遍历 List users = Arrays.asList(new User("zhangsan", "123456"), new User("lisi", "123444"), new User("haha", "aaaaa"), new User("hehe ", "aaddd")); model.addAttribute("users",users); return "table/dynamic_table"; } ``` ```html
# 用户名 密码
Trident Internet [[${user.password}]]
``` ### 2.6、拦截器 #### 2.6.1、HandlerInterceptor 接口 ```java /** * 登录检查 * 1、配置好拦截器要拦截那些请求 * 2、把这些配置放在容器中 */ @Slf4j public class LoginInterceptor implements HandlerInterceptor { /** * 目标方法执行之前 * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestURI = request.getRequestURI(); log.info("preHandle拦截的路径为:"+ requestURI); // 登录检查逻辑 HttpSession session = request.getSession(); Object loginUser = session.getAttribute("loginUser"); if (loginUser != null){ return true; } /* session.setAttribute("msg", "请先登录!"); response.sendRedirect("/"); */ request.setAttribute("msg","请先登录!"); request.getRequestDispatcher("/").forward(request, response); return false; } /** * 目标方法执行完成以后 * @param request * @param response * @param handler * @param modelAndView * @throws Exception */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { log.info("postHandle执行"+ modelAndView); } /** * 页面渲染以后 * @param request * @param response * @param handler * @param ex * @throws Exception */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info("afterCompletion执行异常"+ ex); } } ``` #### 2.6.2、配置拦截器 ```java @Configuration public class AdminWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) //所有资源都会被拦截,包括静态资源 .addPathPatterns("/**") // 放行资源 .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); } } ``` #### 2.6.3、拦截器原理 1. 根据当前请求,找到 HandlerExecutionChain 【可以处理请求的 handler 以及 handler 的所有拦截器】 2. 先来**顺序执行** 所有拦截器的 preHandle 方法 1. 如果当前拦截器 prehandler返回为 ture,则执行下一个拦截器的 preHandle 2. 如果当前拦截器返回为 false,直接倒序执行所有以及执行了的拦截器的 afterCompletion 3. **如果任何一个拦截器返回 false。直接跳出不执行目标方法** 4. **所有拦截器都返回 ture,执行目标方法** 5. **倒序执行所有拦截器的 postHandle 方法** 6. **前面的步骤有任何异常都会直接倒序触发** afterCompletion 7. 页面成功渲染完成以后,也会倒序触发 afterCompletion | ![image-20221020190929851](https://cdn.jsdelivr.net/gh/a-jingchao/picture-bed/BlogImages/202210201909032.png) | | :----------------------------------------------------------: | ### 2.7、文件上传 #### 2.7.1、页面表单 ```html

``` #### 2.7.2、文件上传代码 ```java /** * MultipartFile 自动封装上传过来的文件 * @param email * @param username * @param headerImg * @param photos * @return */ @PostMapping("/upload") public String upload(@RequestParam("email") String email, @RequestParam("username") String username, @RequestPart("headerImg") MultipartFile headerImg, @RequestPart("photos") MultipartFile[] photos) throws IOException { log.info("上传消息: email={}, username={}, headerImg={}, photos={}", email, username, headerImg.getSize(),photos.length); if(!headerImg.isEmpty()){ // 保存到文件服务器 String filename = headerImg.getOriginalFilename(); headerImg.transferTo(new File("E:\\jingchao\\"+filename)); } if (photos.length > 0){ for (MultipartFile photo : photos) { if (!photo.isEmpty()){ String photoOriginalFilename = photo.getOriginalFilename(); photo.transferTo(new File("E:\\jingchao\\"+photoOriginalFilename)); } } } return "index"; } ``` #### 2.7.3、自动配置原理 文件上传自动配置类 MultipartAutoConfiguration MultipartProperties - 自动配置好了 StandardServletMultipartResolver 【文件上传解析器】 - 原理步骤 1. 请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求 2. 参数解析器来解析请求中的文件内容封装成MultipartFile 3. 将request中文件信息封装为一个 Map MultiValueMap - FileCopyUtils 实现文件流的拷贝 ### 2.8、异常处理 #### 2.8.1、错误处理 ##### ① 默认规则 - 默认情况下,Spring Boot提供 /error 处理所有错误的映射 - 对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“whitelabel”错误视图,以HTML格式呈现相同的数据 | ![image-20221022092815772](https://cdn.jsdelivr.net/gh/a-jingchao/picture-bed/BlogImages/202210220928762.png) | | :----------------------------------------------------------: | | ![image-20221022092012575](https://cdn.jsdelivr.net/gh/a-jingchao/picture-bed/BlogImages/202210220920704.png) | - 要对其进行自定义,添加 View 解析为 error - 要完全替换默认行为,可以实现 ErrorController 并注册该类型的 Bean 定义,或添加 ErrorAttributes 类型的组件 以实现现有机制但替换其内容 - error/ 下的 4xx,5xx 页面会被自动解析; | ![image-20221022094332243](https://cdn.jsdelivr.net/gh/a-jingchao/picture-bed/BlogImages/202210220943296.png) | | :----------------------------------------------------------: | ##### ② 定制错误处理逻辑 - 自定义错误页 - error/404.html error/5xx.html 有精确的错误状态码页面就匹配精确,没有就找4xx.html,如果都没有就触发白页 - @ControllerAdvice + @ExceptionHandler 处理全局异常,底层是 ExceptionHandlerExeptionResolver 支持的 - @ResponseStatus + 自定义异常,底层是 ResponseStatusExceptionResolver,把responsestatus注解的信息底层调用 response.sendError(statusCode, resolverReason), tomcat发送的 /error - Spring 底层的异常,如参数类型转换异常,DefaultHandlerExceptionResolver 处理框架底层的异常 - response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage()); - 自定义实现 HandlerExceptionResolver 处理异常,可以作为默认的全局异常处理规则 - ErrorViewResolver 实现自定义处理异常 - response.sendError error 请求就会转给 controller - 你的异常没有任何人能处理,tomcat底层 response.sendError error请求就会转给controller - basicErrorController 要去的页面地址是 ErrorViewController ##### ③ 异常处理自动配置原理 - ErrorMvcAutoConfiguration 自动配置异常处理规则 - 容器中的组件:类型:DeafultErrorAttributes —> id:errorAttributes - public class DefaultErrorAttributes implements ErrorAttribubtes, HandlerExceptionResolver - DefaultErrorAttributes: 定义错误页面中包含那些数据 - 容器中的组件:类型:BasicErrorController —> id: basicErrorController(json + 白页 适配响应) - 容器中有组件 View —> id 是 error(响应默认错误页) - 容器中放组件 BeanNameViewResolver (视图解析器);按照返回的视图名作为组件的 id 去容器中找到 View 对象 - 容器中的组件:类型:DefaultErrorViewResolver —> id: conventionErrorViewResolver - 如果发生错误,会以 HTTP 的状态码 作为视图页地址(viewName)找到真正的页面 - error/404、5xx.html 如果想要返回页面,就会找 error 视图【StaticView】,默认是一个白页 ##### ④ 异常处理步骤流程 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 ### 2.9、Web原生组件注入(Servlet、Filter、Listener) #### 2.9.1、使用Servlet API 指定原生 Servlet 组件放置的位置 ```java @ServletComponentScan(basePackages = "com.jingchao.admin") ``` 使用 ```java @WebServlet(urlPatterns = "/my") public class MyServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("666666666666"); } } ``` > 直接响应,没有经过 Spring 的拦截器 > > 解决办法 > > ```java > @WebFilter(urlPatterns = {"/css/*","/images/*"}) > public class MyFilter implements Filter {} > ``` > > ```java > @WebListener > public class MyServletContextListener implements ServletContextListener {} > ``` #### 2.9.2、使用RegistrationBean ServletRegistrationBean, FilterRegistrationBean, ServletListenerRegistrationBean ```java @Configuration public class MyRegistryConfig { @Bean public ServletRegistrationBean myServlet(){ MyServlet myServlet = new MyServlet(); return new ServletRegistrationBean(myServlet, "/my","/my1"); } @Bean public FilterRegistrationBean myFilter(){ MyFilter myFilter = new MyFilter(); // return new FilterRegistrationBean(myFilter, myServlet()); FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter); filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/images/*")); return filterRegistrationBean; } @Bean public ServletListenerRegistrationBean myListener(){ MyServletContextListener myServletContextListener = new MyServletContextListener(); return new ServletListenerRegistrationBean(myServletContextListener); } } ``` ### 2.10、嵌入式Servlet容器 #### 2.10.1、切换嵌入式Servlet容器 - 默认支持的 webServer - Tomcat、Jetty、Undertow - ServletWebServerApplicationContext 容器自动寻找 ServletWebServerFactory 并引导创建服务器 - 切换服务器 ```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-tomcat ``` - 原理 - SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat - web应用会创建一个web版的ioc容器 ServletWebServerApplicationContext - ServletWebServerApplicationContext 启动的时候寻找 **ServletWebServerFactory**(Servlet 的web服务器工厂---> Servlet 的web服务器) - SpringBoot底层默认有很多的WebServer工厂;TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory - 底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration - ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类) - ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory - TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize---this.tomcat.start() - 内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在) #### 2.10.2、定制Servlet容器 - 实现 WebServerFactoryCustomizer - 把配置文件的值和 ServletWebServerFactory 进行绑定 - 修改配置文件 server.xxx - 直接自定义 ConfigurableServletWebServerFactory **xxxCustomizer:定制化器,可以改变 xxx 的默认规则** ```java import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; import org.springframework.stereotype.Component; @Component public class CustomizationBean implements WebServerFactoryCustomizer { @Override public void customize(ConfigurableServletWebServerFactory server) { server.setPort(9000); } } ``` ### 2.11、定制化原理 #### 2.11.1、定制化的常见方式 - 修改配置文件 - xxxxCustomizer - **编写自定义组件的配置类** **xxxConfiguration;+** **@Bean替换、增加容器中默认组件;视图解析器** -

Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能 + @Bean给容器中再扩展一些组件

```java @Configuration public class AdminWebConfig implements WebMvcConfigurer ``` - @EnableWebMvc + WebMvcConfigurer —— @Bean 可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能 - 原理 1. WebMvcAutoConfiguration 默认的SpringMVC的自动配置功能类。静态资源、欢迎页..... 2. 一旦使用 @EnableWebMvc 会 @Import(DelegatingWebMvcConfiguration.**class**) 3. **DelegatingWebMvcConfiguration** 的 作用,只保证SpringMVC最基本的使用 - 把所有系统中的 WebMvcConfigurer 拿过来。所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效 - 自动配置了一些非常底层的组件。**RequestMappingHandlerMapping**、这些组件依赖的组件都是从容器中获取 - **public class** DelegatingWebMvcConfiguration **extends** **WebMvcConfigurationSupport** 4. **WebMvcAutoConfiguration** 里面的配置要能生效 必须 @ConditionalOnMissingBean(**WebMvcConfigurationSupport**.**class**) 5. @EnableWebMvc 导致了 **WebMvcAutoConfiguration 没有生效。** #### 2.11.2、原理分析的套路 **场景starter** **- xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties --** **绑定配置文件项** ## 3、数据访问 ### 3.1、SQL #### 3.1.1、数据源的自动配置-HikariDataSource ##### ① 导入JDBC场景 ```xml org.springframework.boot spring-boot-starter-data-jdbc ``` ```xml mysql mysql-connector-java ``` ##### ② 分析自动配置 自动配置的类 - DataSourceAutoCOnfiguration:数据源的自动配置 - 修改数据源相关的配置:spring.datasource - 数据库连接池的配置,是自己容器中没有DataSource才自动配置的 - 底层配置好的连接池是:HikariDataSource - DataSourceTransactionManagerAutoConfiguration:事务管理器的自动配置 - JdbcTemplateAutoConfiguration:JdbcTemplate的自动配置,可以来对数据库进行crud - 可以修改这个配置项 @ConfigurationProperties(prefix = "spring.jdbc") 来修改JbdcTemplate - @Bean @Primary JdbcTemplate 容器中有这个组件 - JndiDataSourceAutoConfiguration:jndi 的自动配置 - XADataSourceAutoConfiguration:分布式事务相关的 ##### ③ 修改配置项 ```yaml spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/info username: root password: jc951753 ``` ##### ④ 测试 ```java @Slf4j @SpringBootTest class Springboot02WebAdminApplicationTests { @Autowired private JdbcTemplate jdbcTemplate; @Test void contextLoads() { // jdbcTemplate.queryForList("select * from account_tbl"); Long aLong = jdbcTemplate.queryForObject("select count(*) from card", Long.class); log.info("记录总数:{}",aLong); } } ``` #### 3.1.2、使用Druid数据源 ##### ① 官方地址 [Github官方地址:https://github.com/alibaba/druid](https://github.com/alibaba/druid) 整合第三方技术的两种方式 1. 自定义 2. 找starter ##### ② 自定义方式 **创建数据源** ```xml com.alibaba druid 1.2.13 ``` ```xml ``` **StatViewServlet** StatViewServlet用途包括: 1. 提供监控信息展示的 HTML 页面 2. 提供监控信息的 JSON API ```xml DruidStatView com.alibaba.druid.support.http.StatViewServlet DruidStatView /druid/* ``` **StatFilter** 用于统计监控信息 如 SQL 监控、URL 监控 ```xml ``` ##### ③ 使用官方starter方式 **引入 druid-starter** ```xml com.alibaba druid-spring-boot-starter 1.2.9 ``` **分析自动配置** - 扩展配置项 **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的配置 **配置示例** ```yaml spring: datasource: url: jdbc:mysql://localhost:3306/db_a username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver druid: aop-patterns: com.atguigu.admin.* #监控SpringBean filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙) stat-view-servlet: # 配置监控页功能 enabled: true login-username: admin login-password: 123456 resetEnable: false web-stat-filter: # 监控web enabled: true urlPattern: /* exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' filter: stat: # 对上面filters里面的stat的详细配置 slow-sql-millis: 1000 logSlowSql: true enabled: true wall: enabled: true config: drop-table-allow: false ``` #### 3.1.3、整合MyBatis操作 [官方地址:https://github.com/mybatis](https://github.com/mybatis) SpringBoot官方的starter:spring-boot-starter-* 第三方的:*-spring-boot-starter ```xml org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.2 ``` ##### ① 配置模式 - 全局配置文件 - SqlSessionFactory:自动配置好了 - SqlSession:自动配置了 SqlSessionTemplate 组合了 SqlSession - @Import(AutoConfiguredMapperScannerRegistrar.class) - Mapper:只需要我们写操作 MyBatis 的接口标准了 @Mapper 就会被自动扫描进来 在配置文件中进行配置 ```yaml mybatis: # 全局配置文件 config-location: classpath:mybatis/mybatis-config.xml # 映射文件位置 mapper-locations: classpath:mybatis/mapper/*.xml ``` Mapper 接口与映射文件的绑定 ```xml ``` **步骤** 1. 导入 mybatis 官方starter 2. 编写 mapper 接口,标注 @Mapper 注解 3. 编写 SQL 映射文件并绑定 mapper 接口 4. 在application.yml 中指定 Mapper 配置文件的位置,以及指定全局配置文件的信息(建议**配置在 mybaits.configuration**) ```yaml mybatis: #(不推荐) config-location: classpath:mybatis/mybatis-config.xml mapper-locations: classpath:mybatis/mapper/*.xml #(推荐) 指定全局配置文件中的配置项(不能使用上面的mybatis-config文件了) configuration: map-underscore-to-camel-case: true ``` ##### ② 注解模式 ```java @Mapper public interface CityMapper { @Select("select * from city where id = #{id}") public City getCityById(Integer id); } ``` ##### ③ 混合模式 ```java @Mapper public interface CityMapper { @Select("select * from city where id = #{id}") public City getCityById(Integer id); // @Insert("insert into city(`name`,`state`,`country`) values(#{name},#{state},#{country})") // @Options(useGeneratedKeys = true, keyProperty = "id") public void insertCity(City city); } ``` ```xml insert into city(`name`,`state`,`country`) values(#{name},#{state},#{country}) ``` **步骤** 1. 引入 mybaits-starter 2. 配置 application.yml ,指定 mapper-location 位置 3. 编写 Mapper 接口方法,标注 @Mapper 注解 4. 简单方法直接注解方式 5. 复杂方法编写 mapper映射文件进行绑定 6. @MapperScan("com.jingchao.admin.mapper")简化,其他接口就可以不用标注 @Mapper 注解 #### 3.1.4、整合MyBatis-Plus完成 CRUD ##### ① MyBaits-Plus 简介 [MyBatis-Plus](https://github.com/baomidou/mybatis-plus)(简称 MP)是一个 [MyBatis](http://www.mybatis.org/mybatis-3/) 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。 [mybatis plus 官网](https://baomidou.com/) 建议安装 **MybatisX** 插件 ##### ② 整合MyBatis-Plus ```xml com.baomidou mybatis-plus-boot-starter 3.5.2 ``` 自动配置 - MybatisPlusAutoConfiguration 配置类,MybatisPlusProperties 配置项绑定。**mybatis-plus:xxx 就是对****mybatis-plus的定制** - **SqlSessionFactory 自动配置好。底层是容器中默认的数据源** - **mapperLocations 自动配置好的。有默认值。classpath\*:/mapper/\**/\*.xml;任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件,放在 mapper下** - **容器中也自动配置好了** **SqlSessionTemplate** - **@Mapper 标注的接口也会被自动扫描;建议直接** @MapperScan(**"com.atguigu.admin.mapper"**) 批量扫描就行 优点 - 只需要我们的 Mapper **继承 BaseMapper** 就可以拥有 crud 能力 ##### ③ CRUD功能 ```java @GetMapping("/user/delete/{id}") public String deleteUser(@PathVariable("id") Long id, @RequestParam(value = "pn",defaultValue = "1")Integer pn, RedirectAttributes ra){ userService.removeById(id); ra.addAttribute("pn",pn); return "redirect:/dynamic_table"; } @GetMapping("/dynamic_table") public String dynamic_table(@RequestParam(value="pn",defaultValue = "1") Integer pn,Model model){ //表格内容的遍历 // response.sendError // List users = Arrays.asList(new User("zhangsan", "123456"), // new User("lisi", "123444"), // new User("haha", "aaaaa"), // new User("hehe ", "aaddd")); // model.addAttribute("users",users); // // if(users.size()>3){ // throw new UserTooManyException(); // } //从数据库中查出user表中的用户进行展示 //构造分页参数 Page page = new Page<>(pn, 2); //调用page进行分页 Page userPage = userService.page(page, null); // userPage.getRecords() // userPage.getCurrent() // userPage.getPages() model.addAttribute("users",userPage); return "table/dynamic_table"; } ``` ```java @Service public class UserServiceImpl extends ServiceImpl implements UserService {} public interface UserService extends IService {} ``` ### 3.2、NoSQL Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、**缓存**和消息中间件。 它支持多种类型的数据结构,如 [字符串(strings)](http://www.redis.cn/topics/data-types-intro.html#strings), [散列(hashes)](http://www.redis.cn/topics/data-types-intro.html#hashes), [列表(lists)](http://www.redis.cn/topics/data-types-intro.html#lists), [集合(sets)](http://www.redis.cn/topics/data-types-intro.html#sets), [有序集合(sorted sets)](http://www.redis.cn/topics/data-types-intro.html#sorted-sets) 与范围查询, [bitmaps](http://www.redis.cn/topics/data-types-intro.html#bitmaps), [hyperloglogs](http://www.redis.cn/topics/data-types-intro.html#hyperloglogs) 和 [地理空间(geospatial)](http://www.redis.cn/commands/geoadd.html) 索引半径查询。 Redis 内置了 [复制(replication)](http://www.redis.cn/topics/replication.html),[LUA脚本(Lua scripting)](http://www.redis.cn/commands/eval.html), [LRU驱动事件(LRU eviction)](http://www.redis.cn/topics/lru-cache.html),[事务(transactions)](http://www.redis.cn/topics/transactions.html) 和不同级别的 [磁盘持久化(persistence)](http://www.redis.cn/topics/persistence.html), 并通过 [Redis哨兵(Sentinel)](http://www.redis.cn/topics/sentinel.html)和自动 [分区(Cluster)](http://www.redis.cn/topics/cluster-tutorial.html)提供高可用性(high availability)。 #### 3.2.1、Redis自动配置 ```xml org.springframework.boot spring-boot-starter-data-redis ``` 自动配置: - RedisAutoConfiguration 自动配置类。RedisProperties 属性类 --> **spring.redis.xxx是对redis的配置** - 连接工厂是准备好的。**Lettuce**ConnectionConfiguration、**Jedis**ConnectionConfiguration - **自动注入了RedisTemplate**<**Object**, **Object**> : xxxTemplate; - **自动注入了StringRedisTemplate;k:v都是String** - **key:value** - **底层只要我们使用** **StringRedisTemplate、****RedisTemplate就可以操作redis** redis环境搭建: 1. 阿里云按量付费redis。经典网络 2. 申请redis的公网连接地址 3. 修改白名单 允许0.0.0.0/0 访问 #### 3.2.2、RedisTemplate与Lettuce ```java @Test void testRedis(){ ValueOperations operations = redisTemplate.opsForValue(); operations.set("hello","world!"); String hello = operations.get("hello"); System.out.println("hello = " + hello); } ``` #### 3.2.3、切换至jedis ```xml org.springframework.boot spring-boot-starter-data-redis redis.clients jedis ``` ```yaml spring: redis: host: 127.0.0.1 port: 6379 client-type: jedis lettuce: pool: max-active: 10 min-idle: 5 ``` ## 4、单元测试 ### 4.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的测试引擎。 | ![image-20221025211949395](https://cdn.jsdelivr.net/gh/a-jingchao/picture-bed/BlogImages/202210252119558.png) | | :----------------------------------------------------------: | 注意: **SpringBoot 2.4 以上版本移除了默认对** **Vintage 的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能 @Test****)** **JUnit 5’s Vintage Engine Removed from** **`spring-boot-starter-test,如果需要继续兼容junit4需要自行引入vintage`** ```xml org.junit.vintage junit-vintage-engine test org.hamcrest hamcrest-core ``` ```xml org.springframework.boot spring-boot-starter-test test ``` 以前使用:@SpringBootTest + @RunWith(SpringTest.class) SpringBoot整合 Junit以后 - 编写测试方法:@Test标注(注意需要使用junit5版本的注解) - Junit类具有Spring的功能,@Autowired、比如 @Transactional 标注测试方法,测试完成后自动回滚 ### 4.2、JUnit常用注解 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 :**为测试类或测试方法提供扩展类引用 ### 4.3、断言(assertions) 断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。**这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法**。JUnit 5 内置的断言可以分成如下几个类别: **检查业务逻辑返回的数据是否合理。** **所有的测试运行结束以后,会有一个详细的测试报告;** #### 4.3.1、简单断言 用来对单个值进行简单的验证。如: | 方法 | 说明 | | --------------- | ------------------------------------ | | assertEquals | 判断两个对象或两个原始类型是否相等 | | assertNotEquals | 判断两个对象或两个原始类型是否不相等 | | assertSame | 判断两个对象引用是否指向同一个对象 | | assertNotSame | 判断两个对象引用是否指向不同的对象 | | assertTrue | 判断给定的布尔值是否为 true | | assertFalse | 判断给定的布尔值是否为 false | | assertNull | 判断给定的对象引用是否为 null | | assertNotNull | 判断给定的对象引用是否不为 null | ```java @Test @DisplayName("simple assertion") public void simple() { assertEquals(3, 1 + 2, "simple math"); assertNotEquals(3, 1 + 1); assertNotSame(new Object(), new Object()); Object obj = new Object(); assertSame(obj, obj); assertFalse(1 > 2); assertTrue(1 < 2); assertNull(null); assertNotNull(new Object()); } ``` #### 4.3.2、数组断言 通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等 ```java @Test @DisplayName("array assertion") public void array() { assertArrayEquals(new int[]{1, 2}, new int[] {1, 2}); } ``` #### 4.3.3、组合断言 assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言 ```java @Test @DisplayName("assert all") public void all() { assertAll("Math", () -> assertEquals(2, 1 + 1), () -> assertTrue(1 > 0) ); } ``` #### 4.3.4、异常断言 在JUnit4时期,想要测试方法的异常情况时,需要用**@Rule**注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式**Assertions.assertThrows()** ,配合函数式编程就可以进行使用。 ```java @Test @DisplayName("异常测试") public void exceptionTest() { ArithmeticException exception = Assertions.assertThrows( //扔出断言异常 ArithmeticException.class, () -> System.out.println(1 % 0)); } ``` #### 4.3.5、超时断言 Junit5还提供了**Assertions.assertTimeout()** 为测试方法设置了超时时间 ```java @Test @DisplayName("超时测试") public void timeoutTest() { //如果测试方法时间超过1s将会异常 Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500)); } ``` #### 4.3.6、快速失败 通过 fail 方法直接使得测试失败 ```java @Test @DisplayName("fail") public void shouldFail() { fail("This should fail"); } ``` ### 4.4、前置条件(assumptions) JUnit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。 ```java @DisplayName("前置条件") public class AssumptionsTest { private final String environment = "DEV"; @Test @DisplayName("simple") public void simpleAssume() { assumeTrue(Objects.equals(this.environment, "DEV")); 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") ); } } ``` assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。 ### 4.5、嵌套测试 JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。 ```java @DisplayName("A stack") class TestingAStackDemo { Stack 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()); } } } } ``` ### 4.6、参数化测试 参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。 利用**@ValueSource**等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。 **@ValueSource**: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型 **@NullSource**: 表示为参数化测试提供一个null的入参 **@EnumSource**: 表示为参数化测试提供一个枚举入参 **@CsvFileSource**:表示读取指定CSV文件内容作为参数化测试入参 **@MethodSource**:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流) 当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现**ArgumentsProvider**接口,任何外部文件都可以作为它的入参 ```java @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 method() { return Stream.of("apple", "banana"); } ``` ### 4.7、迁移指南 在进行迁移的时候需要注意如下的变化: - 注解在 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。 ## 5、指标监控 ### 5.1、SpringBoot Actuator #### 5.1.1、简介 未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。 ```xml org.springframework.boot spring-boot-starter-actuator ``` #### 5.1.2、1.x 与 2.x 的不同 | ![image-20221026092136802](https://cdn.jsdelivr.net/gh/a-jingchao/picture-bed/BlogImages/202210260921971.png) | | :----------------------------------------------------------: | #### 5.1.3、如何使用 - 引入场景 - 访问 http://localhost:8080/actuator/** - 暴露所有监控信息为HTTP ```yaml management: endpoints: enabled-by-default: true #暴露所有端点信息 web: exposure: include: '*' #以web方式暴露 ``` - 测试 > http://localhost:8080/actuator/beans > > http://localhost:8080/actuator/configprops > > http://localhost:8080/actuator/metrics > > http://localhost:8080/actuator/metrics/jvm.gc.pause > > [http://localhost:8080/actuator/](http://localhost:8080/actuator/metrics)endpointName/detailPath #### 5.1.4、可视化 https://github.com/codecentric/spring-boot-admin ### 5.2、Actuator Endpoint #### 5.2.1、最常用的端点 | 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`属性)。支持使用HTTP`Range`标头来检索部分日志文件的内容。 | | `prometheus` | 以Prometheus服务器可以抓取的格式公开指标。需要依赖`micrometer-registry-prometheus`。 | 最常用的Endpoint - **Health:监控状况** - **Metrics:运行时指标** - **Loggers:日志记录** #### 5.2.2、Health Endpoint 健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。 重要的几点: - health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告 - 很多的健康检查默认已经自动配置好了,比如:数据库、redis等 - 可以很容易的添加自定义的健康检查机制 #### 5.2.3、Metrics Endpoint 提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到; - 通过Metrics对接多种监控系统 - 简化核心Metrics开发 - 添加自定义Metrics或者扩展已有Metrics #### 5.2.4、管理Endpoint ##### ① 开启与禁用 Endpoints - 默认所有的Endpoint除过shutdown都是开启的。 - 需要开启或者禁用某个Endpoint。配置模式为 **management.endpoint..enabled = true** ```yaml management: endpoint: beans: enabled: true ``` - 或者禁用所有的Endpoint然后手动开启指定的Endpoint ```yaml management: endpoints: enabled-by-default: false endpoint: beans: enabled: true health: enabled: true ``` ##### ② 暴露Endpoints 支持的暴露方式 - HTTP:默认只暴露**health**和**info** Endpoint - **JMX**:默认暴露所有Endpoint - 除过health和info,剩下的Endpoint都应该进行保护访问。如果引入SpringSecurity,则会默认配置安全访问规则 | ID | JMX | Web | | ------------------ | ---- | ---- | | `auditevents` | Yes | No | | `beans` | Yes | No | | `caches` | Yes | No | | `conditions` | Yes | No | | `configprops` | Yes | No | | `env` | Yes | No | | `flyway` | Yes | No | | `health` | Yes | Yes | | `heapdump` | N/A | No | | `httptrace` | Yes | No | | `info` | Yes | Yes | | `integrationgraph` | Yes | No | | `jolokia` | N/A | No | | `logfile` | N/A | No | | `loggers` | Yes | No | | `liquibase` | Yes | No | | `metrics` | Yes | No | | `mappings` | Yes | No | | `prometheus` | N/A | No | | `scheduledtasks` | Yes | No | | `sessions` | Yes | No | | `shutdown` | Yes | No | | `startup` | Yes | No | | `threaddump` | Yes | No | ### 5.3、定制Endpoint #### 5.3.1、定制Health信息 ```java import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.stereotype.Component; @Component public class MyHealthIndicator implements HealthIndicator { @Override public Health health() { int errorCode = check(); // perform some specific health check if (errorCode != 0) { return Health.down().withDetail("Error Code", errorCode).build(); } return Health.up().build(); } } // 构建Health Health build = Health.down() .withDetail("msg", "error service") .withDetail("code", "500") .withException(new RuntimeException()) .build(); ``` ```yaml management: health: enabled: true show-details: always #总是显示详细信息。可显示每个模块的状态信息 ``` ```java @Component public class MyComHealthIndicator extends AbstractHealthIndicator { /** * 真实的检查方法 * @param builder * @throws Exception */ @Override protected void doHealthCheck(Health.Builder builder) throws Exception { //mongodb。 获取连接进行测试 Map map = new HashMap<>(); // 检查完成 if(1 == 2){ // builder.up(); //健康 builder.status(Status.UP); map.put("count",1); map.put("ms",100); }else { // builder.down(); builder.status(Status.OUT_OF_SERVICE); map.put("err","连接超时"); map.put("ms",3000); } builder.withDetail("code",100) .withDetails(map); } } ``` #### 5.3.2、定制info信息 常用两种方式 ##### ① 编写配置文件 ```yaml info: appName: boot-admin version: 2.0.1 mavenProjectName: @project.artifactId@ #使用@@可以获取maven的pom文件值 mavenProjectVersion: @project.version@ ``` ##### ② 编写InfoController ```java import java.util.Collections; import org.springframework.boot.actuate.info.Info; import org.springframework.boot.actuate.info.InfoContributor; import org.springframework.stereotype.Component; @Component public class ExampleInfoContributor implements InfoContributor { @Override public void contribute(Info.Builder builder) { builder.withDetail("example", Collections.singletonMap("key", "value")); } } ``` http://localhost:8080/actuator/info 会输出以上方式返回的所有info信息 #### 5.3.3、定制Metrics信息 ##### ① SpringBoot支持自动适配的Metrics - 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](https://docs.spring.io/spring-integration/docs/5.4.1/reference/html/system-management.html#micrometer-integration) metrics ##### ② 增加定制Metrics ```java class MyService{ Counter counter; public MyService(MeterRegistry meterRegistry){ counter = meterRegistry.counter("myservice.method.running.counter"); } public void hello() { counter.increment(); } } //也可以使用下面的方式 @Bean MeterBinder queueSize(Queue queue) { return (registry) -> Gauge.builder("queueSize", queue::size).register(registry); } ``` #### 5.3.4、定制Endpoint ```java @Component @Endpoint(id = "container") public class DockerEndpoint { @ReadOperation public Map getDockerInfo(){ return Collections.singletonMap("info","docker started..."); } @WriteOperation private void restartDocker(){ System.out.println("docker restarted...."); } } ``` 场景:开发**ReadinessEndpoint**来管理程序是否就绪,或者**Liveness****Endpoint**来管理程序是否存活; 当然,这个也可以直接使用 https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-kubernetes-probes ## 6、原理解析 ### 6.1、Profile功能 为了方便多环境适配,springboot简化了profile功能。 #### 6.1.1、application-profile功能 - 默认配置文件 application.yaml;任何时候都会加载 - 指定环境配置文件 application-{env}.yaml - 激活指定环境 - - 配置文件激活 - 命令行激活:java -jar xxx.jar --**spring.profiles.active=prod --person.name=haha** - - - **修改配置文件的任意值,命令行优先** - 默认配置与环境配置同时生效 - 同名配置项,profile配置优先 #### 6.1.2、@Profile条件装配功能 ```java @Configuration(proxyBeanMethods = false) @Profile("production") public class ProductionConfiguration { // ... } ``` #### 6.1.3、profile分组 ```properties spring.profiles.group.production[0]=proddb spring.profiles.group.production[1]=prodmq # 使用:--spring.profiles.active=production 激活 ``` ### 6.2、外部化配置 https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config 1. Default properties (specified by setting `SpringApplication.setDefaultProperties`). 2. `@PropertySource` annotations on your `@Configuration` classes. Please note that such property sources are not added to the `Environment` until the application context is being refreshed. This is too late to configure certain properties such as `logging.*` and `spring.main.*` which are read before refresh begins. 3. **Config data (such as** `**application.properties**` **files)** 4. A `RandomValuePropertySource` that has properties only in `random.*`. 5. OS environment variables. 6. Java System properties (`System.getProperties()`). 7. JNDI attributes from `java:comp/env`. 8. `ServletContext` init parameters. 9. `ServletConfig` init parameters. 10. Properties from `SPRING_APPLICATION_JSON` (inline JSON embedded in an environment variable or system property). 11. Command line arguments. 12. `properties` attribute on your tests. Available on `@SpringBootTest` and the [test annotations for testing a particular slice of your application](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-autoconfigured-tests). 13. `@TestPropertySource` annotations on your tests. 14. [Devtools global settings properties](https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-devtools-globalsettings) in the `$HOME/.config/spring-boot` directory when devtools is active #### 6.2.1、外部配置源 常用:**Java属性文件**、**YAML文件**、**环境变量**、**命令行参数**; #### 6.2.2、配置文件查找位置 1) classpath 根路径 2) classpath 根路径下config目录 3) jar包当前目录 4) jar包当前目录的config目录 5) /config子目录的直接子目录 #### 6.2.3、配置文件加载顺序 1. 当前jar包内部的application.properties和application.yml 2. 当前jar包内部的application-{profile}.properties 和 application-{profile}.yml 3. 引用的外部jar包的application.properties和application.yml 4. 引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml #### 6.2.4、环境顺序 外部优先,后面的覆盖前面的同名的配置项 ### 6.3、自定义starter #### 6.3.1、starter启动原理 - starter-pom 引入 autoconfigurer 包 | ![image-20221026123505541](https://cdn.jsdelivr.net/gh/a-jingchao/picture-bed/BlogImages/202210261235674.png) | | :----------------------------------------------------------: | - autoconfigure包中配置使用 **META-INF/spring.factories** 中 **EnableAutoConfiguration 的值,使得项目启动加载指定的自动配置类** - **编写自动配置类 xxxAutoConfiguration -> xxxxProperties** - **@Configuration** - *@Conditional - @EnableConfigurationProperties - @Bean** - ###### ...... **引入starter** **--- xxxAutoConfiguration --- 容器中放入组件 ---- 绑定xxxProperties ----** **配置项** #### 6.3.2、自定义starter **jingchao-hello-spring-boot-starter(启动器)** **jingchao-hello-spring-boot-starter-autoconfigure(自动配置包)** ### 6.4、SpringBoot原理 Spring原理【[Spring注解](https://www.bilibili.com/video/BV1gW411W7wy?p=1)】、**SpringMVC**原理、**自动配置原理**、SpringBoot原理 #### 6.4.1、SpringBoot启动过程 - 创建 **SpringApplication** - - 保存一些信息。 - 判定当前应用的类型。ClassUtils。Servlet - **bootstrappers****:初始启动引导器(**List**):去spring.factories文件中找** org.springframework.boot.**Bootstrapper** - 找 **ApplicationContextInitializer**;去**spring.factories****找** **ApplicationContextInitializer** - - - List> **initializers** - - **找** **ApplicationListener ;应用监听器。**去**spring.factories****找** **ApplicationListener** - - - List> **listeners** - 运行 **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** #### 6.4.2、Application Events And Listener https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-application-events-and-listeners **ApplicationContextInitializer** **ApplicationListener** **SpringApplicationRunListener** #### 6.4.3、ApplicationRunner 与 CommandLineRunner