最新要闻
- 徐鸿平
- 快看点丨努比亚首款AR眼镜官宣:将亮相MWC 2023
- 全球动态:更耐摔?苹果上架iPhone 14 Pro屏幕保护膜:348元买吗
- 焦点快播:女车主买奇瑞QQ冰淇淋电动车一天坏3次!4S店承认故障 退车
- 天天速读:3月见!魅族20系列宣布首发无界生态系统Flyme 10
- Reno时隔4年再度回归潜望长焦!OPPO Reno 10系列曝光
- 有个n字的鞋子是什么牌子?国产运动品牌有哪些?
- 贡菜是什么菜?贡菜的功效与作用是什么?
- 四王下山管天下是哪四个王?四王下山管天下是什么生肖?
- 春雨贵如油是什么节气?密密的春雨像什么比喻?
- 二十不惑人物关系是什么?二十不惑2演员表
- 世界热讯:BBA直呼内行!吉利“仰望银河”出圈:多家国产品牌跟风“望天”
- 天天时讯:首发1399元!小米小顽智能全自动猫砂盆开售:14天不用管
- 苹果iOS 16.4首个测试版发布 新增15个表情符号被吐槽太丑
- 今日热文:网传杭州取消限行 官方辟谣:假的、Chatgpt撰写生成
- 《狂野之心》IGN 8分:足以媲美《怪猎》狩猎大作
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
焦点速递!SpringBoot
SpringBoot2核心技术与响应式编程
SpringBoot2核心技术
SpringBoot2基础入门
Spring能做什么?
Spring的生态
【资料图】
覆盖了:
web开发
数据访问
安全控制
分布式
消息服务
移动开发
批处理
Spring5的重大升级
内部源码设计
- 基于Java8的一些新特性,如:接口默认实现。重新设计源码架构。
什么是SpringBoot?
Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run"
能快速创建出生产级别的Spring应用
SpringBoot优点
Create stand-alone Spring applications
- 创建独立Spring应用
Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)
- 内嵌web服务器
Provide opinionated "starter" dependencies to simplify your build configuration
- 自动starter依赖,简化构建配置
Automatically configure Spring and 3rd party libraries whenever possible
- 自动配置Spring以及第三方功能
Provide production-ready features such as metrics, health checks, and externalized configuration
- 提供生产级别的监控、健康检查及外部化配置
Absolutely no code generation and no requirement for XML configuration
- 无代码生成、无需编写XM
SpringBoot是整合Spring技术栈的一站式框架
SpringBoot是简化Spring技术栈的快速开发脚手架
SpringBoot缺点
- 人称版本帝,迭代快,需要时刻关注变化
- 封装太深,内部原理复杂,不容易精通
SpringBoot时代背景
微服务
James Lewis and Martin Fowler (2014) 提出微服务完整概念。https://martinfowler.com/microservices/
In short, the microservice architectural styleis an approach to developing a single application as a suite of small services, each running in its own processand communicating with lightweightmechanisms, often an HTTPresource API. These services are built around business capabilitiesand independently deployableby fully automated deploymentmachinery. There is a bare minimum of centralized managementof these services, which may be written in different programming languagesand use different data storage technologies.-- James Lewis and Martin Fowler (2014)
- 微服务是一种架构风格
- 一个应用拆分为一组小型服务
- 每个服务运行在自己的进程内,也就是可独立部署和升级
- 服务之间使用轻量级HTTP交互
- 服务围绕业务功能拆分
- 可以由全自动部署机制独立部署
- 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术
分布式
- 分布式的困难
- 远程调用
- 服务发现
- 负载均衡
- 服务容错
- 配置管理
- 服务监控
- 链路追踪
- 日志管理
- 任务调度
分布式的解决
SpringBoot+SpringCloud
云原生
原生应用如何上云。 Cloud Native
- 上云的困难
- 服务自愈
- 弹性伸缩
- 服务隔离
- 自动化部署
- 灰度发布
- 流量治理
- ......
- 上云的解决
如何学习SpringBoot
官方文档架构
查看版本新特性;
https://github.com/spring-projects/spring-boot/wiki#release-notes
快速体验SpringBoot
系统要求
- Java 8 & 兼容java14 .
- Maven 3.3+
- idea 2019.1.2
Maven设置
nexus-aliyun central Nexus aliyun http://maven.aliyun.com/nexus/content/groups/public jdk-1.8 true 1.8 1.8 1.8 1.8
Hello Word
需求:浏览发送/hello请求,响应 Hello,Spring Boot 2
创建maven工程
引入依赖
org.springframework.boot spring-boot-starter-parent 2.3.4.RELEASE org.springframework.boot spring-boot-starter-web
创建主程序
package com.lkjedu.boot;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;/** * 主程序类 * @SpringBootApplication:告诉SpringBoot这是一个SpringBoot应用 * * */@SpringBootApplicationpublic class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class,arg s); }}
编写业务
package com.lkjedu.boot.controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class HelloController { @RequestMapping("hello") public String handle01(){ return "Hello SpringBoot2"; }}
测试
直接运行main方法
简化配置
简化部署
org.springframework.boot spring-boot-maven-plugin
把项目打成jar包,直接在目标服务器执行即可。
注意点:
- 取消掉cmd的快速编辑模式
依赖管理
- 父项目做依赖管理
依赖管理 org.springframework.boot spring-boot-starter-parent 2.3.4.RELEASE 他的父项目 org.springframework.boot spring-boot-dependencies 2.3.4.RELEASE 几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制
- 开发导入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-starter4、见到的 *-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器。5、所有场景启动器最底层的依赖 org.springframework.boot spring-boot-starter 2.3.4.RELEASE compile
- 无需关注版本号,自动版本仲裁
1、引入依赖默认都可以不写版本2、引入非版本仲裁的jar,要写版本号。
- 可以修改默认版本号
1、查看spring-boot-dependencies里面规定当前依赖的版本 用的 key。2、在当前项目里面重写配置 5.1.43
自动配置
自动配好Tomcat
- 引入Tomcat依赖。
- 配置Tomcat
org.springframework.boot spring-boot-starter-tomcat 2.3.4.RELEASE compile
自动配好SpringMVC
- 引入SpringMVC全套组件
- 自动配好SpringMVC常用组件(功能)
自动配好Web常见功能,如:字符编码问题
- SpringBoot帮我们配置好了所有web开发的常见场景
默认的包结构
- 主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来
- 无需以前的包扫描配置
- 想要改变扫描路径,@SpringBootApplication(scanBasePackages="com.atguigu")
- 或者@ComponentScan 指定扫描路径
@SpringBootApplication等同于@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan("com.atguigu.boot")
各种配置拥有默认值
- 默认配置最终都是映射到某个类上,如:MultipartProperties
- 配置文件的值最终会绑定每个类上,这个类会在容器中创建对象
按需加载所有自动配置项
- 非常多的starter
- 引入了哪些场景这个场景的自动配置才会开启
- SpringBoot所有的自动配置功能都在 spring-boot-autoconfigure 包里面
......
容器功能
- 组件添加
1、@Configuration
基本使用
Full模式与Lite模式
- 示例
- 最佳实战
- 配置 类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断
- 配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式
#############################Configuration使用示例######################################################/** * 1、配置类里面使用@Bean标注在方法上给容器注册组件,默认也是单实例的 * 2、配置类本身也是组件 * 3、proxyBeanMethods:代理bean的方法 * Full(proxyBeanMethods = true)、【保证每个@Bean方法被调用多少次返回的组件都是单实例的】 * Lite(proxyBeanMethods = false)【每个@Bean方法被调用多少次返回的组件都是新创建的】 * 组件依赖必须使用Full模式默认。其他默认是否Lite模式 * * * */@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件public class MyConfig { /** * Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象 * @return */ @Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值,就是组件在容器中的实例 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"); }}################################@Configuration测试代码如下########################################@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan("com.atguigu.boot")public class MainApplication { public static void main(String[] args) { //1、返回我们IOC容器 ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args); //2、查看容器里面的组件 String[] names = run.getBeanDefinitionNames(); for (String name : names) { System.out.println(name); } //3、从容器中获取组件 Pet tom01 = run.getBean("tom", Pet.class); Pet tom02 = run.getBean("tom", Pet.class); System.out.println("组件:"+(tom01 == tom02)); //4、com.atguigu.boot.config.MyConfig$$EnhancerBySpringCGLIB$$51f1e1ca@1654a892 MyConfig bean = run.getBean(MyConfig.class); System.out.println(bean); //如果@Configuration(proxyBeanMethods = true)代理对象调用方法。SpringBoot总会检查这个组件是否在容器中有。 //保持组件单实例 User user = bean.user01(); User user1 = bean.user01(); System.out.println(user == user1); User user01 = run.getBean("user01", User.class); Pet tom = run.getBean("tom", Pet.class); System.out.println("用户的宠物:"+(user01.getPet() == tom)); }}
2@Bean、@Component、@Controller、@Service、@Repository
* 4、@Import({User.class, DBHelper.class}) * 给容器中自动创建出这两个类型的组件、默认组件的名字就是全类名 * * * */@Import({User.class, DBHelper.class})@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件public class MyConfig {}
@Import 高级用法: https://www.bilibili.com/video/BV1gW411W7wy?p=8
3、@ComponentScan、@Import
* 4、@Import({User.class, DBHelper.class}) * 给容器中自动创建出这两个类型的组件、默认组件的名字就是全类名 * * * */@Import({User.class, DBHelper.class})@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件public class MyConfig {}
@Import 高级用法: https://www.bilibili.com/video/BV1gW411W7wy?p=8
4、@Conditional
条件装配:满足Conditional指定的条件,则进行组件注入
=====================测试条件装配==========================@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件//@ConditionalOnBean(name = "tom")@ConditionalOnMissingBean(name = "tom")public class MyConfig { /** * Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象 * @return */ @Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值,就是组件在容器中的实例 public User user01(){ User zhangsan = new User("zhangsan", 18); //user组件依赖了Pet组件 zhangsan.setPet(tomcatPet()); return zhangsan; } @Bean("tom22") public Pet tomcatPet(){ return new Pet("tomcat"); }}public static void main(String[] args) { //1、返回我们IOC容器 ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args); //2、查看容器里面的组件 String[] names = run.getBeanDefinitionNames(); for (String name : names) { System.out.println(name); } boolean tom = run.containsBean("tom"); System.out.println("容器中Tom组件:"+tom); boolean user01 = run.containsBean("user01"); System.out.println("容器中user01组件:"+user01); boolean tom22 = run.containsBean("tom22"); System.out.println("容器中tom22组件:"+tom22); }
2.2、原生配置文件引入
@ImportResource
======================beans.xml=========================
@ImportResource("classpath:beans.xml")public class MyConfig {}======================测试================= boolean haha = run.containsBean("haha"); boolean hehe = run.containsBean("hehe"); System.out.println("haha:"+haha);//true System.out.println("hehe:"+hehe);//true
- 配置绑定
@ConfigurationProperties
如何使用Java读取到properties文件中的内容,并且把它封装到JavaBean中,以供随时使用;
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。 } } }
@Component + @ConfigurationProperties
@EnableConfigurationProperties + @ConfigurationProperties
@EnableConfigurationProperties(Car.class)//1、开启Car配置绑定功能//2、把这个Car这个组件自动注册到容器中public class MyConfig {}
##### 自动配置原理###### 引导加载自动配置类~~~Java@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })public @interface SpringBootApplication{}======================
@SpringBootConfiguration
@Configuration:代表当前是一个配置类
@ComponentScan
指定我们扫描那些包
@EnableAutoConfiguration
启用 SpringBoot 的自动配置机制
//也是两个注解的合成注解@AutoConfigurationPackage@Import({AutoConfigurationImportSelector.class})public @interface EnableAutoConfiguration {}
@AutoConfigurationPackage:
自动配置包
@Import(AutoConfigurationPackages.Registrar.class) //给容器中导入一个组件public @interface AutoConfigurationPackage {}//利用Registrar给容器中导入一系列组件//将指定的一个包下的所有组件导入进来?MainApplication 所在包下。
@Import(AutoConfigurationImportSelector.class)
1、利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件2、调用List
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
按需开启自动配置项
虽然我们127个场景的所有自动配置启动的时候默认全部加载。xxxxAutoConfiguration按照条件装配规则(@Conditional),最终会按需配置。
修改默认配置
@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 incorrectlyreturn resolver;}给容器中加入了文件上传解析器;
SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先
@Bean@ConditionalOnMissingBeanpublic CharacterEncodingFilter characterEncodingFilter() { }
总结:
● SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration● 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定● 生效的配置类就会给容器中装配很多组件● 只要容器中有这些组件,相当于这些功能就有了● 定制化配置 ○ 用户直接自己@Bean替换底层的组件 ○ 用户去看这个组件是获取的配置文件什么值就去修改。xxxxxAutoConfiguration ---> 组件 ---> xxxxProperties里面拿值 ----> application.properties
最佳实践
引入场景依赖
- 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;
- ......
开发小技巧
Lombok
- import lombok.Data(@Data):生成已有属性的geter和seter方法
- import lombok.ToString(@ToString):生成已有属性的toString方法
- import lombok.NoArgsConstructor(@NoArgsConstructor):生成已有属性的无参构造器
- import lombok.AllArgsConstructor(@AllArgsConstructor):生成已有属性的全参构造器
- import lombok.EqualsAndHashCode(@EqualsAndHashCode):重写我们的Equals和HashCode方法
- import lombok.slf4j(@slf4j):相当于我们的日志类
org.projectlombok lombok idea中搜索安装lombok插件
===============================简化JavaBean开发===================================@NoArgsConstructor//@AllArgsConstructor@Data@ToString@EqualsAndHashCodepublic class User { private String name; private Integer age; private Pet pet; public User(String name,Integer age){ this.name = name; this.age = age; }}================================简化日志开发===================================@Slf4j@RestControllerpublic class HelloController { @RequestMapping("/hello") public String handle01(@RequestParam("name") String name){ log.info("请求进来了...."); return "Hello, Spring Boot 2!"+"你好:"+name; }}
dev-tools
org.springframework.boot spring-boot-devtools true
项目或者页面修改以后:Ctrl+F9;
Spring Initailizr(项目初始化向导)
选择我们需要的开发场景
2.1、自动依赖引入
自动创建项目结构
4.自动编写好主配置类
1、自动依赖引入
SpringBoot2核心功能
配置文件
文件类型
properties
同以前的properties用法
yaml
简介
- YAML 是 "YAML Ain"t Markup Language"(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)。非常适合用来做以数据为中心的配置文件
基本语法
- key: value;kv之间有空格
- 大小写敏感
- 使用缩进表示层级关系
- 缩进不允许使用tab,只允许空格
- 缩进的空格数不重要,只要相同层级的元素左对齐即可
- "#"表示注释
- 字符串无需加引号,如果要加,""与""表示字符串内容 会被 转义/不转义
数据类型
字面量:单个的、不可再分的值。date、boolean、string、number、null
k: v
对象:键值对的集合。map、hash、set、object
行内写法: k: {k1:v1,k2:v2,k3:v3}#或k: k1: v1 k2: v2 k3: v3
数组:一组按次序排列的值。array、list、queue
行内写法: k: [v1,v2,v3]#或者k: - v1 - v2 - v3
提示:
- 在yaml文件格里面式,单引号会将“\n”简单的字符串输出
- 在yaml文件格里面式,双引号会将“\n”则会作为转义符
配置提示
自定义的类和配置文件绑定一般没有提示。
org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-maven-plugin org.springframework.boot spring-boot-configuration-processor
web开发
SpringMVC自动配置概览
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)
The auto-configuration adds the following features on top of Spring’s defaults:
Inclusion of
ContentNegotiatingViewResolver
andBeanNameViewResolver
beans.- 内容协商视图解析器和BeanName视图解析器
Support for serving static resources, including support for WebJars (covered later in this document)).
- 静态资源(包括webjars)
Automatic registration of
Converter
,GenericConverter
, andFormatter
beans.- 自动注册
Converter,GenericConverter,Formatter
- 自动注册
Support for
HttpMessageConverters
(covered later in this document).- 支持
HttpMessageConverters
(后来我们配合内容协商理解原理)
- 支持
Automatic registration of
MessageCodesResolver
(covered later in this document).- 自动注册
MessageCodesResolver
(国际化用)
- 自动注册
Static
index.html
support.- 静态index.html 页支持
Custom
Favicon
support (covered later in this document).- 自定义
Favicon
- 自定义
Automatic use of a
ConfigurableWebBindingInitializer
bean (covered later in this document).- 自动使用
ConfigurableWebBindingInitializer
,(DataBinder负责将请求数据绑定到JavaBean上)
- 自动使用
If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own
@Configuration
class of typeWebMvcConfigurer
but without@EnableWebMvc
.不用@EnableWebMvc注解。使用
**@Configuration**
+**WebMvcConfigurer**
自定义规则If you want to provide custom instances of
RequestMappingHandlerMapping
,RequestMappingHandlerAdapter
, orExceptionHandlerExceptionResolver
, and still keep the Spring Boot MVC customizations, you can declare a bean of typeWebMvcRegistrations
and use it to provide custom instances of those components.声明
**WebMvcRegistrations**
改变默认底层组件If you want to take complete control of Spring MVC, you can add your own
@Configuration
annotated with@EnableWebMvc
, or alternatively add your own@Configuration
-annotatedDelegatingWebMvcConfiguration
as described in the Javadoc of@EnableWebMvc
.使用
**@EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC**
简单功能分析
静态资源访问
静态资源目录
只要静态资源放在类路径下: called
/static
(or/public
or/resources
or/META-INF/resources
访问:当前项目根路径/+静态资源名
原理:静态映射/**
请求进来先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则直接报404
改变默认的静态资源路径
spring: mvc: static-path-pattern: /res/**#改变姿态资源路径 resources: static-locations: [classpath:/haha/]
静态资源访问前缀
默认无前缀
spring: mvc: static-path-pattern: /res/**
webjar
自动映射 /webjars/**
https://www.webjars.org/
org.webjars jquery 3.5.1 访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js后面地址要按照依赖里面的包路径
欢迎页支持
静态资源路径下 index.html
- 可以配置静态资源路径
- 但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问
spring:# mvc:# static-path-pattern: /res/** 这个会导致welcome page功能失效 resources: static-locations: [classpath:/haha/]
2.3、自定义 Favicon
favicon.ico 放在静态资源目录下即可。
spring:# mvc:# static-path-pattern: /res/** 这个会导致 Favicon 功能失效
静态资源配置原理
- SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
- SpringMVC功能的自动配置类 WebMvcAutoConfiguration,生效
@Configuration(proxyBeanMethods = false)@ConditionalOnWebApplication(type = Type.SERVLET)@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,ValidationAutoConfiguration.class })public class WebMvcAutoConfiguration {}
- 给容器中配了什么。
@Configuration(proxyBeanMethods = false)@Import(EnableWebMvcConfiguration.class)@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })@Order(0)public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}
- 配置文件的相关属性和xxx进行了绑定。WebMvcPropertiesspring.mvc、ResourcePropertiesspring.resources
配置类只有一个有参构造器
//有参构造器所有参数的值都会从容器中确定//ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象//WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象//ListableBeanFactory beanFactory Spring的beanFactory//HttpMessageConverters 找到所有的HttpMessageConverters//ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。=========//DispatcherServletPath //ServletRegistrationBean 给应用注册Servlet、Filter....public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,ListableBeanFactory beanFactory, ObjectProvider messageConvertersProvider,ObjectProvider resourceHandlerRegistrationCustomizerProvider,ObjectProvider dispatcherServletPath,ObjectProvider> servletRegistrations) {this.resourceProperties = resourceProperties;this.mvcProperties = mvcProperties;this.beanFactory = beanFactory;this.messageConvertersProvider = messageConvertersProvider;this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();this.dispatcherServletPath = dispatcherServletPath;this.servletRegistrations = servletRegistrations;}
spring:# mvc:# static-path-pattern: /res/** resources: add-mappings: false 禁用所有静态资源规则
@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/" };/** * Locations of static resources. Defaults to classpath:[/META-INF/resources/, * /resources/, /static/, /public/]. */private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
欢迎页的处理规则
HandlerMapping:处理器映射。保存了每一个Handler能处理哪些请求。@Beanpublic 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 welcomePage, String staticPathPattern) {if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) { //要用欢迎页功能,必须是/**logger.info("Adding welcome page: " + welcomePage.get());setRootViewName("forward:index.html");}else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) { // 调用Controller /indexlogger.info("Adding welcome page template: index");setRootViewName("index");}}
请求参数处理
- 请求映射
rest使用与原理
@xxxMapping;
Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
- 以前:**/getUser获取用户/deleteUser删除用户/editUser修改用户/saveUser保存用户
- 现在: /userGET-**获取用户DELETE-**删除用户PUT-**修改用户POST-**保存用户
- 核心Filter;HiddenHttpMethodFilter
- 用法: 表单method=post,隐藏域 _method=put
- SpringBoot中手动开启
- 扩展:如何把_method 这个名字换成我们自己喜欢的。
@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-张三"; }@Bean@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {return new OrderedHiddenHttpMethodFilter();}//自定义filter @Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter(){ HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter(); methodFilter.setMethodParam("_m"); return methodFilter; }
Rest原理(表单提交要使用REST的时候)
- 表单提交会带上_method=PUT
- 请求过来被HiddenHttpMethodFilter拦截
- 请求是否正常,并且是POST
- 获取到_method的值。
- 兼容以下请求;PUT.DELETE.PATCH
- 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。
- 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用****requesWrapper的。
Rest使用客户端工具,
- 如PostMan直接发送Put、delete等方式请求,无需Filter。
spring: mvc: hiddenmethod: filter: enabled: true #开启页面表单的Rest功能
请求映射原理
SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet-》doDispatch(
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {ModelAndView mv = null;Exception dispatchException = null;try {processedRequest = checkMultipart(request);multipartRequestParsed = (processedRequest != request);// 找到当前请求使用哪个Handler(Controller的方法)处理mappedHandler = getHandler(processedRequest); //HandlerMapping:处理器映射。/xxx->>xxxx
RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则。
所有的请求映射都在HandlerMapping中。
SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;
SpringBoot自动配置了默认 的 RequestMappingHandlerMapping
请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
- 如果有就找到这个请求对应的handler
- 如果没有就是下一个 HandlerMapping
我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping。自定义 HandlerMapping
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {if (this.handlerMappings != null) {for (HandlerMapping mapping : this.handlerMappings) {HandlerExecutionChain handler = mapping.getHandler(request);if (handler != null) {return handler;}}}return null;}
普通参数与基本注解
注解:
@PathVariable:映射 URL 绑定的[占位符]
@RequestHeader:能够将请求头中的变量值映射到控制器的参数中
@ModelAttribute:被
@ModelAttribute
注解的方法会在Controller
每个方法执行之前都执行,因此对于一个Controller
中包含多个URL的时候,要谨慎使用。@RequestParam:@RequestParam用于将请求参数区数据映射到功能处理方法的参数上。
@MatrixVariable:虽然从Spring 3.2就已经支持
@MatrixVariable
特性,但直至现在其依然为默认禁用的状态。我们需要手工配置开启才能使用。@Configuration public class SpringBootConfig implements WebMvcConfigurer { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } }
@CookieValue:@RequestHeader以及@CookieValue这两个注解用法类似,属性也相同,所以,写在一起。二者属性和RequestParam的属性一样,用法也几乎一样。
- @RequestHeader注解主要是将请求头的信息区数据,映射到功能处理方法的参数上。@CookieValue注解主要是将请求的Cookie数据,映射到功能处理方法的参数上。
@RequestBody:@RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的);而最常用的使用请求体传参的无疑是POST请求了,所以使用@RequestBody接收数据时,一般都用POST方式进行提交。在后端的同一个接收方法里,@RequestBody与@RequestParam()可以同时使用,@RequestBody最多只能有一个,而@RequestParam()可以有多个
@RestControllerpublic 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 以上的部分参数
@Overridepublic 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(); 获取到值的
数据响应与内容协商
视图解析与模板引擎
视图解析:SpringBoot默认是不支持JSP, 需要引入第三方模板引擎技术实现页面渲染
视图解析
视图解析原理流程
1、目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面。包括数据和视图地址
2、方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在ModelAndViewContainer
3、任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)。
4、****processDispatchResult 处理派发结果(页面改如何响应)
1、render(mv, request, response); 进行页面渲染逻辑
- 1、根据方法的String返回值得到 View对象【定义了页面的渲染逻辑】
- 1、所有的视图解析器尝试是否能根据当前返回值得到View对象
- 2、得到了 redirect:/main.html--> Thymeleaf new RedirectView()
- 3、ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
- 4、view.render(mv.getModelInternal(), request, response); 视图对象调用自定义的render进行页面渲染工作
- RedirectView 如何渲染【重定向到一个页面】
- 1、获取目标url地址
- 2、****response.sendRedirect(encodedURL);
视图解析:
- 返回值以 forward: 开始: new InternalResourceView(forwardUrl); --> 转发****request.getRequestDispatcher(path).forward(request, response);
- 返回值以redirect: 开始:new RedirectView() --》 render就是重定向
- 返回值是普通字符串: new ThymeleafView()--->
自定义视图解析器+自定义视图; 大厂学院。
模板引擎-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)
特殊操作
- 无操作: _
设置属性值-th:attr
设置单个值
设置多个值
以上两个的代替写法 th:xxxx
所有h5兼容的标签写法
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes
迭代
Onions 2.41 yes
Onions 2.41 yes
条件运算
view
User is an administrator
User is a manager
User is some other thing
属性优先级
thymeleaf使用
引入Starter
org.springframework.boot spring-boot-starter-thymeleaf
自动配置好了thymeleaf
@Configuration(proxyBeanMethods = false)@EnableConfigurationProperties(ThymeleafProperties.class)@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })public class ThymeleafAutoConfiguration { }
自动配号的策略
- 所有thymeleaf的配置值都在 ThymeleafProperties
- 配置好了 SpringTemplateEngine
- 配好了ThymeleafViewResolver
- 我们只需要直接开发页面
public static final String DEFAULT_PREFIX = "classpath:/templates/";public static final String DEFAULT_SUFFIX = ".html"; //xxx.html
页面开发
Title 哈哈
去百度
去百度2
拦截器
HandlerInterceptor 接口
/** * 登录检查 * 1、配置好拦截器要拦截哪些请求 * 2、把这些配置放在容器中 */@Slf4jpublic class LoginInterceptor implements HandlerInterceptor { /** * 目标方法执行之前 * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestURI = request.getRequestURI(); log.info("preHandle拦截的请求路径是{}",requestURI); //登录检查逻辑 HttpSession session = request.getSession(); Object loginUser = session.getAttribute("loginUser"); if(loginUser != null){ //放行 return true; } //拦截住。未登录。跳转到登录页 request.setAttribute("msg","请先登录");// re.sendRedirect("/"); request.getRequestDispatcher("/").forward(request,response); return false; } /** * 目标方法执行完成以后 * @param request * @param response * @param handler * @param modelAndView * @throws Exception */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { log.info("postHandle执行{}",modelAndView); } /** * 页面渲染以后 * @param request * @param response * @param handler * @param ex * @throws Exception */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info("afterCompletion执行异常{}",ex); }}
配置拦截器
/** * 1、编写一个拦截器实现HandlerInterceptor接口 * 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors) * 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】 */@Configurationpublic class AdminWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") //所有请求都被拦截包括静态资源 .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行的请求 }}
拦截器原理
1、根据当前请求,找到HandlerExecutionChain【可以处理请求的handler以及handler的所有 拦截器】
2、先来顺序执行所有拦截器的 preHandle方法
- 1、如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
- 2、如果当前拦截器返回为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion;
3、如果任何一个拦截器返回false。直接跳出不执行目标方法
4、所有拦截器都返回True。执行目标方法
5、倒序执行所有拦截器的postHandle方法。
6、前面的步骤有任何异常都会直接倒序触发afterCompletion
7、页面成功渲染完成以后,也会倒序触发 afterCompletion
文件上传
页面表单
上传文件代码
/** * MultipartFile 自动封装上传过来的文件 * @param email * @param username * @param headerImg * @param photos * @return */ @PostMapping("/upload") public String upload(@RequestParam("email") String email, @RequestParam("username") String username, @RequestPart("headerImg") MultipartFile headerImg, @RequestPart("photos") MultipartFile[] photos) throws IOException { log.info("上传的信息:email={},username={},headerImg={},photos={}", email,username,headerImg.getSize(),photos.length); if(!headerImg.isEmpty()){ //保存到文件服务器,OSS服务器 String originalFilename = headerImg.getOriginalFilename(); headerImg.transferTo(new File("H:\\cache\\"+originalFilename)); } if(photos.length > 0){ for (MultipartFile photo : photos) { if(!photo.isEmpty()){ String originalFilename = photo.getOriginalFilename(); photo.transferTo(new File("H:\\cache\\"+originalFilename)); } } } return "main"; }
自动配置原理
文件上传自动配置类-MultipartAutoConfiguration-****MultipartProperties
自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
原理步骤
- 1、请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
- 2、参数解析器来解析请求中的文件内容封装成MultipartFile
- 3、将request中文件信息封装为一个Map;MultiValueMap
FileCopyUtils。实现文件流的拷贝
@PostMapping("/upload") public String upload(@RequestParam("email") String email, @RequestParam("username") String username, @RequestPart("headerImg") MultipartFile headerImg, @RequestPart("photos") MultipartFile[] photos)
异常处理
错误处理
默认规则
默认情况下,Spring Boot提供
/error
处理所有错误的映射对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
要对其进行自定义,添加
**View**
解析为****error- 要完全替换默认行为,可以实现
ErrorController
并注册该类型的Bean定义,或添加ErrorAttributes类型的组件
以使用现有机制但替换其内容。 - error/下的4xx,5xx页面会被自动解析;
- 要完全替换默认行为,可以实现
指定错误逻辑处理
- 自定义错误页
- error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
@ControllerAdvice+@ExceptionHandler处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的
@ResponseStatus+自定义异常 ;底层是 ResponseStatusExceptionResolver ,把responsestatus注解的信息底层调用response.sendError(statusCode, resolvedReason);tomcat发送的/error
Spring底层的异常,如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常。
- response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则
ErrorViewResolver实现自定义处理异常;
- response.sendError 。error请求就会转给controller
- 你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给controller
- basicErrorController 要去的页面地址是ErrorViewResolver;
异常处理自动配置原理
- ErrorMvcAutoConfiguration 自动配置异常处理规则
- 容器中的组件:类型:DefaultErrorAttributes ->id:errorAttributes
- public classDefaultErrorAttributesimplementsErrorAttributes, HandlerExceptionResolver
- DefaultErrorAttributes:定义错误页面中可以包含哪些数据。
- 容器中的组件:类型:****BasicErrorController --> id:basicErrorController(json+白页 适配响应)
- 处理默认/error 路径的请求;页面响应newModelAndView("error", model);
- 容器中有组件 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处理器异常解析器】
系统默认的 异常解析器;
- 1、DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,并且返回null;
- 2、默认没有任何人能处理异常,所以异常会被抛出
- 1、如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理
- 2、解析错误视图;遍历所有的ErrorViewResolver 看谁能解析。
- 3、默认的DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html
- 4、模板引擎最终响应这个页面error/500.html
Web原生组件注入(Servlet、Filter、Listener)
1、使用Servlet API
@ServletComponentScan(basePackages = "com.atguigu.admin") :指定原生Servlet组件都放在那里
@WebServlet(urlPatterns = "/my"):效果:直接响应,没有经过Spring的拦截器?
@WebFilter(urlPatterns={"/css/*","/images/*"})
@WebListener
推荐可以这种方式;
扩展:DispatchServlet 如何注册进来
- 容器中自动配置了 DispatcherServlet 属性绑定到 WebMvcProperties;对应的配置文件配置项是 spring.mvc。
- 通过ServletRegistrationBean
把 DispatcherServlet 配置进来。 - 默认映射的是 / 路径。
Tomcat-Servlet;
多个Servlet都能处理到同一层路径,精确优选原则
A: /my/
B: /my/1
2、使用RegistrationBean
ServletRegistrationBean`, `FilterRegistrationBean`, and `ServletListenerRegistrationBean
@Configurationpublic class MyRegistConfig { @Bean public ServletRegistrationBean myServlet(){ MyServlet myServlet = new MyServlet(); return new ServletRegistrationBean(myServlet,"/my","/my02"); } @Bean public FilterRegistrationBean myFilter(){ MyFilter myFilter = new MyFilter();// return new FilterRegistrationBean(myFilter,myServlet()); FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter); filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*")); return filterRegistrationBean; } @Bean public ServletListenerRegistrationBean myListener(){ MySwervletContextListener mySwervletContextListener = new MySwervletContextListener(); return new ServletListenerRegistrationBean(mySwervletContextListener); }}
嵌入式Servlet容器
切换嵌入式Servlet容器
默认支持的webServer
Tomcat
,Jetty
, orUndertow
ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器
切换服务器
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
, orUndertowServletWebServerFactory
底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration
ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize---this.tomcat.start();
内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)
2、定制Servlet容器
实现 WebServerFactoryCustomizer
- 把配置文件的值和
**ServletWebServerFactory 进行绑定**
- 把配置文件的值和
修改配置文件 server.xxx
直接自定义 ConfigurableServletWebServerFactory
xxxxxCustomizer:定制化器,可以改变xxxx的默认规则
import org.springframework.boot.web.server.WebServerFactoryCustomizer;import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;import org.springframework.stereotype.Component;@Componentpublic class CustomizationBean implements WebServerFactoryCustomizer { @Override public void customize(ConfigurableServletWebServerFactory server) { server.setPort(9000); }}
定制化原理
1、定制化的常见方式
- 修改配置文件;
- xxxxxCustomizer;
- 编写自定义的配置类 xxxConfiguration;+@Bean替换、增加容器中默认组件;视图解析器
- Web应用 编写一个配置类实现WebMvcConfigurer 即可定制化web功能;+ @Bean给容器中再扩展一些组件
@Configurationpublic class AdminWebConfig implements WebMvcConfigurer
@Configurationpublic class AdminWebConfig implements WebMvcConfigurer
2、原理分析套路
场景starter- xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties --绑定配置文件项
数据访问
SQL
1、数据源的自动配置-HikariDataSource
导入JDBC场景
org.springframework.boot spring-boot-starter-data-jdbc
数据库驱动?
为什么导入JDBC场景,官方不导入驱动?官方不知道我们接下要操作什么数据库。
数据库版本和驱动版本对应
默认版本:8.0.22 mysql mysql-connector-java 想要修改版本1、直接依赖引入具体版本(maven的就近依赖原则)2、重新声明版本(maven的属性的就近优先原则) 1.8 5.1.49
分析自动配置
自动配置的类
DataSourceAutoConfiguration : 数据源的自动配置
- 修改数据源相关的配置:spring.datasource
- 数据库连接池的配置,是自己容器中没有DataSource才自动配置的
- 底层配置好的连接池是:HikariDataSource
@Configuration(proxyBeanMethods = false) @Conditional(PooledDataSourceCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class, DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class }) protected static class PooledDataSourceConfiguration
DataSourceTransactionManagerAutoConfiguration: 事务管理器的自动配置
JdbcTemplateAutoConfiguration: JdbcTemplate的自动配置,可以来对数据库进行crud
- 可以修改这个配置项@ConfigurationProperties(prefix = "spring.jdbc") 来修改JdbcTemplate
- @Bean@Primary JdbcTemplate;容器中有这个组件
JndiDataSourceAutoConfiguration: jndi的自动配置
XADataSourceAutoConfiguration: 分布式事务相关的
修改配置项
spring: datasource: url: jdbc:mysql://localhost:3306/db_account username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver
测试
@Slf4j@SpringBootTestclass Boot05WebAdminApplicationTests { @Autowired JdbcTemplate jdbcTemplate; @Test void contextLoads() {// jdbcTemplate.queryForObject("select * from account_tbl")// jdbcTemplate.queryForList("select * from account_tbl",) Long aLong = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class); log.info("记录总数:{}",aLong); }}
2、使用Druid数据源
druid官方github地址:https://github.com/alibaba/druid
整合第三方技术的两种方式
- 自定义
- 找starter
自定义方式
- 创建数据源
com.alibaba druid 1.1.17 StatViewServlet
StatViewServlet的用途包括:
提供监控信息展示的html页面
提供监控信息的JSON API
DruidStatView com.alibaba.druid.support.http.StatViewServlet DruidStatView /druid/*
StatFilter
用于统计监控信息;如SQL监控、URI监控
需要给数据源中配置如下属性;可以允许多个filter,多个用,分割;如:
系统中所有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
慢SQL配置
使用 slowSqlMillis 定义慢SQL的时长
使用官方starter方式
引入druid-starter
com.alibaba druid-spring-boot-starter 1.1.17
分析自动配置
- 扩展配置项 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的配置
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";
配置示例
spring: datasource: url: jdbc:mysql://localhost:3306/db_account 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: admin 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
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
整合MyBatis操作
https://github.com/mybatis
starter
SpringBoot官方的Starter:spring-boot-starter-*
第三方的: *-spring-boot-starter
org.mybatis.spring.boot mybatis-spring-boot-starter 2.1.4
- 配置模式
- 全局配置文件
- SqlSessionFactory: 自动配置好了
- SqlSession:自动配置了 SqlSessionTemplate 组合了SqlSession
- @Import(AutoConfiguredMapperScannerRegistrar.class);
- Mapper: 只要我们写的操作MyBatis的接口标准了 @Mapper 就会被自动扫描进来
@EnableConfigurationProperties(MybatisProperties.class) : MyBatis配置项绑定类。@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })public class MybatisAutoConfiguration{}@ConfigurationProperties(prefix = "mybatis")public class MybatisProperties
可以修改配置文件中 mybatis 开始的所有;
# 配置mybatis规则mybatis: config-location: classpath:mybatis/mybatis-config.xml #全局配置文件位置 mapper-locations: classpath:mybatis/mapper/*.xml #sql映射文件位置 Mapper接口--->绑定Xml
配置 privateConfiguration configuration; mybatis.configuration下面的所有,就是相当于改mybatis全局配置文件中的值
# 配置mybatis规则mybatis:# config-location: classpath:mybatis/mybatis-config.xml mapper-locations: classpath:mybatis/mapper/*.xml configuration: map-underscore-to-camel-case: true 可以不写全局;配置文件,所有全局配置文件的配置都放在configuration配置项中即可
导入mybatis官方starter
编写mapper接口。标准@Mapper注解
编写sql映射文件并绑定mapper接口
在application.yaml中指定Mapper配置文件的位置,以及指定全局配置文件的信息 (建议;配置在mybatis.configuration)
2.注解模式
@Mapperpublic interface CityMapper { @Select("select * from city where id=#{id}") public City getById(Long id); public void insert(City city);}
3.混合模式
@Mapperpublic interface CityMapper { @Select("select * from city where id=#{id}") public City getById(Long id); public void insert(City city);}
最佳实战:
- 引入mybatis-starter
- 配置application.yaml中,指定mapper-location位置即可
- 编写Mapper接口并标注@Mapper注解
- 简单方法直接注解方式
- 复杂方法编写mapper.xml进行绑定映射
- @MapperScan("com.atguigu.admin.mapper") 简化,其他的接口就可以不用标注@Mapper注解
4、整合 MyBatis-Plus 完成CRUD
什么是MyBatis-Plus
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
mybatis plus 官网
建议安装 MybatisX插件
整合MyBatis-Plus
com.baomidou mybatis-plus-boot-starter 3.4.1 自动配置
- 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功能
@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"; } @Servicepublic class UserServiceImpl extends ServiceImpl
implements UserService {}public interface UserService extends IService {}
NoSQL
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
1、Redis自动配置
org.springframework.boot spring-boot-starter-data-redis
自动配置:
- RedisAutoConfiguration 自动配置类。RedisProperties 属性类 --> spring.redis.xxx是对redis的配置
- 连接工厂是准备好的。LettuceConnectionConfiguration、JedisConnectionConfiguration
- 自动注入了RedisTemplate<Object, Object> : xxxTemplate;
- 自动注入了StringRedisTemplate;k:v都是String
- key:value
- 底层只要我们使用StringRedisTemplate、****RedisTemplate就可以操作redis
redis环境搭建
1、阿里云按量付费redis。经典网络
2、申请redis的公网连接地址
3、修改白名单 允许0.0.0.0/0 访问
2、RedisTemplate与Lettuce
@Test void testRedis(){ ValueOperations operations = redisTemplate.opsForValue(); operations.set("hello","world"); String hello = operations.get("hello"); System.out.println(hello); }
3、切换至jedis
org.springframework.boot spring-boot-starter-data-redis redis.clients jedis
spring: redis: host: r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.com port: 6379 password: lfy:Lfy123456 client-type: jedis jedis: pool: max-active: 10
JUnit5单元测试
生产指标监控
SpringBoot核心原理解析
SpringBoot2场景整合
虚拟化技术
安全控制
缓存技术
消息中间件
分布式入门
SpringBoot2响应式编程
焦点速递!SpringBoot
2023年跳槽,.NET工程师们,准备好了吗?
徐鸿平
快看点丨努比亚首款AR眼镜官宣:将亮相MWC 2023
全球动态:更耐摔?苹果上架iPhone 14 Pro屏幕保护膜:348元买吗
焦点快播:女车主买奇瑞QQ冰淇淋电动车一天坏3次!4S店承认故障 退车
天天速读:3月见!魅族20系列宣布首发无界生态系统Flyme 10
Reno时隔4年再度回归潜望长焦!OPPO Reno 10系列曝光
有个n字的鞋子是什么牌子?国产运动品牌有哪些?
贡菜是什么菜?贡菜的功效与作用是什么?
四王下山管天下是哪四个王?四王下山管天下是什么生肖?
春雨贵如油是什么节气?密密的春雨像什么比喻?
二十不惑人物关系是什么?二十不惑2演员表
iphone屏幕分辨率是多少?iphone屏幕分辨率怎么调?
天天热推荐:[django]钩子函数的一些细节(clean)
环球短讯!火山引擎入选《2022 爱分析 · DataOps 厂商全景报告》,旗下 DataLeap 产品能力获认可
快播:ETL的系统核心特征
环球热议:SQL Server创建Oracle链接服务器
电脑自动重启是什么原因?电脑自动重启频繁怎么解决?
CAD布局怎么使用?cad布局背景怎么调成黑色?
gta5怎么切换角色?GTA5怎么刷钱?
deb是什么意思?deb怎么安装?
世界热讯:BBA直呼内行!吉利“仰望银河”出圈:多家国产品牌跟风“望天”
天天时讯:首发1399元!小米小顽智能全自动猫砂盆开售:14天不用管
苹果iOS 16.4首个测试版发布 新增15个表情符号被吐槽太丑
今日热文:网传杭州取消限行 官方辟谣:假的、Chatgpt撰写生成
《狂野之心》IGN 8分:足以媲美《怪猎》狩猎大作
天天视讯!银河麒麟V10系统安装步骤
Java 文件上传
世界新动态:机器学习-集成学习GBDT
如何压缩图片大小?简单的图片压缩方法分享
世界视点!一降再降!一汽丰田首款纯电SUV大促:优惠6万到手13.98万起
祈福者放生2.5万斤鲇鱼被追责:属外来物种 危害极大
合资燃油车危险了!比亚迪秦PLUS DM-i冠军版上市7天订单超3.2万
当前动态:促销“狂飙”结束 特斯拉Model Y全系已上涨2000元
微软进一步拥抱AI:Win11新功能“记住”用户常用窗口布局
世界热点!《分布式技术原理与算法解析》学习笔记Day14
环球最新:Django Cannot assign "A1": "B1" must be a "C1" instance.
把选择语句和重复语句视为块
营收创6年最大跌幅!苹果被曝裁员外包员工 库克最后手段?
时隔3年半漫威重返内地!《蚁人与黄蜂女:量子狂潮》今日上映
天天热头条丨《星际争霸2》世界冠军李培楠回国:黄旭东等为其接风
当前视讯!三亚3米长搁浅抹香鲸救治无效不幸死亡:或被鲨鱼咬伤
全球看热讯:干货分享丨店长管理的五大核心你抓住了吗!
当前要闻:没买车的等等党们:赢了
每日看点!男子购买1200元年货32天快递仍未送达 邮政客服:车还在路上
每日动态!注意!特斯拉宣布召回超36万辆车 会突然停车发生车祸:马斯克火速回应
环球新动态:iPadOS 16:拖后腿了
读Java实战(第二版)笔记12_重构、测试和调试
【环球报资讯】2023开年火药味儿十足 “大内存”机型卷出了新高度
今日播报!期末复习——同步、互斥、死锁
男子卖出游戏账号后3次恶意找回 为何被判盗窃而不是诈骗?
世界热推荐:一年节省185亿度电!中车研制复合材料“超级铜”登上央视
魔兽国服重开遥遥无期 消息称暴雪找不到接盘方:能谈的都试了
每日播报!我想知道知乎现在是怎么了?
女子询问换杯 却被奈雪店员辱骂:拒绝出面道歉
即时看!70城新房价格指数时隔12个月首次止跌
全球观天下!苹果7喇叭灰尘怎么清理 苹果7喇叭灰尘清理的方法
世界微资讯!汉诺塔
观察:真正“搞”懂HTTPS协议19之HTTPS优化
pnpm的基本原理及快速使用
环球焦点!Windows 环境下安装与配置 Node.js
fusion app远程公告(微云)
全球微动态丨北京多个地铁口有人扫码送“大鹅”:小心隐私泄露
游戏《如龙7外传》夜店真人女郎上新:一大波日本纯美小姐姐
二叉树中某一值的路径之 先序遍历 + 二叉搜索树转化为循环双向链表 之 中序遍历
全球快播:设计模式(八)----创建型模式之建造者模式与工厂模式区别
【全球热闻】真实项目开发中高并发实战经验总结
环球讯息:Python 爬虫方法总结
刘德华剧透《流浪地球3》:"数字生命"延续
重点聚焦!被苹果踢出果链后淡化手机业务?欧菲光回应:指纹识别还是龙头地位
天天微头条丨Vue急速入门-3
Python多任务教程
每日短讯:数据结构刷题2023.02.16小记
全球今日讯!Xcode Swift自动格式化
环球观天下!反结账的钱退到哪里_反结账快捷键
豪掷7500万!ChatGPT开发商OpenAI买下极品域名AI.com
环球聚焦:为了搞死《霍格沃茨之遗》“黑巫师”们用尽了魔法
每日动态!【2023.02.16】威佐夫博弈详解
今日热闻!女子丽江60元买2串烧烤仅6只虫2只蝎 景区回应其吐槽:特色小吃贵
精准发力“二次元”群体:盈通推出“樱瞳水着”RTX 4080显卡
当前快讯:女子吃饭后疯狂打嗝 医生:赶紧转急诊
环球热推荐:全球只有18块!极品飞车特别版RTX 4090现身海鲜市场:要价1.6万
python处理xml文件
天天时讯:因衣服长时间放购物车被客服嘲讽 还被拉黑:遭无数网友吐槽
【热闻】交个朋友公司被曝欠薪、恶意裁员 罗永浩回应:第一时间协调妥善解决
每日快看:1099元 微星PRO MP161便携式显示屏来了:15.6寸IPS屏 仅重0.75kg
高速路上一觉醒来车在冒烟无人驾驶:副驾小伙慌忙跳车逃生
世界时讯:联想拯救者Y9000P 2023游戏本性能狂飙:满血RTX 4090、240Hz高刷
江西芦溪:田园春意浓 乡村美如画
记录--TS类型写不好?一起来训练提升吧!
判断一个给定数组是否为二叉搜索树后序遍历
防患未然 | AIRIOT城市管廊智能运维解决方案
全球今亮点!xcodebuild命令行工具使用详解
今日热文:欧洲2035禁售燃油车!专家:电动车难完全取代燃油车
环球焦点!Wind11新预览版25300发布:实时字幕对中国用户更加友好!
当前时讯:新日登北京电动自行车不合格“黑榜”:多批电池管理系统有问题
新消息丨不愧是玩无人机的 大疆车载产品矩阵揭秘:高阶辅助只需摄像头
这合理吗?公厕免费取纸先关注微信公众号
快资讯丨pip设置镜像