SpringBoot(2) 基础篇
简介
- 原始的Spring程序初始搭建繁琐——最基本的Spring程序至少有一个配置文件或配置类,用来描述Spring的配置信息;原始的Spring程序开发过程繁琐——导入对应的jar包(或坐标),将相关核心对象交给Spring容器管理,即配置成Spring容器管控的bean
- 简化方式:
- 起步依赖(简化依赖配置)
- 自动配置(简化常用工程相关配置)
- 辅助功能(内置tomcat服务器)
具体体现
parent:将各种技术配合使用的常见依赖版本进行整理,得到最合理的依赖版本配置方案,由parent统一管理各种技术的版本(parent仅仅管理版本,但不负责导入坐标)
项目的pom.xml会继承一个坐标:
1
2
3
4
5<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
</parent>该坐标会再继承一个坐标:
1
2
3
4
5<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.5.4</version>
</parent>该坐标定义了各种依赖的版本号,以及各种依赖的坐标信息——坐标中没有具体的依赖版本号,而是引用第一组信息中定义的依赖版本值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<properties>
<activemq.version>5.16.3</activemq.version>
<aspectj.version>1.9.7</aspectj.version>
...
</properties>
<dependencyManagement>
<dependencies>
...
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
</dependencies>
</dependencyManagement>依赖坐标定义在<dependencyManagement>标签中,因此只是管理坐标而非实际使用坐标。项目继承这组parent信息后,如果不使用对应坐标,相关的定义不会导入
starter:实际开发时,对于依赖坐标的使用往往都有一些固定的组合方式,例如使用spring-webmvc就一定使用spring-web。因此设定使用某种技术时对于依赖的固定搭配格式,即starter,帮助开发者减少依赖配置
pom.xml定义SpringMVC的starter(所有的starter依赖于spring-boot-starter)
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>spring-boot-starter-web中定义若干个具体依赖的坐标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.5.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.5.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.5.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.9</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.9</version>
<scope>compile</scope>
</dependency>
</dependencies>其中的starter进一步包含一些坐标
可能会导致过量导入
与parent:
- starter:一个坐标包含若干个坐标,减少依赖配置书写
- parent:定义数百版本号,减少依赖冲突
实际开发中,先找starter,没有再手写坐标
坐标冲突:手工书写的方式添加对应依赖
- 可以直接写坐标
- 覆盖<properties>中定义的版本号
引导类:带有main的类
Spring运行的基础是创建自己的Spring容器对象(IoC容器)并将所有对象交给容器管理。引导类运行后会产生一个Spring容器象,通过容器对象直接操作Bean
1
2
3
4
5
6
7
8
public class SpringbootTestApplication {
public static void main(String[] args) {
ConfigurableApplicationContext ctx = SpringApplication.run(SpringbootTestApplication.class, args);
TestController bean = ctx.getBean(TestController.class);
System.out.println("bean======>" + bean);
}
}
内嵌tomcat
内嵌tomcat的定义位置:上文已经有一个spring-boot-starter-tomcat,在这里定义。其中有一个坐标,tomcat-embed-core
运行原理:
- tomcat运行起来也是一个对象,而Spring用于管理对象,因此tomcat实际在容器中运行
- 具体的运行对象就是tomcat-embed-core
更换服务器:将tomcat依赖排除,然后加入其他的starter(例如jetty)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
</dependencies>
基础配置
- application.properties(yml格式、yaml格式,这两个后缀不同,内部格式一致)
- 开发者配置的书写位置集中在该文件中,例如修改服务器的端口等(原本需要在服务器的配置文件中修改)
- 能配置什么?参考官网文档中的Application Properties
- 配置项和什么有关?pom用导入了什么,才能做什么的配置
- 配置文件优先级:properties > yml > yaml
- 不同配置文件中相同配置按照加载优先级相互覆盖,不同配置文件中不同配置全部保留
yml数据读取
单一数据读取:使用@Value注解读取yml的单个数据——${一级属性名.二级属性名……}
读取全部数据:SpringBoot把所有的数据都封装到Environment对象中,通过@Autowired自动专配数据
读取对象数据:将一组yml数据封装成一个对象;必须定义成一个bean(component),通过@ConfigurationProperties指定该对象加载哪一组yml中配置的信息
必须知道数据前缀,从而封装该前缀下的所有属性
数据属性名和对象的变量名一一对应
yml中的数据引用
可能多个值具有相同的目录前缀,此时可以搞一个变量名,引用该变量
1
2
3
4
5
6baseDir: /usr/local/fire
center:
dataDir: ${baseDir}/data
tmpDir: ${baseDir}/tmp
logDir: ${baseDir}/log
msgDir: ${baseDir}/msgDir书写字符串时,如果需要使用转义字符,需要将数据字符串使用双引号包裹
1
Spring: "Spring\tboot\n"
SSM整合
JUnit(单元测试)
Spring整合:
1
2
3
4
5
6
7
8
9
10
11
12
13
14//加载spring整合junit专用的类运行器
//指定对应的配置信息
public class AccountServiceTestCase {
//注入要测试的对象
private AccountService accountService;
public void testGetById(){
//执行要测试的对象对应的方法
System.out.println(accountService.findById(2));
}
}- @RunWith:设置Spring专用于测试的类运行器,不能使用JUnit自带的类运行方式,格式是固定的
- @ContextConfiguration:设置Spring核心配置文件或配置类,即指定Spring具体的环境配置
SpringBoot整合:
1
2
3
4
5
6
7
8
9
10
11
12
class Springboot04JunitApplicationTests {
//注入你要测试的对象
private BookDao bookDao;
void contextLoads() {
//执行要测试的对象对应的方法
bookDao.save();
System.out.println("two...");
}
}- 加载的配置类或者配置文件即为启动程序使用的引导类
- 手工指定引导类:
@SpringBootTest(classes = SpringbootApplication.class)
@ContextConfiguration(classes = SpringbootApplication.class)
- 测试类如果存在于引导类所在包或子包中无需指定引导类
MyBatis
Spring整合:
pom.xml整合坐标:mysql驱动坐标、jdbc坐标、MyBatis坐标、Spring整合MyBatis坐标
配置Spring
设置MyBatis交给Spring管理的bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22//定义mybatis专用的配置类
public class MyBatisConfig {
// 定义创建SqlSessionFactory对应的bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
//SqlSessionFactoryBean是由mybatis-spring包提供的,专用于整合用的对象
SqlSessionFactoryBean sfb = new SqlSessionFactoryBean();
//设置数据源替代原始配置中的environments的配置
sfb.setDataSource(dataSource);
//设置类型别名替代原始配置中的typeAliases的配置
sfb.setTypeAliasesPackage("com.itheima.domain");
return sfb;
}
// 定义加载所有的映射配置
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.itheima.dao");
return msc;
}
}设置数据源的bean(这里用Druid数据源)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class JdbcConfig {
private String driver;
private String url;
private String userName;
private String password;
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
}设置数据库连接信息(properties格式)
1
2
3
4com.mysql.jdbc.Driver =
jdbc:mysql://localhost:3306/spring_db?useSSL=false =
root =
root =
SpringBoot整合:
创建模块时勾选MyBatis Framework和对应数据库MySQL Driver
yml中配置数据源相关信息
1
2
3
4
5
6spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=Asia/Shanghai
username: root
password: root
综上,整合时做的是:
- 导入对应技术的starter坐标
- 配置相关信息
补充
使用lombok可以通过一个注解@Data完成一个实体类对应的getter,setter,toString,equals,hashCode等操作的快速添加
- SpringBoot默认集成了lombok,并提供对应的版本控制,只需要在pom.xml提供坐标即可
1
2
3
4
5
6
7<dependencies>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>1
2
3
4
5
6
7
8import lombok.Data;
@Data
public class Book {
private Integer id;
private String type;
private String name;
private String description;
}具体开发时,为了给前端传递数据,通常使用Restful风格。功能测试通过Postman工具进行。并且为了避免“不同的操作结果所展示的数据格式差异化严重”的问题,必须将所有操作的操作结果数据格式统一起来,需要设计表现层返回结果的模型类,用于后端与前端进行数据格式统一,也称为前后端数据协议
差异化问题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 查询单个数据的结果
{
"id": 1,
"type": "计算机理论",
"name": "Spring实战 第5版",
}
// 查询全部数据的结果
[
{
"id": 1,
"type": "计算机理论",
"name": "Spring实战 第5版",
},
{
"id": 2,
"type": "计算机理论",
"name": "Spring 5核心原理",
}
]表示层返回结果的模型类(需要考虑正确操作时的数据格式,以及错误操作时的数据格式):
1
2
3
4
5
6
public class Res {
private Boolean flag; // 标识操作是否成功
private Object data; // 用于封装操作数据
private String msg; //用于封装消息,传递给前端页面,补充说明操作的结果
}表现层的返回结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BookController {
private IBookService bookService;
public Res getAll(){
return new R(true, bookService.list());
}
public R save( Book book)throws IOException {
if (book.getName().equals("!@#$!@#") )
throw new IOException();
boolean flag = bookService.insert(book);
return new R(flag, flag ? "添加成功^_^" : "添加失败-_-!");
}
}设置SpringMVC异常处理器
1
2
3
4
5
6
7
8
9
10
public class ProjectExceptionAdvice {
//拦截所有的异常信息
public R doOtherException(Exception ex){
//记录日志
ex.printStackTrace();
return new R(false,null,"系统错误,请稍后再试!");
}
}
程序打包
打包后放到服务器运行
打包:idea下执行
mvn package
,生成文件“模块名+版本号.jar”运行:当前路径下执行
java -jar 工程包名.jar
(pom.xml中下段配置不能删除)1
2
3
4
5
6
7
8<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
配置高级
临时属性:项目中有个别属性需要重新配置,可以使用临时属性的方式快速修改某些配置(例如,修改端口号)
1
java –jar springboot.jar –-server.port=80
属性加载优先级:
- 有14种配置(官网)
- 可能yml中配置某一属性,但读取时不是配置的值,此时可以根据该顺序进行排查
配置文件分类:
- 分类:
- 类路径下配置文件(一直使用的是这个,即resources目录中的application.yml)——开发人员本机开发与测试
- 类路径下config目录下配置文件——项目经理整体调控
- 程序包所在目录中配置文件——运维人员配置涉密线上环境
- 程序包所在目录中config目录下配置文件——运维人员整体调控
- 优先级:
- file :config/application.yml 【最高】
- file :application.yml
- classpath:config/application.yml
- classpath:application.yml 【最低】
- 级别一、二一般在程序打包以后使用
- 分类:
自定义配置文件——使用临时属性设置配置文件路径:
–spring.config.location=dev
(使用全路径名或者无扩展名的文件名)
多环境开发
单个yml配置文件
针对不同的环境设置不同的配置属性,不同环境用
---
分割,不同环境起名不同1
2
3
4
5
6
7
8
9
10
11
12
13
14
15spring:
profiles:
active: pro # 默认启动pro
server:
port: 80
spring:
profiles: dev
server:
port: 81
spring:
profiles: test
server:
port: 82
多个yml配置文件(每个环境一个yml)
主配置文件(设置公共配置(全局))
1
2
3spring:
profiles:
active: pro # 启动pro环境配置文件(每个环境配置文件只关心自己的配置项)
1
2server:
port: 80- 通过文件名区分:
application-环境名.yml
- 通过文件名区分:
properties文件类似
还可以根据具体功能,进一步拆分:application-devDB.yml、application-devRedis.yml、application-devMVC.yml等
使用include属性在激活指定环境的情况下,同时对多个环境进行加载使其生效,多个环境间使用逗号分隔
1
2
3
4spring:
profiles:
active: dev
include: devDB,devRedis,devMVC如果子环境有多个相同属性,则最后加载的环境有效
多环境开发控制:
maven和springboot同时设置多个环境时,可以在maven中设置具体环境,Springboot直接读取maven的环境
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<profiles>
<profile>
<id>env_dev</id>
<properties>
<profile.active>dev</profile.active>
</properties>
<activation>
<activeByDefault>true</activeByDefault> <!--默认启动环境-->
</activation>
</profile>
<profile>
<id>env_pro</id>
<properties>
<profile.active>pro</profile.active>
</properties>
</profile>
</profiles>Springboot读取:
@属性名@
占位符即为读取maven中配置的属性值的语法格式1
2
3spring:
profiles:
active: @profile.active@
日志
日志的级别分为6种,分别是:
- TRACE:运行堆栈信息,使用率低
- DEBUG:程序员调试代码使用(开发时使用)
- INFO:记录运维过程数据(上线后使用)
- WARN:记录运维过程报警数据(运维时使用)
- ERROR:记录错误堆栈信息
- FATAL:灾难信息,合并计入ERROR
设置日志级别:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19# 开启debug模式,输出调试信息,常用于检查系统运行状况——简单粗暴
debug: true
# 设置日志级别,root表示根节点,即整体应用日志级别——粒度控制
logging:
level:
root: debug
logging:
# 设置日志组,控制指定包对应的日志输出级别
group:
# 自定义组名,设置当前组中所包含的包
ebank: com.itheima.controller
level:
root: warn
# 为对应组设置日志级别
ebank: debug
# 直接对一个package设置日志级别
com.itheima.controller: debug每个类都创建日志记录对象作为类变量
1
2
3
4
5
public class BookController extends BaseClass{
private static final Logger log = LoggerFactory.getLogger(BookController.class);
}可以导入lombok,通过注解@Slf4j省略
1
2
3
4
5
6//这个注解替代了下面那一行
public class BookController extends BaseClass{
...
}
日志格式:
日期,触发位置,记录信息是最核心的信息。级别用于做筛选过滤,PID与线程名用于做精准分析
1
2
3logging:
pattern:
console: "%d %clr(%p) --- [%16t] %clr(%-40.40c){cyan} : %m %n"
记录日志到文件:设置日志文件名即可,并限制每个日志的大小
1
2
3
4
5
6
7
8
9logging:
file:
name: server.log
logging:
logback:
rollingpolicy:
max-file-size: 3KB
file-name-pattern: server.%d{yyyy-MM-dd}.%i.log- 基于logback设置每日日志文件的设置格式
- 容量到达3KB后转存信息到第二个文件中
- 文件命名规则:%d标识日期,%i是一个递增变量,用于区分日志文件
热部署
不需要重新启动服务器,就能将更新后的程序重新加载——是一个开发阶段使用的功能,而非线上运行时的功能
非SpringBoot项目的热部署(假设部署在tomcat上)
- 在tomcat服务器的配置文件中进行配置
- 通过IDE工具配置,核心思想是服务器监控其中加载的应用,发现产生了变化就重新加载一次
SpringBoot项目的热部署
此时tomcat内嵌,和当前程序都是Spring容器中的组件。此时设置一个程序X,如果部署的程序变化了,X令tomcat容器重新加载程序——只需要重新加载开发的程序那一部分就可以,不需要加载其他的bean
手动启动:
导入开发者工具对应的坐标
1
2
3
4
5<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>build project
原理:
- springboot项目运行时,根据加载内容不同,分成base类加载器与restart类加载器
- base类加载器:加载jar包中的类——jar包中的类和配置文件不会发生变化
- restart类加载器:加载开发者自己开发的类、配置文件、页面等信息
- springboot项目启动时,base类加载器执行,加载jar包中的信息后,restart类加载器执行,加载开发者制作的内容。热部署的过程实际是重新加载restart类加载器中的信息
- springboot项目运行时,根据加载内容不同,分成base类加载器与restart类加载器
自动启动热部署:与IDE有关,略
设置热部署监控的文件范围:
配置中默认不参与热部署的目录:
- /META-INF/maven
- /META-INF/resources
- /resources
- /static
- /public
- /templates
通过application.yml设定哪些文件不参与热部署
1
2
3
4
5spring:
devtools:
restart:
# 设置不参与热部署的文件或文件夹
exclude: static/**,public/**,config/application.yml
关闭:
配置文件中关闭
1
2
3
4spring:
devtools:
restart:
enabled: false在启动容器前通过系统属性设置关闭
1
2
3
4
5
6
7
public class SSMPApplication {
public static void main(String[] args) {
System.setProperty("spring.devtools.restart.enabled","false");
SpringApplication.run(SSMPApplication.class);
}
}
配置高级
@ConfigurationProperties
该注解为自定义的bean绑定yml中的属性(见基础篇)
为第三方bean加载属性:(上面是在自定义的bean中加载配置属性,而此时无法到源码中添加该注解)
首先,使用@Bean注解定义第三方bean(通过函数返回值确定)
1
2
3
4
5
public DruidDataSource datasource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}yml中定义要绑定的属性,datasource此时全小写
1
2datasource:
driverClassName: com.mysql.jdbc.Driver@ConfigurationProperties注解为第三方bean进行属性绑定,前缀是全小写的datasource
1
2
3
4
5
6
public DruidDataSource datasource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}
@ConfigurationProperties注解添加到类上是为spring容器管理的当前类的对象绑定属性,添加到方法上是为spring容器管理的当前方法的返回值对象绑定属性
进一步的,有注解@EnableConfigurationProperties,专门标注使用@ConfigurationProperties绑定属性的bean是哪些
配置类上开启@EnableConfigurationProperties注解,说明要使用@ConfigurationProperties注解绑定属性的类
1
2
3
4@SpringBootApplication
@EnableConfigurationProperties(ServerConfig.class)
public class SpringbootConfigurationApplication {
}对应的类上直接使用@ConfigurationProperties进行属性绑定
1
2
3
4
5
6
7
public class ServerConfig {
private String ipAddress;
private int port;
private long timeout;
}
出现一个提示信息:
添加坐标:
1 | <dependency> |
宽松绑定/松散绑定
- 配置文件中的命名格式与变量名的命名格式可以进行格式上的最大化兼容——几乎主流的命名格式都支持,例如以下4种模式最终都可以匹配到ipAddress属性名(springboot官方推荐使用烤肉串模式)
1 | servers: |
- 该规则仅针对springboot中@ConfigurationProperties注解进行属性绑定时有效,对@Value注解进行属性映射无效
校验
- SpringBoot有数据校验功能,避免非法属性值注入(yml文件中对于数字的定义支持进制书写格式,如需使用字符串,则使用引号明确标注)
1 | <!--导入JSR303规范--> |
1 |
|
1 |
|
测试
加载测试专用属性、配置:
属性:在测试用例程序中,可以通过对注解@SpringBootTest添加属性来模拟临时属性
1
2
3
4
5
6
7
8
9
10
11
public class PropertiesAndArgsTest {
private String msg;
void testProperties(){
System.out.println(msg);
}
}配置:略
模拟web环境:略
数据层测试回滚:在原始测试用例中添加注解@Transactional即可实现当前测试用例的事务不提交
1
2
3
4
5
6
7
8
9
10
11
12
public class DaoTest {
private BookService bookService;
void testSave(){
...
}
}
略
SpringBoot内嵌的数据层解决方案(SQL)
- 一般的数据层解决方案包括:Mysql+Druid+MyBatis
数据源
springboot提供3款内嵌数据源技术(管理数据库的连接)
- HikariCP(默认,若不配置数据源,则使用这个)
- Tomcat提供DataSource(将HikartCP技术的坐标排除掉后,默认使用这个)
- Commons DBCP(既不使用HikartCP也不使用tomcat的DataSource)
配置内容(以HikariCP为例):
1
2
3
4
5
6
7
8spring:
datasource:
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
maximum-pool-size: 50
持久化
spring提供的JdbcTemplate,回归到jdbc最原始的编程形式来进行数据层的开发
导入坐标
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>配置jdbcTemplate
1
2
3
4
5
6spring:
jdbc:
template:
query-timeout: -1 # 查询超时时间
max-rows: 500 # 最大行数
fetch-size: -1 # 缓存行数自动装配jdbc
1
2
3
4
5
6
class Springboot15SqlApplicationTests {
void testJdbcTemplate( JdbcTemplate jdbcTemplate){
}
}
数据库
springboot提供3款内置的数据库,采用内嵌的形式运行在spirngboot容器
- H2
- HSQL
- Derby
导入H2坐标
1
2
3
4
5
6
7
8<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>配置H2数据库控制台访问,web端访问路径/h2,访问密码123456
1
2
3
4
5
6
7
8
9
10
11spring:
h2:
console:
enabled: true
path: /h2
datasource:
url: jdbc:h2:~/test
hikari:
driver-class-name: org.h2.Driver
username: sa
password: 123456
整合其他技术
NoSQL
整合redis
导入redis starter坐标
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>基础配置
1
2
3
4spring:
redis:
host: localhost
port: 6379redis专用客户端的接口(下为RedisTemplate)——需要先确认操作何种数据,根据数据种类得到操作接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Springboot16RedisApplicationTests {
private RedisTemplate redisTemplate;
void set() {
ValueOperations ops = redisTemplate.opsForValue();
ops.set("age",41);
}
void get() {
ValueOperations ops = redisTemplate.opsForValue();
Object age = ops.get("name");
System.out.println(age);
}
void hset() {
HashOperations ops = redisTemplate.opsForHash();
ops.put("info","b","bb");
}
void hget() {
HashOperations ops = redisTemplate.opsForHash();
Object val = ops.get("info", "b");
System.out.println(val);
}
}默认提供的是lettucs客户端,可以根据需要切换成指定的客户端技术,例如jedis
1
2
3
4
5<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<!--jedis坐标受springboot管理,无需提供版本号-->
</dependency>1
2
3
4
5
6
7
8
9
10
11spring:
redis:
host: localhost
port: 6379
client-type: jedis # 客户端类型,设置为jedis
lettuce:
pool:
max-active: 16
jedis:
pool:
max-active: 16lettcus与jedis区别
- jedis连接Redis服务器是直连模式,多线程模式下使用存在线程安全问题——通过配置连接池使每个连接专用,但整体性能下降
- lettcus基于Netty框架与Redis服务器连接,底层设计中采用StatefulRedisConnection,线程安全,一个连接可以被多线程复用
整合ES
ES(Elasticsearch):一个分布式全文搜索引擎,重点是全文搜索,加速数据的查询
全文搜索:搜索的条件不再是对某一个字段进行比对,而是在一条数据中使用搜索条件去比对更多的字段,只要能匹配上就列入查询结果
原理:
- 被查询的字段的数据全部文本信息进行查分,分成若干个词,例如“中华人民共和国”被拆分成三个词,此过程称为分词。不同的分词策略称为分词器
存储分词结果,对应每条数据的id。id为1的数据中名称这一项的值是“中华人民共和国”,分词结束后,“中华”对应id为1,“人民”对应id为1,“共和国”对应id为1,最终结果汇总到一个表格中
分词结果关键字 对应id 中华 1 人民 1,2 共和国 1 代表 2 大会 2 进行查询时,如果输入“人民”作为查询条件,比对上述表格得到id值1,2,根据id值得到查询结果
分词结果关键字不是一个完整的字段值,只是一个字段中的其中的一部分内容。关键字查询后得到的是数据的id,还要再次查询,该关键字称为倒排索引
ES基本操作
- 操作ES可以通过Rest风格的请求来进行,即一个请求可以执行一个操作,如新建索引,删除索引
创建索引,books是索引名称,下同
1
PUT请求 http://localhost:9200/books
发送请求后,看到如下信息即索引创建成功
1
2
3
4
5{
"acknowledged": true,
"shards_acknowledged": true,
"index": "books"
}重复创建已经存在的索引会出现错误信息,reason属性中描述错误原因
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17{
"error": {
"root_cause": [
{
"type": "resource_already_exists_exception",
"reason": "index [books/VgC_XMVAQmedaiBNSgO2-w] already exists",
"index_uuid": "VgC_XMVAQmedaiBNSgO2-w",
"index": "books"
}
],
"type": "resource_already_exists_exception",
"reason": "index [books/VgC_XMVAQmedaiBNSgO2-w] already exists", # books索引已经存在
"index_uuid": "VgC_XMVAQmedaiBNSgO2-w",
"index": "book"
},
"status": 400
}查询索引
1
GET请求 http://localhost:9200/books
查询索引得到索引相关信息,如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25{
"book": {
"aliases": {},
"mappings": {},
"settings": {
"index": {
"routing": {
"allocation": {
"include": {
"_tier_preference": "data_content"
}
}
},
"number_of_shards": "1",
"provided_name": "books",
"creation_date": "1645768584849",
"number_of_replicas": "1",
"uuid": "VgC_XMVAQmedaiBNSgO2-w",
"version": {
"created": "7160299"
}
}
}
}
}如果查询了不存在的索引,会返回错误信息,例如查询名称为book的索引后信息如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21{
"error": {
"root_cause": [
{
"type": "index_not_found_exception",
"reason": "no such index [book]",
"resource.type": "index_or_alias",
"resource.id": "book",
"index_uuid": "_na_",
"index": "book"
}
],
"type": "index_not_found_exception",
"reason": "no such index [book]", # 没有book索引
"resource.type": "index_or_alias",
"resource.id": "book",
"index_uuid": "_na_",
"index": "book"
},
"status": 404
}删除索引
1
DELETE请求 http://localhost:9200/books
删除所有后,给出删除结果
1
2
3{
"acknowledged": true
}如果重复删除,会给出错误信息,同样在reason属性中描述具体的错误原因
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21{
"error": {
"root_cause": [
{
"type": "index_not_found_exception",
"reason": "no such index [books]",
"resource.type": "index_or_alias",
"resource.id": "book",
"index_uuid": "_na_",
"index": "book"
}
],
"type": "index_not_found_exception",
"reason": "no such index [books]", # 没有books索引
"resource.type": "index_or_alias",
"resource.id": "book",
"index_uuid": "_na_",
"index": "book"
},
"status": 404
}创建索引并指定分词器——前面没有指定分词器。国内较为流行的分词器是IK分词器,需要先下载:https://github.com/medcl/elasticsearch-analysis-ik/releases。下载后解压到ES安装目录的plugins目录。使用IK分词器创建索引格式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29PUT请求 http://localhost:9200/books
请求参数如下(注意是json格式的参数)
{
"mappings":{ #定义mappings属性,替换创建索引时对应的mappings属性
"properties":{ #定义索引中包含的属性设置
"id":{ #设置索引中包含id属性
"type":"keyword" #当前属性可以被直接搜索
},
"name":{ #设置索引中包含name属性
"type":"text", #当前属性是文本信息,参与分词
"analyzer":"ik_max_word", #使用IK分词器进行分词
"copy_to":"all" #分词结果拷贝到all属性中
},
"type":{
"type":"keyword"
},
"description":{
"type":"text",
"analyzer":"ik_max_word",
"copy_to":"all"
},
"all":{ #定义属性,用来描述多个字段的分词结果集合,当前属性可以参与查询
"type":"text",
"analyzer":"ik_max_word"
}
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52{
"books": {
"aliases": {},
"mappings": { #mappings属性已经被替换
"properties": {
"all": {
"type": "text",
"analyzer": "ik_max_word"
},
"description": {
"type": "text",
"copy_to": [
"all"
],
"analyzer": "ik_max_word"
},
"id": {
"type": "keyword"
},
"name": {
"type": "text",
"copy_to": [
"all"
],
"analyzer": "ik_max_word"
},
"type": {
"type": "keyword"
}
}
},
"settings": {
"index": {
"routing": {
"allocation": {
"include": {
"_tier_preference": "data_content"
}
}
},
"number_of_shards": "1",
"provided_name": "books",
"creation_date": "1645769809521",
"number_of_replicas": "1",
"uuid": "DohYKvr_SZO4KRGmbZYmTQ",
"version": {
"created": "7160299"
}
}
}
}
}以上为创建索引。添加文档(ES中称数据为文档),有三种方式
1
2
3
4
5
6
7
8
9
10POST请求 http://localhost:9200/books/_doc #使用系统生成id
POST请求 http://localhost:9200/books/_create/1 #使用指定id
POST请求 http://localhost:9200/books/_doc/1 #使用指定id,不存在创建,存在更新(版本递增)
文档通过请求参数传递,数据格式json
{
"name":"springboot",
"type":"springboot",
"description":"springboot"
}查询文档
1
2GET请求 http://localhost:9200/books/_doc/1 #查询单个文档
GET请求 http://localhost:9200/books/_search #查询全部文档条件查询
1
GET请求 http://localhost:9200/books/_search?q=name:springboot # q=查询属性名:查询属性值
删除文档
1
DELETE请求 http://localhost:9200/books/_doc/1
修改文档(全量更新)
1
2
3
4
5
6
7
8PUT请求 http://localhost:9200/books/_doc/1
文档通过请求参数传递,数据格式json
{
"name":"springboot",
"type":"springboot",
"description":"springboot"
}修改文档(部分更新)
1
2
3
4
5
6
7
8POST请求 http://localhost:9200/books/_update/1
文档通过请求参数传递,数据格式json
{
"doc":{ #部分更新并不是对原始文档进行更新,而是对原始文档对象中的doc属性中的指定属性更新
"name":"springboot" #仅更新提供的属性值,未提供的属性值不参与更新操作
}
}
整合
Low Level Client
导入starter坐标,基础配置
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>1
2
3
4spring:
elasticsearch:
rest:
uris: http://localhost:9200使用专用客户端接口ElasticsearchRestTemplate操作
1
2
3
4
5
class Springboot18EsApplicationTests {
private ElasticsearchRestTemplate template;
}High Level Client:
导入坐标
1
2
3
4<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>设置连接的ES服务器,并获取客户端对象
1
2
3
4
5
6
7
8
9
10
11
12
class Springboot18EsApplicationTests {
private RestHighLevelClient client;
void testCreateClient() throws IOException {
HttpHost host = HttpHost.create("http://localhost:9200");
RestClientBuilder builder = RestClient.builder(host);
client = new RestHighLevelClient(builder);
client.close();
}
}操作,例如创建索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Springboot18EsApplicationTests {
private RestHighLevelClient client;
void testCreateIndex() throws IOException {
HttpHost host = HttpHost.create("http://localhost:9200");
RestClientBuilder builder = RestClient.builder(host);
client = new RestHighLevelClient(builder);
CreateIndexRequest request = new CreateIndexRequest("books");
client.indices().create(request, RequestOptions.DEFAULT);
client.close();
}
}一些其他的测试案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
class Springboot18EsApplicationTests {
//在测试类中每个操作运行前运行的方法
void setUp() {
HttpHost host = HttpHost.create("http://localhost:9200");
RestClientBuilder builder = RestClient.builder(host);
client = new RestHighLevelClient(builder);
}
//在测试类中每个操作运行后运行的方法
void tearDown() throws IOException {
client.close();
}
private RestHighLevelClient client;
void testCreateIndex() throws IOException {
CreateIndexRequest request = new CreateIndexRequest("books");
client.indices().create(request, RequestOptions.DEFAULT);
}
//添加文档
void testCreateDoc() throws IOException {
Book book = bookDao.selectById(1);
IndexRequest request = new IndexRequest("books").id(book.getId().toString());
String json = JSON.toJSONString(book);
request.source(json,XContentType.JSON);
client.index(request,RequestOptions.DEFAULT);
}
//批量添加文档
void testCreateDocAll() throws IOException {
List<Book> bookList = bookDao.selectList(null);
BulkRequest bulk = new BulkRequest();
// 一个保存request对象的容器,将所有的请求都初始化好后,添加到BulkRequest对象中,再使用BulkRequest对象的bulk方法,一次性执行完毕
for (Book book : bookList) {
IndexRequest request = new IndexRequest("books").id(book.getId().toString());
String json = JSON.toJSONString(book);
request.source(json,XContentType.JSON);
bulk.add(request);
}
client.bulk(bulk,RequestOptions.DEFAULT);
}
//按id查询
void testGet() throws IOException {
GetRequest request = new GetRequest("books","1");
GetResponse response = client.get(request, RequestOptions.DEFAULT);
String json = response.getSourceAsString();
System.out.println(json);
}
// 创建索引(IK分词器) 通过请求参数的形式进行设置
void testCreateIndexByIK() throws IOException {
CreateIndexRequest request = new CreateIndexRequest("books");
String json = "{\n" +
" \"mappings\":{\n" +
" \"properties\":{\n" +
" \"id\":{\n" +
" \"type\":\"keyword\"\n" +
" },\n" +
" \"name\":{\n" +
" \"type\":\"text\",\n" +
" \"analyzer\":\"ik_max_word\",\n" +
" \"copy_to\":\"all\"\n" +
" },\n" +
" \"type\":{\n" +
" \"type\":\"keyword\"\n" +
" },\n" +
" \"description\":{\n" +
" \"type\":\"text\",\n" +
" \"analyzer\":\"ik_max_word\",\n" +
" \"copy_to\":\"all\"\n" +
" },\n" +
" \"all\":{\n" +
" \"type\":\"text\",\n" +
" \"analyzer\":\"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
//设置请求中的参数
request.source(json, XContentType.JSON);
client.indices().create(request, RequestOptions.DEFAULT);
}
//按条件查询
//查询时调用SearchRequest对象的termQuery方法,需要给出查询属性名
void testSearch() throws IOException {
SearchRequest request = new SearchRequest("books");
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.termQuery("all","spring"));
request.source(builder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
String source = hit.getSourceAsString();
//System.out.println(source);
Book book = JSON.parseObject(source, Book.class);
System.out.println(book);
}
}
}
缓存
内置缓存方案
导入springboot提供的缓存技术的starter,在引导类上用注解@EnableCaching配置springboot程序中可以使用缓存
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>1
2
3
4
5
6
7
8
//开启缓存功能
public class Springboot19CacheApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot19CacheApplication.class, args);
}
}设置操作的数据是否使用缓存
1
2
3
4
5
6
7
8
9
10
public class BookServiceImpl implements BookService {
private BookDao bookDao;
// 默认是使用方法参数的值,可以使用 spEL 表达式,或者“#参数名”或者“#p参数index”
public Book getById(Integer id) {
return bookDao.selectById(id);
}
}- 在Service方法用@Cacheable声明当前方法的返回值放入缓存中,要指定缓存的存储位置,以及缓存中保存当前方法返回值对应的名称——上例中value描述缓存的存储位置,可以理解为是一个存储空间名,key描述缓存中保存数据的名称,使用#id读取形参中的id值作为缓存名称
- 使用@Cacheable注解后,执行当前操作,如果发现对应名称在缓存中没有数据,就正常读取数据,然后放入缓存;如果对应名称在缓存中有数据,就终止当前业务方法执行,直接返回缓存中的数据
@CachePut(key = "#p0")
:指定key,将更新的结果同步到缓存中
手机验证码案例
使用缓存保存手机验证码——输入手机号获取验证码,组织文档以短信形式发送给用户(页面模拟),输入手机号和验证码验证结果
根据用户提供的手机号生成一个验证码放入缓存,使用传入的手机号和验证码进行匹配,并返回最终匹配结果
过程:
导入starter,启用缓存
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>1
2
3
4
5
6
7
8
//开启缓存功能
public class Springboot19CacheApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot19CacheApplication.class, args);
}
}验证码对应的实体类,封装手机号与验证码两个属性
1
2
3
4
5
public class SMSCode {
private String tele;
private String code;
}定义验证码功能的业务层接口与实现类。@Cacheable注解是缓存中没有值则放入值,缓存中有值则取值。此处的功能仅仅是生成验证码并放入缓存,应该使用仅具有向缓存中保存数据的功能,使用@CachePut注解即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public interface SMSCodeService {
public String sendCodeToSMS(String tele);
public boolean checkCode(SMSCode smsCode);
}
public class SMSCodeServiceImpl implements SMSCodeService {
private CodeUtils codeUtils;
public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
return code;
}
public boolean checkCode(SMSCode smsCode) {
//取出内存中的验证码与传递过来的验证码比对,如果相同,返回true
String code = smsCode.getCode();
String cacheCode = codeUtils.get(smsCode.getTele());
return code.equals(cacheCode);
}
}校验验证码的功能放入工具类中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CodeUtils {
private String [] patch = {"000000","00000","0000","000","00","0",""};
public String generator(String tele){
int hash = tele.hashCode();
int encryption = 20206666;
long result = hash ^ encryption;
long nowTime = System.currentTimeMillis();
result = result ^ nowTime;
long code = result % 1000000;
code = code < 0 ? -code : code;
String codeStr = code + "";
int len = codeStr.length();
return patch[len] + codeStr;
}
public String get(String tele){
return null;
}
}验证码功能的web层接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SMSCodeController {
private SMSCodeService smsCodeService;
public String getCode(String tele){
String code = smsCodeService.sendCodeToSMS(tele);
return code;
}
public boolean checkCode(SMSCode smsCode){
return smsCodeService.checkCode(smsCode);
}
}
Redis
加坐标,配置缓存实现类型为redis,配置redis——不是对原始的redis进行配置,而是配置redis作为缓存使用,属于spring.cache.redis
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>1
2
3
4
5
6spring:
redis:
host: localhost
port: 6379
cache:
type: redis1
2
3
4
5
6
7
8
9
10
11spring:
redis:
host: localhost
port: 6379
cache:
type: redis
redis:
use-key-prefix: false
key-prefix: sms_
cache-null-values: false
time-to-live: 10s
jetcache
目前我们使用的缓存都是要么A要么B,能不能AB一起用呢?这一节就解决这个问题。springboot针对缓存的整合仅仅停留在用缓存上面,如果缓存自身不支持同时支持AB一起用,springboot也没办法,所以要想解决AB缓存一起用的问题,就必须找一款缓存能够支持AB两种缓存一起用,有这种缓存吗?还真有,阿里出品,jetcache。
jetcache是一个缓存框架,将别的缓存放到jetcache中管理,支持多个缓存一起用——本地缓存支持两种,远程缓存支持两种
- 本地缓存(Local)
- LinkedHashMap
- Caffeine
- 远程缓存(Remote)
- Redis
- Tair
- 本地缓存(Local)
LinkedHashMap+Redis实现本地与远程缓存方案同时使用:
纯远程方案
导入jetcache坐标,远程方案配置
1
2
3
4
5<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-starter-redis</artifactId>
<version>2.6.2</version>
</dependency>1
2
3
4
5
6
7
8jetcache:
remote:
default:
type: redis
host: localhost
port: 6379
poolConfig: # poolConfig是必配项
maxTotal: 50启用缓存,引导类上方标注@EnableCreateCacheAnnotations使得用注解的形式创建缓存
1
2
3
4
5
6
7
8
//jetcache启用缓存的主开关
public class Springboot20JetCacheApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot20JetCacheApplication.class, args);
}
}创建缓存对象Cache,注解@CreateCache标记当前缓存的信息,用Cache对象的API操作缓存——put写缓存,get读缓存(可以为某个缓存对象设置过期时间)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SMSCodeServiceImpl implements SMSCodeService {
private CodeUtils codeUtils;
private Cache<String ,String> jetCache;
public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
jetCache.put(tele,code);
return code;
}
public boolean checkCode(SMSCode smsCode) {
String code = jetCache.get(smsCode.getTele());
return smsCode.getCode().equals(code);
}
}配置中的default是个名字,可以随便写,也可以随便加。如果想使用名称是sms的缓存,需要创建缓存时指定参数area,声明使用对应缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14jetcache:
remote:
default:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 50
sms:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 501
2
3
4
5
6
7
8
9
public class SMSCodeServiceImpl implements SMSCodeService {
private CodeUtils codeUtils;
private Cache<String ,String> jetCache;
...
}
纯本地方案:配置中换成local就是本地
导入starter,配置本地缓存
1
2
3
4
5<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-starter-redis</artifactId>
<version>2.6.2</version>
</dependency>1
2
3
4
5jetcache:
local:
default:
type: linkedhashmap
keyConvertor: fastjson # 指定key的类型转换器启用缓存,缓存对象Cache标注当前使用本地缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//jetcache启用缓存的主开关
public class Springboot20JetCacheApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot20JetCacheApplication.class, args);
}
}
public class SMSCodeServiceImpl implements SMSCodeService {
private Cache<String ,String> jetCache;
public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
jetCache.put(tele,code);
return code;
}
public boolean checkCode(SMSCode smsCode) {
String code = jetCache.get(smsCode.getTele());
return smsCode.getCode().equals(code);
}
}
本地+远程方案:两种配置合并到一起
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18jetcache:
local:
default:
type: linkedhashmap
keyConvertor: fastjson
remote:
default:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 50
sms:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 50配置cacheType为BOTH,则本地缓存与远程缓存同时使用(cacheType如果不进行配置,默认值是REMOTE)
1
2
3
4
5
public class SMSCodeServiceImpl implements SMSCodeService {
private Cache<String ,String> jetCache;
}
远程方案的数据同步:远程方案中redis保存的数据可以被多个客户端共享,存在数据同步问题。jetcache提供了3个注解,分别在更新、删除操作时同步缓存数据,和读取缓存时定时刷新数据
更新缓存
1
2
3
4
public boolean update(Book book) {
return bookDao.updateById(book) > 0;
}删除缓存
1
2
3
4
public boolean delete(Integer id) {
return bookDao.deleteById(id) > 0;
}定时刷新缓存
1
2
3
4
5
public Book getById(Integer id) {
return bookDao.selectById(id);
}
数据报表:帮助开发者快速查看缓存命中信息,只需要添加一个配置即可。设置后,每1分钟在控制台输出缓存数据命中信息
1
2jetcache:
statIntervalMinutes: 11
2
3
4
5[DefaultExecutor] c.alicp.jetcache.support.StatInfoLogger : jetcache stat from 2022-02-28 09:32:15,892 to 2022-02-28 09:33:00,003
cache | qps| rate| get| hit| fail| expire| avgLoadTime| maxLoadTime
---------+-------+-------+------+-------+-------+---------+--------------+--------------
book_ | 0.66| 75.86%| 29| 22| 0| 0| 28.0| 188
---------+-------+-------+------+-------+-------+---------+--------------+--------------
任务
- 定时任务,例如年度报表、系统脏数据的处理等
Quartz
Quartz的几个概念
- 工作(Job):用于定义具体执行的工作
- 工作明细(JobDetail):用于描述定时工作相关的信息
- 触发器(Trigger):描述工作明细与调度器的对应关系(工作和调度是独立定义的,二者通过触发器配合)
- 调度器(Scheduler):用于描述触发工作的执行规则,通常使用cron表达式定义规则(工作执行的时间)
整合:
导入starter
定义任务bean,按照Quartz的开发规范制作,继承QuartzJobBean
创建Quartz配置类,定义工作明细(JobDetail)与触发器的(Trigger)bean
工作明细中要设置对应的具体工作,使用newJob()传入对应的工作任务类型
触发器需要绑定任务,使用forJob()操作传入绑定的工作明细对象。可以为工作明细设置名称然后使用名称绑定,也可以直接调用对应方法绑定。触发器中最核心的规则是执行时间,用调度器定义执行时间,执行时间描述方式使用的是cron表达式
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>1
2
3
4
5
6public class MyQuartz extends QuartzJobBean {
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
System.out.println("quartz task run...");
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class QuartzConfig {
public JobDetail printJobDetail(){
//绑定具体的工作
return JobBuilder.newJob(MyQuartz.class).storeDurably().build();
}
public Trigger printJobTrigger(){
ScheduleBuilder schedBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
//绑定对应的工作明细
return TriggerBuilder.newTrigger().forJob(printJobDetail()).withSchedule(schedBuilder).build();
}
}
Task
定时执行什么任务直接告诉对应的bean什么时间执行
整合:
开启定时任务,引导类用注解@EnableScheduling开启定时任务功能
定义Bean,在对应要定时执行的操作上方,使用注解@Scheduled定义执行的时间,执行时间的描述方式还是cron表达式
通过yml文件配置定时任务
1
2
3
4
5
6
7
8
//开启定时任务功能
public class Springboot22TaskApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot22TaskApplication.class, args);
}
}1
2
3
4
5
6
7
public class MyBean {
public void print(){
System.out.println(Thread.currentThread().getName()+" :spring task run...");
}
}1
2
3
4
5
6
7
8
9spring:
task:
scheduling:
pool:
size: 1 # 任务调度线程池大小 默认 1
thread-name-prefix: ssm_ # 调度线程名称前缀 默认 scheduling-
shutdown:
await-termination: false # 线程池关闭时等待所有任务完成
await-termination-period: 10s # 调度线程关闭前最大等待时间,确保最后一定关闭
邮件
- SMTP(Simple Mail Transfer Protocol):简单邮件传输协议,发送电子邮件的传输协议
- POP3(Post Office Protocol - Version 3):接收电子邮件的标准协议
- IMAP(Internet Mail Access Protocol):互联网消息协议,POP3的替代协议
简单邮件
导入javamail的starter,配置邮箱的登录信息
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>1
2
3
4
5spring:
mail:
host: smtp.126.com
username: test@126.com
password: testjava程序仅用于发送邮件,邮件的功能是邮件供应商提供的。host配置提供邮件服务的主机协议
password不是邮箱账号的登录密码,是邮件供应商提供的一个加密后的密码。每个邮件供应商提供的获取该密码的方式都不一样
JavaMailSender接口发送邮件:将发送邮件的必要信息(发件人、收件人、标题、正文)封装到SimpleMailMessage对象中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SendMailServiceImpl implements SendMailService {
private JavaMailSender javaMailSender;
//发送人
private String from = "test@qq.com";
//接收人
private String to = "test@126.com";
//标题
private String subject = "测试邮件";
//正文
private String context = "测试邮件正文内容";
public void sendMail() {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from+"(thomas)");
message.setTo(to);
message.setSubject(subject);
message.setText(context);
javaMailSender.send(message);
}
}
多组件邮件(附件、复杂正文)
使用MimeMessage发送特殊邮件
发送网页正文邮件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class SendMailServiceImpl2 implements SendMailService {
private JavaMailSender javaMailSender;
//发送人
private String from = "test@qq.com";
//接收人
private String to = "test@126.com";
//标题
private String subject = "测试邮件";
//正文
private String context = "<img src='ABC.JPG'/><a href='https://www.itcast.cn'>点开有惊喜</a>";
public void sendMail() {
try {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setFrom(to+"(thomas)");
helper.setTo(from);
helper.setSubject(subject);
helper.setText(context,true); // 设置正文支持html解析
javaMailSender.send(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}带有附件的邮件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class SendMailServiceImpl2 implements SendMailService {
private JavaMailSender javaMailSender;
//发送人
private String from = "test@qq.com";
//接收人
private String to = "test@126.com";
//标题
private String subject = "测试邮件";
//正文
private String context = "测试邮件正文";
public void sendMail() {
try {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message,true); //此处设置支持附件
helper.setFrom(to+"(thomas)");
helper.setTo(from);
helper.setSubject(subject);
helper.setText(context);
//添加附件
File f1 = new File("springboot_23_mail-0.0.1-SNAPSHOT.jar");
File f2 = new File("resources\\logo.png");
helper.addAttachment(f1.getName(),f1);
helper.addAttachment("me.png",f2);
javaMailSender.send(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
消息中间件
消息的概念
信息通常被定义为一组数据,消息除了具有数据外,还有消息的来源与接收者的概念
发送的消息可以只包含数据,但可以通过控制不同的人接收此消息来确认要做的事情——通过接收消息的主体不同,进而执行不同的操作,而不会在消息内部定义数据的操作行为
根据消息的生产者与消费者的工作模式,可以将消息分成:同步消息与异步消息
- 同步消息:生产者发送完消息,等待消费者处理,消费者处理完将结果告知生产者,然后生产者继续向下执行业务
- 异步消息:生产者发送完消息,继续向下执行其他动作
Java处理消息的标准规范
- 消息处理技术共三大类
- JMS:JMS(Java Message Service),是一个规范,作用等同于JDBC规范,提供与消息服务相关的API接口
- 规定消息有两种模型:点对点模型和发布订阅模型
- 将消息种类分成6个:TextMessage、MapMessage、BytesMessage、StreamMessage、ObjectMessage、Message (只有消息头和属性)
- 主张不同种类的消息,消费方式不同
- AMQP:解决消息传递时使用的消息种类的问题,仅仅是一种协议,规范了数据传输的格式
- 实现了AMQP协议的消息中间件技术:RabbitMQ、StormMQ、RocketMQ
- AMQP消息种类:byte[]
- 生产者,消费者可以使用不同的语言来实现
- MQTT:消息队列遥测传输,专为小设备设计,是物联网(IOT)生态系统的重要组成
- KafKa:高吞吐量的分布式发布订阅消息系统,提供实时消息功能。不是作为消息中间件为主要功能的产品,但拥有发布订阅的工作模式,可以充当消息中间件来使用
- JMS:JMS(Java Message Service),是一个规范,作用等同于JDBC规范,提供与消息服务相关的API接口
- 各种消息中间件必须先安装再使用
购物订单发送手机短信案例
一个购物过程生成订单时为用户发送短信的案例环境,模拟使用消息中间件实现发送手机短信的过程
需求:
- 执行下单业务时(模拟此过程),调用消息服务,将要发送短信的订单id传递给消息中间件
- 消息处理服务接收到要发送的订单id后输出订单id(模拟发短信)
- 不涉及数据读写,仅开发业务层与表现层
订单业务:
业务层接口与实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public interface OrderService {
void order(String id);
}
public class OrderServiceImpl implements OrderService {
private MessageService messageService;
// 模拟传入订单id,执行下订单业务,参数为虚拟设定,实际应为订单对应的实体类
public void order(String id) {
//一系列操作,包含各种服务调用,处理各种业务
System.out.println("订单处理开始");
//短信消息处理
messageService.sendMessage(id);
System.out.println("订单处理结束");
}
}表现层对外开放接口,传入订单id即可
1
2
3
4
5
6
7
8
9
10
11
12
public class OrderController {
private OrderService orderService;
public void order( String id){
orderService.order(id);
}
}
短信处理业务
业务层接口与实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public interface MessageService {
void sendMessage(String id); // 发送要处理的订单id到消息中间件
String doMessage(); // 处理消息(实际消息的处理过程不应该是手动执行,应该是自动执行)
}
public class MessageServiceImpl implements MessageService {
private ArrayList<String> msgList = new ArrayList<String>(); // 目前用集合模拟消息队列
public void sendMessage(String id) {
System.out.println("待发送短信的订单已纳入处理队列,id:"+id);
msgList.add(id);
}
public String doMessage() {
String id = msgList.remove(0);
System.out.println("已完成短信发送业务,id:"+id);
return id;
}
}表现层
1
2
3
4
5
6
7
8
9
10
11
12
13
public class MessageController {
private MessageService messageService;
public String doMessage(){
String id = messageService.doMessage();
return id;
}
}
RabbitMQ
遵从AMQP协议,底层实现语言为Erlang,安装RabbitMQ需要先安装Erlang
服务器启动:运行sbin目录下的rabbitmq-service.bat命令即可
1
2
3rabbitmq-service.bat start # 启动服务
rabbitmq-service.bat stop # 停止服务
rabbitmqctl status # 查看服务状态web控制台服务(需要先启用相应插件)
1
2rabbitmq-plugins.bat list # 查看当前所有插件的运行状态
rabbitmq-plugins.bat enable rabbitmq_management # 启动rabbitmq_management插件- web管理服务默认端口15672
- 初始化用户名和密码相同,均为:guest
整合(direct模型)
导入springboot整合amqp的starter,amqp协议默认实现为rabbitmq
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>配置RabbitMQ的服务器地址
1
2
3
4spring:
rabbitmq:
host: localhost
port: 5672初始化直连模式:RabbitMQ不同模型要使用不同的交换机,需要先初始化RabbitMQ相关的对象,例如队列,交换机等。队列Queue与直连交换机DirectExchange创建后,还需要绑定他们之间的关系,从而通过交换机操作对应队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RabbitConfigDirect {
public Queue directQueue(){
return new Queue("direct_queue");
}
public Queue directQueue2(){
return new Queue("direct_queue2");
}
public DirectExchange directExchange(){
return new DirectExchange("directExchange");
}
public Binding bindingDirect(){
return BindingBuilder.bind(directQueue()).to(directExchange()).with("direct");
}
public Binding bindingDirect2(){
return BindingBuilder.bind(directQueue2()).to(directExchange()).with("direct2");
}
}AmqpTemplate操作RabbitMQ
1
2
3
4
5
6
7
8
9
10
11
public class MessageServiceRabbitmqDirectImpl implements MessageService {
private AmqpTemplate amqpTemplate;
public void sendMessage(String id) {
System.out.println("待发送短信的订单已纳入处理队列(rabbitmq direct),id:"+id);
amqpTemplate.convertAndSend("directExchange","direct",id);
}
}消息监听器在服务器启动后,监听指定位置,当消息出现后,立即消费消息——注解@RabbitListener定义当前方法监听RabbitMQ中指定名称的消息队列
1
2
3
4
5
6
7
public class MessageListener {
public void receive(String id){
System.out.println("已完成短信发送业务(rabbitmq direct),id:"+id);
}
}
整合(topic模型)
starter、配置同上
初始化主题模式设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RabbitConfigTopic {
public Queue topicQueue(){
return new Queue("topic_queue");
}
public Queue topicQueue2(){
return new Queue("topic_queue2");
}
public TopicExchange topicExchange(){
return new TopicExchange("topicExchange");
}
public Binding bindingTopic(){
return BindingBuilder.bind(topicQueue()).to(topicExchange()).with("topic.*.id");
}
public Binding bindingTopic2(){
return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("topic.orders.*");
}
}主题模式支持routingKey匹配模式,*表示匹配一个单词,#表示匹配任意内容,这样通过主题交换机将消息分发到不同的队列中
匹配键 topic.*.* topic.# topic.order.id true true order.topic.id false false topic.sm.order.id false true topic.sm.id false true topic.id.order true true topic.id false true topic.order false true
AmqpTemplate操作RabbitMQ。发送消息后,当前提供的routingKey与绑定交换机时设定的routingKey匹配,规则匹配成功消息才会进入到对应的队列
1
2
3
4
5
6
7
8
9
10
11
public class MessageServiceRabbitmqTopicImpl implements MessageService {
private AmqpTemplate amqpTemplate;
public void sendMessage(String id) {
System.out.println("待发送短信的订单已纳入处理队列(rabbitmq topic),id:"+id);
amqpTemplate.convertAndSend("topicExchange","topic.orders.id",id);
}
}消息监听器在服务器启动后,监听指定队列
1
2
3
4
5
6
7
8
9
10
11
public class MessageListener {
public void receive(String id){
System.out.println("已完成短信发送业务(rabbitmq topic 1),id:"+id);
}
public void receive2(String id){
System.out.println("已完成短信发送业务(rabbitmq topic 22222222),id:"+id);
}
}
RocketMQ
- RocketMQ工作模式
- 处理业务的服务器称为broker,生产者与消费者不直接与broker联系,而是通过命名服务器进行通信
- broker启动后通知命名服务器自己上线,命名服务器保存所有的broker信息
- 生产者与消费者需要连接broker时,通过命名服务器找到对应的处理业务的broker——命名服务器是信息中心,broker启动前必须保障命名服务器先启动
启动服务器
1
2mqnamesrv # 启动命名服务器
mqbroker # 启动broker- 运行bin目录下的mqnamesrv命令即可启动命名服务器,默认对外服务端口9876。
- 运行bin目录下的mqbroker命令即可启动broker服务器,如果环境变量中没有设置NAMESRV_ADDR则需要在运行mqbroker指令前通过set指令设置NAMESRV_ADDR的值,并且每次开启均需要设置此项。
整合(异步消息)
导入starter,此坐标不由springboot维护版本;配置RocketMQ的服务器地址,设置默认的生产者消费者所属组group。
1
2
3
4
5<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>1
2
3
4rocketmq:
name-server: localhost:9876
producer:
group: group_rocketmqRocketMQTemplate操作RocketMQ,使用asyncSend方法发送异步消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MessageServiceRocketmqImpl implements MessageService {
private RocketMQTemplate rocketMQTemplate;
public void sendMessage(String id) {
System.out.println("待发送短信的订单已纳入处理队列(rocketmq),id:"+id);
SendCallback callback = new SendCallback() {
public void onSuccess(SendResult sendResult) {
System.out.println("消息发送成功");
}
public void onException(Throwable e) {
System.out.println("消息发送失败!!!!!");
}
};
rocketMQTemplate.asyncSend("order_id",id,callback);
}
}消息监听器在服务器启动后,监听指定位置,当消息出现后,立即消费消息。监听器必须按照标准格式开发,实现RocketMQListener接口,泛型为消息类型。注解@RocketMQMessageListener定义当前类监听RabbitMQ中指定组、指定名称的消息队列
1
2
3
4
5
6
7
8
public class MessageListener implements RocketMQListener<String> {
public void onMessage(String id) {
System.out.println("已完成短信发送业务(rocketmq),id:"+id);
}
}
Kafka
kafka服务器相当于RocketMQ中的broker,因此还需要一个类似于命名服务器的服务——zookeeper
1
2zookeeper-server-start.bat ..\..\config\zookeeper.properties # 启动zookeeper 默认对外服务端口2181
kafka-server-start.bat ..\..\config\server.properties # 启动kafka 默认对外服务端口9092整合
导入starter,配置服务器地址,设置默认的生产者消费者所属组id
1
2
3
4<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>1
2
3
4
5spring:
kafka:
bootstrap-servers: localhost:9092
consumer:
group-id: order用KafkaTemplate操作Kafka。send方法发送消息,需要传入topic名称
1
2
3
4
5
6
7
8
9
10
11
public class MessageServiceKafkaImpl implements MessageService {
private KafkaTemplate<String,String> kafkaTemplate;
public void sendMessage(String id) {
System.out.println("待发送短信的订单已纳入处理队列(kafka),id:"+id);
kafkaTemplate.send("itheima2022",id);
}
}消息监听器在服务器启动后,监听指定位置,当消息出现后,立即消费消息。注解@KafkaListener定义当前方法监听Kafka中指定topic的消息,接收到的消息封装在对象ConsumerRecord中,获取数据从ConsumerRecord对象中获取即可
1
2
3
4
5
6
7
public class MessageListener {
public void onMessage(ConsumerRecord<String,String> record){
System.out.println("已完成短信发送业务(kafka),id:"+record.value());
}
}