博主个人博客已经搭建好,可以移步访问 http://lss-coding.top/
本文图片丢失,后期有时间在补充上
SpringBoot 就是一个 javaweb 开发框架,与 SpringMVC 类似,相较于其他框架好处,简化开发,约定大于配置,能够迅速开发 web 应用。
所有的技术框架的发展似乎都遵循了一条主线规律:从一个复杂应用场景衍生一种规范框架,只需要进行各种配置而不需要自己去实现,这时候强大的配置功能成了优点;发展到一定程度后,根据实际生产应用情况,选取其中实用功能和设计精华,重构出一些轻量级的框架,之后为了提高开发效率,嫌弃原先的各类配置过于麻烦,于是开始提倡 约定大于配置,进而衍生出一些一站式的解决方案。
随着 Spring 不断的发展,涉及的领域越来越多,项目整合开发需要配合各种各样的文件,慢慢变得不那么易用简单,违背了最初的理念,甚至人称配置地狱。SpringBoot 正是在这样的一个背景下被抽象出来的开发框架,目的为了让大家更容易的使用 Spring,更容易的集成各种常用的中间件,开源软件。
Spring Boot 基于Spring 开发,SpringBoot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速,敏捷地开发新一代基于 Spring 框架的应用程序。也就是说,它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。SpringBoot以 约定大于配置的核心思想,默认帮我们进行了很多设置,多数 SpringBoot 应用只需要很少的 Spring 配置。同时它集成了大量常用的第三方库配置,例如:Redis,MongoDB 等,SpringBoot 应用中这些第三方库几乎可以零配置的开箱即用。
简单来说 SpirngBoot 不是什么新的框架,它默认配置了很多框架的使用方式,就像 maven 整合了所有的 jar 包,SpringBoot 整合了所有的框架。生态完善。
主要优点:
随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用框架已无法应对,分布式框架以及流行计算框架势在必行,需要一个治理系统确保框架有条不紊的演进。
微服务就是一种框架风格,要求我们在开发一个应用的时候,这个应用必须构建成一系列小服务的组合,可以通过 http 的方式进行互通,要说微服务架构,先说单体应用框架。
单一应用框架
所谓单体应用框架(all in one)是指我们将一个应用中的所有应用服务封装在一个应用中。
无论什么系统,都把数据库访问,web访问等各个功能放到一个 war 包中
垂直应用框架
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,提升效率的方法之一是将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的 web 框架(MVC)是关键
分布式服务框架
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。
流动计算架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。
微服务架构
all in one的架构方式,我们把所有的功能单元放在一个应用里面。然后我们把整个应用部署到服务器上。如果负载能力不行,我们将整个应用进行水平复制,进行扩展,然后在负载均衡。
所谓微服务架构,就是打破之前all in one的架构方式,把每个功能元素独立出来。把独立出来的功能元素的动态组合,需要的功能元素才去拿来组合。需要多一些时可以整合多个功能元素。所以微服务架构是对功能元素进行复制,而没有对整个应用进行复制。
**好处:**节省了调用资源;每个功能元素的服务调度是一个可替换的,可独立升级的软件代码。
这篇文章阐述了什么是微服务:
一个大型系统的微服务框架就像一个复杂交织的神经网络,每一个神经元素就是一个功能元素,它们各自完成自己的功能,然后通过 http 相互请求调用。比如一个电商系统,查缓存、连接数据库、浏览页面、结账、支付等服务都是一个个独立的功能服务,被微服务了,它们作为一个个微服务共同构建了一个庞大的系统,如果修改其中一个功能,只需要更新升级其中一个功能服务单元就可以。
但是这种庞大的系统架构给部署和运维带来很大的难度,于是 Spring 为我们带来了构建大型分布式微服务的全套,全程产品:
下载好的 zip 压缩包解压后,打开 idea import 就可以使用了
删掉无用的文件就行
启动测试访问 8080 端口就可以了,因为是初始化的项目,所以会访问 localhost:8080/error
新建文件夹 controller ,新建 HelloSpringBoot.java
@Controller
public class HelloSpringBoot {
@RequestMapping("/hello")
@ResponseBody
public String hello(){
return "first SprongBoot Project";
}
}
然后启动访问这个接口即可
至此创建完成
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.4.2version>
<relativePath/>
parent>
<groupId>com.examplegroupId>
<artifactId>springboot-01-helloworldartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>springboot-01-helloworldname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
HelloSpringBoot.java
@RestController
public class HelloSpringBoot {
@RequestMapping("/hello")
public String hello(){
return "Hello,World";
}
}
测试访问
如果遇到错误,可以配置打包时跳过项目运行测试用例:
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-surefire-pluginartifactId>
<configuration>
<skipTests>trueskipTests>
configuration>
plugin>
打包完成后,会在 target 目录下生成一个 jar 包
在 resources 目录下新建一个 banner.txt 文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6KjfKkGT-1628259661085)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210617112226390.png)]
按住 Ctrl 点进去就可以
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6FcYB0aH-1628259661086)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210617112257888.png)]
同样的方法再次点进去就可以看到大量的 jar 包的版本管理信息了。我们在写或者引入一些 SpringBoot 依赖的时候,不需要指定版本,因为有这些版本仓库。
启动器
启动器说白了就是 SpringBoot 的启动场景;比如 spring-boot-starter-web,他就会帮我们自动导入web环境的所有的依赖,springboot会将所有的功能场景都变成一个个的启动器。需要使用什么功能,就需要找到相对应的启动器就可以了。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
//@SpringBootApplication:标注这个类是一个 springboot 的应用
@SpringBootApplication
public class Springboot01HelloworldApplication {
public static void main(String[] args) {
//将springboot应用启动
SpringApplication.run(Springboot01HelloworldApplication.class, args);
}
}
按住 Ctrl 点 @SpringBootApplication 进去源码
@SpringBootConfiguration:springboot的配置
@Configuration:spring配置类
@Component:说明这个也是一个 spring 组件
@EnableAutoConfiguration:自动配置
@AutoConfigurationPackage:自动配置包
@Import({Registrar.class}):自动配置`包注册`
@Import({AutoConfigurationImportSelector.class}):自动配置导入选择
//获取所有的配置
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
//获取候选的配置
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
//首先通过加载器加载所有的配置
List<String> configurations =
SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S7Wi3eWW-1628259661087)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210617123325396.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-65yNFEJK-1628259661089)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210617123656887.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kwl97iX1-1628259661090)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210617124813243.png)]
总结:springboot 所有的自动配置都是在启动的时候扫描并加载:spring.factories所有的自动配置类都在这里面,但是不一定生效,要判断条件是否成立,只要导入了对应的start,就有对应的启动器了,有了启动器,我们自动装配就会生效,然后就配置成功!
在springboot 中启动程序后不只是运行了一个main方法,而是开启了一个服务;
在 SpringBoot 中使用一个全局的配置文件,配置文件的名称是固定的
application.properties
key=value
application.yml
key:空格value
**配置文件作用:**修改 SpringBoot 自动配置的默认值,因为 SpringBoot 在底层都给我们自动配置好了。
这种语言以数据作为中心,而不是以标记语言为重点。
以前的配置文件大多数都是用 xml 配置:
xml
<server>
<port>8080<port>
server>
yaml
server:
prot: 8080
语法要求严格
- 空格不能省略
- 以缩进来控制层级关系
- 属性和值的大小写敏感
注意:
双引号不会转义字符串里面的特殊字符
name: “name \n aaa” 输出:name \n aaa
单引号会转义特殊字符
name: “name \n aaa” 输出:name 换行 aaa
# 1. 普通值,int,boolean,string ...
字符串默认不用加引号
name: li
# 2. 对象,Map格式
student:
name: li
age: 20
# 行内写法
student: {name: li,age: 20}
# 3. 数组(List,Set)
pets:
- cat
- dog
- pig
# 行内写法
pets: [cat,dog,pig]
#修改SpringBoot默认端口号
server:
port: 8081
创建一个 SpringBoot
编写实体类
@Component//注册bean 到容器中
public class Dog {
@Value("小黄")
private String name;
@Value("20")
private Integer age;
//省略 有无参构造,get/set 方法,toString方法
}
之前给 bean 注入属性值方法,@Value
@Component//注册bean 到容器中
public class Dog {
@Value("小黄")
private String name;
@Value("20")
private Integer age;
}
在 SpringBoot 的测试类下注入测试输出
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired//将 Dog 自动注入进来
Dog dog;
@Test
void contextLoads() {
System.out.println(dog);
}
}
结果成功输出,这是以前属性注入值的方法 @Value
新建 Person.java 类较复杂,引用 Dog 类
/*
@ConfigurationProperties作用:
将配置文件中配置的每一个属性的值映射到这个组件中
告诉 SpringBoot 将本类中的所有属性和配置文件中相关的配置进行绑定
参数:prefix = "person":将配置文件中的 person 下面的所有属性一一对应
*/
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
private String name;
private Integer age;
private Boolean happy;
private Date birthday;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;
//省略 有无参构造,get/set方法,toString 方法
}
在 resources 下创建 application.yaml 配置文件进行注入。
person:
name: li
age: 20
happy: false
birthday: 2001/01/01
maps: {k1: v1,k2: v2}
lists:
- music
- dance
dog:
name: 小黄
age: 1
提示:SpringBoot 配置注解处理器没有找到,需要进入官方文档导入依赖
有的之后官网可能进不去
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
配置好之后测试类测试
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired//将 person 自动注入进来
Person person;
@Test
void contextLoads() {
System.out.println(person);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U7CwBUYt-1628259661093)(D:\notes\note\SpringBoot\SpringBoot.assets\image-20210130122808661.png)]
加载 .properties 配置文件
新建 prop.properties 文件
name=小黄
Dog.java 类中注入
@PropertySource(value = "classpath:prop.properties")
@Component//注册bean 到容器中
public class Dog {
@Value("${name}")
private String name;
private Integer age;
//省略 有无参构造,get/set 方法,toString 方法
}
测试
输出中文会有乱码问题
File – setting – FileEncodings 进行设置
可以使用占位符
person:
#${random.int}
name: li${random.int}
#${person.birthday}
age: 20${person.birthday}_岁
happy: false
birthday: 2001/01/01
maps: {k1: v1,k2: v2}
lists:
- music
- dance
dog:
name: 小黄
age: 1
@ConfigurationProperties | @Value | |
---|---|---|
功能 | 批量注入配置文件中的属性 | 一个个指定 |
松散绑定 | 支持 | 不支持 |
SPEL | 不支持 | 支持 |
JSR303数据校验 | 支持 | 不支持 |
复杂性类型封装 | 支持 | 不支持 |
yaml properties 都可以获取值,但是推荐使用yaml
如果需要获取某个文件中的某一个值可以使用 @Value
但是如果编写一个 javaBean 来配置文件进行一一映射,直接使用 @ConfigurationProperties
@Component //注册bean
@ConfigurationProperties(prefix = "person")
@Validated //数据校验
public class Person {
@Email(message="邮箱格式错误") //name必须是邮箱格式
private String name;
}
运行结果在控制台,如果配置文件中 name 不是电子邮件地址
default message [不是一个合法的电子邮件地址];
使用数据校验可以保证数据的正确性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-APCgBLWj-1628259661095)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210617160101281.png)]
常见参数:
@NotNull(message="名字不能为空")
private String userName;
@Max(value=120,message="年龄最大不能查过120")
private int age;
@Email(message="邮箱格式错误")
private String email;
空检查
@Null 验证对象是否为null
@NotNull 验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty 检查约束元素是否为NULL或者是EMPTY.
Booelan检查
@AssertTrue 验证 Boolean 对象是否为 true
@AssertFalse 验证 Boolean 对象是否为 false
长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
@Length(min=, max=) string is between min and max included.
日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则
.......等等
除此以外,我们还可以自定义一些数据校验规则
在主配置文件中编写的时候,文件名可以是 application-{profile}.properties/yaml,用来指定多个环境版本。
4 个位置优先级:
使用 properties 配置文件需要建立多个文件
#比如在配置文件中指定使用dev环境,我们可以通过设置不同的端口号进行测试; #我们启动SpringBoot,就可以看到已经切换到dev下的配置了; spring.profiles.active=dev
yaml 的多文档
不需要建立多个文档
#选择激活那个模块
spring:
profiles:
active: test
---
server:
port: 8081
spring:
profiles:test
---
server:
port: 8082
spring:
profiles: dev #配置环境的名称
---
server:
port: 8083
spring:
profiles: prod #配置环境的名称
配置文件到底能写什么?
进入源码 spring.factories 文件,找到 HttpEncodingAutoConfiguratin.class 类进入
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cLhnv2ae-1628259661099)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210617202253592.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ll0LDkL6-1628259661100)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210617202545066.png)]
@Conditional扩展注解 | 作用(判断是否满足当前指定条件) |
---|---|
@ConditionalOnJava | 系统的Java版本是否符合要求 |
@ConditionalOnBean | 容器中存在指定Bean |
@ConditionalOnMissingBean | 容器中不存在指定Bean |
@ConditionalOnExpression | 满足SpEL表达式指定 |
@ConditionalOnClass | 系统中有指定的类 |
@ConditionalOnMissingClass | 系统中没有指定的类 |
@ConditionalOnSingleCandidate | 容器中只有一个指定的Bean,或者这个Bean是首选Bean |
@ConditionalOnProperty | 系统指定的属性是否有指定的值 |
@ConditionalOnResource | 类路径下是否存在指定资源文件 |
@ConditionalOnWebApplication | 当前是web环境 |
@ConditionalOnNotWebApplication | 当前不是web环境 |
@ConditionalOnJndi | JNDI存在指定项 |
自动装配的原理:
SpringBoot 启动会加载大量的自动配置类
我们看我们需要的功能有没有在 SpringBoot默认写好的自动配置类当中
我们再看这个自动配置类中到底配置了那些组件;(只要我们要用的组件存在其中,我们就不需要再手动配置了)
给容器中自动配置类添加组件的时候,会从properties类中获取某些属性,我们只需要在配置文件中指定这些属性的值即可
XXXAutoConfiguration:自动配置类;给容器中添加组件
XXXProperties:封装配置文件中相关属性
在我们这配置文件中能配置的东西,都存在一个固定的规律 XXXAutoConfiguration:会有默认值,如果需要使用修改在-->XXXProperties文件中修改 @ConfigurationProperties(prefix = "server") 这里绑定配置文件 这样我们就可以使用自定义的配置了 每一个XXXAutoConfiguration 类都是容器中的一个组件,最后都加入到容器中;用他们来做自动配置; 每一个自动配置类可以进行自动配置功能; 根据当前不同的条件判断,决定这个配置类是否生效 一旦这个配置类生效;这个配置类就会给容器中添加各种组件;这些组件的属性是从对应的properties 类中 获取的,这些类里面的每一个属性又是和配置文件绑定的; 所有在配置文件中能配置的属性都是在XXXProperties类中封装;配置文件能配置什么就可以参照某个功能对应的属性类 debug: true 可以通过这个查看哪些配置生效,那些不生效
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ecbRGYh7-1628259661100)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210618091620969.png)]
找到WebMvcAutoConfiguration.class 类,并找到方法:
//添加静态资源方法
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//这个判断,如果在yaml中添加属性则不生效,有(spring.mvc.servlet.path="...")
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
//只要是 "/webjars/**" 都会去 "classpath:/META-INF/resources/webjars/" 这里面找
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});
}
});
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K8zu9xwW-1628259661101)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210618094739745.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RVXbbBRn-1628259661102)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210618095020090.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-03xq9oaO-1628259661103)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210618100306688.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s7P1kC6W-1628259661105)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210618101343005.png)]
优先级:/resources > /static > /public
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aMRuRiya-1628259661106)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210618114448364.png)]
2.2.0之前的版本可以在源码中找到 setFavico 方法,之后找不见了,只需要将 favico.ico 文件放到静态资源路径下就可以使用了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rhHtRU3a-1628259661108)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210618150128592.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5EmiZTca-1628259661109)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210618150138082.png)]
前端交给我们的页面,是html页面。如果我们以前开发,我们需要把他们转换成jsp页面,jsp好处就是当我们查出一些数据转发到jsp页面以后,我们可以用jsp轻松实现数据的显示,及交互等。jsp支持非常强大的功能,包括能写java代码,但是,我们现在的这种情况,SpringBoot 这个项目首先是以jar的方式,不是war,其次,我们用的还是嵌入式的Tomcat,所以,他现在默认是不支持jsp的。
SpringBoot 推荐可以使用模板引擎。其实 jsp 就是一个模板引擎,还有用的比较多的freemarker,包括SpringBoot推荐使用的Thymeleaf,模板引擎有很多,但他们的思想都是一样。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4d5l628p-1628259661110)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210618150735748.png)]
模板引擎的作用就是我们来写一个页面模板,比如有些值是动态的,我们写一些表达式。而这些值,从哪来呢?我们来组装一些数据,我们把这些数据找到,然后把这个模板和这个数据交给我们模板引擎,模板引擎按照我们这个数据帮你把表达式解析、填充到我们指定的位置,然后把这个数据最终生成一个我们想要的内容给我们写出去,这就是模板引擎,不管是jsp还是其他的,都是这个思想。只不过是模板引擎之间的语法有点不一样。
首先需要引入 thymelead
从github 找到依赖引入到 springboot 项目中
<dependency>
<groupId>org.thymeleafgroupId>
<artifactId>thymeleaf-spring5artifactId>
dependency>
<dependency>
<groupId>org.thymeleaf.extrasgroupId>
<artifactId>thymeleaf-extras-java8timeartifactId>
dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9yeHipOU-1628259661111)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210618154346358.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1rMYQh0I-1628259661112)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210618154519333.png)]
**总结:**只要需要使用thymeleaf,只需要导入对应的依赖就可以了,我们将html放在我们的templates目录
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-keQxKHIu-1628259661113)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210618160545349.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8qf5PDIw-1628259661114)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210618163014887.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m01HLhM1-1628259661114)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210618163108894.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C8lE3dUz-1628259661115)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210619080950616.png)]
如果我们要自己定制一个视图解析器,我们要做的就是编写一个 @Configuration 注解类,并且类型要为 WebMvcConfigurer,还不能标注 @EnableWebMvc 注解。
//如果想要自己定制一些功能,只要写这个组件,然后将他交给springboot,springboot会帮我们自动装配
//全部扩展springmvc
//首先注解为一个配置类
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
//ViewResolver 实现了视图解析器接口类,我们就可以把它看作试图解析器
@Bean
public ViewResolver myViewResolver(){
return new MyViewResolver();
}
//自定义一个自己的视图解析器MyViewResolver
//实现了 ViewResolver 接口的类,我们就可以叫他视图解析器
public static class MyViewResolver implements ViewResolver{
@Override
public View resolveViewName(String s, Locale locale) throws Exception {
return null;
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tOJHB90v-1628259661116)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210619083822991.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sV3WbSgG-1628259661117)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210619083917697.png)]
这么多的自动配置,原理都是一样。
SpringBoot 在自动配置很多组件的时候,先看容器中有没有用户自己配置的(如果用户自己配置@Bean),如果有就用用户配置的,如果没有就用自动配置的;如果有些组件可以存在多个,比如我们的视图解析器,就将用户配置的和自己默认的结合起来。
在springboot中,有很多的 XXXX Configuration 帮助我们进行扩展配置,只要看见了这个东西,我们就要注意看一下扩展了什么东西。
在resources下建立文件夹 i18n,然后建立配置文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-czMYFrmj-1628259661118)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210619123935062.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sbWaJ77D-1628259661118)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210619124049253.png)]
# 设置本地国际化文件的真实位置
spring.messages.basename=i18n.login
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-omuHN7vx-1628259661119)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210619125013389.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z9z0KAJv-1628259661120)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210619125510074.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7nrSfmr6-1628259661122)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210619125644720.png)]
创建一个类 MyLocaleResolver,并且注册到 spring 中使其生效
@Configuration
public class MyLocaleResolver implements LocaleResolver {
//自定义的国际化生效
@Bean
public LocaleResolver localeResolver(){
return new MyLocaleResolver();
}
//解析请求,国际化
@Override
public Locale resolveLocale(HttpServletRequest httpServletRequest) {
//获取请求中的语言参数
String lang = httpServletRequest.getParameter("l");
Locale locale = Locale.getDefault();//如果没有就使用默认的
//如果请求的链接携带了国际化的参数
if (!StringUtils.isEmpty(lang)){
String[] split = lang.split("_");
locale = new Locale(split[0], split[1]);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rwhvP0Uh-1628259661123)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210619132725236.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wKr6xDNZ-1628259661124)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210619132742306.png)]
总结:
- 需要配置 i18n 文件
- 如果需要在项目中进行按钮自动切换,我要我们自定义一个组件 LocaleResolver
- 将自己写的组件配置到 spring 容器中:@Bean
/**
* 定义一个拦截器用于处理用户是否登录
* 首先需要继承 HandlerInterceptor 接口,实现执行前过滤,用于判断session是否为空
*/
public class LoginHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object userInfo = request.getSession().getAttribute("userInfo");
if (userInfo == null){
request.setAttribute("msg","没有权限,请先登录");
request.getRequestDispatcher("/index.html").forward(request,response);
return false;
}else {
return true;
}
}
}
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
//接管拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**").
excludePathPatterns("/index.html","/","/user/login","/css/**","/font/**","/img/**","/js/**");
}
}
对于数据访问层,无论是 SQL,还是 NOSQL,SpringBoot 底层都是采用 Spring Data 的方式进行统一处理的。
SpringBoot 底层都是采用 Spring Data 的方式进行统一处理各种数据库,Spring Data 也是Spring 中与SpringBoot、Spring Cloud 等齐名的知名项目。
Spring Data 官网:https://spring.io/projects/spring-data
SpringBoot 所有启动器链接:https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.devtools
创建好一个 springboot 项目,然后导入需要的依赖,也可以在创建时候选择依赖信息
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
首先需要建立 application.yml 配置文件,修改springboot 默认的配置信息
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/studentgrade?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
driver-class-name: com.mysql.jdbc.Driver
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AHW0kP9Z-1628259661125)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210620123049143.png)]
@Controller
public class JDBCController {
//导入 JDBCTemplate 模板,可以直接使用
@Autowired
JdbcTemplate jdbcTemplate;
@RequestMapping("/list")
@ResponseBody
//得到表里面的信息,没有实体类,所以可以存储到 Map 中
public List<Map<String, Object>> list(){
String sql = "select * from course";
List<Map<String, Object>> result = jdbcTemplate.queryForList(sql);
return result;
}
//删除一条记录,通过 localhost:8080/delete/4 进行删除即可
@RequestMapping("/delete/{cno}")
@ResponseBody
public void delete(@PathVariable("cno") Integer cno){
String sql = "delete from course where cno = ?";
jdbcTemplate.update(sql,cno);
}
}
//在springboot test类中输出数据源的类型
@SpringBootTest
class Springboot05JdbcApplicationTests {
@Autowired
DataSource dataSource;
@Test
void contextLoads() throws SQLException {
//查看一下默认的数据源
System.out.println(dataSource.getClass());
//获得数据库链接
Connection connection = dataSource.getConnection();
System.out.println(connection);
connection.close();
}
}
class com.zaxxer.hikari.HikariDataSource //结果是这个,springboot 默认是 hikari
Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3PO、DBCP、PROXOOL 等 DB 池的优点,同时加入了日志监控。
Druid 可以很好的监控 DB 连接池和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。
SpringBoot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Druid 都是当前 JavaWeb 上最优秀的数据源。
首先要引入依赖
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.6version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
然后修改 application.yml 配置文件的最后一行
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/studentgrade?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
driver-class-name: com.mysql.jdbc.Driver
# 修改数据源类型为 Druid
type: com.alibaba.druid.pool.DruidDataSource
在 test 类中输出数据源的类型进行验证
@SpringBootTest
class Springboot05JdbcApplicationTests {
@Autowired
DataSource dataSource;
@Test
void contextLoads() throws SQLException {
//查看一下默认的数据源
System.out.println(dataSource.getClass());
//获得数据库链接
Connection connection = dataSource.getConnection();
System.out.println(connection);
connection.close();
}
}
结果:class com.alibaba.druid.pool.DruidDataSource
自定义配置 Druid
首先要修改 application.yml 文件,配置监听过滤等属性
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/studentgrade?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
#SpringBoot默认是不注入这些的,需要自己绑定
#druid数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
#如果允许报错,java.lang.ClassNotFoundException: org.apache.Log4j.Properity
#则导入log4j 依赖就行
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionoProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
创建一个 DruidConfig 类
@Configuration
public class DruidConfig {
//绑定 application.yml 配置文件
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource(){
return new DruidDataSource();
}
//后台监控功能:web.xml ServletRegistrationBean,这个类会自动监控后台,生成一些可视化东西
//因为 springboot 内置了 servlet 容器,所有没有web.xml,替代方法:servletRegistrationBean
@Bean
public ServletRegistrationBean statViewServlet(){
//固定的代码,设置一个访问路径,一般为 /druid/
ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
//后台需要有人登录,账号密码配置,都是固定的
Map<String, String> initParameters = new HashMap<>();
//增加配置
initParameters.put("loginUsername","admin");//登录的 key 是固定的,loginUsername,loginPassword
initParameters.put("loginPassword","123456");
//允许谁能访问
initParameters.put("allow","");//如果参数为空表示所有人可以访问,具体的具体能访问
//禁止谁能访问
//initParameters.put("li","192.168.1.1");//不重要
bean.setInitParameters(initParameters);//设置初始化参数
return bean;
}
// 自己配置过滤器filter
@Bean
public FilterRegistrationBean webStatFilter(){
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
bean.setFilter(new WebStatFilter());
//可以过滤哪些请求
Map<String,String> initParameters = new HashMap<>();
//这些东西不进行统计
initParameters.put("exclusions","*.js,*.css,/druid/*");
return bean;
}
}
配置好之后可以通过 localhost:8080/druid 可以进入后台监控的页面,需要使用用户名密码进行登录,如果依赖中没有配置 log4j 的坐标是没办法看到效果,所以需要在 pom.xml 文件中进行 log4j 的引入
进入登录页面:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v55xG83p-1628259661126)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210620134520843.png)]
这里可以看到刚才执行的sql语句
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vyGtVG9U-1628259661127)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210620134604710.png)]
首先整合mybatis 需要先导入依赖
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.1
org.springframework.boot
spring-boot-starter-jdbc
mysql
mysql-connector-java
runtime
创建实体类,mapper接口,service,mapper.xml 文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pfH0arl2-1628259661129)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210620162917946.png)]
在 application.properties 文件中配置数据源以及绑定Mapper.xml 文件
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql://localhost:3306/studentgrade?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 整合 mybatis
# 别名设置
mybatis.type-aliases-package=com.example.pojo
# 扫描 xml 文件
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
实体类
public class Course {
private Integer cno;
private String cname;
private Integer classHour;
//省略 get、set方法
}
mapper接口
//这个注解表示是mybatis 的mapper
@Mapper
@Controller
public interface CourseMapper {
//得到全部
List<Course> getAll();
//删除
Integer delete(Integer cno);
}
服务层接口
public interface CourseService {
//得到全部
List<Course> getAll();
//删除
Integer delete(Integer cno);
}
服务层实现类
@Service
public class CourseServiceImpl implements CourseService {
@Autowired
CourseMapper courseMapper;
@Override
public List<Course> getAll() {
List<Course> all = courseMapper.getAll();
return all;
}
@Override
public Integer delete(Integer cno) {
return courseMapper.delete(cno);
}
}
mapper.xml 文件
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.CourseMapper">
<select id="getAll" resultType="Course">
select * from course
select>
<delete id="delete">
delete from course where cno = #{cno}
delete>
mapper>
Controller 层代码
@Controller
public class CourseController {
@Autowired
CourseService courseService;
@RequestMapping("/getAll")
@ResponseBody
public List<Course> getAll(){
List<Course> all = courseService.getAll();
return all;
}
@RequestMapping("/delete/{cno}")
@ResponseBody
public String delete(@PathVariable("cno") Integer cno){
Integer delete = courseService.delete(cno);
if (delete > 0){
return "删除成功";
}
return "失败";
}
}
最后使用浏览器通过访问 localhost:8080/getAll 就可以得到表中的信息,通过 localhost:8080/delete/1 就可以实现删除操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3cQUG1rT-1628259661130)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210620163432544.png)]
主要是为了简化,之前使用过滤器、拦截器比较繁琐,所有衍生出一些框架用于简化开发。
在 Web 开发中,安全是第一位的。
SpringSecurity 是针对 Spring 项目的安全框架,也是 SpringBoot 底层安全模块默认的技术造型,他可以实现强大的 Web 安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security
模块,进行少量的配置,即可实现强大的安全管理。
SpringSecurity 的两个主要目标是“认证(Authentication)”和“授权(访问控制Authorization)”,通用概念
Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.
There is no PasswordEncoder mapped for the id “null”
导入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
编写配置类
//1. 开启 WebSecurity 模式
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//采用的链式编程
//2. 授权,重写 WebSecurityConfigurerAdapter 的 configure(HttpSecurity http) 方法
@Override
protected void configure(HttpSecurity http) throws Exception {
//首页所有人可以访问,但是里面的功能页只有对应有权限的人才能访问
//请求授权的规则
// 采用链式编程来设置访问权限 hasRole 判断权限,permitAll() 表示所有权限
http.authorizeRequests().antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
//没有权限需要跳到登录页,需要开启登录的页面
http.formLogin();
//方式网站攻击工具,关闭 csrf 功能,登录失败肯定存在的原因
http.csrf().disable();
//注销,开启了注销功能,注销后回到主页
http.logout().logoutSuccessUrl("/");
}
//3. 认证
// springboot 2.1.x 可以直接使用的
// 如果不加编码会认为密码不安全,报错:There is no PasswordEncoder mapped for the id "null"
// spring security 5.0+ 中新增加了很多的加密方法
// 使用链式编程设置编码格式、用户名、密码、角色权限,可以使用 and() 连接多个
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//数据正常应该从数据库中读取,这里是从内存中读取
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("li").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3").and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3").and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}
}
/** auth
* // enable in memory based authentication with a user named
* // "user" and "admin"
* .inMemoryAuthentication().withUser("user").password("password").roles("USER").and()
* .withUser("admin").password("password").roles("USER", "ADMIN");
* }**/ 源码给与说明
首先引入整合包
<dependency>
<groupId>org.thymeleaf.extrasgroupId>
<artifactId>thymeleaf-extras-springsecurity5artifactId>
<version>3.0.4.RELEASEversion>
dependency>
html 代码中引入命名空间
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
编写代码
<div sec:authorize="!isAuthenticated()">
<a class="item" th:href="@{/toLogin}">
<i class="address card icon">i> 登录
a>
div>
<div sec:authorize="isAuthenticated()">
<a class="item">
用户名: <span sec:authentication="name">span>
a>
div>
<div sec:authorize="isAuthenticated()">
<a class="item" th:href="@{/logout}">
<i class="address card icon">i> 注销
a>
div>
<div class="column" sec:authorize="hasRole('vip1')">
在 SecurityConfig 类的 onfigure(HttpSecurity http) 方法中添加如下一句代码
//开启记住我功能,有效期2周
http.rememberMe();
这样在登录页面下面会有一个选择框,如果选择则会记录登录的信息到 Cookie 中,就算关闭浏览器,重新访问也会生效的,有效期限 2周左右。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qqjqHlk3-1628259661131)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210629124158250.png)]
在 SecurityConfig 类的 onfigure(HttpSecurity http) 方法中添加如下一句代码
这里的参数修改表示如果页面的name属性不是 username或者 password 则根据修改的值进行接受参数
http.formLogin().loginPage("/toLogin").usernameParameter("name").passwordParameter("word");
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NSb5HSGd-1628259661132)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210629131525067.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EXKFMfyB-1628259661132)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210629160528790.png)]
从外部来看 Shiro,即从应用程序角度来观察如何使用 Shiro 完成工作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1yROugk3-1628259661133)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210629161651339.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1U9qjowF-1628259661134)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210629162312228.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sFw8bBmZ-1628259661135)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210629232238244.png)]
首先导入依赖
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.7.1version>
dependency>
编写 UserRealm 类
// 自定义的 UserRealm
public class UserRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了-----授权doGetAuthorizationInfo");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了-----认证doGetAuthorizationInfo");
return null;
}
}
创建 ShiroConfig 类
@Configuration
public class ShiroConfig {
//3. ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
/**
* anno : 无需认证就可以访问
* authc:必须认证了才能访问
* user:必须拥有 记住我 功能才能用
* perms:拥有对某个资源的权限才能访问
* role:拥有某个角色权限才能访问
*/
//添加 Shiro 的内置过滤器
//拦截
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/*","authc");
bean.setFilterChainDefinitionMap(filterMap);
//设置登录的请求
bean.setLoginUrl("/toLogin");
return bean;
}
//2. DefaultWebSecurityManager
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联 UserRealm
securityManager.setRealm(userRealm);
return securityManager;
}
//1. 创建 realm 对象,需要自定义
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
}
认证信息主要是在 UserRealm 类中
首先在 Controller 中接受前端页面输入的用户名密码信息
@RequestMapping("/login")
public String login(String username,String password, Model model){
//1. 获取当前用户
Subject subject = SecurityUtils.getSubject();
//2. 封装用户的登录数据
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
try {
subject.login(token); // 执行登录方法,如果没有异常就ok了
return "index";
} catch (UnknownAccountException e) { //用户名错误
model.addAttribute("msg", "用户名错误");
return "login";
} catch (IncorrectCredentialsException e){ // 密码错误
model.addAttribute("msg","密码错误");
return "login";
}
}
在 UserRealm 类中进行信息的认证,认证的信息自定义变量
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了-----认证doGetAuthorizationInfo");
//1. 用户名,密码 正常从数据库中取出,这里模拟使用
String name = "root";
String password = "123456";
// 这里的 (UsernamePasswordToken) authenticationToken; 对应于 Controller 中的封装好的 token
UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
if (!userToken.getUsername().equals(name)){
return null; //抛出异常 UnknownAccountException
}
//密码认证,shiro做,不让接触密码
return new SimpleAuthenticationInfo("",password,"");
}
在 UserRealm 类中进行信息的认证,认证的信息从数据库中取出用户名密码
首先需要测试连接好数据库,保证数据库可以使用
然后在 认证
修改即可
@Autowired
UserService userService;
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了-----认证doGetAuthorizationInfo");
UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
//连接数据库,从数据库中取出
User user = userService.queryUserByName(userToken.getUsername());
if (user == null){ // 登录失败
return null; //抛出异常 UnknownAccountException
}
//密码认证,shiro做,不让接触密码
return new SimpleAuthenticationInfo("",user.getUser_password(),"");
}
首先在 ShiroConfig.java 类中 getShiroFilterFactoryBean() 方法中进行设置访问的权限
//添加 Shiro 的内置过滤器
Map<String, String> filterMap = new LinkedHashMap<>();
// 授权,正常的情况下,没有授权会跳到未授权页面
filterMap.put("/user/add","perms[add]");
filterMap.put("/user/update","perms[update]");
/**
*
**/
// 设置未授权页面
bean.setUnauthorizedUrl("/noauth");
然后在 UserRealm.java 中授权部分设置授权的用户信息
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了-----授权doGetAuthorizationInfo");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 拿到当前登录的这个对象
Subject subject = SecurityUtils.getSubject();
User currentUser = (User) subject.getPrincipal();//拿到 User 对象
//设置当前用户的权限,getUser_perms 从数据库中拿到权限信息 用于判断
info.addStringPermission(currentUser.getUser_perms());
return info;
}
前提需要在认证部分返回 principal 的值,用户获得当前用户的信息
return new SimpleAuthenticationInfo(user,user.getUser_password(),"");
首先需要引入整合的依赖
<dependency>
<groupId>com.github.theborakompanionigroupId>
<artifactId>thymeleaf-extras-shiroartifactId>
<version>2.0.0version>
dependency>
html 页面中引入命名空间
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"
然后编写分模块
判断是否登录,如果没有登录过就显示首页,点击首页可以登录
<div th:if="${session.userInfo == null}">
<a th:href="@{/toLogin}"><h1>首页h1>a>
div>
<p th:text="${msg}">p>
<hr>
获得 shiro:hasPermission 权限,进行区分,如果只有 add 权限就显示 add,以此类推
<div shiro:hasPermission="add">
<a th:href="@{/user/add}">adda>
div>
<div shiro:hasPermission="update">
<a th:href="@{/user/update}">updatea>
div>
号称世界上最流行的 Api 框架;
RestFul Api 文档在线自动生成工具 => Api 文档与 API 定义同步更新;
直接运行,可以在线测试 API 接口;
支持多种语言;
官网:https://swagger.io/
新建 springboot-web 项目
导入相关依赖
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>3.0.0version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>3.0.0version>
dependency>
编写一个 hello 接口
集成Swagger2 的配置文件,开启 Swagger2
@Configuration
@EnableSwagger2 //开启 Swagger2
public class SwaggerConfig {
}
测试运行:http://localhost:8080/swagger-ui.html
可以看到一下界面信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YB9a1fsX-1628259661136)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210630174307805.png)]
@Configuration
@EnableSwagger2
public class SwaggerConfig {
//配置了 swagger 的 Docket 的 bean 实例
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
}
// 配置 Swagger 信息 ---- apiInfo
public ApiInfo apiInfo() {
Contact contact = new Contact("lishisen", "www.miss.com", "[email protected]");
return new ApiInfo(
"SpringBoot Swagger",
"一个接口的文档",
"v1.1",
"www.baidu.com",
contact,
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList<>());
}
}
//配置了 swagger 的 Docket 的 bean 实例
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
//RequestHandlerSelectors ,配置要扫描接口的方式
//basePackage:指定要扫描的包
//any():扫描全部
//none():不扫描
//withClassAnnotation: 扫描类上的注解,参数是一个注解的反射对象
//withMethodAnnotation:扫描方法上的注解
// .enable(false) 是否启用 swagger,如果为 false 不能访问swagger
.select().apis(RequestHandlerSelectors.basePackage("com.example.controller"))
//paths() 过滤什么路径
.paths(PathSelectors.ant("/com/example/**")).build();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JzNDN7S3-1628259661137)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210630192010023.png)]
因为设置扫描的 basePackage 和 paths 过滤的冲突了,所以上面没有信息输出。
首先创建两套环境
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K6kUIIYX-1628259661138)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210630213605117.png)]
设置环境选择的文件,并且判断是否启用 swagger
//配置了 swagger 的 Docket 的 bean 实例
@Bean
public Docket docket(Environment environment) {
// 设置要显示的 swawgger 环境
Profiles profiles = Profiles.of("dev", "pro1");
// 通过 environment.acceptsProfiles 判断是否处在自己设定的环境当中
boolean flag = environment.acceptsProfiles(profiles);
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.enable(flag)
.select().apis(RequestHandlerSelectors.basePackage("com.example.controller"))
//paths() 过滤什么路径
// .paths(PathSelectors.ant("/com/example/**"))
.build();
}
在 docket 类中进行设置
.groupName("myGroup")
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VWFQQcST-1628259661139)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210630214057788.png)]
设置多个分组
// 配置多个 swagger ,查看分组
@Bean
public Docket docket1(){
return new Docket(DocumentationType.SWAGGER_2).groupName("docket1");
}
@Bean
public Docket docket2(){
return new Docket(DocumentationType.SWAGGER_2).groupName("docket2");
}
@Bean
public Docket docket3(){
return new Docket(DocumentationType.SWAGGER_2).groupName("docket3");
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nmxyL0Js-1628259661140)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210630214536045.png)]
编写接口
// 只要接口返回的实体类,就会被记录到 Swagger 的Model中
@GetMapping("/user")
// Operation 接口,放在方法上,说明方法作用
@ApiOperation("hello 控制方法")
// @ApiParam("") 可以放在方法形参上,说明参数
public User user(@ApiParam("用户名") String username){
return new User();
}
还有实体类可以使用
@ApiModel("用户实体类")
public class User {
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("用户密码")
private String password;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EguGehQA-1628259661141)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210630220448554.png)]
总结:
我们可以通过 Swagger 给一些比较难以理解的属性或者接口,增加注释信息;接口文档实时更新;可以在线测试;
【注意】:在正式发布的时候,关闭 Swagger,处于安全考虑,节省运行的内存;
模拟异步数据,分别创建两个类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LnXpO8gC-1628259661142)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210630223704453.png)]
AsynService
@Service
public class AsynService {
public void hello(){
try {
// 休眠 3秒后在执行下面内容
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据正在处理中.....");
}
}
HelloController
@Controller
public class HelloController {
@Autowired
AsynService asynService;
@RequestMapping("/hello")
@ResponseBody
public String hello(){
asynService.hello();
return "OK";
}
}
启动运行后 3 秒后才输出内容
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Za5QMX1c-1628259661143)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210630223905276.png)]
**解决等待时间问题 : **
在 Service 方法上添加注解 @Async
@Service
public class AsynService {
@Async //告诉 spring 这是一个异步方法
public void hello(){
在启动类上添加注解 @EnableAsync 启动异步任务
@EnableAsync // 开启异步
首先需要导入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
dependency>
通过查看源码 MailSenderAutoConfiguration.class,MailProperties.class 得到需要的属性文件,然后编写配置文件
[email protected]
spring.mail.password=kvprsezzhjfbjgbi
# 发送的服务器
spring.mail.host=smtp.qq.com
# 开启加密验证
spring.mail.properties.mail.smtp.ssl.enable=true
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IuZtDI39-1628259661144)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210701091930801.png)]
测试
@SpringBootTest
class Springboot12TaskApplicationTests {
@Autowired
JavaMailSenderImpl mailSender;
@Test
void contextLoads() {
// 一个简单的邮件
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
simpleMailMessage.setSubject("你好");
simpleMailMessage.setText("好好学习 JAVAEE");
simpleMailMessage.setTo("[email protected]");
simpleMailMessage.setFrom("[email protected]");
mailSender.send(simpleMailMessage);
}
@Test
void contextLoads2() throws MessagingException {
// 一个复杂的邮件
MimeMessage mimeMessage = mailSender.createMimeMessage();
// 组装
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setSubject("nihao");
helper.setText("好好学习
",true);
// 附件
helper.addAttachment("1.jpg",new File("C:\\Users\\lishisen\\Desktop\\1.jpg"));
helper.setTo("[email protected]");
helper.setFrom("[email protected]");
mailSender.send(mimeMessage);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BAPDQSed-1628259661144)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210701093433472.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Buuh6HnR-1628259661145)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210701093446651.png)]
TaskExecutor 任务执行者
TaskScheduler 任务调度者
@EnableScheduling //开启定时功能的注解
@Scheduled // 什么时候执行
在 Test 类上开启定时功能
@SpringBootApplication
@EnableScheduling//开启定时功能
public class Springboot12TaskApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot12TaskApplication.class, args);
}
}
编写定时功能的服务
@Service
public class ScheduledService {
// 在一个特定的时间执行这个方法
// cron 表达式 秒 分 时 日 月 周
@Scheduled(cron = "0 * * * * 0-7")
public void hello(){
System.out.println("hello,你被执行了.....");
}
}
什么是分布式系统?
分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统;
分布式系统是由一组通过网络进行通信,为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。
分布式系统(distributed system) 是建立在网络之上的软件系统。
首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的 CPU) 高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。。。
Dubbo 文档
官网:https://dubbo.apache.org/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ACZkkCY-1628259661146)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210701101949736.png)]
RPC
什么是 RPC?
RPC(Remote Rroducure Call) 是指远程过程调用,是一种进程间通信方式,它是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序显示编程这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
也就是说两台服务器 A,B,一个应用部署在 A 服务器上,想要调用 B 服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据,为什么要用 RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC 就是要像调用本地的函数一样去调远程函数;
解释RPC文章:https://www.jianshu.com/p/2accc2840a1b
RPC 基本原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HideiVTa-1628259661147)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210701103111263.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uus4HrZZ-1628259661147)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210701103150943.png)]
什么是 dubbo?
Apache Dubbo 是一款高性能、轻量级的开源 Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y1johoMx-1628259661148)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210701104344059.png)]
Dubbo 环境搭建
点进 dubbo 官方文档,推荐我们使用 Zookeeper 注册中心
window 下安装 zookeeper
下载 http://archive.apache.org/dist/zookeeper/zookeeper-3.4.14/
运行 /bin/zkServer.cmd ,初次运行会报错,因为在 conf 中没有 zoo.cfg 配置文件,可能会闪退
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rh7U8gCx-1628259661149)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210701125257576.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IVa8skLb-1628259661150)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210701125324414.png)]
解决方法**:E:\Environment\zookeeper-3.4.14\conf**
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iAgrYwUi-1628259661150)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210701125410860.png)]
zoo.cfg 配置文件
使用 zkCli.cmd 测试
ls / :列出 zookeeper 根下保存的所有结点
[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper]
create -e /li 123 : 创建一个 li 节点,值为 123
[zk: localhost:2181(CONNECTED) 1] create -e /li 123
Created /li
get /li : 获取 /li 节点的值
[zk: localhost:2181(CONNECTED) 3] get /li
123 // 这里是自己设置的值
cZxid = 0x6
ctime = Thu Jul 01 13:02:52 CST 2021
mZxid = 0x6
mtime = Thu Jul 01 13:02:52 CST 2021
pZxid = 0x6
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x1001ab811750001
dataLength = 3
numChildren = 0
windows 下安装 dubbo-admin
dubbo 本身并不是一个服务软件,它其实就是一个 jar 包,能够帮你的 java 程序连接到 zookeeper,并利用 zookeeper 消费、提供服务。
但是为了让用户更好的管理监控众多的 dubbo 服务,官方提供了一个可视化的监控程序 dubbo-admin,不过这个监控即使不装也不影响使用。
下载 dubbo-admin
下载地址:https://github.com/apache/dubbo-admin/tree/master
解压进入目录
修改 E:\Environment\dubbo-admin-master\dubbo-admin\src\main\resources \application.properties 指定 zookeeper 地址
server.port=7001
spring.velocity.cache=false
spring.velocity.charset=UTF-8
spring.velocity.layout-url=/templates/default.vm
spring.messages.fallback-to-system-locale=false
spring.messages.basename=i18n/message
spring.root.password=root
spring.guest.password=guest
# 注册中心的地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
在项目目录下打包 dubbo-admin
mvn clean package -Dmaven.test.skip=true
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8MI1oLYt-1628259661151)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210701132331726.png)]
成功后会在 target 目录下生成 jar 包文件 dubbo-admin-0.0.1-SNAPSHOT.jar
cmd 中执行命令运行:
java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
注意:zookeeper 的服务一定要打开
执行完毕后用浏览器访问 http://localhost:7001/,需要输入登录账户和密码,默认都为 root
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2PO8WZOu-1628259661152)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210701132914379.png)]
zookeeper : 注册中心
dubbo-admin:是一个监控管理后台,查看我们注册了哪些服务,哪些服务被消费了
首先创建两个项目 provider-service
和 consumer-service
,并在 provider-service 写服务接口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7r2ZuHtn-1628259661153)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210701152746409.png)]
在 provider-service
项目中导入依赖,编写配置文件,编写服务
依赖导入
<dependency>
<groupId>org.apache.dubbogroupId>
<artifactId>dubbo-spring-boot-starterartifactId>
<version>3.0.0version>
dependency>
<dependency>
<groupId>com.github.sgroschupfgroupId>
<artifactId>zkclientartifactId>
<version>0.1version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-frameworkartifactId>
<version>2.12.0version>
dependency>
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.4.14version>
<exclusions>
<exclusion>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
exclusion>
exclusions>
dependency>
配置文件
server.port=8081
# 服务应用名字
dubbo.application.name=provider-server
# 注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
# 哪些服务要被注册
dubbo.scan.base-packages=com.example.service
编写服务内容
import org.apache.dubbo.config.annotation.Service;
import org.springframework.stereotype.Component;
//zookeeper:服务注册与发现
@Service //可以被扫描到,在项目已启动就自动注册到注册中心
@Component //使用了 Dubbo 后尽量不要使用 Service 注解
public class TicketServiceImpl implements TicketService{
@Override
public String getTicket() {
return "你好啊!!!dubbo,zookeeper";
}
}
编写好以上内容后,启动 zkServer.cmd
执行 jar 包内容,访问 localhost:7001 查看监控信息,现在是没有的,需要启动一下编写的服务就可以看到服务的信息
java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6AjUJjcQ-1628259661154)(C:\Users\lishisen\AppData\Roaming\Typora\typora-user-images\image-20210701155003380.png)]
学习参考视频网址