SpringBoot 教程
SpringBoot
本项目参考 【狂神说Java】SpringBoot 最新教程 IDEA 版通俗易懂
Github 地址:https://github.com/Lockegogo/Java-Study
Hello, World!
什么是 Spring
- Spring 是一个开源框架,2003 年兴起的一个轻量级的Java 开发框架,作者:Rod Johnson
- Spring是为了解决企业级应用开发的复杂性而创建的,简化开发
Spring 是如何简化 Java 开发的
为了降低 Java 开发的复杂性,Spring采用了以下 4 种关键策略:
- 基于 POJO 的轻量级和最小侵入性编程,所有东西都是 bean;
- 通过 IOC,依赖注入(DI)和面向接口实现松耦合;
- 基于切面(AOP)和惯例进行声明式编程;
- 通过切面和模板减少样式代码:RedisTemplate,xxxTemplate;
什么是 SpringBoot
SpringBoot 是一个 javaweb 的开发框架,和 SpringMVC 类似,对比其他 javaweb 框架的好处,官方说是简化开发,约定大于配置,能迅速的开发 web 应用,几行代码开发一个 http 接口。
所有的技术框架的发展似乎都遵循了一条主线规律:从一个复杂应用场景,衍生一种规范框架,人们只需要进行各种配置而不需要自己去是实现它,这时候强大的配置功能成了优点;发展到一定程度后,人们根据实际生产应用情况,选取其中实用功能和设计精华,重构出一些轻量级的框架,之后为了提高开发效率,嫌弃原先的各类配置过于麻烦,于是开始提倡约定大于配置,进而衍生出一些一站式的解决方案,这就是 Java 企业级应用
SpringBoot 基于 Spring 开发,SpringBoot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。也就是说,它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。SpringBoot 以约定大于配置的核心思想,默认帮我们进行了很多设置,多数 SpringBoot 应用只需要很少的 Spring 配置。同时它集成了大量常用的第三方库配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等),SpringBoot 应用中这些第三方库几乎可以零配置的开箱即用。
SpringBoot 的主要优点:
- 为所有 Spring 开发者更快的入门
- 开箱即用,提供各种默认配置来简化项目配置
- 内嵌式容器简化 Web 项目
- 没有冗余代码生成和 XML 配置的要求
第一个 SpringBoot 程序
HelloController.java
@RestController
public class HelloController {
// 接口:http://localhost:8080/hello
@RequestMapping("/hello")
public String hello() {
// 调用业务,接收前端的参数
return "Hello World";
}
}
pom.xml
:自动生成
<dependencies>
<!--web 依赖:tomcat,dispatcherServlet,xml...-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 更改项目的端口号
# 更改项目的端口号
server.port=8081
运行原理初探
父依赖
- spring-boot-dependencies:核心依赖在父工程中,管理项目的资源过滤及插件!
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
- 点进去后发现还有一个父依赖,这才是真正管理 SpringBoot 应用里面所有版本依赖的地方
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.0.2</version>
</parent>
- 我们在写或者引入一些 Springboot 依赖的时候,不需要指定版本,就因为有这些版本仓库
启动器 spring-boot-starter
springboot-boot-starter-xxx:Springboot 的启动场景,Springboot 会将所有的功能场景,都变成一个个的启动器,例如 spring-boot-starter-web 会帮我们自动导入 web 的所有依赖,我们需要使用什么功能,就只需要找到对应的启动器就好了。
主程序
默认的主启动类
// @SpringBootApplication 来标注一个主程序类
// 说明这是一个 SpringBoot 应用
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
// 以为是启动了一个方法,没想到启动了一个服务
// 该方法返回一个 ConfigurableApplicationContext 对象
// 参数一:应用入口的类; 参数二:命令行参数
SpringApplication.run(SpringbootApplication.class, args);
}
}
SpringApplication 的实例化
这个类主要做了以下四件事情:
- 推断应用的类型是普通的项目还是 Web 项目
- 查找并加载所有可用初始化器 , 设置到 initializers 属性中
- 找出所有的应用程序监听器,设置到 listeners 属性中
- 推断并设置 main 方法的定义类,找到运行的主类
run 方法的执行
注解(@SpringBootApplication)
作用:标注在某个类上说明这个类是 SpringBoot 的主配置
SpringBoot 就应该运行这个类的 main 方法来启动 SpringBoot 应用;
进入这个注解:可以看到上面还有很多其他注解!
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
// ......
}
- @ComponentScan
- 对应 XML 配置中的元素
- 自动扫描并加载符合条件的组件或者 bean,将这个 bean 定义加载到 IOC 容器中
- @SpringBootConfiguration
- SpringBoot 的配置类,标注在某个类上,表示这是一个 SpringBoot 的配置类
- @EnableAutoConfiguration
- 开启自动配置功能
- @Import({AutoConfigurationImportSelector.class}):给容器导入组件
- AutoConfigurationImportSelector:自动配置导入选择器
自动配置真正实现是从 classpath 中搜寻所有的
META-INF/spring.factories
配置文件 ,并将其中对应的org.springframework.boot.autoconfigure
. 包下的配置项,通过反射实例化为对应标注了@Configuration 的 JavaConfig
形式的 IOC 容器配置类 , 然后将这些都汇总成为一个实例并加载到 IOC 容器中。
yaml 配置注入
yaml 语法学习
配置文件
SpringBoot 使用一个全局的配置文件 , 配置文件名称是固定的:
application.properties
- 语法结构 :key=value
application.yaml
- key: value
配置文件的作用:修改 SpringBoot 自动配置的默认值,因为 SpringBoot 在底层都给我们自动配置好了。
YAML
YAML是 "YAML Ain't a Markup Language" (YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)
这种语言以数据作为中心,而不是以标记语言为重点!
以前的配置文件,大多数都是使用 xml 来配置;比如一个简单的端口配置,我们来对比下 yaml 和 xml:
- xml
<server>
<port>8081<port>
</server>
- yaml
server:
port: 8080
基础语法
说明:语法要求严格!
-
空格不能省略
-
以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。
-
属性和值的大小写都是十分敏感的。
字面量:普通的值 [ 数字,布尔值,字符串 ]
- 字面量直接写在后面就可以,字符串默认不用加上双引号或者单引号;
k: v
- 双引号不会转义字符串里面的特殊字符,特殊字符会作为本身想表示的意思;
- 比如 :name: "kuang \n shen" 输出 :kuang 换行 shen
- 单引号,会转义特殊字符,特殊字符最终会变成和普通字符一样输出
- 比如 :name: ‘kuang \n shen’ 输出 :kuang \n shen
- 双引号不会转义字符串里面的特殊字符,特殊字符会作为本身想表示的意思;
对象、Map(键值对)
# 对象、Map格式
k:
v1:
v2:
在下一行来写对象的属性和值的关系,注意缩进;比如:
student:
name: qinjiang
age: 3
行内写法
student: {name: qinjiang,age: 3}
数组( List、set )
用 - 值表示数组中的一个元素,比如:
pets:
- cat
- dog
- pig
行内写法
pets: [cat,dog,pig]
修改 SpringBoot 的默认端口号
配置文件中添加,端口号的参数,就可以切换端口;
server:
port: 8082
注入配置文件
yaml 注入配置文件
yaml 文件更强大的地方在于,他可以给我们的实体类直接注入匹配值!
-
在 springboot 项目中的 resources 目录下新建一个文件 application.yaml
-
编写一个实体类 Dog;
@Component // 注册 bean 到容器中
public class Dog {
private String name;
private Integer age;
// 有参无参构造、get、set方法、toString()方法
}
- 给 bean 注入属性值:
@Value
@Component // 注册 bean
public class Dog {
@Value("阿黄")
private String name;
@Value("18")
private Integer age;
}
- 在 SpringBoot 的测试类下注入狗狗输出一下
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired
private Dog dog;
@Test
void contextLoads() {
System.out.println(dog);
}
}
@Autowired 是自动装配(按类型,这样就不用在代码中 new 了),@Component 是注册 bean 到容器中,注意这其中的概念差异。
- 再编写一个复杂一点的实体类:Person
@Component
public class Person {
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;
// 有参无参构造、get、set方法、toString()方法
}
- 使用 yaml 配置的方式进行注入:
person:
name: locke
age: 3
happy: false
birth: 2000/01/01
maps: {k1: v1,k2: v2}
lists:
- code
- girl
- music
dog:
name: 旺财
age: 1
- 我们刚才已经把 person 这个对象的所有值都写好了,我们现在来注入到我们的类中!
/*
@ConfigurationProperties 作用:
将配置文件中配置的每一个属性的值,映射到这个组件中;
告诉 SpringBoot 将本类中的所有属性和配置文件中相关的配置进行绑定
参数 prefix = “person” : 将配置文件中的 person 下面的所有属性一一对应
*/
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;
//有参无参构造、get、set方法、toString()方法
}
- IDEA 提示,配置注解器没有找到,查看文档,找到一个依赖
- 注解
@ConfigurationProperties(prefix = "person")
- 点击 open Decumentation 进入官网
- 在 pom 中导入依赖
- 注解
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
- 测试类测试
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired
private Person person;
@Test
void contextLoads() {
System.out.println(person);
}
}
加载指定的配置文件
两种注解:
@PropertySource
:加载指定的配置文件;@ConfigurationProperties
:默认从全局配置文件中获取值;
具体操作:
- 在 resources 目录下新建一个
person.properties
文件
name=locke
- 在代码中加载 person.properties 文件
@PropertySource(value = "classpath:person.properties")
@Component //注册bean
public class Person {
@Value("${name}")
private String name;
......
}
配置文件占位符
配置文件还可以编写占位符生成随机数:
person:
name: qinjiang${random.uuid}
age: ${random.int}
happy: false
birth: 2020/07/13
maps: {k1: v1,k2: v2}
lists:
- code
- music
- girl
dog:
name: ${person.hello:hello}_旺财
age: 3
对比小结
@Value
这个使用起来并不友好!我们需要为每个属性单独注解赋值,比较麻烦;我们来看个功能对比图:
@ConfigurationProperties | @Value | |
---|---|---|
功能 | 批量注入配置文件中的属性 | 一个个指定 |
松散绑定 | 支持 | 不支持 |
SpEL | 不支持 | 支持 |
JSR303 数据校验 | 支持 | 不支持 |
复杂类型封装 | 支持 | 不支持 |
-
@ConfigurationProperties
只需要写一次即可 , @Value 则需要每个字段都添加 -
松散绑定:这个什么意思呢? 比如我的 yaml 中写的 last-name,这个和 lastName 是一样的, - 后面跟着的字母默认是大写的。这就是松散绑定。
-
JSR303 数据校验 , 这个就是我们可以在字段是增加一层过滤器验证,可以保证数据的合法性。
-
复杂类型封装,yaml 中可以封装对象 , 使用 value 就不支持。
JSR303 数据校验原理
JSR 303
Springboot 中可以用 @validated
来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。
我们这里来写个注解让我们的 name 只能支持 Email格式:
- 添加 validation 启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
@Email
添加
@Component // 注册 bean
@ConfigurationProperties(prefix = "person")
@Validated // 数据校验
public class Person {
@Email(message="邮箱格式错误") // name 必须是邮箱格式
private String name;
}
- 运行结果 :default message [不是一个合法的电子邮件地址]
使用数据校验,可以保证数据的正确性。
常见参数
@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 对象是否符合正则表达式的规则
.......等等
除此以外,我们还可以自定义一些数据校验规则
多环境切换
profile 是 Spring 对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境;(不同位置的优先级如下图)
多配置文件
我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml
, 用来指定多个环境版本;
例如:
application-test.properties
代表测试环境配置
application-dev.properties
代表开发环境配置
但是 Springboot 并不会直接启动这些配置文件,它默认使用 application.properties 主配置文件。
我们需要通过一个配置来选择需要激活的环境:
# 比如在配置文件中指定使用 dev 环境,我们可以通过设置不同的端口号进行测试;
# 我们启动 SpringBoot,就可以看到已经切换到 dev 下的配置了;
spring.profiles.active=dev
yaml 的多文档块
和 properties 配置文件中一样,但是使用 yaml 去实现不需要创建多个配置文件,更加方便了 !
server:
port: 8081
# 选择要激活那个环境块
spring:
profiles:
active: test
---
server:
port: 8083
spring:
profiles: dev # 配置环境的名称
---
server:
port: 8084
spring:
profiles: test # 配置环境的名称
注意:如果 yaml 和 properties 同时都配置了端口,并且没有激活其他环境 , 默认会使用 properties 配置文件的!
配置文件加载方式
springboot 启动会扫描以下位置的 application.properties 或者 application.yaml 文件作为 SpringBoot 的默认配置文件:
优先级1:项目路径下的 config 文件夹配置文件
优先级2:项目路径下配置文件
优先级3:资源路径下的 config 文件夹配置文件
优先级4:资源路径下配置文件
运维小技巧
指定位置加载配置文件:通过 spring.config.location
来改变默认的配置文件位置。
项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置(这种情况,一般是后期运维做的多,相同配置,外部指定的配置文件优先级最高);
java -jar spring-boot-config.jar --spring.config.location=F:/application.properties
自动配置原理
分析自动配置原理
-
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 类(类名)对应的值,然后把他们添加在容器中
-
在类路径下,
META-INF/spring.factories
里面配置的所有 EnableAutoConfiguration 的值加入到容器中; -
每一个这样的 xxxAutoConfiguration 类都是容器中的一个组件,都加入到容器中;用他们来做自动配置;
-
-
每一个自动配置类进行自动配置功能;
-
我们以 HttpEncodingAutoConfiguration(Http 编码自动配置)为例解释自动配置原理;
// 表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件;
@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;
}
}
一句话总结:根据当前不同的条件判断,决定这个配置类是否生效!
- 一旦这个配置类生效,这个配置类就会给容器中添加各种组件;
- 这些组件的属性是从对应的 properties 类中获取的,这些类里面的每一个属性又是和配置文件绑定的;
- 所有在配置文件中能配置的属性都是在 xxxxProperties 类中封装着;
- 配置文件能配置什么就可以参照某个功能对应的这个属性类。
// 从配置文件中获取指定的值和 bean 的属性进行绑定
@ConfigurationProperties(prefix = "spring.http")
public class HttpProperties {
// .....
}
这就是自动装配的原理。
重点
-
SpringBoot 启动会加载大量的自动配置类
-
我们看我们需要的功能有没有在 SpringBoot 默认写好的自动配置类当中;
-
我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在在其中,我们就不需要再手动配置了)
-
给容器中自动配置类添加组件的时候,会从 properties 类中获取某些属性。我们只需要在配置文件中指定这些属性的值即可;
- xxxxAutoConfigurartion:自动配置类;给容器中添加组件
- xxxxProperties:封装配置文件中相关属性;
@Conditional
了解完自动装配的原理后,我们来关注一个细节问题,自动配置类必须在一定的条件下才能生效;
@Conditional派生注解(Spring注解版原生的 @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
属性;
在控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效;
# 开启springboot的调试类
debug=true
-
Positive matches:(自动配置类启用的:正匹配)
-
Negative matches:(没有启动,没有匹配成功的自动配置类:负匹配)
-
Unconditional classes: (没有条件的类)
-
演示:查看输出的日志
自定义 starter
分析完源码以及自动装配的过程,我们可以尝试自定义一个启动器来玩儿~
说明
启动器模块是一个 空 jar 文件,仅提供辅助性依赖管理,这些依赖可能用于自动装配或者其他类库;
命名归约:
-
官方命名:
-
前缀:spring-boot-starter-xxx
-
比如:spring-boot-starter-web....
-
-
自定义命名:
-
xxx-spring-boot-starter
-
比如:mybatis-spring-boot-starter
-
编写启动器
- 在 IDEA 中新建一个空项目:
spring-boot-starter-diy
- 新建一个普通的 Maven 模块:
locke-spring-boot-starter
- 新建一个 SpringBoot 模块:
locke-spring-boot-starter-autoconfigure
- 点击 apply 即可,基本结构
- 在 starter 中导入 autoconfigure 的依赖
<!-- 启动器 -->
<dependencies>
<!-- 引入自动配置模块 -->
<dependency>
<groupId>com.locke</groupId>
<artifactId>locke-spring-boot-starter-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
- 将
autoconfigure
项目下多余的文件都删掉,Pom 中只留下一个 starter,这是所有的启动器基本配置! - 编写一个自己的服务:
public class HelloService {
HelloProperties helloProperties;
public HelloProperties getHelloProperties() {
return helloProperties;
}
public void setHelloProperties(HelloProperties helloProperties) {
this.helloProperties = helloProperties;
}
public String sayHello(String name){
return helloProperties.getPrefix() + name + helloProperties.getSuffix();
}
}
- 编写
HelloProperties
配置类
// 前缀 locke.hello
@ConfigurationProperties(prefix = "locke.hello")
public class HelloProperties {
private String prefix;
private String suffix;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
- 编写我们的自动配置类并注入 bean,测试
@Configuration
@ConditionalOnWebApplication // web 应用生效
@EnableConfigurationProperties(HelloProperties.class)
public class HelloServiceAutoConfiguration {
@Autowired
HelloProperties helloProperties;
@Bean
public HelloService helloService(){
HelloService service = new HelloService();
service.setHelloProperties(helloProperties);
return service;
}
}
- 在 resources 编写一个自己的
META-INF\spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=nuc.ss.HelloServiceAutoConfiguration
- 编写完成后,可以安装到 maven 仓库中。
测试启动器
- 新建一个 SpringBoot 项目
- 导入我们自己写的启动器
<dependency>
<groupId>nuc.ss</groupId>
<artifactId>ss-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- 编写一个
HelloController
进行测试我们自己写的接口!
@RestController
public class HelloController {
@Autowired
HelloService helloService;
@RequestMapping("/hello")
public String hello(){
return helloService.sayHello("zxc");
}
}
- 编写配置文件:
application.properties
locke.hello.prefix="ppp"
locke.hello.suffix="sss"
- 启动项目进行测试!
Web 开发
使用 SpringBoot 的步骤:
-
创建一个 SpringBoot 应用,选择我们需要的模块,SpringBoot 就会默认将我们的需要的模块自动配置好
-
手动在配置文件中配置部分配置项目就可以运行起来了
-
专注编写业务代码,不需要考虑以前那样一大堆的配置了。
- 向容器中自动配置组件 :*** Autoconfiguration
- 自动配置类,封装配置文件的内容:***Properties
静态资源处理
静态资源映射规则
SpringBoot 如何处理静态资源,如 css, js 等文件?
如果我们是一个 web 应用,main
下面会有一个 webapp,之前是将所有页面导入在这里面的,但现在的 pom 打包方式是 jar,那怎么办?
我们先来聊聊这个静态资源映射规则:
- SpringBoot 中,SpringMVC 的 web 配置都在 WebMvcAutoConfiguration 这个配置类里面;
- WebMvcAutoConfigurationAdapter 有很多配置方法;有一个方法:addResourceHandlers 添加资源处理
@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
Webjars 本质就是以 jar 包的方式引入我们的静态资源 , 我们以前要导入一个静态资源文件,直接导入即可。
第一种静态资源映射规则
SpringBoot 导入静态资源需要使用 Webjars。
- 引入 jQuery 对应版本的 pom 依赖:
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.4.1</version>
</dependency>
- 导入完毕,查看 webjars 目录结构,并访问 Jquery.js 文件!
- 访问:只要是静态资源,SpringBoot 就会去对应的路径寻找资源,我们这里访问:http://localhost:8080/webjars/jquery/3.4.1/jquery.js
第二种静态资源映射规则
如果是自己的静态资源该怎么导入呢?
- 去找
staticPathPattern
发现第二种映射规则 :/** , 访问当前的项目任意资源,它会去找resourceProperties
这个类,我们可以点进去看一下分析:
// 进入方法
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/"
};
- ResourceProperties 可以设置和我们静态资源有关的参数;这里面指向了它会去寻找资源的文件夹,即上面数组的内容。
- 以下四个目录存放的静态资源可以被我们识别:
"classpath:/META-INF/resources/" // localhost:8080/webjars
"classpath:/resources/" // localhost:8080
"classpath:/static/" // localhost:8080
"classpath:/public/" // localhost:8080
- 我们可以在 resources 根目录下新建对应的文件夹,都可以存放我们的静态文件;
- 比如我们访问 http://localhost:8080/1.js , 他就会去这些文件夹中寻找对应的静态资源文件;
自定义静态资源路径
我们也可以自己通过配置文件来指定一下,哪些文件夹是需要我们放静态资源文件的,在 application.properties 中配置;
spring.resources.static-locations=classpath:/coding/,classpath:/ss/
但是最好不要这么做。
总结
- 在springboot,我们可以使用一下方式处理静态资源
- webjars
localhost:8080/webjars/
- public,static,/**,resources
localhost:8080/
- webjars
- 优先级:resources > static(默认) > public
首页处理
首页定制
静态资源文件夹说完后,我们继续向下看源码!可以看到一个欢迎页的映射,就是我们的首页!
-
欢迎页,静态资源文件夹下的所有
index.html
页面;被 /** 映射。 -
比如我访问 http://localhost:8080/ ,就会找静态资源文件夹下的 index.html
-
新建一个
index.html
,在我们上面的 3 个目录中任意一个;然后访问测试 http://localhost:8080/ 看结果!
首页图标
- 关闭 SpringBoot 默认图标:
# 关闭默认图标
spring.mvc.favicon.enabled=false
-
自己放一个图标在静态资源目录下,我放在 public 目录下
-
清除浏览器缓存
Ctrl + F5
!刷新网页,发现图标已经变成自己的了!
2.2.x之后的版本(如2.3.0)直接执行 2 和 3 就可以了
Thymeleaf 模板引擎
-
前端交给我们的页面,是 html 页面。如果是我们以前开发,我们需要把他们转成 jsp 页面,JSP 好处就是当我们查出一些数据转发到 JSP 页面以后,我们可以用 jsp 轻松实现数据的显示,及交互等。
-
jsp 支持非常强大的功能,包括能写 Java 代码,但是第一 SpringBoot 这个项目首先是以 jar 的方式,不是 war;第二,我们用的还是嵌入式的 Tomcat,所以现在默认是不支持 jsp。
-
不支持 jsp,如果直接用纯静态页面的方式,会给开发带来非常大的麻烦,怎么办?
现在就该轮到模板引擎出场了:Thymeleaf
模板引擎的作用:接收后台传递的模板和数据,将数据进行解析,填充到指定位置并进行展示。
引入 Thymeleaf
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
分析
引入之后如何使用?
首先按照 SpringBoot 的自动配置原理看一下 Thymeleaf 的自动配置规则,然后根据规则使用。
- 找到 Thymeleaf 的自动配置类:
ThymeleafProperties
@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;
}
- 我们可以在其中看到默认的前缀和后缀!只需要把 html 页面放在类路径下的 templates 下,就可以自动渲染了
测试
- 编写一个 TestController
@Controller
public class TestController {
@RequestMapping("/test")
public String test1(){
//classpath:/templates/test.html
return "test";
}
}
- 编写一个测试页面 test.html 放在 templates 目录下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Test 页面</h1>
</body>
</html>
- 启动项目请求测试
语法学习
要学习语法,还是参考官网文档最为准确,我们找到对应的版本看一下;
Thymeleaf 官网:https://www.thymeleaf.org/ , 简单看一下官网!我们去下载 Thymeleaf 的官方文档!
入门
- 修改测试请求,增加数据传输
@RequestMapping("/t1")
public String test1(Model model){
// 存入数据
model.addAttribute("msg","Hello,Thymeleaf");
// classpath:/templates/test.html
return "test";
}
- 在 html 文件中导入命名空间的约束,方便提示
<html lang="en" xmlns:th="http://www.thymeleaf.org">
- 编写前端页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>狂神说</title>
</head>
<body>
<h1>测试页面</h1>
<!--th:text 就是将 div 中的内容设置为它指定的值,和之前学习的 Vue 一样-->
<div th:text="${msg}"></div>
</body>
</html>
进阶
- 我们可以使用任意的
th:attr
来替换 html 中原生属性的值 - 可以写的表达式如下:
Simple expressions:(表达式语法)
Variable Expressions: ${...}:获取变量值;OGNL;
1)、获取对象的属性、调用方法
2)、使用内置的基本对象:#18
#ctx : the context object.
#vars: the context variables.
#locale : the context locale.
#request : (only in Web Contexts) the HttpServletRequest object.
#response : (only in Web Contexts) the HttpServletResponse object.
#session : (only in Web Contexts) the HttpSession object.
#servletContext : (only in Web Contexts) the ServletContext object.
3)、内置的一些工具对象:
#execInfo : information about the template being processed.
#uris : methods for escaping parts of URLs/URIs
#conversions : methods for executing the configured conversion service (if any).
#dates : methods for java.util.Date objects: formatting, component extraction, etc.
#calendars : analogous to #dates , but for java.util.Calendar objects.
#numbers : methods for formatting numeric objects.
#strings : methods for String objects: contains, startsWith, prepending/appending, etc.
#objects : methods for objects in general.
#bools : methods for boolean evaluation.
#arrays : methods for arrays.
#lists : methods for lists.
#sets : methods for sets.
#maps : methods for maps.
#aggregates : methods for creating aggregates on arrays or collections.
==================================================================================
Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样;
Message Expressions: #{...}:获取国际化内容
Link URL Expressions: @{...}:定义URL;
Fragment Expressions: ~{...}:片段引用表达式
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)
Special tokens:
No-Operation: _
测试
- 编写一个Controller,放一些数据
@RequestMapping("/test2")
public String test2(Map<String,Object> map){
// 存入数据
map.put("msg","<h1>Hello</h1>");
map.put("users", Arrays.asList("qinjiang","kuangshen"));
// classpath:/templates/test.html
return "test";
}
- 测试页面读取数据
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<h1>Test页面</h1>
<!--不转义-->
<div th:text="${msg}"></div>
<!--转义-->
<div th:utext="${msg}"></div>
<hr>
<!--遍历数据-->
<!--th:each每次遍历都会生成当前这个标签:官网#9-->
<h3 th:each="user:${users}" th:text="${user}"></h3>
<hr>
<!--行内写法:官网#12-->
<h3 th:each="user:${users}">[[ ${user} ]]</h3>
</div>
</body>
</html>
MVC 自动配置原理
在进行项目编写前,我们还需要知道一个东西,就是 SpringBoot 对我们的 SpringMVC 还做了哪些配置,包括如何扩展,如何定制。
只有把这些都搞清楚了,我们在之后使用才会更加得心应手。途径一:源码分析,途径二:官方文档!
内容协商视图解析器
ContentNegotiatingViewResolver
- 自动配置了 ViewResolver,就是我们之前学习的 SpringMVC 的视图解析器
- 即根据方法的返回值取得视图对象(View),然后由视图对象决定如何渲染(转发,重定向)。
转发和重定向的区别:转发是服务器行为;重定向是客户端行为。
- 地址栏显示
- 转发 forward:服务器请求资源,服务器直接访问目标地址的 URL, 把那个 URL 的响应内容读取过来,然后把这些内容再发给浏览器。浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址。
- 重定向 redirect:服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址。所以地址栏显示的是新的 URL。
- 数据共享
- 转发 forward:转发页面和转发到的页面可以共享 request 里面的数据。
- 重定向 redirect:不能共享数据。
- 运用地方
- 转发 forward:一般用于用户登录的时候,根据角色转发到相应的模块。
- 重定向 redirect:一般用于用户注销登录返回主页面和跳转到其他的网站等。
- 效率
- 转发 forward:效率高。
- 重定向 redirect:效率低。
- 我们去看看这里的源码:我们找到
WebMvcAutoConfiguration
, 然后搜索ContentNegotiatingViewResolver
。找到viewResolver
,继续点进去查看找到对应的解析视图的代码:resolveViewName
,继续点看看其是如何获得候选的视图的:Iterator var5 = this.viewResolvers.iterator();
- 得出结论:ContentNegotiatingViewResolver 这个视图解析器就是用来组合所有的视图解析器的
- 继续研究组合逻辑,看到有个属性
viewResolvers
,它是在哪里进行赋值的?
protected void initServletContext(ServletContext servletContext) {
// 这里它是从 beanFactory 工具中获取容器中的所有视图解析器
// ViewRescolver.class 把所有的视图解析器来组合的
Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.obtainApplicationContext(), ViewResolver.class).values();
ViewResolver viewResolver;
if (this.viewResolvers == null) {
this.viewResolvers = new ArrayList(matchingBeans.size());
}
// ...............
}
- 既然它是在容器中去找视图解析器,那我们也可以去实现一个视图解析器了
自定义视图解析器
- 在主程序中写一个视图解析器:
// 扩展 springmvc DispatchServlet
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
// public interface ViewResolver 实现了视图解析器接口的类,我们就可以把它看做视图解析器
@Bean
public ViewResolver myViewResolver() {
return new MyViewResolver();
}
// 自定义了一个自己的视图解析器
public static class MyViewResolver implements ViewResolver {
@Override
public View resolveViewName(String s, Locale locale) throws Exception {
return null;
}
}
}
注意
@Bean
和@ Component
(@Controller
/@Service
/@Repository
) 的区别:
- @Component: 作用于类上,告知 Spring,为这个类创建 Bean,通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中
- @Bean:主要作用于方法上 ,告知 Spring,这个方法会返回一个对象,且要注册在 Spring 的上下文中。通常方法体中包含产生 Bean 的逻辑,相当于 xml 文件中的
标签
- 给 DispatcherServlet 中的 doDispatch方法 加个断点进行调试一下,因为所有的请求都会走到这个方法中
- 启动我们的项目(以 debug 的方式),然后随便访问一个页面,回到 IDEA 看一下 Debug 信息,找到
this
(DispatcherServlet)
- 找到视图解析器 (viewResolvers),就可以看到自己定义的
如果我们想要使用自己定制化的东西,只需要给容器在添加这个组件就好了,剩下的事情 SpringBoot 会帮我们做。
转换器和格式化器
- 在
WebMvcAutoConfiguration
中找到格式化转换器:
@Bean
@Override
public FormattingConversionService mvcConversionService() {
// 拿到配置文件中的格式化规则
WebConversionService conversionService =
new WebConversionService(this.mvcProperties.getDateFormat());
addFormatters(conversionService);
return conversionService;
}
- 点击去:可以看到在我们的 Properties 文件中,我们可以进行自动配置它!
public String getDateFormat() {
return this.format.getDate();
}
public String getDate() {
return this.date;
}
/**
* Date format to use, for example `dd/MM/yyyy`.默认的
*/
private String date;
- 如果配置了自己的格式化方式,就会注册到 Bean 中生效,我们可以在配置文件中配置日期格式化的规则:
spring.mvc.date=
@Deprecated
public void setDateFormat(String dateFormat) {
this.format.setDate(dateFormat);
}
public void setDate(String date) {
this.date = date;
}
修改默认配置
学习心得:
- 通过 WebMVC 的自动配置原理分析,我们要学会通过源码探究,得出结论;
- SpringBoot 的底层,大量地用到了这些设计思想,是很好的学习资料;
- SPringleBoot 在自动配置很多组件的时候,先看容器中有没有用户自己配置的,如果没有就用自动配置的;
- 如果有些组件可以存在多个,比如我们的视图解析器,就将用户配置的和自己默认的组合起来;
- 扩展使用 SpringMVC,我们需要编写一个
@Configuration
注解类,并且类型要为WebMvcConfigurer
,还不能标注@EnableWebMvc
注解
@Configuration 用于定义配置类,可替换 xml 配置文件,被注解的类内部包含有一个或多个被 @Bean 注解的方法,这些方法将会被 AnnotationConfigApplicationContext 或 AnnotationConfigWebApplicationContext 类进行扫描,并用于构建 bean 定义,初始化 Spring 容器。
具体实现:
- 新建一个
config
包,写一个类MyMvcConfig
:
// 如果我们要扩展 springmvc,官方建议我们这样去做 @Configuration
// 应为类型要求为 WebMvcConfigurer,所以我们实现其接口
// 扩展 springmvc DispatchServlet
// @EnableWebMvc // 这玩意就是导入了一个类,DelegatingWebMvcConfiguration,从容器中获取所有的 webMvcConfig
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 浏览器发送 /test2,就会跳转到 test 页面;
registry.addViewController("/test2").setViewName("test");
}
}
全面接管 SpringMVC
- 全面接管即:SpringBoot 对 SpringMVC 的自动配置不需要了,所有都是我们自己去配置!
- 只需在我们的配置类中要加一个
@EnableWebMvc
。 - 如果我们全面接管 SpringMVC,之前 SpringBoot 给我们配置的静态资源映射一定会无效,可以测试一下;
当然在开发中,我们不推荐全面接管 SpringMVC。
员工管理:重要
准备工作
前端页面
-
将 html 页面放入 templates 目录
-
将 css,js,img 放入到 static 目录
实体类的编写
- Department
// 部门表
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
private Integer id;
private String departmentName;
}
- Employee
// 员工表
@Data
@NoArgsConstructor
public class Employee {
private Integer id;
private String lastName;
private String email;
private Integer gender; // 0:女,1:男
private Department department;
private Date birth;
public Employee(Integer id, String lastName, String email, Integer gender, Department department) {
this.id = id;
this.lastName = lastName;
this.email = email;
this.gender = gender;
this.department = department;
// 默认的创建日期
this.birth = new Date();
}
}
dao 层模拟数据库
- DepartmentDao
// 部门 Dao
@Repository
public class DepartmentDao {
// 模拟数据库数据
private static Map<Integer, Department> departments = null;
static {
departments = new HashMap<Integer, Department>(); // 创建一个部门表
departments.put(101,new Department(101,"教学部"));
departments.put(102,new Department(102,"市场部"));
departments.put(103,new Department(103,"教研部"));
departments.put(104,new Department(104,"运营部"));
departments.put(105,new Department(105,"后勤部"));
}
// 获得所有部门信息
public Collection<Department> getDepartment() {
return departments.values();
}
// 通过 id 得到部门
public Department getDepartmentById(Integer id) {
return departments.get(id);
}
}
- EmployeeDao
// 员工 Dao
@Repository
public class EmployeeDao {
// 模拟数据库数据
private static Map<Integer, Employee> employees = null;
// 员工所属部门
@Autowired
private DepartmentDao departmentDao;
static {
employees = new HashMap<Integer, Employee>();//创建一个员工表
employees.put(1001, new Employee(1001, "AA", "A123456@qq.com", 1, new Department(101, "教学部")));
employees.put(1002, new Employee(1002, "BB", "B123456@qq.com", 0, new Department(102, "市场部")));
employees.put(1003, new Employee(1003, "CC", "C123456@qq.com", 1, new Department(103, "教研部")));
employees.put(1004, new Employee(1004, "DD", "D123456@qq.com", 0, new Department(104, "运营部")));
employees.put(1005, new Employee(1005, "EE", "E123456@qq.com", 1, new Department(105, "后勤部")));
}
// 主键自增
private static Integer ininId = 1006;
// 增加一个员工
public void save(Employee employee) {
if (employee.getId() == null) {
employee.setId(ininId++);
}
employee.setDepartment(departmentDao.getDepartmentById(employee.getDepartment().getId()));
employees.put(employee.getId(),employee);
}
// 查询全部员工信息
public Collection<Employee> getAll() {
return employees.values();
}
// 通过 id 查询员工
public Employee getEmployeeById(Integer id) {
return employees.get(id);
}
// 删除员工通过 id
public void delete(Integer id) {
employees.remove(id);
}
}
首页实现
第一种方式
创建一个 IndexController
,写一个返回首页的方法(不建议使用)
@Controller
public class IndexController{
@RequestMapping({"/","/index.html"})
public String index() {
return "index";
}
}
第二种方式
创建一个 config
目录,在里面写一个 MyMvcConfig
,里面重写 addViewControllers
方法:
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
}
}
加载静态资源
- 导入 thymeleaf 包
<html lang="en" xmlns:th="http://www.thymeleaf.org">
- 将所有页面的静态资源使用 thymeleaf 接管:注意在
application.properties
中关掉模板引擎的缓存spring.thymeleaf.cache=false
<!-- css 的导入 -->
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<link th:href="@{/css/signin.css}" rel="stylesheet">
<!-- 图片的导入 -->
<img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">
<!-- js 导入 -->
<script type="text/javascript" th:src="@{/js/jquery-3.2.1.slim.min.js}"></script>
<script type="text/javascript" th:src="@{/js/popper.min.js}"></script>
<script type="text/javascript" th:src="@{/js/bootstrap.min.js}"></script>
<script type="text/javascript" th:src="@{/js/feather.min.js}"></script>
<script type="text/javascript" th:src="@{/js/Chart.min.js}"></script>
下面是完整的 index.html
:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Signin Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<!-- Custom styles for this template -->
<link th:href="@{/css/signin.css}" rel="stylesheet">
</head>
<body class="text-center">
<form class="form-signin" action="dashboard.html">
<img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
<label class="sr-only">Username</label>
<input type="text" class="form-control" placeholder="Username" required="" autofocus="">
<label class="sr-only">Password</label>
<input type="password" class="form-control" placeholder="Password" required="">
<div class="checkbox mb-3">
<label><input type="checkbox" value="remember-me">Remember me</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
<p class="mt-5 mb-3 text-muted">© 2022-2023</p>
<a class="btn btn-sm">中文</a>
<a class="btn btn-sm">English</a>
</form>
</body>
</html>
再次看一下首页页面:
页面国际化
- 在
resources
文件夹下新建i18n
(internationalization 的简称)
注意名字千万不要写错!!!
- 在
application.properties
中配置路径:
spring.thymeleaf.cache=false
# server.servlet.context-path=""
# 我们的配置文件的真实位置
spring.messages.basename=i18n.login
# 时间日期格式化
spring.mvc.format.date=yyyy-MM-dd
- 在
index.html
中进行修改:使用#{}
号进行取值
@{}
:thymeleaf 中的超链接表达式#{}
:thymeleaf 中的消息表达式,或者资源表达式,一般和th:text
一起使用${}
:变量表达式,用于访问的是容器上下文的变量,比如域变量:request 域,session 域
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
下面是完整的 index.html
:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:h="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Signin Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<!-- Custom styles for this template -->
<link th:href="@{/css/signin.css}" rel="stylesheet">
</head>
<body class="text-center">
<form class="form-signin" action="dashboard.html">
<img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
<input type="text" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
<input type="password" class="form-control" th:placeholder="#{login.password}" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me" th:text="#{login.remember}">
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button>
<p class="mt-5 mb-3 text-muted">© 2022-2023</p>
<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>
</form>
</body>
</html>
- 在 config 文件夹中新建
MyLocalResolver
类
public class MyLocalResolver implements LocaleResolver {
// 解析请求
@Override
public Locale resolveLocale(HttpServletRequest request) {
// 获取请求中的语言参数
String language = request.getParameter("l");
Locale locale = Locale.getDefault(); // 如果没有就使用默认的
// 如果请求的链接携带了国家化的参数
if (!StringUtils.isEmpty(language)) {
// zh_CN
String[] split = language.split("_");
// 国家,地区
locale = new Locale(split[0], split[1]);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
}
}
- 在 MyMvcConfig 中将自己写的组件配置到 spring 容器中:
// 自定义的国际化组件就生效了
@Bean
public LocaleResolver localeResolver() {
return new MyLocalResolver();
}
登录页面
- 首页登录页面表单的修改
<form class="form-signin" th:action="@{/user/login}">
......
<!--如果 msg 的消息不为空,则显示这个消息-->
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty({msg})}"></p>
<input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
<input type="password" name="password" class="form-control" th:placeholder="#{login.password}" required="">
......
</form>
- 写一个 LoginController 登录验证
@Controller
public class LoginController {
@RequestMapping("/user/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
Model model,
HttpSession session) {
// 具体的业务,登录成功跳转到 dashboard 页面
if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
session.setAttribute("loginUser", username);
return "redirect:/main.html";
} else {
model.addAttribute("msg", "用户名或者密码错误");
return "index";
}
}
}
注意这里 session 和 model 两个属性的区别:
- session:即会话,是客户为实现特定应用目的与系统的多次交互请求。Http 协议是一种 无状态 的协议,客户端每打开一个 web 页面,它就会与服务器建立一个新连接,发送新请求到服务器,服务器处理请求并将该请求返回到客户端。服务器不记录任何客户端信息,session 是一种能将信息保存于服务器端的技术,能记录特定的客户端到服务器的一系列请求。session 里放的数据保存在服务器,可以供其他页面使用,只要用户不退出或者 SESSION 过期,这个值就一直可以保留。在当前的 request 周期之内,调用 getAttribute 方法同样也可以得到。
- model:一个 request 级别的接口,可以将数据放入视图中。model 的数据,只能在 Controller 返回的页面使用,其他页面不能使用。
- 登录页面不友好(密码泄露)
-
解决密码泄露的问题
- 加一个 main 映射在
MyMvcConfig
中
public class MyMvcConfig implements WebMvcConfigurer{ @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("index"); registry.addViewController("/index.html").setViewName("index"); registry.addViewController("/main.html").setViewName("dashboard"); } }
- 修改
LoginController
跳转页面代码 ( redirect 跳转,这样地址栏就会显示新地址)
@Controller public class LoginController { @RequestMapping("/user/login") public String login(@RequestParam("username") String username, @RequestParam("password") String password, Model model, HttpSession session) { // 具体的业务, 登录成功跳转到 dashboard 页面 if (!StringUtils.isEmpty(username) && "123456".equals(password)) { return "redirect:/main.html"; } else { model.addAttribute("msg", "用户名或者密码错误"); return "index"; } } }
- 加一个 main 映射在
-
是否存在问题?
- 登录成功才可以进入 main 页面,否则直接输入 http://localhost:8080/main.html 就可以访问首页了,需要拦截器实现
登录拦截器
- 在
LoginController
中添加一个 session 判断登录
@Controller
public class LoginController {
@RequestMapping("/user/login")
public String login(@RequestParam("username") String username,
@RequestParam("password")String password,
Model model,
HttpSession session) {
// 具体的业务, 登录成功跳转到 dashboard 页面
if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
session.setAttribute("loginUser",username);
return "redirect:/main.html";
} else {
model.addAttribute("msg","用户名或者密码错误");
return "index";
}
}
}
- 在
config
页面写一个LoginHandlerInterceptor
拦截器
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) {
request.setAttribute("msg","没有权限,请先登录");
// 转发,服务器跳转
request.getRequestDispatcher("/index.html").forward(request,response);
return false;
} else {
return true;
}
}
}
MyMvcConfig
页面重写拦截器方法
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/index.html","/","/user/login","/css/**","/js/**","/img/**");
}
注意:静态资源的过滤,否则页面渲染效果会消失。
- 在
dashboard.html
页面修改登录信息为 session[[ ${session.loginUser} ]]
,登录成功之后会显示用户名
员工列表显示
后台编写
后台编写 EmployeeController
@Controller
public class EmployeeController {
@Autowired
EmployeeDao employeeDao;
@RequestMapping("/emps")
public String list(Model model) {
Collection<Employee> employees = employeeDao.getAll();
model.addAttribute("emps",employees);
return "emp/list";
}
}
提取公共页面
- 员工管理前端页面地址的修改(list.html 和 dashboard.html)
@{/emps}
<li class="nav-item">
<a class="nav-link" th:href="@{/emps}">
......
员工管理
</a>
</li>
-
抽取公共的代码(list.html 和 dashboard.html)
- dashboard.html 页面
<!--顶部导航栏--> <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar"> <!--...--> </nav> <!--侧边栏--> <nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar"> <!--...--> </nav>
- list.html 页面
<!--顶部导航栏--> <div th:insert="~{dashboard::topbar}"></div> <!--侧边栏--> <div th:insert="~{dashboard::sidebar}"></div>
-
进一步抽取公共的代码
- 在
templates
目录下面创建commons
目录,在commons
目录下面创建commons.html
放公共代码
<!--只写改变的代码--> <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <!--顶部导航栏--> <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar"> ............. </nav> <!--侧边栏--> <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 class="nav-link active" th:href="@{/index.html}"> ............. 首页 <span class="sr-only">(current)</span> </a> </li> ............. <li class="nav-item"> <a class="nav-link" th:href="@{/emps}"> ............. 员工管理 </a> </li> ............. </ul> ............. </div> </nav> </html>
dashboard.html
和list.html
页面一样
<!--顶部导航栏--> <div th:replace="~{commons/commons::topbar}"></div> <!--侧边栏--> <div th:replace="~{commons/commons::sidebar}"></div>
- 在
-
添加侧边栏点亮
- 在 dashboard.html 和 list.html 页面中侧边栏传参(在括号里面直接传参)
<!--侧边栏--> <div th:replace="~{commons/commons::sidebar(active='list.html')}"></div>
- 在 commons.html 中接收参数并判断
<!--侧边栏--> <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 th:class="${active=='main.html'?'nav-link active':'nav-link'}" th:href="@{/main.html}"> ............. 首页 <span class="sr-only">(current)</span> </a> </li> ............. <li class="nav-item"> <a th:class="${active=='list.html'?'nav-link active':'nav-link'}" th:href="@{/emps}"> ............. 员工管理 </a> </li> ............. </ul> ............. </div> </nav> </html>
列表循环展示
员工列表循环(list.html)
<!--侧边栏-->
<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.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>
添加员工信息
按钮提交
list.html
页面编写:
<h2><a class="btn btn-sm btn-success" th:href="@{/emp}">添加员工</a></h2>
跳转到添加页面
- 后台页面的编写(跳转到 add.html 页面)
// 一定别忘记注入!!!!
@Autowired
DepartmentDao departmentDao;
@GetMapping("/emp")
public String toAddPage(Model model) {
// 查出所有部门的信息
Collection<Department> department = departmentDao.getDepartment();
model.addAttribute("departments",department);
return "emp/add";
}
add.html
页面的编写(其他部分和list.html
页面一样,只改 main 中的代码即可)
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<form th:action="@{/emp}" 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>
注意:下拉框提交的时候应提交一个属性,因为其在 controller
接收的是一个 Employee
,否则会报错。
添加员工成功
后台页面的编写:
@Autowired
DepartmentDao departmentDao;
@PostMapping("/emp")
public String addEmp(Employee employee) {
employeeDao.save(employee); // 调用底层业务方法保存员工信息
return "redirect:/emps";
注意
return "redirect:/emps";
和return "redirect:/index.html";
区别!
日期格式的修改
- 如果输入的日期格式为 2020-01-01,则报错
application.properties
文件中添加配置
spring.mvc.format.date=yyyy-MM-dd
修改员工信息
按钮提交
list.html
页面编辑按钮的编写(’+‘ 报红别管)
<a class="btn btn-sm btn-primary" th:href="@{/emp/}+${emp.getId()}">编辑</a>
跳转到修改页面
- 后台页面的接收参数(Restful 风格)
// 去到员工的修改页面
@GetMapping("/emp/{id}")
public String toUpdateEmp(@PathVariable("id") Integer id, Model model) {
// 查出原来的数据
Employee employee = employeeDao.getEmployeeById(id);
model.addAttribute("emp",employee);
// 查出所有部门的信息
Collection<Department> department = departmentDao.getDepartment();
model.addAttribute("departments",department);
return "emp/update";
}
update.html
页面(main
里面修改,其他和list.html
页面一样)
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<form th:action="@{/updateEmp}" method="post">
<input type="hidden" name="id" th:value="${emp.getId()}">
<div class="form-group">
<label>LastName</label>
<input th:value="${emp.getLastName()}" type="text" name="lastName" class="form-control" placeholder="海绵宝宝">
</div>
<div class="form-group">
<label>Email</label>
<input th:value="${emp.getEmail()}" 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 th:checked="${emp.getGender()==1}" 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 th:checked="${emp.getGender()==0}" 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:selected="${dept.getId()==emp.getDepartment().getId()}" th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}"></option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input th:value="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}" type="text" name="birth" class="form-control" placeholder="2020-07-25 00:00:00">
</div>
<button type="submit" class="btn btn-primary">修改</button>
</form>
</main>
注意
update.html
和add.html
的区别!
update.html
需要把待修改的员工信息展示出来!<input th:value="${emp.getLastName()}" type="text" name="lastName" class="form-control">
add.html
只需要显示默认值!<input type="text" name="lastName" class="form-control" placeholder="海绵宝宝">
修改员工成功
修改员工信息成功:
@PostMapping("/updateEmp")
public String updateEmp(Employee employee) {
employeeDao.save(employee);
return "redirect:/emps";
}
删除员工信息
按钮提交
list.html
页面删除按钮的修改:
<a class="btn btn-sm btn-danger" th:href="@{/delemp/}+${emp.getId()}">删除</a>
接收参数删除用户信息
// 删除员工
@GetMapping("/delemp/{id}")
// @PathVariable 映射 URL 绑定的占位符
public String deleteEmp(@PathVariable("id") Integer id) {
employeeDao.delete(id);
return "redirect:/emps";
}
404 页面
- 将
404.html
页面放入到 templates 目录下面的 error 目录中 - 错误运行页面:
注销功能的实现
- 在
commons.html
中修改注销按钮
<a class="nav-link" th:href="@{/user/logout}">注销</a>
- 在
LoginController.java
中编写注销页面代码
@RequestMapping("/user/logout")
public String logout(HttpSession session) {
session.invalidate();
return "redirect:/index.html";
}
如何写一个网站
步骤:
- 搞定前端:页面长什么样子
- 设计数据库(难点)
- 前端让他能自动运行,独立化工程
- 数据接口如何对接:json,对象,all in one
- 前后端联调测试
模板:
- 有一套自己熟悉的后台模板:工作必要 x-admin
- 前端页面:至少自己能够通过前端框架(Bootstrap | Layui | semantic-ui),组合出来一个网站页面
- 栅格系统
- 导航栏
- 侧边栏
- 表单
- 让这个网站能够独立运行
整合 JDBC
创建测试项目测试数据源
- 新建一个项目测试:引入相应的模块
- 项目建好之后,发现自动帮我们导入了如下的启动器:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
- 编写 yaml 配置文件连接数据库:
spring:
datasource:
username: root
password: 130914
# ?serverTimezone=UTC 解决时区的报错
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
- 测试类测试
@SpringBootTest
class SpringbootDataJdbcApplicationTests {
// DI 注入数据源
@Autowired
DataSource dataSource;
@Test
public void contextLoads() throws SQLException {
// 看一下默认数据源
System.out.println(dataSource.getClass());
// 获得连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
// 关闭连接
connection.close();
}
}
JDBCTemplate
-
有了数据源 (com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接 (java.sql.Connection),有了连接,就可以使用原生的 JDBC 语句来操作数据库;
-
即使不使用第三方第数据库操作框架,如 MyBatis 等,Spring 本身也对原生的 JDBC 做了轻量级的封装,即 JdbcTemplate。
-
数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。
-
Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用
-
JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 类
JdbcTemplate主要提供以下几类方法:
execute
方法:可以用于执行任何 SQL 语句,一般用于执行 DDL 语句;update
方法及 batchUpdate 方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;query
方法及 queryForXXX 方法:用于执行查询相关语句;call
方法:用于执行存储过程、函数相关语句。
测试
- 编写一个 Controller,注入 jdbcTemplate,编写测试方法进行访问测试;
@RestController
public class JDBCController {
final
JdbcTemplate jdbcTemplate;
public JDBCController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// 查询数据库的所有信息
// 没有实体类,获取数据库的东西,怎么获取? Map
@GetMapping("/userList")
public List<Map<String, Object>> userList() {
String sql = "select * from user";
return jdbcTemplate.queryForList(sql);
}
@GetMapping("/addUser")
public String addUser() {
String sql = "insert into mybatis.user(id, name, pwd) values(7,'小明','123456')";
jdbcTemplate.update(sql);
return "update-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[2];
objects[0] = "小明2";
objects[1] = "aaaaaaa";
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";
}
}
整合 Druid
Java 程序很大一部分要操作数据库,为了提高性能操作数据库的时候,又不得不使用数据库连接池。
Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。
Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。
Spring Boot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源,我们来重点介绍 Spring Boot 如何集成 Druid 数据源,如何实现数据库监控。
Github地址:https://github.com/alibaba/druid/
配置数据源
- 添加上 Druid 数据源依赖:这个依赖可以从 Maven 仓库官网Maven Respository中获取,注意,如果使用 springboot 3.x,
artifactId
一定要改为druid-spring-boot-3-starter
,若使用druid-spring-boot-starter
会报错
<!-- springboot 版本为 3.0.2 -->
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.20</version>
</dependency>
- 切换数据源:之前已经说过 Spring Boot 2.0 以上默认使用
com.zaxxer.hikari.HikariDataSource
数据源,但可以通过spring.datasource.type
指定数据源。
spring:
datasource:
username: root
password: 130914
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource # 自定义数据源
-
在测试类中注入 DataSource,然后获取到它,输出一看便知是否成功切换
-
切换成功后,就可以设置数据源连接初始化大小、最大连接数、等待时间、最小连接数,参考文档:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
spring:
datasource:
username: root
password: 130914
# ?serverTimezone=UTC 解决时区的报错
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
# druid 数据源专有配置
druid:
# 初始化时建立物理连接的个数
initial-size: 5
# 连接池的最小空闲数
min-idle: 5
# 连接池最大连接数
max-active: 20
# 获取连接时最大等待时间,单位毫秒
max-wait: 60000
# 既作为检测的间隔时间又作为 test-while-idle 执行的依据
time-between-eviction-runs-millis: 60000
# 连接保持空闲而不被驱逐的最小时间
min-evictable-idle-time-millis: 300000
# 用来检测连接是否有效的 sql,要求是一个查询语句
validation-query: SELECT 1 FROM DUAL
# 申请连接时执行 validationQuery 检测连接是否有效,开启会降低性能
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
# 配置监控统计拦截的filters,stat:监控统计、slf4j:日志记录、wall:防御 sql 注入
filters: stat,wall,slf4j
Max-pool-prepared-statement-per-connection-size: 20
use-global-data-source-stat: true
connect-properties:
druid.stat.mergeSql: true
druid.stat.slowSqlMillis: 500
- 现在需要程序员自己为 DruidDataSource 绑定全局配置文件中的参数,再添加到容器中,而不再使用 SpringBoot 的自动生成;我们需要自己添加 DruidDataSource 组件到容器中,并绑定属性:
@Configuration
public class DruidConfig {
/*
将自定义的 Druid 数据源添加到容器中,不再让 Spring Boot 自动创建
绑定全局配置文件中的 druid 数据源属性到 com.alibaba.druid.pool.DruidDataSource 从而让它们生效
@ConfigurationProperties(prefix = "spring.datasource.druid"):作用就是将全局配置文件中
前缀为 spring.datasource 的属性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名参数中
*/
@ConfigurationProperties(prefix = "spring.datasource.druid")
@Bean
public DruidDataSource druidDataSource() {
return DruidDataSourceBuilder.create().build();
}
}
- 去测试类中测试一下,看是否成功
@SpringBootTest
class SpringbootDataJdbcApplicationTests {
// DI 注入数据源
@Autowired
DataSource dataSource;
@Test
public 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();
}
}
- 输出结果:可见配置类已经生效
配置 Druid 数据源监控
Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看,类似安装路由器时,人家也提供了一个默认的 web 页面。所以第一步需要设置 Druid 的后台管理页面,比如登录账号、密码等;配置后台管理等
继续配置 appliacation.yaml
文件:
spring:
datasource:
...
# druid 数据源专有配置
druid:
...
# StatViewServlet 配置:启用内置的监控页面
stat-view-servlet:
enabled: true
url-pattern: /druid/*
login-username: admin
login-password: 123
allow: 127.0.0.1 # 为空或者为 null 时,表示允许所有访问
deny: 192.168.1.20 # 拒绝此 ip 访问
reset-enable: false
配置完毕后,访问 :http://localhost:8080/druid/login.html
配置 Druid web 监控 filter 过滤器:
spring:
datasource:
...
# druid 数据源专有配置
druid:
...
# WebStatFilter 配置
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*,/jdbc/*'
session-stat-enable: true
执行一条 SQL 语句后:http://localhost:8080/queryUserList
,查看 Druid 界面的 SQL 监控:
整合 MyBatis
- 导入 MyBatis 所需要的依赖:直接浏览器搜索 mybatis-spring-boot-starter 中文文档,查看版本依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.1</version>
</dependency>
- 配置数据库连接信息(不变)
spring:
datasource:
username: root
password: 130914
#?serverTimezone=UTC 解决时区的报错
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
# 整合 mybatis
mybatis:
type-aliases-package: com.locke.pojo
mapper-locations: classpath:mybatis/mapper/*.xml
- 测试数据库是否连接成功
- 创建实体类,导入 lombok
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}
- 创建 mapper 目录以及对应的 Mapper 接口:
UserMapper.java
// 这个注解表示了这是一个 mybatis 的 mapper 类
@Mapper
@Repository
public interface UserMapper {
List<User> queryUserList();
User queryUserById(int id);
int addUser(User user);
int updateUser(User user);
int deleteUser(int id);
}
- 在 resources 文件夹下建立 mybatis.mapper 包,建立对应的 Mapper 映射文件:
UserMapper.xml
<?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.locke.mapper.UserMapper">
<select id="queryUserList" resultType="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>
- maven 配置资源过滤问题
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
- 创建 controller 文件夹,编写 UserController 进行测试
@RestController
public class UserController {
@Autowired
private UserMapper userMapper;
@GetMapping("/queryUserList")
public List<User> queryUserList() {
List<User> userList = userMapper.queryUserList();
for (User user : userList) {
System.out.println(user);
}
return userList;
}
// 添加一个用户
@GetMapping("/addUser")
public String addUser() {
userMapper.addUser(new User(7, "阿毛", "123456"));
return "ok";
}
// 修改一个用户
@GetMapping("/updateUser")
public String updateUser() {
userMapper.updateUser(new User(7, "阿毛", "123456"));
return "ok";
}
@GetMapping("/deleteUser")
public String deleteUser() {
userMapper.deleteUser(7);
return "ok";
}
}
- 启动项目访问进行测试!
SpringSecurity
安全简介
在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。
市面上存在比较有名的:Shiro,Spring Security
每一个框架的出现都是为了解决某一个问题,Spring Security 是为了解决什么问题?
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,侧重于为 java 应用程序提供身份验证和授权,与所有 Spring 项目一样,Spring 安全性的真正强大之处在于它可以轻松地扩展以满足定制需求。
一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。
- 用户认证:指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
- 用户授权:指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
对于上面提到的两者应用场景,Spring Security 框架都有很好的支持:
- 用户认证:Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。
- 用户授权:Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。
实战测试
实验环境搭建
- 新建一个初始的 springboot 项目 web 模块,thymeleaf 模块,注意 springboot 降级到 2.6.0
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
-
为了有一个更好的学习体验,先不要引入
spring-boot-starter-security
-
导入静态资源:
- controller 跳转:
RouterController.java
@Controller
public class RouterController {
@RequestMapping({"/", "/index"})
public String index() {
return "index";
}
@RequestMapping("/toLogin")
public String toLogin() {
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;
}
}
- 测试实验环境是否 OK
- 首页 & 登录
认识 Spring Security
对于安全控制,我们仅需要引入 spring-boot-starter-security
模块,进行少量的配置,即可实现强大的安全管理!
记住几个类:
WebSecurityConfigurerAdapter
:自定义Security
策略AuthenticationManagerBuilder
:自定义认证策略@EnableWebSecurity
:开启WebSecurity
模式
Spring Security 的两个主要目标是 “认证” 和 “授权”(访问控制)。
认证和授权
目前,我们的测试环境,是谁都可以访问的,我们使用 Spring Security 增加上认证和授权的功能。
- 引入 Spring Security 模块
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
-
编写 Spring Security 配置类
-
查看我们自己项目中的版本,找到对应的帮助文档:https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5
-
servlet-applications 8.16.4
-
编写基础配置类:
// 开启 WebSecurity 模式
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
}
- 定制请求的授权规则:看源码 + 仿写
// 链式编程
@Override
protected void configure(HttpSecurity http) throws Exception {
// 首页所有人都可以访问,功能也只有对应有权限的人才能访问到
// 请求授权的规则
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
}
- 测试一下:发现除了首页都进不去了,因为我们目前没有登录的角色,请求需要登录的角色拥有对应的权限才可以!
- 在
configure()
方法中加入以下配置,开启自动配置的登录功能!
// 开启自动配置的登录功能
// /login 请求来到登录页
// /login?error 重定向到这里表示登录失败
http.formLogin();
- 测试一下:发现没有权限的时候,会跳转到登录的页面
- 查看刚才登录页的注释信息:我们可以定义认证规则,重写 configure 的另一个方法
// 认证,springboot 2.1.x 可以直接使用
// 密码编码:PasswordEncoder
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 这些数据正常应该中数据库中读
auth.inMemoryAuthentication()
.withUser("kuangshen").password("123456").roles("vip2","vip3")
.and()
.withUser("root").password("123456").roles("vip1","vip2","vip3")
.and()
.withUser("guest").password("123456").roles("vip1");
}
- 测试:我们可以使用这些账号登录进行测试,发现会报错
- 原因:我们要将前端传过来的密码进行某种方式加密,否则就无法登录,修改代码
// 认证,springboot 2.1.x 可以直接使用
// 密码编码:PasswordEncoder
// 在spring Secutiry 5.0+ 新增了很多加密方法
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 这些数据正常应该中数据库中读
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");
}
- 测试:发现登录成功,并且每个角色只能访问自己认证下的规则
权限控制和注销
- 开启自动配置的注销的功能
// 定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
// ....
// 开启自动配置的注销的功能
// /logout 注销请求
http.logout();
}
- 在前端,增加一个注销的按钮,
index.html
导航栏中
<!--注销-->
<a class="item" th:href="@{/logout}">
<i class="sign-out icon"></i> 注销
</a>
-
我们可以去测试一下,登录成功后点击注销,发现注销完毕会跳转到登录页面!
-
但是,我们想让他注销成功后,依旧可以跳转到首页,该怎么处理呢?
// .logoutSuccessUrl("/"); 注销成功来到首页
http.logout().logoutSuccessUrl("/");
-
我们现在又来一个需求:用户没有登录的时候,导航栏上只显示登录按钮,用户登录之后,导航栏可以显示登录的用户信息及注销按钮!还有就是,比如 locke 这个用户,它只有 vip2,vip3 功能,那么登录则只显示这两个功能,而 vip1 的功能菜单不显示!这个就是真实的网站情况了!该如何做呢?
-
我们需要结合 thymeleaf 中的一些功能:
sec:authorize="isAuthenticated()"
:是否认证登录!来显示不同的页面- Maven 依赖
<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity4 --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency>
-
-
修改前端页面
- 导入命名空间
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
- 修改导航栏,增加认证判断
<!--登录注销--> <div class="right menu"> <!--如果未登录--> <div sec:authorize="!isAuthenticated()"> <a class="item" th:href="@{/login}"> <i class="address card icon"></i> 登录 </a> </div> <!--如果已登录--> <div sec:authorize="isAuthenticated()"> <a class="item"> <i class="address card icon"></i> 用户名:<span sec:authentication="principal.username"></span> 角色:<span sec:authentication="principal.authorities"></span> </a> </div> <div sec:authorize="isAuthenticated()"> <a class="item" th:href="@{/logout}"> <i class="sign-out icon"></i> 注销 </a> </div> </div>
-
重启测试
-
点击注销产生的问题
- 整合包 4(springsecurity4)
- 整合包 5(springsecurity5)
- 解决问题:
- 默认防止 csrf 跨站请求伪造,因为会产生安全问题
- 将请求改为 post 表单提交
- 在 spring security 中关闭 csrf 功能
http.csrf().disable();
-
继续将下面的角色功能块认证完成:
<!--菜单根据用户的角色动态的实现-->
<div class="column" sec:authorize="hasRole('vip1')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 1</h5>
<hr>
<div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
<div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
<div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
</div>
</div>
</div>
</div>
<div class="column" sec:authorize="hasRole('vip2')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 2</h5>
<hr>
<div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
<div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
<div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
</div>
</div>
</div>
</div>
<div class="column" sec:authorize="hasRole('vip3')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 3</h5>
<hr>
<div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
<div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
<div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
</div>
</div>
</div>
</div>
记住我
- 开启记住我功能
//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
//开启记住我功能: cookie,默认保存两周
http.rememberMe();
}
-
再次测试
-
如何实现的?
- 查看浏览器 cookie
- 点击注销的时候,可以发现 spring security 帮我们自动删除了这个 cookie
- cookie 发送给浏览器保存,以后登录带上这个 cookie,只要通过检查就可以免登陆了,如果点注销,则会删除这个 cookie
定制登录页
如何实现自己的 Login 界面?
- 在刚才的登录页配置后面指定 loginpage
protected void configure(HttpSecurity http) throws Exception {
//......
// 没有权限默认会到登录页面,需要开启登录的页面
// /login页面
http.formLogin().loginPage("/toLogin");
//......
}
- 然后前端也需要指向我们自己定义的 login 请求
<div sec:authorize="!isAuthenticated()">
<a class="item" th:href="@{/toLogin}">
<i class="address card icon"></i> 登录
</a>
</div>
- 登录后需要将这些信息发送到哪里?我们也需要配置,login.html 配置提交请求及方式,方式必须为 post:
protected void configure(HttpSecurity http) throws Exception {
//......
// 没有权限默认会到登录页面,需要开启登录的页面
// /login 页面
http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/toLogin")
.loginProcessingUrl("/login"); // 登陆表单提交请求
//......
}
- 在登录页增加我的多选框
<input type="checkbox" name="remember"> 记住我
- 后端验证处理
protected void configure(HttpSecurity http) throws Exception {
//......
//开启记住我功能: cookie,默认保存两周,自定义接收前端的参数
http.rememberMe().rememberMeParameter("remember");
}
完整配置
// AOP:拦截器
// 开启 WebSecurity 模式
@EnableWebSecurity
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页面
http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/toLogin")
.loginProcessingUrl("/login");
// 注销,开启了注销功能,跳到首页
http.logout().logoutSuccessUrl("/");
// 防止网站工具:get,post
http.csrf().disable();//关闭csrf功能,登录失败肯定存在的原因
// 开启记住我功能: cookie,默认保存两周,自定义接收前端的参数
http.rememberMe().rememberMeParameter("remember");
}
// 认证,springboot 2.1.x 可以直接使用
// 密码编码: PasswordEncoder
// 在spring Secutiry 5.0+ 新增了很多加密方法
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 这些数据正常应该中数据库中读
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");
}
}
Shiro
概述
功能
Apache Shiro 是一个强大且易用的 Java 安全框架,可以完成身份验证、授权、密码和会话管理。
Authentication
:身份认证 / 登录,验证用户是不是拥有相应的身份;Authorization
:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;Session Manager
:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;Cryptography
:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;Web Support
:Web 支持,可以非常容易的集成到 Web 环境;Caching
:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;Concurrency
:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;Testing
:提供测试支持;Run As
:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;Remember Me
:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
从外部看
对于我们而言,最简单的一个 Shiro 应用:
- 应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;
- 我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。
认证流程
用户 提交 身份信息、凭证信息 封装成 令牌 交由 安全管理器 认证:
快速入门
拷贝案例
- 按照官网提示找到快速入门案例:shiro/samples/quickstart/
- 复制快速入门案例 POM.xml 文件中的依赖分析案例
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.11.0</version>
</dependency>
<!-- configure logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
- 把快速入门案例中的 resource 下的
log4j.properties
复制下来 - 复制一下
shiro.ini
文件 - 复制一下
Quickstart.java
文件 - 运行
Quickstart.java
,得到结果
分析案例
-
通过 SecurityUtils 获取当前执行的用户 Subject
Subject currentUser = SecurityUtils.getSubject();
-
通过当前用户拿到 Session
Session session = currentUser.getSession();
-
用 Session 存值取值
session.setAttribute("someKey", "aValue"); String value = (String) session.getAttribute("someKey");
-
判断用户是否被认证
currentUser.isAuthenticated()
-
执行登录操作
currentUser.login(token);
-
打印其标识主体
currentUser.getPrincipal()
-
判断用户是否有角色
currentUser.hasRole()
-
注销
currentUser.logout();
SpringBoot 集成 Shiro
注意:SringBoot 的版本为 2.7.3,java 版本为 11
编写配置文件
- 在刚刚的父项目中新建一个 springboot 模块
- 导入 SpringBoot 和 Shiro 整合包的依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.6.0</version>
</dependency>
-
编写配置的三大要素:
- subject
ShiroFilterFactoryBean - securityManager
DefaultWebSecurityManager - realm
- subject
-
实际操作中对象创建的顺序:realm
securityManager subject -
编写自定义的
UserRealm.java
,需要继承AuthorizingRealm
// 自定义的 Realm
public class UserRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 打印一个提示
System.out.println("执行了授权方法");
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 打印一个提示
System.out.println("执行了认证方法");
return null;
}
}
- 新建一个
ShiroConfig
配置文件:ShiroConfig.java
@Configuration
public class ShiroConfig {
// 第三步:ShiroFilterFactoryBean
@Bean(name = "shiroFilterFactoryBean") // 务必设置 Bean name 为 shiroFilterFactoryBean,否则会报错
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
return bean;
}
// 第二步:securityManager -> DefaultWebSecurityManager
// @Qualifier("userRealm") 指定 Bean 的名字为 userRealm
// spring 默认的 BeanName 就是方法名
// name 属性指定 BeanName
@Bean(name = "SecurityManager")
public DefaultWebSecurityManager getDefaultWebSecurity(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 需要关联自定义的 Realm,通过参数把 Realm 对象传递过来
securityManager.setRealm(userRealm);
return securityManager;
}
// 第一步:创建 realm 对象,需要自定义类
// 让 spring 托管自定义的 realm 类
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
}
搭建简单测试环境
- 新建一个登录页面
- 新建一个首页:三个链接,一个登录、一个增加用户、一个删除用户
- 新建一个增加用户页面
- 新建一个删除用户页面
- 编写对应的 Controller
@Controller
public class MyController {
@RequestMapping({"/", "/index"})
public String toIndex(Model model) {
model.addAttribute("msg", "hello, Shiro");
return "index";
}
@RequestMapping({"user/add"})
public String add() {
return "user/add";
}
@RequestMapping({"user/update"})
public String update() {
return "user/update";
}
@RequestMapping("/tologin")
public String toLogin() {
return "login";
}
}
- 登录页面如下:注意在 pom 中加上
thymeleaf
依赖
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
<h1>登录</h1>
<hr>
<form th:action="@{/login}">
<p>用户名:<input type="text" name="username"></p>
<p>密码:<input type="text" name="password"></p>
<p>提交:<input type="submit"></p>
<p style="color:red;" th:text="${msg}"></p>
</form>
</body>
</html>
使用
登录拦截
在上面的 getShiroFilterFactoryBean
方法中加上需要拦截的登录请求:
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//添加 Shiro 的内置过滤器=======================
/*
anon : 无需认证,就可以访问
authc : 必须认证,才能访问
user : 必须拥有 “记住我”功能才能用
perms : 拥有对某个资源的权限才能访问
role : 拥有某个角色权限才能访问
*/
Map<String, String> map = new LinkedHashMap<>();
// 设置 /user/addUser 这个请求,只有认证过才能访问
// map.put("/user/addUser","authc");
// map.put("/user/deleteUser","authc");
// 设置 /user/ 下面的所有请求,只有认证过才能访问
map.put("/user/*","authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
// 设置登录的请求
shiroFilterFactoryBean.setLoginUrl("/tologin");
//============================================
return shiroFilterFactoryBean;
}
用户认证
- 在 Controller 中写一个登录的控制器:
// 登录的方法
@RequestMapping("/login")
public String login(String username, String password, Model model) {
// 获取当前用户
Subject subject = SecurityUtils.getSubject();
// 没有认证过
// 封装用户的登录数据,获得令牌
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 登录及异常处理
try {
// 用户登录
subject.login(token);
return "index";
} catch (UnknownAccountException uae) {
// 如果用户名不存在
System.out.println("用户名不存在");
model.addAttribute("exception", "用户名不存在");
return "login";
} catch (IncorrectCredentialsException ice) {
// 如果密码错误
System.out.println("密码错误");
model.addAttribute("exception", "密码错误");
return "login";
}
}
- 重启并测试
- 可以看出,是先执行了自定义的
UserRealm
中的AuthenticationInfo
方法,再执行了登录的相关操作 - 下面去自定义的
UserRealm
中的AuthenticationInfo
方法中去获取用户信息
- 可以看出,是先执行了自定义的
- 修改
UserRealm
中的AuthenticationInfo
// 自定义的 Realm
public class UserRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 打印一个提示
System.out.println("执行了授权方法");
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 打印一个提示
System.out.println("执行了认证方法");
// 用户名密码 (暂时先自定义一个做测试)
String name = "root";
String password = "1234";
// 1. 用户名认证
// 通过参数获取登录的控制器中生成的令牌
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 用户名认证
if (!token.getUsername().equals(name)){
// 用户名不存在,shiro 底层就会抛出 UnknownAccountException 异常
// return null 就表示控制器中抛出的相关异常
return null;
}
// 2. 密码认证, Shiro 自己做,为了避免和密码的接触
// 最后返回一个 AuthenticationInfo 接口的实现类,这里选择 SimpleAuthenticationInfo
// 三个参数:获取当前用户的认证, 密码, 认证名
return new SimpleAuthenticationInfo("", password, "");
}
}
退出登录
- 在控制器中添加一个退出登录的方法
// 退出登录
@RequestMapping("/logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "login";
}
Swagger
-
前后端分离
- Vue + SpringBoot
- 后端时代:前端只用管理静态页面;html
后端。模板引擎 JSP 后端才是主力
-
前后端分离时代
- 前端
前端控制层、视图层 - 伪造后端数据,json。已经存在了,不需要后端,前端工程队依旧能够跑起来
- 后端
后端控制层、服务层、数据访问层 - 前后端通过 API 进行交互
- 前后端相对独立且松耦合
- 前端
-
产生的问题
- 前后端集成联调,前端或者后端无法做到“及时协商,尽早解决”,最终导致问题集中爆发
-
解决方案
- 首先定义 schema [ 计划的提纲 ],并实时跟踪最新的 API,降低集成风险;
- 早些年:指定 word 计划文档;
- 前后端分离:
- 前端测试后端接口:postman
- 后端提供接口,需要实时更新最新的消息及改动
Swagger
-
号称世界上最流行的 API 框架
-
Restful Api 文档在线自动生成器
API 文档 与API 定义同步更新 -
直接运行,在线测试 API
-
支持多种语言 (如:Java,PHP 等)
Spring 集成
版本依赖问题很大!
SpringBoot 集成 Swagger
使用 Swagger 步骤:
-
新建一个 SpringBoot-web 项目,注意 springboot 版本要降到 2.5.6
-
添加 Maven 依赖(注意: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>
- 编写 HelloController,测试确保运行成功!
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "hello!";
}
}
- 要使用 Swagger,我们需要编写一个配置类 SwaggerConfig 来配置 Swagger
@Configuration // 配置类
@EnableSwagger2 // 开启Swagger2的自动配置
public class SwaggerConfig {
}
- 访问测试 :Swagger UI ,可以看到 swagger 的界面;
- Swagger 信息
- 接口信息
- 实体类信息
- 组
配置 Swagger
-
Swagger 实例 Bean 是 Docket,所以通过配置 Docket 实例来配置 Swaggger。
@Configuration // 开启 Swagger @EnableSwagger2 public class SwaggerConfig { @Bean // 配置 docket 以配置 Swagger 具体参数 public Docket docket() { return new Docket(DocumentationType.SWAGGER_2); } }
-
可以通过
apiInfo()
属性配置文档信息// 配置文档信息 private ApiInfo apiInfo() { Contact contact = new Contact("Lockegogo", "https://www.cnblogs.com/lockegogo/", "联系人邮箱"); return new ApiInfo( "LK's Swagger", // 标题 "Life is like a Markov chain.", // 描述 "v1.0", // 版本 "http://terms.service.url/组织链接", // 组织链接 contact, // 联系人信息 "Apach 2.0 许可", // 许可 "许可链接", // 许可连接 new ArrayList<>()// 扩展 );
-
Docket 实例关联上 apiInfo()
@Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()); }
-
重启项目,访问测试 http://localhost:8080/swagger-ui.html 看下效果;
配置扫描接口
-
构建 Docket 时通过
select()
方法配置怎么扫描接口。@Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() // 通过.select()方法,去配置扫描接口, RequestHandlerSelectors 配置如何扫描接口 .apis(RequestHandlerSelectors.basePackage("com.locke.swagger.controller")) .build(); }
-
重启项目测试,由于我们配置根据包的路径扫描接口,所以我们只能看到一个类
-
除了通过包路径配置扫描接口外,还可以通过配置其他方式扫描接口,这里注释一下所有的配置方式:
// 根据包路径扫描接口 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)
-
除此之外,我们还可以配置接口扫描过滤:
@Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.locke.swagger.controller")) // 配置如何通过 path 过滤, 即这里只扫描请求以 /locke 开头的接口(网站上的路径) .paths(PathSelectors.ant("/locke/**")) .build(); }
-
这里的可选值还有
any() // 任何请求都扫描 none() // 任何请求都不扫描 regex(final String pathRegex) // 通过正则表达式控制 ant(final String antPattern) // 通过 ant() 控制
配置 Swagger 开关
-
通过
enable()
方法配置是否启用 swagger,如果是 false,swagger 将不能在浏览器中访问了@Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) // 配置是否启用 Swagger,如果是 false,在浏览器将无法访问 .enable(false) .select() .apis(RequestHandlerSelectors.basePackage("com.locke.swagger.controller")) .paths(PathSelectors.ant("/locke/**")) .build(); }
-
如何动态配置当项目处于 test、dev 环境时显示 swagger,处于 prod 时不显示?
@Bean // 获得当前的生产环境 public Docket docket(Environment environment) { // 设置要显示 swagger 的环境 Profiles of = Profiles.of("dev", "test"); // 判断当前是否处于该环境 // 通过 enable() 接收此参数判断是否要显示 boolean b = environment.acceptsProfiles(of); return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .enable(b) .select() // 通过.select()方法,去配置扫描接口, RequestHandlerSelectors 配置如何扫描接口 .apis(RequestHandlerSelectors.basePackage("com.locke.swagger.controller")) .build(); }
-
可以在项目中增加配置文件
- dev 测试环境
server.port=8081
项目运行结果:
- pro 测试环境
server.port=8082
项目运行结果
配置 API 分组
-
如果没有配置分组,默认是 default。通过 groupName() 方法即可配置分组:
@Bean public Docket docket(Environment environment) { return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()) .groupName("狂神") // 配置分组 // 省略配置.... }
-
重启项目查看分组
-
如何配置多个分组?配置多个分组只需要配置多个 docket 即可:
@Bean public Docket docket1(){ return new Docket(DocumentationType.SWAGGER_2).groupName("group1"); } @Bean public Docket docket2(){ return new Docket(DocumentationType.SWAGGER_2).groupName("group2"); } @Bean public Docket docket3(){ return new Docket(DocumentationType.SWAGGER_2).groupName("group3"); }
-
重启项目查看即可
实体配置
-
新建一个实体类
@Data @AllArgsConstructor @NoArgsConstructor // @Api("注释") @ApiModel("用户实体") public class User { @ApiModelProperty("用户名") private String username; @ApiModelProperty("密码") private String password; }
-
只要这个实体在请求接口的返回值上(即使是泛型),都能映射到实体项中:
@RestController public class HelloController { // /error 默认错误请求 @GetMapping("/hello") public String hello() { return "hello"; } // 只要我们的接口中,返回值中存在实体类,他就会被扫描到 Swagger 中 @PostMapping("/user") public User user() { return new User(); } }
-
重启查看测试:
注:并不是因为 @ApiModel 这个注解让实体显示在这里了,而是只要出现在接口方法的返回值上的实体都会显示在这里,而 @ApiModel 和 @ApiModelProperty 这两个注解只是为实体添加注释的。
- @ApiModel 为类添加注释
- @ApiModelProperty 为类属性添加注释
- @ApiOperation 为方法添加注释
- @ApiParam 给参数加上注释
总结:
- 我们可以通过 Swagger 给一些比较难理解的接口或者属性,增加注释信息
- 接口文档实时更新
- 可以在线测试
Swagger 是一个优秀的工具,几乎所有大公司都有使用它
【注意点】:在正式发布的时候,关闭 Swagger!!!
- 出于安全考虑
- 而且节省内存
常用注解
Swagger 的所有注解定义在 io.swagger.annotations 包下
下面列一些经常用到的,未列举出来的可以另行查阅说明:
Swagger注解 | 简单说明 |
---|---|
@Api(tags = "xxx模块说明") | 作用在模块类上 |
@ApiOperation("xxx接口说明") | 作用在接口方法上 |
@ApiModel("xxxPOJO说明") | 作用在模型类上:如VO、BO |
@ApiModelProperty(value = "xxx属性说明",hidden = true) | 作用在类方法和属性上,hidden设置为true可以隐藏该属性 |
@ApiParam("xxx参数说明") | 作用在参数、方法和字段上,类似@ApiModelProperty |
我们也可以给请求的接口配置一些注释
-
在 HelloController 控制类中的接口添加 api 接口注释
@RestController public class HelloController { ...... @ApiOperation("Hello 控制接口") @GetMapping("/hello") public String hello2(@ApiParam("用户名") String username) { return "hello" + username; } @ApiOperation("get 测试") @GetMapping("/get") public User hello2(@ApiParam("用户") User user) { return user; } }
-
进行 try it out 测试
测试结果
总结:
-
这样的话,可以给一些比较难理解的属性或者接口,增加一些配置信息,让人更容易阅读!
-
相较于传统的 Postman 或 Curl 方式测试接口,使用 swagger 简直就是傻瓜式操作,不需要额外说明文档(写得好本身就是文档)而且更不容易出错,只需要录入数据然后点击 Execute,如果再配合自动化框架,可以说基本就不需要人为操作了。
-
Swagger 是个优秀的工具,现在国内已经有很多的中小型互联网公司都在使用它,相较于传统的要先出 Word 接口文档再测试的方式,显然这样也更符合现在的快速迭代开发行情。当然了,提醒下大家在正式环境要记得关闭 Swagger,一来出于安全考虑二来也可以节省运行时内存。
拓展
我们可以导入不同的包实现不同的皮肤定义:
1、默认的 访问 http://localhost:8080/swagger-ui.html
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
2、bootstrap-ui 访问 http://localhost:8080/doc.html
<!-- 引入 swagger-bootstrap-ui包 /doc.html-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.1</version>
</dependency>
3、Layui-ui 访问 http://localhost:8080/docs.html
<!-- 引入swagger-ui-layer包 /docs.html-->
<dependency>
<groupId>com.github.caspar-chen</groupId>
<artifactId>swagger-ui-layer</artifactId>
<version>1.1.3</version>
</dependency>
4、mg-ui 访问 http://localhost:8080/document.html
<!-- 引入swagger-ui-layer包 /document.html-->
<dependency>
<groupId>com.zyplayer</groupId>
<artifactId>swagger-mg-ui</artifactId>
<version>1.0.6</version>
</dependency>
异步、定时、邮件任务
异步任务
-
创建一个
service
包 -
创建一个类
AsyncService
异步处理还是非常常用的,比如我们在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以我们一般会采用多线程的方式去处理这些任务。
编写方法,假装正在处理数据,使用线程设置一些延时,模拟同步等待的情况;
@Service
public class AsyncService {
public void hello() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("业务进行中....");
}
}
- 编写
controller
包 - 编写
AsyncController
类
@RestController
public class AsyncController {
@Autowired
AsyncService asyncService;
@GetMapping("/hello")
public String hello() {
asyncService.hello(); // 停止 3 秒
return "OK";
}
}
- 访问 http://localhost:8080/hello 进行测试,3 秒后出现 OK,这是同步等待的情况。
问题:我们如果想让用户直接得到消息,就在后台使用多线程的方式进行处理即可,但是每次都需要自己手动去编写多线程的实现的话,太麻烦了,我们只需要用一个简单的办法,在我们的方法上加一个简单的注解即可,如下:
- 给 hello 方法添加 @Async 注解;
// 告诉 Spring 这是一个异步方法
@Async
public void hello() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("业务进行中....");
}
SpringBoot 就会自己开一个线程池,进行调用!但是要让这个注解生效,我们还需要在主程序上添加一个注解 @EnableAsync ,开启异步注解功能;
//开启异步注解功能
@EnableAsync
@SpringBootApplication
public class SpringbootTaskApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTaskApplication.class, args);
}
}
- 重启测试,网页瞬间响应,后台代码依旧执行!
邮件任务
邮件发送,在我们的日常开发中,也非常的多,Springboot 也帮我们做了支持
- 邮件发送需要引入 spring-boot-start-mail
- SpringBoot 自动配置 MailSenderAutoConfiguration
- 定义 MailProperties 内容,配置在
application.yaml
中 - 自动装配 JavaMailSender
- 测试邮件发送
测试:
-
引入 pom 依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>
看它引入的依赖,可以看到 jakarta.mail
<dependency> <groupId>com.sun.mail</groupId> <artifactId>jakarta.mail</artifactId> <scope>compile</scope> </dependency>
-
查看自动配置类:MailSenderAutoConfiguration
这个类中存在bean,JavaMailSenderImpl
然后我们去看下配置文件
@ConfigurationProperties(prefix = "spring.mail") public class MailProperties { private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; private String host; private Integer port; private String username; private String password; private String protocol = "smtp"; private Charset defaultEncoding = DEFAULT_CHARSET; private Map<String, String> properties = new HashMap<>(); private String jndiName; //set、get方法省略。。。 }
-
配置文件:
spring.mail.username=1710841251@qq.com spring.mail.password=你的qq授权码 spring.mail.host=smtp.qq.com # qq 需要配置 ssl spring.mail.properties.mail.smtp.ssl.enable=true
获取授权码:在QQ邮箱中的设置
账户 开启 pop3 和 smtp 服务 -
Spring 单元测试
@Autowired JavaMailSenderImpl javaMailSender; @Test//邮件设置1:一个简单的邮件 void contextLoads() { SimpleMailMessage mailMessage = new SimpleMailMessage(); mailMessage.setSubject("狂神,你好"); mailMessage.setText("谢谢你的狂神说Java系列课程"); mailMessage.setTo("24736743@qq.com"); mailMessage.setFrom("1710841251@qq.com"); javaMailSender.send(mailMessage); } @Test// 一个复杂的邮件 void contextLoads2() throws MessagingException { MimeMessage mimeMessage = javaMailSender.createMimeMessage(); //组装 MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); //正文 helper.setSubject("狂神,你好~plus"); helper.setText("<p style='color:red'>谢谢你的狂神说Java系列课程</p>", true); //附件 helper.addAttachment("1.jpg", new File("")); helper.addAttachment("2.jpg", new File("")); helper.setTo("24736743@qq.com"); helper.setFrom("1710841251@qq.com"); javaMailSender.send(mimeMessage); }
-
查看邮箱,邮件接收成功!
我们只需要使用 Thymeleaf 进行前后端结合即可开发自己网站邮件收发功能了!
定时任务
项目开发中经常需要执行一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息,Spring 为我们提供了异步执行任务调度的方式,提供了两个接口。
- TaskExecutor 接口(任务执行者)
- TaskScheduler 接口(任务调度者)
两个注解:
- @EnableScheduling:开启定时功能的注解
- @Scheduled:什么时候执行
cron 表达式:
字段 | 允许值 | 允许特殊字符 |
---|---|---|
秒 | 0-59 | , - * / |
分 | 0-59 | , - * / |
小时 | 0-23 | , - * / |
日期 | 1-31 | , - * / ? L W C |
月份 | 1-12 | , - * / |
星期 | 0-1 或 SUN-SAT 0,7 是 SUN | , - * / ? L W C |
特殊字符 | 代表含义 |
---|---|
, | 枚举 |
- | 区间 |
* | 任意 |
/ | 步长 |
? | 日/星期冲突匹配 |
L | 最后 |
W | 工作日 |
C | 和 calendar 练习后计算过的值 |
# | 星期,4#2 第二个星期三 |
测试步骤:
- 创建一个 ScheduledService
我们里面存在一个 hello 方法,他需要定时执行,怎么处理呢?
@Service
public class ScheduledService {
// 在一个特定的时间执行这个方法——Timer
//cron表达式
// 秒 分 时 日 月 周几
/*
0 49 11 * * ? 每天的11点49分00秒执行
0 0/5 11,12 * * ? 每天的11点和12点每个五分钟执行一次
0 15 10 ? * 1-6 每个月的周一到周六的10点15分执行一次
0/2 * * * * ? 每2秒执行一次
*/
@Scheduled(cron = "0/2 * * * * ?")
public void hello() {
System.out.println("hello, 你被执行了");
}
}
- 这里写完定时任务之后,我们需要在主程序上增加 @EnableScheduling 开启定时任务功能
// 开启异步注解功能
@EnableAsync
// 开启基于注解的定时任务
@EnableScheduling
@SpringBootApplication
public class SpringbootTaskApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTaskApplication.class, args);
}
}
-
我们来详细了解下cron表达式:http://www.bejson.com/othertools/cron/
-
常用的表达式
(1)0/2 * * * * ? 表示每2秒 执行任务
(1)0 0/2 * * * ? 表示每2分钟 执行任务
(1)0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务
(2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业
(3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作
(4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
(5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
(6)0 0 12 ? * WED 表示每个星期三中午12点
(7)0 0 12 * * ? 每天中午12点触发
(8)0 15 10 ? * * 每天上午10:15触发
(9)0 15 10 * * ? 每天上午10:15触发
(10)0 15 10 * * ? 每天上午10:15触发
(11)0 15 10 * * ? 2005 2005年的每天上午10:15触发
(12)0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
(13)0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
(14)0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
(15)0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
(16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发
(17)0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
(18)0 15 10 15 * ? 每月15日上午10:15触发
(19)0 15 10 L * ? 每月最后一日的上午10:15触发
(20)0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
(21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
(22)0 15 10 ? * 6#3 每月的第三个星期五上午10:15触
Dubbo 和 Zookeeper 集成
分布式理论
什么是分布式系统
在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”;
分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。
分布式系统(distributed system)是建立在网络之上的软件系统。
首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。
Dubbo 文档
随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,急需一个治理系统确保架构有条不紊的演进。
在 Dubbo 的官网文档有这样一张图:
单一应用架构
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架 (ORM) 是关键。

适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。
缺点:
- 性能扩展比较难
- 协同开发问题
- 不利于升级维护
垂直应用架构
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的 Web 框架 (MVC) 是关键。

通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职,更易于管理,性能扩展也更方便,更有针对性。
缺点:
- 公用模块无法重复利用,开发性的浪费。
分布式服务架构
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架 (RPC)是关键。

优点:
- 每个子系统变成小型系统,功能简单,前期开发成本低,周期短
- 每个子系统可按需伸缩
- 每个子系统可采用不同的技术
缺点:
- 子系统之间存在数据冗余、功能冗余,耦合性高
- 按需伸缩粒度不够,对同一个子系统中的不同的业务无法实现,比如订单管理和用户管理
流动计算架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心 (SOA) [ Service Oriented Architecture]是关键。

特点:
- 基于 SOA 的架构思想,将重复公用的功能抽取为组件,以服务的方式向各各系统提供服务
- 各各系统与服务之间采用 webservice、rpc 等方式进行通信
- ESB 企业服务总线作为系统与服务之间通信的桥梁
优点:
- 将重复的功能抽取为服务,提高开发效率,提高系统的可重用性、可维护性
- 可以针对不同服务的特点按需伸缩
- 采用 ESB 减少系统中的接口耦合
缺点:
- 系统与服务的界限模糊,会导致抽取的服务的粒度过大,系统与服务之间耦合性高
- 虽然使用了 ESB,但是服务的接口协议不固定,种类繁多,不利于系统维护。
微服务架构
基于 SOA 架构的思想,为了满足移动互联网对大型项目及多客户端的需求,对服务层进行细粒度的拆分,所拆分的每个服务只完成某个特定的业务功能,比如订单服务只实现订单相关的业务,用户服务实现用户管理相关的业务等等,服务的粒度很小,所以称为微服务架构。
特点:
- 服务层按业务拆分为一个一个的微服务
- 微服务的职责单一
- 微服务之间采用 RESTful、RPC 等轻量级协议传输
- 有利于采用前后端分离架构。
优点:
- 服务拆分粒度更细,有利于资源重复利用,提高开发效率
- 可以更加精准的制定每个服务的优化方案,按需伸缩
- 适用于互联网时代,产品迭代周期更短
缺点:
- 开发的复杂性增加,因为一个业务流程需要多个微服务通过网络交互来完成
- 微服务过多,服务治理成本高,不利于系统维护
什么是 RPC
RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数;
推荐阅读文章:https://www.jianshu.com/p/2accc2840a1b
RPC基本原理
RPC 两个核心模块:通讯,序列化。
- 序列化:数据传输需要转换
测试环境搭建
Dubbo
Apache Dubbo 是一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
调用关系说明
- 服务容器负责启动,加载,运行服务提供者。
- 服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者在启动时,向注册中心订阅自己所需的服务。
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
安装 zookeeper
https://juejin.cn/post/6974556676816896030
在 zoo.cfg 中添加:admin.serverPort=2182
- 双击
zkServer.cmd
开启服务:端口号是 2181 - 使用
zkCli.cmd
测试ls /
:列出 zookeeper 根下保存的所有节点create –e /locke 123
:创建一个 locke 节点,值为 123get /locke
:获取 /locke 节点的值
安装 dubbo-admin
dubbo
本身并不是一个服务软件。它其实就是一个 jar 包,能够帮你的 java 程序连接到 zookeeper,并利用 zookeeper 消费、提供服务;
但是为了让用户更好的管理监控众多的 dubbo 服务,官方提供了一个可视化的监控程序 dubbo-admin(监控管理后台),不过这个监控即使不装也不影响使用。
- 下载 dubbo-admin
- 在项目目录下 cmd 打包:
mvn clean package -Dmaven.test.skip=true
,也可以参考该项目的 README 文件打包
- 启动后端:
# 切换到目录:直接在路径前输入 cmd
dubbo-Admin-develop/dubbo-admin-distribution/target>
# 执行下面的命令启动 dubbo-admin,dubbo-admin 后台由 SpringBoot 构建
java -jar ./dubbo-admin-0.5.0-SNAPSHOT.jar
-
执行完毕后,访问 http://localhost:38080/, 用户名和密码都是 root
-
登录成功后查看界面:
总结:
- zookeeper:注册中心
- dubbo-admin:其实就是一个监控管理后台,可以查看我们注册了那些服务,哪些服务被消费了
- Dubbo:jar 包
SpringBoot + Dubbo + zookeeper
注意:SringBoot 的版本为 2.7.3
框架搭建
-
启动 zookeeper
-
IDEA 创建一个空项目
-
创建一个模块,实现服务提供者 provider-server,选择 web 依赖即可,
pom.xml
如下:<?xml version="1.0" encoding="UTF-8"?> <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.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.3</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.locke</groupId> <artifactId>provider-server</artifactId> <version>0.0.1-SNAPSHOT</version> <name>provider-server</name> <description>provider-server</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
项目创建完毕,我们写一个服务,比如买票的服务
- 编写接口:
public interface TicketService { public String getTicket(); }
- 编写实现类:
public class TicketServiceImpl implements TicketService { @Override public String getTicket() { return "《狂神说Java》"; } }
-
创建一个模块,实现服务消费者 consumer-server,选择 web 依赖即可,
pom.xml
文件同上 -
项目创建完毕,我们写一个服务,比如用户的服务
- 编写 service
public interface UserService { // 我们需要去拿去注册中心的服务 }
需求:现在我们的用户想使用买票的服务,如何实现?
服务提供者
- 将服务提供者注册到注册中心,我们需要整合 Dubbo 和 zookeeper,所以需要导包
<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-spring-boot-starter -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-registry-zookeeper -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
<version>3.1.0</version>
</dependency>
- 在 springboot 配置文件中配置 Dubbo 的相关属性
server.port=8001
# 当前应用名字
dubbo.application.name=provider-server
# 注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
# 扫描指定包下服务
dubbo.scan.base-packages=com.locke.service
注意,如果在服务提供者的启动类 ProviderServerApplication 前加上 @EnableDubbo 注解,则配置文件中的
dubbo.scan.base-packages
可以不用加,@EnableDubbo 会自动将服务注册到 Dubbo 中。
- 在 service 的实现类中配置服务注解,发布服务!注意导包问题,不要导入spring 的包
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Component;
// 服务注册与发现
@DubboService // 可以被扫描到,在项目一启动就自动注册到注册中心 zookeeper
@Component // 使用 Dubbo 后尽量不要用 Service 注解
@Slf4j
public class TicketServiceImpl implements TicketService {
@Override
public String getTicket() {
log.info("买票服务被调用");
return "《狂神说Java》";
}
}
逻辑理解 :应用启动起来,dubbo 就会扫描指定的包下带有 @component 注解的服务,将它发布在指定的注册中心中!
- 运行测试
- 查看服务是否在 zookeeper 中注册成功:
- 在可视化界面上查看:http://localhost:38080/, 用户名和密码都是 root
- 在
zkCli.cmd
中查看:在/services/provider-server
下面看到了一个 IP+ 端口的地址,说明该服务存在注册的实例,可以使用
[zk: localhost:2181(CONNECTED) 0] ls ls [-s] [-w] [-R] path [zk: localhost:2181(CONNECTED) 1] ls / [dubbo, services, zookeeper] [zk: localhost:2181(CONNECTED) 2] ls /services [dubbo-admin, provider-server] [zk: localhost:2181(CONNECTED) 3] ls /services/provider-server [192.168.31.103:20880] [zk: localhost:2181(CONNECTED) 4]
- 在可视化界面上查看:http://localhost:38080/, 用户名和密码都是 root
服务消费者
- 导入依赖:和之前一样
- 配置参数
server.port=8002
#当前应用名字
dubbo.application.name=consumer-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
- 本来正常步骤是需要将服务提供者的接口打包,然后用 pom 文件导入,我们这里使用简单的方式,直接将服务的接口拿过来,路径必须保证正确,即和服务提供者相同;
- 完善消费者的服务类:注意导入的包
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.stereotype.Service;
@Service // 注入到容器中
public class UserService {
// 想拿到 provider-server 提供的票,要去注册中心拿到服务
// 引用,Pom 坐标,可以定义路径相同的接口名
@DubboReference
TicketService ticketService;
public void bugTicket() {
String ticket = ticketService.getTicket();
System.out.println("在注册中心买到" + ticket);
}
}
- 测试类编写
@SpringBootTest
public class ConsumerServerApplicationTests {
@Autowired
UserService userService;
@Test
public void contextLoads() {
userService.bugTicket();
}
}
启动测试
- 开启 zookeeper
- 打开 dubbo-admin 实现监控
- 开启服务者
- 消费者消费测试
以上就是 SpingBoot + dubbo + zookeeper
实现分布式开发的应用,其实就是一个服务拆分的思想。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2020-02-14 SQL 相关知识点