0%

SpringBoot_笔记(狂神)

狂神SpringBoot教学视频学习笔记,包括SpringBoot运行原理、配置文件、自动配置原理、静态资源导入、管理系统实战项目、整合Mybatis、SpringSecurity、Swagger等内容

1 SpringBoot和微服务

1.1 SpringBoot介绍

Spring家族

Spring是为了解决企业级应用开发的复杂性而创建的,简化开发。Spring家族为我们提供了整个从开始构建应用到大型分布式应用全流程方案

  • springboot用来快速构建一个个功能独立的微服务应用单元
  • spring cloud实现分布式,完成对大型分布式网络服务的调用
  • spring cloud data flow用于在分布式中间,进行流式数据计算、批处理

SpringBoot

简单来说就是进一步减轻开发的难度和步骤、解放开发人员的一些复杂繁琐的代码和配置文件。

  • 开箱即用,提供各种默认配置来简化项目配置。集成了大量常用的第三方库配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等)
  • 内嵌式容器简化Web项目
  • 没有冗余代码生成和XML配置的要求

1.2 微服务

狂神讲的比较狗屎,这里参考一个博客园的文章:

https://www.cnblogs.com/skabyy/p/11396571.html

1 最开始简单的需求

只需要一个网站挂在公网,用户能够在这个网站上浏览商品、购买商品;另外还需一个管理后台,可以管理商品、用户、以及订单数据。

img

2 业务发展 增加营销手段

开展促销活动。比如元旦全场打折,春节买二送一,情人节狗粮优惠券等等。

拓展渠道,新增移动端营销。除了网站外,还需要开发移动端APP,微信小程序等。

精准营销。利用历史数据对用户进行分析,提供个性化服务。

img

粗暴的增加架构、带来了很多不合理的地方:

  • 网站和移动端应用有很多相同业务逻辑的重复代码。
  • 数据有时候通过数据库共享,有时候通过接口调用传输。接口调用关系杂乱。
  • 单个应用为了给其他应用提供接口,渐渐地越改越大,包含了很多本来就不属于它的逻辑。应用边界模糊,功能归属混乱。
  • 数据库表结构被多个应用依赖,无法重构和优化。
  • 所有应用都在一个数据库上操作,数据库出现性能瓶颈。特别是数据分析跑起来的时候,数据库性能急剧下降。
  • 开发、测试、部署、维护愈发困难。即使只改动一个小功能,也需要整个应用一起发布。

3 做出改变——微服务架构

在编程的世界中,最重要的便是抽象能力。微服务改造的过程实际上也是个抽象的过程。小明和小红整理了网上超市的业务逻辑,抽象出公用的业务能力,做成几个公共服务:

  • 用户服务、商品服务、促销服务、订单服务、数据分析服务

各个应用后台只需从这些服务获取所需的数据,从而删去了大量冗余的代码,就剩个轻薄的控制层和前端。

img

虽然很好,但因为数据库同时被多个服务依赖,仍有缺点:

  1. 数据库成为性能瓶颈,并且有单点故障的风险。
  2. 数据管理趋向混乱。即使一开始有良好的模块化设计,随着时间推移,总会有一个服务直接从数据库取另一个服务的数据的现象。
  3. 数据库表结构可能被多个服务依赖,牵一发而动全身,很难调整。

4 拆分数据库、缓存、消息队列

如果一直保持共用数据库的模式,则整个架构会越来越僵化,失去了微服务架构的意义。因此小明和小红一鼓作气,把数据库也拆分了。所有持久化层相互隔离,由各个服务自己负责。另外,为了提高系统的实时性,加入了消息队列机制。架构如下:

完全拆分后各个服务可以采用异构的技术。比如数据分析服务可以使用数据仓库作为持久化层,以便于高效地做一些统计计算;商品服务和促销服务访问频率比较大,因此加入了缓存机制等。

img

….后面还有很多、这里就先交待到这里。

2 第一个springboot程序

2.1 创建spirngboot项目框架

这里介绍使用IDEA的创建方式:

1、创建一个新项目 new project

2、选择spring initalizr , 可以看到默认就是去官网的快速构建工具那里实现

3、填写项目信息

4、选择初始化的组件(初学勾选 Web 即可)

5、填写项目路径

6、等待项目构建成功

项目结构分析:

删除暂时不用的文件,发现就是一个普通的maven项目的结果。一个src里面有java和resource;然后还有一个pom.xml的配置文件

1、程序的主启动类

2、一个 application.properties 配置文件

3、一个 测试类

4、一个 pom.xml

image-20210626195613829

2.2 编写一个接口、打包cmd运行

Springboot只需要简单几步,就可以完成了一个web接口的开发,不走如下:

1、在主程序的同级目录下,新建一个controller包,一定要在同级目录下,否则识别不到

image-20210626201703439

2、在包中新建一个HelloController类

1
2
3
4
5
6
7
8
9
//@RestController
@ResponseBody
@Controller
public class HelloController {
@RequestMapping("/hello")
public String hello01(){
return "Hello the fuck world!";
}
}

3、编写完毕后,从主程序启动项目,浏览器发起请求,看页面返回;控制台输出了 Tomcat 访问的端口号!

image-20210626201825744

将项目打包、以便可以在“其他地方”运行

1 点击 maven的 package,如果打包成功,则会在target目录下生成一个 jar 包

1595397063721

如果遇到以上②的错误,可以配置打包时跳过项目运行测试用例(自己百度的)

1
2
3
4
5
6
7
8
9
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>

2 打成了jar包后,就可以在任何地方运行了!测试结果如下

1
java -jar .\helloword01-0.0.1-SNAPSHOT.jar

1595397745294

image-20210626204655154

3.3 彩蛋?

(先码住、回头再装逼。)

  1. 更改端口号

    1
    2
    # 更改项目的端口号
    server.port=8081
  2. 如何更改启动时显示的字符拼成的字母,SpringBoot呢?也就是 banner 图案;

    只需一步:到项目下的 resources 目录下新建一个banner.txt 即可。

    图案可以到:https://www.bootschool.net/ascii 这个网站生成,然后拷贝到文件中即可!

1595409428560

SpringBoot这么简单的东西背后一定有故事,我们之后去进行一波源码分析!

3 运行原理初探

有一说一,狂神这节讲的很拉跨。

springboot自动配置原理

3.1@SpringBootApplication 主配置

作用:标注在某个类上说明这个类是SpringBoot的主配置

  • @EnableAutoConfiguration 开启自动配置

    • @AutoConfigurationPackage自动配置包
      • @Import({Registrar.class}):Spring底层注解@import ,用来导入(这个导入和下面的自动扫描配合使用)
      • Registrar.class 作用:自动配置包注册,将主启动类的所在包及包下面所有子包里面的所有组件扫描到Spring容器 ;
    • @Import({AutoConfigurationImportSelector.class}) :给容器导入组件【自动导包的核心】
    • {AutoConfigurationImportSelector.class} 自动配置导入选择器,选择了什么东西?
      • getAutoConfigurationEntry() 获得自动配置的实体(调用下面)
      • getCandidateConfigurations() 获取候选的配置
        • getSpringFactoriesLoaderFactoryClass()方法,返回的就是我们最开始看的启动自动导入配置文件的注解类:EnableAutoConfiguration
          • getCandidateConfigurations
          • protected Class<?> getSpringFactoriesLoaderFactoryClass() {
          • return EnableAutoConfiguration.class;}
      • loadFactoryNames() 方法,获取所有的加载配置
        • 项目资源,最终获取一个资源:META-INF/spring.factories,位置在spring-boot-autoconfigure-2.5.2.jar包下
          • META-INF
            • spring.factories 所有的自动配置类全部在这里
            • 思考:为什么这么多配置没有生效、需要导入对应的start才能有作用?
              • XXAutoConfiguration,满足一定条件才生效
        • 系统资源,最终获取一个资源:META-INF/spring.factories
        • 从这些资源中遍历了所有的nextElement(自动配置),封装成properties供我们使用
    • 总结
      • springboot的所有配置都在启动时扫描并加载,spirng.factories所有的自动装配类都在这里,但不一定生效,需要判断条件是否成立,只要导入了对应的start,有了对应的启动器,自动装配就会生效、配置成功。
    • 自动装配步骤:
      • SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值
      • 将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;
      • 以前我们需要自动配置的东西,现在springboot帮我们做了
      • 整合JavaEE,整体解决方案和自动配置的东西都在springboot-autoconfigure的jar包中;
      • 它会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器中
      • 它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件 , 并自动配置,@Configuration(javaConfig) ;
      • 有了自动配置类 , 免去了我们手动编写配置注入功能组件等的工作;
  • @SpringBootConfigurationspingboot的配置

    标注在某个类上 , 表示这是一个SpringBoot的配置类;

    • @Configuration 配置(表明配置类,对应Spring的xml 配置文件)
      • @Component 组件 (说明启动类本身也是Spring中的一个组件而已,负责启动应用!)
  • @ComponentScan 自动扫描包并加载符合条件的组件或者bean

3.2 run方法流程分析(跳过)

aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy91SkRBVUtyR0M3TDF2RlFNbmFSSUpTbWVaNThUMmVaaWNqYWZpYXdRTHA5dTh3YzRpYzFNank2T3lmaWJ6ZmpWb2ZlTDVwblMxTlNGS1ZqbElnNm5lSTl5U2cvNjQw

4 配置文件 .yaml和.properties

4.1 配置文件分类

Spring Boot 中有以下两种配置文件:bootstrap.properties(bootstrap.yml) 和 application.properties(application.yml)

application 配置文件主要用于 Spring Boot 项目的自动化配置(这里讲这个)。application.properties和 application.yml的优先级和语法结构不一样、但功能都是一样的。传统的xml与yaml、properties配置语法的对比如下:

  • 传统xml配置:
1
2
3
<server>
<port>8081<port>
</server>
  • yaml配置
1
2
server:
prot: 8080
  • properties配置
1
server.port=8081

springboot推荐使用yaml配置、下面主要讲一下yaml的语法格式

4.2 yaml配置的语法

yaml 的语法:

  • 空格不能省略

  • 以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。

  • 属性和值的大小写都是十分敏感的。

1 普通变量 k: v

1
name: wukang

2 对象、或者键值对Map

1
2
3
4
5
6
7
#对象
student1:
name: huang
age: 22

#对象的行内写法
student2: {name: huang,age: 22}

3 数组(list\set)

1
2
3
4
5
6
7
8
#数组
pet1:
- cat
- dog
- pig

#数组的行类写法
pet2: [cat,dog,pig]

4 修改默认配置,如端口号

1
2
server:
port: 8081

4.3 使用配置文件application.yml

有三种方式,通常也会组合使用,如下:

  • 直接用@Value(“${name}”)获取默认配置文件中的值

  • @PropertySource :加载指定属性的(*.properties)配置文件;(只能适用.properties)

  • @configurationProperties:默认从全局配置文件中获取值;(@ConfigurationProperties(prefix = “personinfo”)这个注解一般加载实体类上面,用来将yaml中定义的所有属性,赋值给实体类的各个示例变量)

1 @Value(“${name}”)

image-20210627163942918

1 resources下建application.yaml

1
2
3
4
5
6
7
8
9
10
# 修改默认端号
server:
port: 8082
#两个普通变量值
name: wukangzuishuai
age: 18
#一个对象
personInfo:
name: wukang
age: 19

2 测试代码如下GetPersonInfoController.java文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
@SpringBootTest:用于测试的注解,可指定入口类或测试环境等
@RunWith(SpringRunner.class):在Spring测试环境中进行测试。
@Test:表示一个测试方法
@Value:获取配置文件中的值
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class GetPersonInfoController {
//获取配置文件中的age
@Value("${age}")
private int age;
//获取配置文件中的name
@Value("${name}")
private String name;
//该注解表示一个测试方法
@Test
public void getAge(){
System.out.println(age);
}
//该注解表示一个测试方法
@Test
public void getName(){
System.out.println(name);
}
}

3 点击测试getName()方法。说明@Value(“${name}”)读取了配置文件中的name

image-20210627164142165

2 @PropertySource()

1 在application.properties文件中,写name和age两个属性值

1
2
name=wukangzuishuai555
age=22

2 在GetPersonInfoController.java类上面加上一个PropertySource注解,指定某个配置文件

1
@PropertySource(value = "classpath:application.properties")

3 运行getName()或getAge()方法,结果说明使用了application.properties这个配置文件的值

image-20210627165643867

3 @configurationProperties

@ConfigurationProperties(prefix = “personinfo”)这个注解一般加载实体类上面,用来将yaml中定义的所有属性,赋值给实体类的各个实例变量

prefix = “personinfo”这个参数好像必须要小写,,我yaml里面是personInfo,然后这个也要用小写personinfo?

1 写一个实体类personInfo,最后的目的就是用yaml的参数给实体类的变量赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
//personinfo是yaml配置文件中的一个对象!!
@ConfigurationProperties(prefix = "personinfo")
public class personInfo {
private String name;
private int age;
public String getName(){
return name;
}
public void setName(String name){
this.name=name;
}
public int getAge(){
return age;
}
public void setAge(int age){
this.age=age;
}
}

2 在GetPersonInfoController.java类下加一个测试方法

1
2
3
4
5
6
7
//@Autowired注解解释:它表示被修饰的类需要注入对象。Spring会扫描所有被@Autowired标注的类,然后根据类型在loC容器中找到匹配的类进行注入。被@Autowired注解后的类不需要再导入文件。
@Autowired
private personInfo personInfo1;
@Test
public void getPersonYaml(){
System.out.println(personInfo1.getName()+personInfo1.getAge());
}

3 并且进行测试,显示了yaml中personInfo对象定义的两个属性值

image-20210627172024276

4 对比@Value和@ConfigurationProperties

@ConfigurationProperties @Value
功能 批量注入配置文件中的属性 一个个指定
松散绑定 支持 不支持
SpEL 不支持 支持
JSR303数据校验 支持 不支持
复杂类型封装 支持 不支持
  1. @ConfigurationProperties只需要写一次即可 , @Value则需要每个字段都添加
  2. 复杂类型封装,yml中可以封装对象 , 使用value就不支持等…
  3. JSR303数据校验 , 这个就是我们可以在字段上增加一层过滤器验证 , 可以保证数据的合法性
  4. 松散绑定:这个什么意思呢? 比如我的yml中写的last-name,这个和lastName是一样的, - 后面跟着的字母默认是大写的。这就是松散绑定。可以测试一下

结论:

  • 配置yml和配置properties都可以获取到值 , **强烈推荐 yml**;

  • 如果我们在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value;

  • 如果说,我们专门编写了一个JavaBean来和配置文件进行一一映射,就直接**@configurationProperties**,不要犹豫!

4.4 多配置环境切换、配置文件的加载顺序

1 多配置环境切换

1 使用application.properties选择不同环境的配置文件

​ 因为开发和测试的环境一般不一样,为了快速切换环境,开发环境和测试环境会各写一个.properties配置文件。然后根据实际环境在主配置环境中,选择激活哪一个环境:

  • application-test.properties 代表测试环境配置

  • application-dev.properties 代表开发环境配置

  • application.properties 代表主配置环境

1595484043622

通过application.properties的一个配置来选择需要激活的环境:

1
2
#比如在配置文件中指定使用dev环境
spring.profiles.active=dev

2 使用application.yaml选择不同环境的配置文件

​ 使用yml去实现不需要创建多个配置文件,直接在主配置文件application.yaml,写多种不同的环境,然后选择一个配置环境spring: profiles: active: dev

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server:
port: 8081
#选择要激活那个环境块
spring:
profiles:
active: dev

---
server:
port: 8083
spring:
profiles: dev #配置环境的名称

---
server:
port: 8084
spring:
profiles: test #配置环境的名称

2 配置文件的加载顺序

springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件:

1595482583892
1
2
3
4
5
优先级1:项目路径下的config文件夹配置文件
优先级2:项目路径下配置文件
优先级3:资源路径下的config文件夹配置文件
优先级4:资源路径下配置文件
优先级由高到底,高优先级的配置会覆盖低优先级的配置;

SpringBoot会从这四个位置全部加载主配置文件;互补配置;并且同一个位置下,properties文件的优先级要大于yaml文件

​ 当然,后期运维时,也可以通过命令行的参数来指定配置文件的新位置;

1
java -jar spring-boot-config.jar --spring.config.location=F:/application.properties

4.5 JSR303数据校验

​ 之前说过了 通过@ConfigurationProperties()注入属性可以使用 JSR303数据校验功能 ,下面介绍一下JSR303校验。

​ JSR303校验是用来规范输入内容的。根据“前端不可信”原则,后台最好再校验一遍数据,Springboot中可以用@validated来校验数据,如果数据异常则会统一抛出异常。

1 环境搭建

这里为了演示校验过程、重新写一个实体类、配置文件进行新的测试

1 编写一个Person实体类 含实例变量name age email hobbies等

2 编写yaml文件 对实体类中的变量赋值注入

3 写一个测试类 PersonControllerTest测试一下

1 编写一个Person实体类

1
2
3
4
5
6
7
8
9
10
11
12
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
private String name;
private Integer age;
private String email;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> hobbies;
//有参无参构造、get、set方法、toString()方法
}
1
2
@Component 表示该类被spring接管,是一个bean
@ConfigurationProperties(prefix = "person") 表示采用yaml中定义的属性,赋值给实体类的各个实例变量

2 编写yaml文件 对实体类中的变量赋值注入

1
2
3
4
5
6
7
8
9
10
11
12
#Person对象
person:
name: luofeng
age: 18
email: 123456
happy: false
birth: 2000/01/01
maps: {k1: v1,k2: v2}
hobbies:
- code
- girl
- music

3 写一个测试类 PersonControllerTest测试,运行contextLoads方法

1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest
@RunWith(SpringRunner.class)
public class PersonControllerTest {

@Autowired
private Person person;
@Test
public void contextLoads() {
System.out.println(person);
}
}

image-20210627200458224

发现所有的实例变量属性都正确的赋值给Person变量了

2 JSR303校验邮箱

接下来使用 JSR303数据校验功能对email格式进行验证,发现如果注入1123456,无法运行;当注入123456@163.com时,可以运行成功。

1 添加validation启动器

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2 Person类上添加 @Validated 激活数据校验,email变量上面添加@Email

1
2
3
4
5
6
7
@Component //注册bean
@ConfigurationProperties(prefix = "person")
@Validated //数据校验
public class Person {
@Email(message="邮箱格式错误")
private String email;
}

3 邮箱为123456,格式错误、运行报错

image-20210627202206772

4 修改yaml中的邮箱值,再运行,正确

1
2
3
4
5
6
#Person对象
person:
name: luofeng
age: 18
email: 123456@163.com
....

image-20210627202417584

3 JSR303校验常见参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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 对象是否符合正则表达式的规则

.......等等

除此以外,我们还可以自定义一些数据校验规则

1595480813196

5 自动配置原理

先思考一个原问题:配置文件到底能写什么?怎么写?答曰:spring.factories,看如下分解:

SpringBoot官方文档中有大量的配置,也就100多个?反正记不住。

https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/html/appendix-application-properties.html#core-properties

1595493746481

配置文件application.yaml如何与 类路径下的META-INF/spring.factories联系起来呢?

一句话的结论就是:yaml配置文件能配置什么 就必然参照某个功能xxProperties对应的一个属性类

5.1 发现 spring.factories

SpringBoot启动的时候加载主配置类,开启了自动配置功能 @EnableAutoConfiguration

@EnableAutoConfiguration 作用

  • 利用EnableAutoConfigurationImportSelector给容器中导入一些组件

  • 可以查看selectImports()方法的内容,他返回了一个autoConfigurationEnty,来自this.getAutoConfigurationEntry(autoConfigurationMetadata,annotationMetadata);这个方法我们继续来跟踪:

  • 这个方法有一个值:List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);叫做获取候选的配置 ,我们点击继续跟踪

    • SpringFactoriesLoader.loadFactoryNames()
    • 扫描所有jar包类路径下META-INF/spring.factories
    • 把扫描到的这些文件的内容包装成properties对象
    • 从properties中获取到EnableAutoConfiguration.class类(类名)对应的值,然后把他们添加在容器中
@EnableAutoConfiguration

现在我们来看一下META-INF/spring.factories(有好几页,这里只看前面一点点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\

AopAutoConfiguration、RabbitAutoConfiguration、BatchAutoConfiguration、HttpEncodingAutoConfiguration….等等等

每一个这样的 xxxAutoConfiguration类都是容器中的一个组件,都加入到容器中;用他们来做自动配置;

5.2 HttpEncodingAutoConfiguration一个具体的自动配置类

我们以HttpEncodingAutoConfiguration(Http编码自动配置)为例解释自动配置原理;该类的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件;
@Configuration

//启动指定类的ConfigurationProperties功能;
//进入这个HttpProperties查看,将配置文件中对应的值和HttpProperties绑定起来;
//并把HttpProperties加入到ioc容器中
@EnableConfigurationProperties({HttpProperties.class})

//Spring底层@Conditional注解
//根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效;
//这里的意思就是判断当前应用是否是web应用,如果是,当前配置类生效
@ConditionalOnWebApplication(
type = Type.SERVLET
)

//判断当前项目有没有这个类CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器;
@ConditionalOnClass({CharacterEncodingFilter.class})

//判断配置文件中是否存在某个配置:spring.http.encoding.enabled;
//如果不存在,判断也是成立的
//即使我们配置文件中不配置pring.http.encoding.enabled=true,也是默认生效的;
@ConditionalOnProperty(
prefix = "spring.http.encoding",
value = {"enabled"},
matchIfMissing = true
)

public class HttpEncodingAutoConfiguration {
//他已经和SpringBoot的配置文件映射了
private final Encoding properties;
//只有一个有参构造器的情况下,参数的值就会从容器中拿
public HttpEncodingAutoConfiguration(HttpProperties properties) {
this.properties = properties.getEncoding();
}

//给容器中添加一个组件,这个组件的某些值需要从properties中获取
@Bean
@ConditionalOnMissingBean //判断容器没有这个组件?
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
return filter;
}
//。。。。。。。
}

如果HttpEncodingAutoConfiguration这个配置类生效,我们就开始装配:

  • 一但这个配置类生效;这个配置类就会给容器中添加各种组件;
  • 这些组件的属性是从对应的XXproperties类中获取的,这些类里面的每一个属性又是和配置文件application.yaml绑定的;
  • 即所有application.yaml配置文件中能配置的属性都是在xxxxProperties类中封装着;

比如:spring.http这个属性

1
2
3
4
5
//从配置文件中获取指定的值和bean的属性进行绑定
@ConfigurationProperties(prefix = "spring.http")
public class HttpProperties {
// .....
}

我们去配置文件里面试试前缀,看提示!

1595493884773

这就是自动装配的原理!

5.3 判断自动配置类是否生效

​ 自动配置类必须在一定的条件下才能生效。用来判断的条件一般就是@Conditional的派生注解。必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效

@Conditional扩展注解 作用(判断是否满足当前指定条件)
@ConditionalOnJava 系统的java版本是否符合要求
@ConditionalOnJava 容器中存在指定Bean ;
@ConditionalOnMissingBean 容器中不存在指定Bean ;
@ConditionalOnExpression 满足SpEL表达式指定
@ConditionalOnClass 系统中有指定的类
@ConditionalOnMissingClass 系统中没有指定的类
@ConditionalOnSingleCandidate 容器中只有一个指定的Bean ,或者这个Bean是首选Bean
@ConditionalOnProperty 系统中指定的属性是否有指定的值
@ConditionalOnResource 类路径下是否存在指定资源文件
@ConditionalOnWebApplication 当前是web环境
@ConditionalOnNotWebApplication 当前不是web环境
@ConditionalOnJndi JNDI存在指定项

​ 那么多的自动配置类,必须在一定的条件下才能生效;也就是说,我们加载了这么多的配置类,但不是所有的都生效了。

查看一个配置类是否生效

​ 我们可以在application.properties通过启用 debug=true属性;在控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效;

1
2
#开启springboot的调试类
debug=true
  • Positive matches:(自动配置类启用的:正匹配)

  • Negative matches:(没有启动,没有匹配成功的自动配置类:负匹配)

  • Unconditional classes: (没有条件的类)

6 静态资源处理

​ 回顾一下以前的web项目,我们的main下会有一个webapp,存放所有的页面(jsp或html)。那么对springboot的项目来说,静态资源应该放在哪里呢?答案是resources下的各种包下面。先看下源码、理解是为什么。

6.1 静态资源映射规则

  • SpringBoot中,SpringMVC的web配置都在 WebMvcAutoConfiguration 这个配置类里面;
  • 我们可以去看看 WebMvcAutoConfigurationAdapter 中有很多配置方法;
  • 有一个方法:addResourceHandlers 添加资源处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
// 已禁用默认资源处理
logger.debug("Default resource handling disabled");
return;
}
// 缓存控制
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
// 方法一:webjars 配置
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
// 方法二:静态资源配置 放在指定的几个文件夹下
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}

看上面的源码,发现"/webjars/**"就是一个资源的存放路径, 都需要去 classpath:/META-INF/resources/webjars/ 找对应的资源。

所谓的Webjars,本质就是以jar包的方式引入我们的静态资源 ,也是下面介绍的第一种导入方式

6.2 导入静态资源的三种方式

1 webjars导入静态资源

Webjars的官网有将Webjars引入spingboot的各种依赖。网站:https://www.webjars.org

要使用jQuery的 静态资源,我们只要要引入jQuery对应版本的pom依赖即可!

1
2
3
4
5
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.4.1</version>
</dependency>

导入完毕,查看webjars目录结构,并访问Jquery.js文件!

1595506633980

访问:只要是静态资源,SpringBoot就会去对应的路径寻找资源,我们这里访问:http://localhost:8080/webjars/jquery/3.4.1/jquery.js

image-20210628102503664

2 导入自己的静态资源

1、那我们项目中要是使用自己的静态资源该怎么导入呢?我们看下一行代码;

1595516976999

2、我们去找staticPathPattern发现第二种映射规则 :/** , 访问当前的项目任意资源,它会去找 resourceProperties 这个类,我们可以点进去看一下分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 进入方法
public String[] getStaticLocations() {
return this.staticLocations;
}
// 找到对应的值
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
// 找到路径
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
};

3、ResourceProperties 可以设置和我们静态资源有关的参数;这里面指向了它会去寻找资源的文件夹,即上面数组的内容。

4、所以得出结论,以下四个目录存放的静态资源可以被我们识别:

1
2
3
4
"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"

5、我们可以在resources根目录下新建对应的文件夹,都可以存放我们的静态文件;

1595517831392

6、比如我们访问 http://localhost:8080/1.js , 他就会去这些文件夹中寻找对应的静态资源文件;

image-20210628103359207

实验了一下,优先级:resources>static>public

3 自定义静态资源路径

我们也可以自己通过配置文件来指定一下,哪些文件夹是需要我们放静态资源文件的,在application.properties中配置;

1
spring.resources.static-locations=classpath:/wukang/
image-20210628103731965 image-20210628103753708

显而易见,自己设置路径的优先级最高,但不推荐这样做,这样做会使其他静态资源的路径都失效!!

6.4 首页的定义

  • WebMvcAutoConfiguration自动装配类
    • welcomePageHandlerMapping()欢迎页面处理
      • getWelcomePage()方法用来获取欢迎页面
        • getIndexHtml() 获取首页的html页面
1
2
3
4
5
6
7
8
9
10
11
12
// 欢迎页就是一个location下的的 index.html 而已
private Resource getIndexHtml(Resource location) {
try {
Resource resource = location.createRelative("index.html");
if (resource.exists() && resource.getURL() != null) {
return resource;
}
} catch (Exception var3) {
}

return null;
}

截图说明(old):

1595550098734

结论:

在上面的3个目录resources、static、public中任意一个中键index.html页面,当我们访问 http://localhost:8080/,就会自动跳转到这个index.html页面!!

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h1>这是首页哦</h1>
</body>
</html>
image-20210628105854439

7 模板引擎Thymeleaf

模板引擎就是将一个模板页面Template和一个后台的数据Data,解析并填充、形成最终的output.html页面

1595555521951

以前我们一般使用jsp,jsp好处就是当我们查出一些数据转发到JSP页面以后,我们可以用jsp轻松实现数据的显示,及交互等。但springboot默认不支持jsp,springboot推荐使用Thymeleaf模板引擎。

7.1 Thymeleaf 引入

怎么引入呢,对于springboot来说,什么事情不都是一个start的事情嘛,我们去在项目中引入一下。给大家三个网址:

找到对应的pom依赖:可以适当点进源码看下本来的包!

1
2
3
4
5
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

引入之后我们看一下thymeleaf的源码,Thymeleaf的自动配置类:ThymeleafProperties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ConfigurationProperties(
prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
private boolean checkTemplate = true;
private boolean checkTemplateLocation = true;
private String prefix = "classpath:/templates/";
private String suffix = ".html";
private String mode = "HTML";
private Charset encoding;
//...
}

我们可以看到默认的路径和后缀:

1
2
private String prefix = "classpath:/templates/";
private String suffix = ".html";

结论:

只需要把我们的html页面放在类路径下的templates下,thymeleaf就可以帮我们自动渲染了。

下面简单写一个引入Thymeleaf的测试示例:

1 在templates包下编写test.html 前端页面 ,注意引入命名空间的约束

2 编写测试请求,传输一个变量值给前端页面

3 启动测试、访问前端界面的url

1 编写test.html 前端页面 ,引入Thymeleaf命名空间

1
2
<!-- 注意引入命名空间的约束 -->
xmlns:th="http://www.thymeleaf.org"

test.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>WuKang</title>
</head>
<body>

<h1>测试一下</h1>
<!--th:text就是将div中的内容设置为它指定的值,和之前学习的Vue一样-->
<div th:text="${value_wk}"></div>

</body>
</html>

2 编写测试请求,传输一个变量值value_wk给前端页面

1
2
3
4
5
6
7
8
9
10
@Controller
public class ThymeleafController {
@RequestMapping("/test")
public String test(Model model){
//存入数据
model.addAttribute("value_wk","Hello,Thymeleaf");
//classpath:/templates/test.html
return "test";
}
}

3 测试并访问

image-20210628140604670

7.2 Thymeleaf 语法

官方文档在此:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#standard-expression-syntax

大体上有个印象即可,不会的直接百度或者看官方文档。这里只演示一个遍历循环的取值。

Thmeleft的循环遍历语法

1
Fragment iteration	th:each

1 编写一个测试方法,给前端传入一个map集合,含两个键值对

1
2
3
4
5
6
7
8
@RequestMapping("/test2")
private String test2(Map<String,Object> map){
map.put("key1","<h1>Hello</h1>");
List<String> list = Arrays.asList("pig","tiger");
map.put("key2",list);
//classpath:/templates/test2.html
return "test2";
}

2 前端界面取数据,并展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>test02</title>
</head>
<body>
<h4>取第一个键值对:</h4>
<!--不转义 用text-->
<div th:text="${key1}"></div>
<!--转义 用utext-->
<div th:utext="${key1}"></div>
<hr>
<h4>取第二个键值对:</h4>
<div th:each="animal:${key2}" th:text="${animal}"></div>
<!--或者用行内写法(不推荐)-->
<div th:each="animal:${key2}">[[${animal}]]</div>
</body>
</html>

3 测试并访问

image-20210628145827744

这里简要介绍一下Thymeleaf的基本变量和运算符号,其中条件运算(三元运算符)用的比较多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Literals(字面量)
Text literals: 'one text' , 'Another one!' ,…
Number literals: 0 , 34 , 3.0 , 12.3 ,…
Boolean literals: true , false
Null literal: null
Literal tokens: one , sometext , main ,…

Text operations:(文本操作)
String concatenation: +
Literal substitutions: |The name is ${name}|

Arithmetic operations:(数学运算)
Binary operators: + , - , * , / , %
Minus sign (unary operator): -

Boolean operations:(布尔运算)
Binary operators: and , or
Boolean negation (unary operator): ! , not

Comparisons and equality:(比较运算)
Comparators: > , < , >= , <= ( gt , lt , ge , le )
Equality operators: == , != ( eq , ne )

Conditional operators:条件运算(三元运算符)
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)

8 实战:员工管理

这里不贴具体的代码呐,只讲讲具体的流程、原理也最好不涉及,对于项目来说、会用就行。对于框架思想、在具体的知识点中学习就好了

8.1 静态资源、实体类的准备

1 前端界面的准备工作

  • 将index.html、404.html、dashboard.html、list.html四个html界面放入templates目录
  • 将css,js,img放入到static目录

2 实体类,有员工和部门两个实体类

  • 编写Department类,id、departmentName
  • 编写Employee类,id、lastName、email、gender、department、birth

3 dao层的编写 因为没有数据库,直接用静态代码写死数据、作为数据库

  • DepartmentDao表示部门的dao层
    • 获得所有部门信息getDepartment()方法
    • 通过id得到部门getDepartmentById(Integer id) 方法
  • EmployeeDao表示员工的dao层
    • 增加一个员工save(Employee employee)
    • 查询全部员工信息getAll()
    • 通过id查询员工getEmployeeById(Integer id)
    • 通过id删除员工 delete(Integer id)
1595732679388

8.2 首页及国际化

指定首页的两种方式

方式一:创建一个IndexController,写一个返回首页的方法(不建议使用

1
2
3
4
5
6
7
@Controller
public class IndexController{
@RequestMapping({"/","/index.html"})
public String index() {
return "index";
}
}

方式二:使用自己的MyMvcConfig配置扩展springboot对mvc的自动配置。创建一个config目录,在里面写一个MyMvcConfig,里面重写addViewControllers方法

1
2
3
4
5
6
7
@Configuration
public class MyMvcConfig implements WebMvcConfigurer{
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
}

然后导入thymeleaf依赖包,来加载静态资源

1
<html lang="en" xmlns:th="http://www.thymeleaf.org">

修改所有页面的静态资源,采用thymeleaf的语法,使用thymeleaf接管

1
2
所有的资源路径用 th:src="@{}"来表示
如:th:src="@{/js/jquery-3.2.1.slim.min.js}"

运行,得到首页的展示如下

1595733822534

页面国际化

在Spring中有一个国际化的Locale (区域信息对象);里面有一个叫做LocaleResolver (获取区域信息对象)的解析器!

我们的目的是可以根据按钮自动切换中文英文!

1 首先在File Encodings里面将所有编码设为UTF-8格式(还有勾选)

2 编写i18n的配置文件(每一个页面都要写一组xxxx.properties配置) 挺烦的

  • 在resources资源文件下新建一个i18n目录,存放国际化配置,这里以login页面的国际化为例
  • 建立一个login.properties文件,还有login_zh_CN.properties、login_en_US.properties文件
  • 编写这三个properties文件,注意可以可视化的对比编写

3 在application.properties配置文件中指定路径,是国际化的配置生效

1
spring.messages.basename=i18n.login

4 修改index.xml页面 中的取值操作,这些地方就对应于主页的那几个提示文字

image-20210629002443214

image-20210629002601751

5 增加根据按钮自动切换中文英文的功能

  • 修改前端页面的跳转连接:
1
2
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
  • 在config包下写一个处理的组件类MyLocaleResolver
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyLocaleResolver implements LocaleResolver {
//解析请求
@Override
public Locale resolveLocale(HttpServletRequest request) {
//获取请求中的语言参数
String language=request.getParameter("l");
Locale locale=Locale.getDefault();//如果没有就使用默认
//如果请求的连接携带了国际化参数
if(!StringUtils.isEmpty(language)){
String[] split=language.split("_");
locale=new Locale(split[0],split[1]);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {
}
}
  • 在我们自己的MvcConofig下添加bean;使区域化信息生效
1
2
3
4
5
//自定义国际化生效
@Bean
public LocaleResolver localeResolver(){
return new MyLocaleResolver();
}

最后重启项目,访问,可以正常切换中英文:

FotoJet

8.3 登录页跳转和拦截器

登录页跳转

​ 验证用户名和密码,进入登录界面。

​ 密码错误可以报错

​ 隐藏url上用户名和密码的明文显示,用main.html字符代替

拦截器

​ 避免直接输入http://localhost:8080/main.html 就能访问首页的i情况,只能登录之后才能进入首页

登录页跳转

1 登录页面表单的修改,指定跳转路径 th:action=”@{/user/login}”

1
2
3
4
5
<!--提交表单的url是/user/login,由LoginController跳转过来-->
<form class="form-signin" th:action="@{/user/login}">

<!--增加错误提示行 当有msgWrong传过时,表示登录失败 给出提示信息-->
<p style="color: #ff0000" th:text="${msgWrong}" th:if="${not #strings.isEmpty(msgWrong)}"></p>

2 写一个LoginController登录验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Controller
public class LoginController {
@RequestMapping("/user/login")
public String login(@RequestParam("username") String userName,
@RequestParam("password") String passWord,
Model model,
HttpSession session){
if(!StringUtils.isEmpty(userName)&&passWord.equals("1")){
//设置session保存已经登录的账号的信息(用户名)
session.setAttribute("loginUser",userName);
//return "dashboard"; //避免密码和用户名泄露,不直接返回
return "redirect:/main.html";
}else{
model.addAttribute("msgWrong","用户名或者密码错误");
return "index";
}
}
}

3 用main.html映射解决 明文密码的问题:

  • 修改LoginController跳转页面代码(redirect跳转) 上文已改
1
2
//return "dashboard"; //避免密码和用户名泄露,不直接返回
return "redirect:/main.html";
  • 加一个main.html映射在MyMvcConfig类的addViewControllers方法中
1
2
//避免url泄露用户名和密码的问题,将实际访问dashboard页面时与/main.html映射
registry.addViewController("/main.html").setViewName("dashboard");

密码为1登录成功 和密码不为1登录失败 分别如下:

image-20210629131537110 image-20210629131700607

拦截器

1 在LoginController中添加一个session判断登录(上文已写)

1
2
//设置session保存已经登录的账号的信息(用户名)
session.setAttribute("loginUser",userName);

2 在config页面写一个LoginHandlerInterceptor拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//登录界面的拦截器,通过session来判断是否拦截
//实现了HandlerInterceptor接口的就是一个拦截器
public class LoginHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//通过session获取用户信息
Object loginUser = request.getSession().getAttribute("loginUser");
if(loginUser==null){
//session里面无信息表示没有登录
request.setAttribute("msgWrong","未登录,请先登录");
//这个是什么意思没搞懂?
request.getRequestDispatcher("/index.html").forward(request,response);
return false; //拦截了
} else{
return true; //不拦截
}
}
}

3 MyMvcConfig页面重写拦截器方法http://localhost:8080/main.html,将会提示错误

1
2
3
4
5
6
7
//重写拦截器方法!!
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截所有界面"/**",,排除主页和静态资源"/index.html","/","/user/login","static/**"
registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/index.html","/","/user/login","/static/**");
}

没有登录直接访问,将提示错误信息

image-20210629132456232

8.4 员工列表展示

1 编写后台EmployeeController,获取员工数据,作为集合传给前端

2 提取dashboard.html和list.html的公共页面:顶部导航栏、侧边栏

3 list.html写列表循环展示后端传来的员工信息数据 th:each命令

1 编写后台EmployeeController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
public class EmployeeController {
//controller层调用dao层 (其实还应该有service层 这里省略)
@Autowired //使用注解实现自动装配
EmployeeDao employeeDao;
//展示所有员工
@RequestMapping("/emps")
public String list(Model model){
//调用dao层,获取数据
Collection<Employee> employees = employeeDao.getAll();
model.addAttribute("emps",employees); //传递一个集合给前端
return "emp/list"; //返回的前端页面是emp文件夹下的list.html
}
}

2 提取dashboard.html和list.html的公共页面

  • templates目录下面创建commons目录,在commons目录下面创建commons.html放公共代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!--只写改变的代码-->

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<!--顶部导航栏,设置框架名为topbar-->
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar">
.............
</nav>

<!--侧边栏 设置框架名为sidebar-->
<nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar">
<div class="sidebar-sticky">
<ul class="nav flex-column">
<li class="nav-item">
<!--这里首页的a标签 直接跳转到登陆页面-->
<a class="nav-link active" th:href="@{/index.html}">
.............
首页 <span class="sr-only">(current)</span>
</a>
</li>
.............
<li class="nav-item">
<!--员工管理的a标签 执行/emps的url会执行EmployeeController中的list方法-->
<a class="nav-link" th:href="@{/emps}">
.............
员工管理
</a>
</li>
.............
</ul>
.............
</div>
</nav>
</html>
  • dashboard.html和list.html页面一样,将原来的导航栏和侧边栏代码,替换为一行代码
1
2
3
4
5
<!--顶部导航栏-->
<div th:replace="~{commons/commons::topbar}"></div>

<!--侧边栏-->
<div th:replace="~{commons/commons::sidebar}"></div>

3 list.html写列表循环展示后端传来的员工信息数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!--侧边栏-->
<div th:replace="~{commons/commons::sidebar(active='list.html')}"></div>

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<h2>Section title</h2>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>id</th>
<th>lastName</th>
<th>email</th>
<th>gender</th>
<th>department</th>
<th>birth</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="emp:${emps}">
<td th:text="${emp.getId()}"></td>
<td th:text="${emp.getLastName()}"></td>
<td th:text="${emp.getEmail()}"></td>
<td th:text="${emp.getGender()==0?'女':'男'}"></td>
<td th:text="${emp.department.getDepartmentName()}"></td>
<td th:text="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"></td>
<td>
<button class="btn btn-sm btn-primary">编辑</button>
<button class="btn btn-sm btn-danger">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</main>

页面展示

image-20210629193959128

基本的框架搭建起来之后,就是后台数据的增删改的操作了!!

8.5 添加员工信息

1 在list.html界面添加一个“新增员工“的按钮

2 后台编写toAddPage()方法,实现点击前端的”新增“按钮,跳转至添加员工的表单页面

3 编写add.html页面

4 后台获取add.html页面表单提交的数据,修改dao层数据

5 注意时间格式,在application.properties文件中添加格式配置

1 在list.html界面添加一个“新增员工“的按钮

1
<h2><a class="btn btn-sm btn-success" th:href="@{/emp}">添加员工</a></h2>

2 后台编写toAddPage()方法

1
2
3
4
5
6
7
8
9
10
@Autowired //使用注解实现自动装配
DepartmentDao departmentDao;
//@getMapping = @requestMapping(method = RequestMethod.GET)。
@GetMapping("/toAddPage") //以get方式传递数据
public String toAddPage(Model model){
//查出所有部门的信息
Collection<Department> department = departmentDao.getDepartment();
model.addAttribute("departments",department);
return "emp/add";
}

3 编写add.html页面(其他部分和list.html页面一样,只改main中的代码即可)

  • 注意:下拉框提交的时候应提交一个属性,因为其在controller接收的是一个Employee,否则会报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<form th:action="@{/addEmp}" method="post">
<div class="form-group">
<label>LastName</label>
<input type="text" name="lastName" class="form-control" placeholder="海绵宝宝">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" name="email" class="form-control" placeholder="1176244270@qq.com">
</div>
<div class="form-group">
<label>Gender</label><br>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="1">
<label class="form-check-label"></label>
</div>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="0">
<label class="form-check-label"></label>
</div>
<div class="form-group">
<label>department</label>
<select class="form-control" name="department.id">
<!--我们在controller接收的是一个Employee,所以我们需要提交的是其中的一个属性-->
<option th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}">1</option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input type="text" name="birth" class="form-control" placeholder="2020/07/25 18:00:00">
</div>
<button type="submit" class="btn btn-primary">添加</button>
</form>
</main>

4 后台获取add.html页面表单提交的数据,修改dao层数据

1
2
3
4
5
6
//@postMapping = @requestMapping(method = RequestMethod.POST)。
@PostMapping("/addEmp")//以post方式传递数据,传递一个Employee对象
public String addEmp(Employee employee) {
employeeDao.save(employee);//调用底层业务方法保存员工信息
return "redirect:/emps"; //新增员工后就立即跳转刷新到展示员工的界面
}

5 日期格式的修改

  • 如果输入的日期格式为2020-01-01,则会报错。在application.properties文件中添加配置
1
spring.mvc.format.date=yyyy-MM-dd

页面展示:

image-20210629205448874 image-20210629205535752 image-20210629205617130

8.6 修改、删除、注销和404页面

1 修改员工信息步骤如下:

1 list页面展示员工的表单里面添加编辑按钮

2 写toUpdateEmp()方法,用于跳转到修改页面

3 编写update.html页面(主体和update.html页面一样,修改main),接受数据,提交数据

4 写updateEmp()方法,修改dao层的数据

2 删除员工信息步骤如下:

1 list页面展示员工的表单里面添加删除按钮

2 编写deleteEmp()方法,删除员工,修改dao层的数据

3 注销功能的实现步骤如下:

1 在commons.html中修改注销按钮

2 在LoginController.java中编写注销页面的logout()方法

4 404页面

将404.html页面放入到templates目录下面的error目录中即可实现自动跳转404界面

8.7 总结:如何搭建一个网站

搭建一个网站的步骤:

  1. 前端搞定:页面长什么样子
  2. 设计数据库(数据库设计难点)
  3. 前端让他能够自动运行,独立化工程
  4. 数据接口如何对接:json,对象,all in one!
  5. 前后端联调测试

模板:

  1. 有一套自己熟悉的后台模板:工作必要!x-admin
  2. 前端页面:至少自己能够通过前端框架,组合出来一个网站页面
    • index
    • about
    • blog
    • post
    • user
  3. 让这个网站能够独立运行!

9 整合JDBC、集成Druid、整合Mybatis

对于数据访问层,无论是 SQL(关系型数据库) 还是 NOSQL(非关系型数据库),Spring Boot 底层都是采用 Spring Data 的方式进行统一处理。Spring Data 也是 Spring 中与 Spring Boot、Spring Cloud 等齐名的知名项目。

Sping Data 官网:https://spring.io/projects/spring-data

数据库相关的启动器 :可以参考官方文档:https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter

9.1 整合JDBC

1 测试显示默认数据源

1 新建一个springboot的web项目 勾选JDBC API和MySQL Driver

2 编写yaml配置文件连接数据库

3 cmd打开Mysql服务,springboot连接数据库、选mybatis库(本来就有)

4 test包下的测试类打印默认数据源和数据库连接connection

2 编写application.yaml配置文件连接数据库

1
2
3
4
5
6
spring:
datasource:
username: root
password: '526736'
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver

4 test包下的测试类Springboot04DataApplicationTests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
class Springboot04DataApplicationTests {
@Autowired //DI注入数据源
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

查看源码:DataSourceAutoConfiguration文件:

1
2
3
4
5
6
7
@Conditional({DataSourceAutoConfiguration.PooledDataSourceCondition.class})
@ConditionalOnMissingBean({DataSource.class, XADataSource.class})
@Import({Hikari.class, Tomcat.class, Dbcp2.class, OracleUcp.class, Generic.class, DataSourceJmxConfiguration.class})
protected static class PooledDataSourceConfiguration {
protected PooledDataSourceConfiguration() {
}
}

这些导入的类都在 DataSourceConfiguration 配置类下,可以看出 Spring Boot 2.2.5 默认使用HikariDataSource 数据源。HikariDataSource 号称 Java WEB 当前速度最快的数据源,相比于传统的 C3P0 、DBCP、Tomcat jdbc 等连接池更加优秀;

当然也可以通过yaml配置指定数据源类型

1
2
3
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource #数据源DruidDataSource

2 使用 JDBCTemplate 操作CURD

  • Spring 本身也对原生的JDBC 做了轻量级的封装,即JdbcTemplate。数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。

  • Spring Boot 默认将 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用。JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 类

    • execute方法可以用于执行任何SQL语句,一般用于执行DDL语句;
    • update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;
    • query方法及queryForXXX方法:用于执行查询相关语句;
    • call方法:用于执行存储过程、函数相关语句

直接编写一个JDBCController,进行增删改查的工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@RestController
public class JdbcController {
@Autowired //注解方式引入bean
JdbcTemplate jdbcTemplate;

//查询数据库
@GetMapping("/userList")
public List<Map<String,Object>> userList(){
String sql = "select * from user";
List<Map<String, Object>> mapList = jdbcTemplate.queryForList(sql);
return mapList;
}

@GetMapping("/addUser")
public String addUser() {
String sql = "insert into mybatis.user(id, name, pwd) values(5,'小明','123456')";
jdbcTemplate.update(sql);
return "add-ok";
}

//修改数据
@GetMapping("/updateUser/{id}")
public String updateUser(@PathVariable("id")int id){
String sql = "update mybatis.user set name = ?,pwd = ? where id = " + id;
Object[] objects = new Object[]{"小明5","ssss"};
jdbcTemplate.update(sql,objects);
return "update-Ok";
}

//删除数据
@GetMapping("/deleteUser/{id}")
public String deleteUser(@PathVariable("id")int id){
String sql = "delete from mybatis.user where id = ?";
jdbcTemplate.update(sql,id);
return "delete-Ok";
}
}
image-20210630193550419 image-20210630193830659 image-20210630193855039 image-20210630193946626 image-20210630194022411

9.2 集成Druid数据源

1 Druid数据源介绍

  • Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。

  • Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。

Github地址:https://github.com/alibaba/druid/

Spring Boot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源,我们来重点介绍 Spring Boot 如何集成 Druid 数据源,如何实现数据库监控。

Druid 数据源部分参数含义如下:

1
2
3
4
5
6
7
8
9
10
11
initialSize: 5 #初始化时建立物理连接的个数
minIdle: 5 #最小连接池数量
maxActive: 20 #最大连接池数量
maxWait: 60000 #获取连接时最大等待时间,单位毫秒。
timeBetweenEvictionRunsMillis: 60000 #Destroy线程会检测连接的间隔时间
minEvictableIdleTimeMillis: 300000 #连接保持空闲而不被驱逐的最长时间
validationQuery: SELECT 1 FROM DUAL #单位:秒,检测连接是否有效的超时时间。
testWhileIdle: true #申请连接的时候检测 不影响性能,并且保证安全性
testOnBorrow: false #申请连接时执行validationQuery检测连接是否有效 降低性能
testOnReturn: false #归还连接时执行validationQuery检测连接是否有效,降低性能
poolPreparedStatements: true #是否缓存preparedStatement

2 集成Druid数据源

1 添加上 Druid 数据源依赖

2 配置文件切换数据源,并设置Druid 数据源的参数

3 导入Log4j 的依赖

4 编写DruidConfig类,为 DruidDataSource 绑定全局配置文件中的参数

5 测试 数据源是否切换成功

6 配置Druid数据源监控(重要)

7 配置 Druid web 监控 filter 过滤器(不重要)

1 Druid 数据源依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.23</version>
</dependency>

2 配置文件切换数据源,并设置Druid 数据源的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 自定义数据源
#Spring Boot 默认是不注入这些属性值的,需要自己绑定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.Priority
#则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

3 导入Log4j 的依赖

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

4 编写DruidConfig类,为 DruidDataSource 绑定全局配置文件中的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class DruidConfig {
/*
将自定义的 Druid数据源添加到容器中,不再让 Spring Boot 自动创建
绑定全局配置文件中的 druid 数据源属性到 com.alibaba.druid.pool.DruidDataSource从而让它们生效
@ConfigurationProperties(prefix = "spring.datasource"):作用就是将 全局配置文件中
前缀为 spring.datasource的属性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名参数中
*/
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource() {
return new DruidDataSource();
}
}

5 测试 数据源是否切换成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SpringBootTest
class Springboot04DataApplicationTests {
@Autowired //DI注入数据源
DataSource dataSource;
@Test
void contextLoads() throws SQLException {
//看一下默认数据源
System.out.println(dataSource.getClass());
//获得连接 打印连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
//打印连接池信息
DruidDataSource druidDataSource = (DruidDataSource) dataSource;
System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive());
System.out.println("druidDataSource 数据源初始化连接数:" + druidDataSource.getInitialSize());
connection.close();
}
}
image-20210701081752908

6 配置Druid数据源监控(重要)

Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看。

但是需要自己设置一下后台管理页面,比如 登录账号、密码 等;配置后台管理;

相当于说Druid的一些功能都需要通过Servlet来实现,而在springboot中实现Servlet是通过注册的方式来实现的,注册一个开始页面的Servlet

在DruidConfig类写一个开始页面的Servlet:statViewServlet()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//配置 Druid 监控管理后台的Servlet;
//springboot内置 Servlet 容器时没有web.xml文件,所以使用注册的 Servlet 方式
//注册返回一个ServletRegistrationBean对象
@Bean
public ServletRegistrationBean statViewServlet(){
ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");

// 这些参数可以在 com.alibaba.druid.support.http.StatViewServlet
// 的父类 com.alibaba.druid.support.http.ResourceServlet 中找到
Map<String, String> initParams = new HashMap<>();
initParams.put("loginUsername", "admin"); //后台管理界面的登录账号
initParams.put("loginPassword", "123456"); //后台管理界面的登录密码

//后台允许谁可以访问
//initParams.put("allow", "localhost"):表示只有本机可以访问
//initParams.put("allow", ""):为空或者为null时,表示允许所有访问
initParams.put("allow", "");
//deny:Druid 后台拒绝谁访问
//initParams.put("kuangshen", "192.168.1.20");表示禁止此ip访问

//设置初始化参数
bean.setInitParameters(initParams);
return bean;
}

配置完毕后,我们可以选择访问 :http://localhost:8080/druid/login.html

image-20200727233409312image-20200727233436583

进入之后

image-20200727233436583

7 配置 Druid web 监控 filter 过滤器(可有可无)

在DruidConfig类写一个过滤器的Servlet:webStatFilter()方法

//WebStatFilter类:用于配置Web和Druid数据源之间的管理关联监控统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 配置 Druid 监控 之  web 监控的 filter
//WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计
@Bean
public FilterRegistrationBean webStatFilter() {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new WebStatFilter());

//exclusions:设置哪些请求进行过滤排除掉,从而不进行统计
Map<String, String> initParams = new HashMap<>();
initParams.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*");
bean.setInitParameters(initParams);

//"/*" 表示过滤所有请求
bean.setUrlPatterns(Arrays.asList("/*"));
return bean;
}

9.3 整合Mybatis

官方文档:http://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/

Maven仓库地址:https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter/2.1.3

这里写的整合步骤是在已经完成数据库连接、数据源的配置、的基础之上,所以只需要引入依赖、写mapper接口、写xml的SQL语句、再写一个controller调用mapper就可。

1 导入 MyBatis 所需要的依赖、导入Lombok的依赖

2 创建实体类

3 创建一个 Mapper 接口,路径为com.kuang.mapper

4 编写对应的Mapper映射文件UserMapper.xml,这里也放在com.kuang.mapper下(其实最好放在resources/mapper下,都可)

5 application.yaml配置文件加上MyBatis 的配置!!

6 编写部门的 UserController 进行测试!

1 导入 MyBatis 所需要的依赖、导入Lombok的依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

2 创建实体类

1
2
3
4
5
6
7
8
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}

3 创建一个 Mapper 接口

1
2
3
4
5
6
7
8
9
@Mapper //表示是一个mybatis的mapper类
@Repository //由springboot接管
public interface UserMapper {
List<User> queryUserList();
User queryUserById(int id);
int addUser(User user);
int updateUser(User user);
int deleteUser(int id);
}

4 映射文件UserMapper.xml

  • 这里一定要注意绑定的命名空间 我因为这个搞了一上午
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace=绑定一个对应的Dao/Mapper接口-->
<mapper namespace="com.kuang.mapper.UserMapper">

<select id="queryUserList" resultType="com.kuang.pojo.User">
select * from mybatis.user;
</select>

<select id="queryUserById" resultType="User">
select * from mybatis.user where id = #{id};
</select>

<insert id="addUser" parameterType="User">
insert into mybatis.user (id, name, pwd) values (#{id},#{name},#{pwd});
</insert>

<update id="updateUser" parameterType="User">
update mybatis.user set name=#{name},pwd = #{pwd} where id = #{id};
</update>

<delete id="deleteUser" parameterType="int">
delete from mybatis.user where id = #{id}
</delete>
</mapper>

5 application.yaml配置文件加上MyBatis 的配置!!

  • 这里给出两种格式的
1
2
3
mybatis:
type-aliases-package: com.kuang.pojo
mapper-locations: classpath:mapper/*.xml
1
2
mybatis.type-aliases-package=com.kuang.pojo
mybatis.mapper-locations=classpath:mapper/*.xml

6 编写 UserController 并测试

  • 注意方法名和mapper.xml中的id保持一致!!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@RestController
public class UserController {

@Autowired
private UserMapper userMapper;
//获取用户列表
@GetMapping("/userList1")
public List<User> queryUserList(){
List<User> users = userMapper.queryUserList();
return users;
}
@GetMapping("/addUser1")
public String addUser(){
userMapper.addUser(new User(8,"abc","223344"));
return "ok";
}
@GetMapping("/updateUser1")
public String updateUser(){
userMapper.updateUser(new User(8,"abcdef","223344"));
return "ok";
}
@GetMapping("/deleteUser1")
public String deleteUser(){
userMapper.deleteUser(8);
return "ok";
}
}

这里只展示http://localhost:8080/userList1 的结果

image-20210702125644590

10 SpringSecurity

认证跟授权

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。

  • 用户认证指的是验证某个用户是否为系统中的合法主体。系统通过校验用户名和密码来完成认证过程。
  • 在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。
  • 用户授权指的是验证某个用户是否有权限执行某个操作。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
  • 在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。

10.1 认证和授权

用户认证:不登陆的话总是跳转到登录界面

1 新建一个springboot项目、导入web模块,thymeleaf模块,security模块

2 导入静态资源 static 和 templates包下

3 写一个RouterController,控制视图跳转

4 启动项目 访问路径

2 导入静态资源

image-20200728130501139

3 写一个RouterController,控制视图跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Controller
public class RouterController {
//跳到主页
@RequestMapping({"/index","/"})
public String index(){
return "index";
}
//跳到Login
@RequestMapping("/toLogin")
public String login(){
return "views/login";
}
@RequestMapping("/level1/{id}")
public String level1(@PathVariable("id") int id) {
return "views/level1/" + id;
}
@RequestMapping("/level2/{id}")
public String level2(@PathVariable("id") int id) {
return "views/level2/" + id;
}
@RequestMapping("/level3/{id}")
public String level3(@PathVariable("id") int id) {
return "views/level3/" + id;
}
}

4 访问设定的路径

http://localhost:8080

http://localhost:8080/index

http://localhost:8080/level/1

http://localhost:8080/login

image-20210702155923689

发现如论访问哪个路径都会跳转到login界面,这就是默认完成了认证功能。我们登录一下,用户名为user,密码在控制台为2c95c9e7-894b-40aa-abc0-6284a890e243。进入主页:

假设level1、levlel2、level3是三种权限,这里都可以访问。

image-20210702160004332

用户授权:不同的角色拥有不同的权限

记住几个类:

  • WebSecurityConfigurerAdapter:自定义Security策略 (观察者模式)
  • AuthenticationManagerBuilder:自定义认证策略(建造者模式)
  • @EnableWebSecurity:开启WebSecurity模式 (@EnableXXXX 开启某个功能)

官方文档

参考官网:https://spring.io/projects/spring-security

查看我们自己项目中的版本,找到对应的帮助文档:https://docs.spring.io/spring-security/site/docs/current/reference/html5/

文档中的16.4. Custom DSLs、16.5. Post Processing Configured Objects如下

1
2
3
4
5
6
7
8
9
10
11
@EnableWebSecurity
public class Config extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.apply(customDsl())
.flag(true)
.and()
...;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
public <O extends FilterSecurityInterceptor> O postProcess(
O fsi) {
fsi.setPublishAuthorizationSuccess(true);
return fsi;
}
})
);
}

配置Security代码如下

1 建一个SecurityConfig类继承WebSecurityConfigurerAdapter接口

2 重写configure(HttpSecurity http)方法,采用链式编程设定授权的规则

3 重写configure(AuthenticationManagerBuilder auth)方法,采用链式编程设定角色、用户账号,(用户密码需要加密,不然会报错)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@EnableWebSecurity //开启WebSecurity模式
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 定义 请求授权的规则
// 首页所有人都可以访问,功能也只有对应有权限的人才能访问到
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
// 开启自动配置的登录功能
// /login 请求来到登录页
// /login?error 重定向到这里表示登录失败
http.formLogin();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 认证,springboot 2.1.x 可以直接使用
// 密码编码: PasswordEncoder

//这些数据正常情况应该中数据库中读,这里仅演示作用
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("kuangshen").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");

}
}

10.2 权限控制和注销

实现注销功能

需求:开启注销功能,注销之后自动跳转到首页

1 开启自动配置的注销的功能

2 我们在前端,增加一个注销的按钮,index.html 导航栏中

3 测试,登录成功后点击注销!

1 configure(HttpSecurity http)方法中开启自动配置的注销的功能

1
2
3
//开启自动配置的注销的功能
//http.logout(); //注销成功返回登录页
http.logout().logoutSuccessUrl("/"); //注销成功来到首页

2 前端,增加一个注销的按钮

1
2
3
4
<!--注销-->
<a class="item" th:href="@{/logout}">
<i class="sign-out icon"></i> 注销
</a>

3 测试,登录成功后点击注销,发现注销完毕会跳转到index主页

image-20210702172023370 image-20210702172034965

实现显示该用户有权限的功能

需求:用户没有登录的时候,导航栏上只显示登录按钮,用户登录之后,导航栏可以显示登录的用户信息及注销按钮!还有就是,比如kuangshen这个用户,它只有 vip2,vip3功能,那么登录则只显示这两个功能,而vip1的功能菜单不显示!

1 修改我们的前端页面:导入命名空间、修改导航栏增加认证判断

2 重启测试,登录后显示了用户信息和注销按钮

3 完成角色功能块认证的功能,只要是编写前端代码

4 最后的测试

1 修改我们的前端页面:导入命名空间、修改导航栏增加认证判断

  • 导入命名空间
1
2
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
  • 修改导航栏,增加认证判断
1
2
3
4
5
6
<!--如果未登录-->
<div sec:authorize="!isAuthenticated()">
</div>
<!--如果已登录和注销按钮-->
<div sec:authorize="isAuthenticated()">
</div>

2 重启测试,登录后显示了用户信息和注销按钮

image-20200728213235625

3 完成角色功能块认证的功能,只要是编写前端代码

1
2
3
4
5
6
<!--菜单根据用户的角色动态的实现 vip1-->
sec:authorize="hasRole('vip1')"
<!--菜单根据用户的角色动态的实现 vip2-->
sec:authorize="hasRole('vip2')"
<!--菜单根据用户的角色动态的实现 vip3-->
sec:authorize="hasRole('vip3')"

4 最后的测试:三个不同的用户对应着不同的视图

image-20210702184407465 image-20210702184333913 image-20210702184252754

10.3 记住我功能

现在的情况,我们只要登录之后,关闭浏览器,再登录,就会让我们重新登录,但是很多网站的情况,就是有一个记住密码的功能,这个该如何实现呢?很简单

1 configure(HttpSecurity http)方法中开启记住我功能

2 测试

1 开启记住我功能

1
2
//开启记住我功能: cookie,默认保存两周
http.rememberMe();

2启动项目测试一下

  • 发现登录页多了一个记住我功能

  • 我们登录之后关闭 浏览器,然后重新打开浏览器访问,发现用户依旧存在!

    image-20200728222312694

思考:如何实现的呢?其实非常简单

我们可以查看浏览器的cookie

image-20200728222706154

我们点击注销的时候,可以发现,spring security 帮我们自动删除了这个 cookie

image-20200728223559077

本来还讲了Shiro框架,和springScurity的功能差不多、先跳过了

11 Swagger

前后端分离带来的最大的问题:前后端团队的信息沟通。一般是后端提供API接口给前端、前端测试并且使用。

而Swagger号称世界上最流行的API框架,API文档能够和API定义程序同步更新

11.1 前后端的问题及解决

前后端分离 Vue+SpringBoot

  • 前端 -> 前端控制层、视图层
    • 伪造后端数据,json。不需要后端,前端工程队依旧能够跑起来
  • 后端 -> 后端控制层Controller、服务层Service、数据访问层Dao
  • 前后端通过API进行交互、前后端相对独立且松耦合

前后端分离产生的问题

  • 前后端集成联调,前端或者后端无法做到“及时协商,尽早解决”,最终导致问题集中爆发

解决方案

  • 首先定义schema [ 计划的提纲 ],并实时跟踪最新的API,降低集成风险;
  • 早些年:指定word计划文档;
  • 前后端分离:
    • 前端测试后端接口:postman
    • 后端提供接口,需要实时更新最新的消息及改动

11.2 集成Swagger

Swagger的优点

  • 号称世界上最流行的API框架
  • Restful Api 文档在线自动生成器 => API 文档 与API 定义同步更新
  • 直接运行,在线测试API
  • 支持多种语言 (如:Java,PHP等)
  • 官网:https://swagger.io/

SpringBoot集成Swagger

1 新建SpringBoot-web项目,导入swagger2、swagger-ui依赖

2 编写HelloController,测试确保运行成功

3 编写SwaggerConfig配置类来配置 Swagger(只写个空白类)

4 访问测试,可以看到swagger原生的自定义的界面

5 配置Swagger信息部分的文档信息

6 重启项目,访问测试,看到了修改

1 新建SpringBoot-web项目,导入swagger2、swagger-ui依赖

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 注意:2.9.2版本之前,之后的不行-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>

<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>

3 编写SwaggerConfig配置类(只写个类,什么都不配,先跑起来)

1
2
3
4
@Configuration //表明是一个配置类
@EnableSwagger2 //开启swagger2
public class SwaggerConfig {
}

4 访问测试, http://localhost:8080/swagger-ui.html ,可以看到swagger的界面;

image-20200731132229265

5 配置Swagger信息部分的文档信息

  • Swagger实例Bean是Docket,所以通过配置Docket实例来配置Swaggger
  • 可以通过apiInfo()属性配置文档信息
  • Docket 实例关联上 apiInfo()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration //表明是一个配置类
@EnableSwagger2 //开启swagger2
public class SwaggerConfig {
@Bean //配置docket以配置Swagger具体参数
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
}

//配置文档信息
private ApiInfo apiInfo() {
Contact contact = new Contact("WK", "http://xxx.xxx.com/", "1111222@qq.com");
return new ApiInfo(
"Swagger学习_XX项目", // 标题
"Swagger学习_狂神学习", // 描述
"v1.0", // 版本
"http://terms.service.url/", // 组织链接
contact, // 联系人信息
"Apach 2.0 许可", // 许可
"http://xxx.xxx.com/", // 许可连接
new ArrayList<>()// 扩展
);
}
}

6 重启项目,访问测试,看到了修改

image-20210702213833452

11.3 Swagger配置【重点】

1 配置扫描接口

1 构建Docket时通过select()方法配置怎么扫描接口。

2 重启项目测试,controller下只有一个类,所以swagger界面只有一个类了

3 其他方式扫描接口

4 接口扫描过滤功能

1 Docket时通过select()方法配置怎么扫描接口

1
2
3
4
5
6
7
8
9
10
@Bean //配置docket以配置Swagger具体参数
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
.select()
//将controller下的接口扫描上
.apis(RequestHandlerSelectors.basePackage("com.kuang.controller"))
.build();
}

2 重启项目测试 只看到一个hello Controller了

image-20210702220614615

3 其他方式扫描接口

1
2
3
4
5
6
7
basePackage(final String basePackage) // 根据包路径扫描接口
any() // 扫描所有,项目中的所有接口都会被扫描到
none() // 不扫描接口
// 通过方法上的注解扫描,如withMethodAnnotation(GetMapping.class)只扫描get请求
withMethodAnnotation(final Class<? extends Annotation> annotation)
// 通过类上的注解扫描,如.withClassAnnotation(Controller.class)只扫描有controller注解的类中的接口
withClassAnnotation(final Class<? extends Annotation> annotation)

4 还可以配置接口扫描过滤:

1
2
3
4
5
6
7
8
9
10
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.kuang.controller"))
// 配置如何通过path过滤,即这里只扫描请求以/ss开头的接口
.paths(PathSelectors.ant("/ss/**"))
.build();
}

可选的配置有

1
2
3
4
5
6
any() // 任何请求都扫描
none() // 任何请求都不扫描
regex(final String pathRegex) // 通过正则表达式控制
ant(final String antPattern) // 通过ant()控制
// 配置如何通过path过滤,即这里只扫描请求以/ss开头的接口
paths(PathSelectors.ant("/ss/**"))

2 配置Swagger开关

通过enable()方法可以配置是否启用swagger。可以在不同的环境中选择是否启用swagger。

1 通过enable()方法配置是否启用swagger,如果是false,swagger将不能在浏览器中访问了

2 动态配置当项目处于test、dev环境时显示swagger,处于prod时不显示

1 通过enable()方法配置是否启用swagger(伪代码)

1
2
3
4
5
6
7
@Bean //配置docket以配置Swagger具体参数
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(false)
....;
}
image-20210702221738702

2 动态配置当项目处于dev环境时显示swagger,处于prod时不显示

  • 新建application-dev.properties、application-pro.properties,端口分别为8081、8082
1
server.port=8081
1
server.port=8082
  • 通过enable()方法配置是否启用swagger
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Bean //配置docket以配置Swagger具体参数
public Docket docket(Environment environment) {
// 设置要显示swagger的环境
Profiles of = Profiles.of("dev");
// 判断当前是否处于该环境
// 通过 enable() 接收此参数判断是否要显示
boolean b = environment.acceptsProfiles(of);

return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(b)
// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
.select()
//将controller下的接口扫描上
.apis(RequestHandlerSelectors.basePackage("com.kuang.controller"))
.build();
}
image-20210702222655441 image-20210702222812577

3 配置API分组

配置多个分组,写多个docket()即可 groupName(“XXXname”)方法

在SwaggerConfig类中增加几个docket()方法,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//分组1
@Bean
public Docket docket1(){
return new Docket(DocumentationType.SWAGGER_2).groupName("group1");
}
//分组2
@Bean
public Docket docket2(){
return new Docket(DocumentationType.SWAGGER_2).groupName("group2");
}
//分组3
@Bean
public Docket docket3(){
return new Docket(DocumentationType.SWAGGER_2).groupName("group3");
}

可以看到多了三个分组,并且三个分组的界面都是初设置的界面(说明各个分组是独立的)

image-20210703151345557

4 实体配置

简单来说就是当接口返回的是一个实体类时,这个实体类会被扫描到Swagger,然后有两个注解可以为该实体类添加注释并在Swagger中显示:

@ApiModel为类添加注释

@ApiModelProperty为类属性添加注释

所以显示了有什么用?

1 写一个实体类User,并用两注解加注释

2 在HelloController类中写个user方法 返回User类的对象

1
2
3
4
5
6
7
8
@ApiModel
public class User {
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("密码")
private String password;
...//构造方法 getset方法
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class HelloController {

// 自带的/error默认错误请求
@GetMapping("/hello")
public String hello() {
return "hello";
}

//只要我们的接口中,返回值中存在实体类,他就会被扫描到Swagger中
@PostMapping("/user")
public User user(){
return new User("wukang","1");
}
}
image-20210703184855701

5 常用注解以及try it out 测试

Swagger的所有注解定义在io.swagger.annotations包下

下面列一些经常用到的,未列举出来的可以另行查阅说明:

Swagger注解 简单说明
@Api(tags = “xxx模块说明”) 作用在模块类上
@ApiOperation(“xxx接口说明”) 作用在接口方法上
@ApiModel(“xxxPOJO说明”) 作用在模型类上:如VO、BO
@ApiModelProperty(value = “xxx属性说明”,hidden = true) 作用在类方法和属性上,hidden设置为true可以隐藏该属性
@ApiParam(“xxx参数说明”) 作用在参数、方法和字段上,类似@ApiModelProperty

狂神这里讲的也不太清楚,我自己改进了几个接口的例子两个get、两个post,都是一个无参、一个有参

1 hello()方法,直接返回一个“hello”字符串

2 get(String s) 返回“hello,”+s的字符串、需要输入s

3 user()方法,返回一个{wukang,1}的User对象

4 post(User user)方法 返回一个user对象 、需要输入user

Controllsr代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RestController
public class HelloController {
@ApiOperation("hello测试_无参")
@GetMapping("/hello")
public String hello(){
return "hello";
}

@ApiOperation("get测试_有参")
@GetMapping("/get")
public String get(String username) {
return "hello," + username;
}

@ApiOperation("user测试_无参")
@PostMapping("/user")
public User user(){
return new User("wukang","1");
}

@ApiOperation("post测试_有参")
@PostMapping("/post")
public User post(@ApiParam("用户") User user) {
return user;
}
}

运行结果:

image-20210703192335145
  • 1 测试hello()方法,直接返回一个“hello”字符串
image-20210703192507821
  • 2 测试get(String s) 返回“hello,”+s的字符串、需要输入s
image-20210703192606022
  • 4 post(User user)方法 返回一个user对象 、需要输入user
image-20210703192717935

总结:

这就是Swagger的测试功能、有一说一也就这样,不过确实方便!

Swagger是个优秀的工具,现在国内已经有很多的中小型互联网公司都在使用它,相较于传统的要先出Word接口文档再测试的方式,显然这样也更符合现在的快速迭代开发行情。当然了,提醒下大家在正式环境要记得关闭Swagger,一来出于安全考虑二来也可以节省运行时内存。

12 异步、定时、邮件任务

-------------感谢阅读没事常来-------------