第一部分 SpringBoot应用回顾
1.1 约定优于配置
概念:约定优于配置(Convention over Configuration),又称按约定编程,是一种软件设计规范。本质上是对系统、类库或框架中一些东西假定一个大众化合理的默认值(缺省值)。
例如在模型中存在一个名为User的类,那么对应到数据库会存在一个名为user的表,此时无需做额外的 配置,只有在偏离这个约定时才需要做相关的配置(例如你想将表名命名为t_user等非user时才需要写 关于这个名字的配置)。
好处 : 大大减少了配置项
1.2 概念
官方的网站: https://spring.io/
1.2.1 SpringBoot介绍
通过Spring Boot,可以轻松地创建独立的,基于生产级别的基于Spring的应用程序,并且可以 “运行”它们。
SpringBoot是由Pivotal团队在2013年开始研发、2014年4月发布第一个版本的全新开源的轻量级框 架。它基于Spring4.0设计,不仅继承了Spring框架原有的优秀特性,而且还通过简化配置来进一步简 化了Spring应用的整个搭建和开发过程。另外SpringBoot通过集成大量的框架使得依赖包的版本冲突, 以及引用的不稳定性等问题得到了很好的解决
1.2.2 SpringBoot主要特性
-
SpringBoot Starter: 将常用的依赖分组进行了整合,将其合并到一个依赖中,这样就可以一次性添加到项目的Maven或Gradle构建中;
-
使编码变得简单,SpringBoot采用 JavaConfig的方式对Spring进行配置,并且提供了大量的注解,极大的提高了工作效率。
-
自动配置:SpringBoot的自动配置特性利用了Spring对条件化配置的支持,合理地推测应用所需的bean并自动化配置他们;
-
使部署变得简单,SpringBoot内置了三种Servlet容器,Tomcat,Jetty, undertow.我们只需要一个Java的运行环境就可以跑SpringBoot的项目了,SpringBoot的项目可以打成一个jar包。
1.3 案例实现
使用Spring Initializr方式构建Spring Boot项目
创建好的Spring Boot项目结构如图
创建Controller,com.lagou包下创建名称为controller的包,在该包下创建一个请求处理控制类HelloController,并编写一个请求处理方法 (注意:将项目启动类SpringBootDemoApplication移动到com.lagou包下,因为是从这个启动类的当前包下开始扫描的)
@RestController // 该注解为组合注解,等同于Spring中@Controller+@ResponseBody注解
public class DemoController {
@RequestMapping("/demo")
public String demo(){
return "hello springBoot";
}
}
解决中文乱码:
解决方法一: 注解@RequestMapping(produces = “application/json; charset=utf-8”)
解决方法二: 配置 spring.http.encoding.force-response=true
疑问:
- starter是什么?我们如何去使用这些starter?
- 为什么包扫描只会扫描核心启动类所在的包及其子包
- 在springBoot启动的过程中,是如何完成自动装配的?
- 内嵌Tomcat是如何被创建及启动的?
- 使用了web场景对应的starter,springmvc是如何自动装配?
1.4 热部署
1.4.1 使用
在开发项目过程中,当修改了某些代码后需要本地验证时,需要重启本地服务进行验证,启动这个项目,如果项目庞大的话还是需要较长时间的,spring开发团队为我们带来了一个插件:spring-boot- devtools,很好的解决了本地验证缓慢的问题。
spring-boot-devtools热部署依赖启动器
<!-- 引入热部署依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
由于使用的是IDEA开发工具,添加热部署依赖后可能没有任何效果,接下来还需要针对IDEA开发 工具进行热部署相关的功能设置
第一步:选择IDEA工具界面的【File】->【Settings】选项,打开Compiler面板设置页面
第二步 在项目任意页面中使用组合快捷键“Ctrl+Shift+Alt+/”打开Maintenance选项框,选中并打开 Registry 页面
(如果是MAC 则是快捷键 option + command + Shift + / )
1.4.2 原理分析
就是在编辑器上启动项目,然后改动相关的代码,编辑器自动触发编译替换掉历史的.class文件后,项目检测到有文件变更后会重启srpring-boot项目。我们引入了插件后,插件会监控我们classpath的资源变化,当classpath有变化后,会触发重启。
1.4.3 排除资源
某些资源在更改后不一定需要触发重新启动。例如,Thymeleaf模板可以就地编辑。默认情况下,改变 资源 /META-INF/maven , /META-INF/resources , /resources , /static , /public ,或 /templates 不触发重新启动,但确会触发现场重装。如果要自定义这些排除项,则可以使用该spring.devtools.restart.exclude 属性。例如,仅排除 /static , /public 您将设置以下属性
spring.devtools.restart.exclude=static/**, public/**
1.5 全局配置文件
1.5.1 概述及优先级
SpringBoot会从这四个位置全部加载配置文件,如果高优先级中配置文件属性与低优先级配置文件不冲突的属性,则会共同存在:互补配置。
备注:
这里说的配置文件,都还是项目里面。最终都会被打进jar包里面的,需要注意。
1、如果同一个目录下,有application.yml也有application.properties,默认先读取application.properties。
2、如果同一个配置属性,在多个配置文件都配置了,默认使用第1个读取到的,后面读取的不覆盖前面读取到的。
3、创建SpringBoot项目时,一般的配置文件放置在“项目的resources目录下”
如果配置文件名字不叫application.properties或者application.yml,可以通过以下参数来指定 配置文件的名字,myapplication.yml是配置文件名
$ java -jar myproject.jar --spring.config.name=myapplication.yml
也可以指定其他位置的配置文件来生效
$ java -jar myproject.jar --spring.config.name=/Users/config/myapplication.yml
Spring Boot 2.4 改进了处理 application.properties 和 application.yml 配置文件的方式,
如果是2.4.0之前版本,优先级properties>yaml
但是如果是2.4.0的版本,优先级yaml>properties
如果想继续使用 Spring Boot 2.3 的配置逻辑,也可以通过在 application.properties 或者 application.yml 配置文件中添加以下参数:
spring.config.use-legacy-processing = true
1.5.2 application.properties配置文件
我们可以在application.properties文件中定义Spring Boot项目的相关属性,当然,这些相关属性可以是系统属性、环境变量、命令参数等信息,也可以是自定义配置文件名称和位置
server.port=8081
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.config.name=application
通过一个案例对Spring Boot项目中application.properties配置文件的具体使用进行讲解
@Data
public class Pet {
private String type;
private String name;
}
@Component //将当前注入属性值的Person类对象作为Bean组件放到Spring容器中,只有这样才能被@ConfigurationProperties注解进行赋值
@ConfigurationProperties(prefix = "person") //将配置文件中以person开头的属性值通过 setXX()方法注入到实体类对应属性中
@Data
public class Person {
private int id;
private String name;
private List hobby;
private String[] family;
private Map map;
private Pet pet;
}
编写application.properties配置文件时,由于要配置的Person对象属性是我们自定义的,Spring Boot无法自动识别,所以不会有任何书写提示。在实际开发中,为了出现代码提示的效果来方便配置, 在使用@ConfigurationProperties注解进行配置文件属性值注入时,可以在pom.xml文件中添加一个 Spring Boot提供的配置处理器依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
测试
@RunWith(SpringRunner.class) //测试启动器,并加载SpringBoot测试注解
@SpringBootTest // 标记为SpringBoot单元测试类,并加载项目的ApplicationContext上下文环境
class SpringbootDemoApplicationTests {
// 配置测试
@Autowired
private Person person;
@Test
void configurationTest() {
System.out.println(person);
}
}
1.5.3 application.yaml配置文件
YAML文件格式是Spring Boot支持的一种JSON超集文件格式,以数据为中心,比properties、xml等更适合做配置文件
- yml和xml相比,少了一些结构化的代码,使数据更直接,一目了然
- 相比properties文件更简洁
- YAML文件的扩展名可以使用.yml或者.yaml。
- application.yml文件使用 “key:(空格)value”格式配置属性,使用缩进控制层级关系。
针对不同数据类型的属性值,介绍一下YAML
(1)value值为普通数据类型(例如数字、字符串、布尔等)
server:
port: 8080
servlet:
context-path: /hello
(2) value值为数组和单列集合 两种书写方式:缩进式写法和行内式写法。
缩进式写法还有两种表示形式,示例代码如下
person:
hobby:
- play # 注意 “-” 和 “play”值 之间的空格
- read
- sleep
或者
person:
hobby:
play,
read,
sleep # 最后一个属性值后不要加逗号
行内式写法 示例代码如下
person:
hobby: [play,read,sleep]
(3)value值为Map集合和对象
缩进式写法 示例代码如下
person:
map:
k1: v1
k2: v2
行内式写法 示例代码如下
person:
map: {k1: v1,k2: v2}
通过配置application.yaml配置文件对Person对象进 行赋值,具体使用如下
#对实体类对象Person进行属性配置
person:
id: 1
name: lucy
hobby: [吃饭,睡觉,打豆豆]
family: [father,mother]
map: {k1: v1,k2: v2}
pet: {type: dog,name: 旺财}
1.6 属性注入
使用Spring Boot全局配置文件设置属性时:
如果配置属性是Spring Boot已有属性,例如服务端口server.port,那么Spring Boot内部会自动扫描并 读取这些配置文件中的属性值并覆盖默认属性。
如果配置的属性是用户自定义属性,例如刚刚自定义的Person实体类属性,还必须在程序中注入这些配 置属性方可生效。
1.6.1 属性注入常用注解
@Configuration:声明一个类作为配置类
@Bean:声明在方法上,将方法的返回值加入Bean容器
@Value:属性注入
@ConfigurationProperties(prefix = “jdbc”):批量属性注入
@PropertySource(“classpath:/jdbc.properties”)指定外部属性文件。在类上添加
1.6.2 @Value属性值注入
pom.xml
<dependency>
<groupId>com.github.drtrang</groupId>
<artifactId>druid-spring-boot2-starter</artifactId>
<version>1.1.10</version>
</dependency>
application.properties
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/springboot_h
jdbc.username=root
jdbc.password=123
创建JdbcConfiguration类: 使用spring中的value注解对每个属性进行注入,用bean注解将返回值添加到容器中
@Configuration
public class JdbcConfiguration {
@Value("${jdbc.url}")
String url;
@Value("${jdbc.driverClassName}")
String driverClassName;
@Value("${jdbc.username}")
String username;
@Value("${jdbc.password}")
String password;
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setDriverClassName(driverClassName);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}
1.6.3 @ConfigurationProperties批量注入
application.properties 添加信息
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/springboot_h
jdbc.username=root
jdbc.password=123
JdbcProperties.java
@ConfigurationProperties(prefix = "jdbc") //这里需要定义出在application文件中定义属性值得前缀信息
@Data
public class JdbcProperties {
private String url;
private String driverClassName;
private String username;
private String password;
}
注:添加@ConfigurationProperties注解后有警告:springboot 配置注释处理器未配置(编写配置文件此时无提示)
添加spring-boot-configuration-processor依赖,加完依赖后通过Ctrl+F9来使之生效
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
接着发现,仍然有红色警告
1.6.4 第三方配置
除了 @ConfigurationProperties 用于注释类之外,还可以在公共 @Bean 方法上使用它。当要将属性绑定到控件之外的第三方组件时,这样做特别有用。
引用的第三方组件我们不能修改,但是想要注入值,我们可以这样做
@Data
public class AnotherComponent {
private boolean enabled;
private InetAddress remoteAddress;
}
创建自己的类
@Configuration
public class MyService {
@ConfigurationProperties("another")
@Bean
public AnotherComponent anotherComponent(){
return new AnotherComponent();
}
}
application.properties 添加信息
another.enabled=true
another.remoteAddress=192.168.10.11
1.6.5 松散绑定
@Data
@Component
@ConfigurationProperties("acme.my-project.person")
public class OwnerProperties {
private String firstName;
}
acme:
my-project:
person:
first-name: 泰森
1.7 日志框架
在项目的开发中,日志是必不可少的一个记录事件的组件,不管是记录运行情况还是追踪线上问题,都 离不开对日志的分析,所以也会相应的在项目中实现和构建我们所需要的日志框架。
市面上常见的日志框架有很多,比如:JCL、SLF4J、Jboss-logging、jUL、log4j、log4j2、logback等 等,我们该如何选择呢?
通常情况下,日志是由一个抽象层+实现层的组合来搭建的。
1.7.1 SLF4J 的使用
在开发的时候不应该直接使用日志实现类,应该使用日志的抽象层。
SLF4J官方网站 https://www.slf4j.org/legacy.html
注意:由于每一个日志的实现框架都有自己的配置文件,所以在使用 SLF4j 之后,配置文件还是要使用实 现日志框架的配置文件。
1.7.2 统一日志框架的使用
遗留问题:A项目(slf4J + logback): Spring(commons logging)、Hibernate(jboss-logging)、 mybatis…
一般情况下,在项目中存在着各种不同的第三方 jar ,且它们的日志选择也可能不尽相同,显然这样是不利于我们使用的,那么如果我们想为项目设置统一的日志框架该怎么办呢?
从图中我们得到一种统一日志框架使用的方式,可以使用一种和要替换的日志框架类完全一样的 jar 进 行替换,这样不至于原来的第三方 jar 报错,而这个替换的 jar 其实使用了 SLF4J API. 这样项目中的日 志就都可以通过 SLF4J API 结合自己选择的框架进行日志输出。
统一日志框架使用步骤归纳如下:
- 排除系统中的其他日志框架。
- 使用中间包替换要替换的日志框架。
- 导入我们选择的 SLF4J 实现。
1.7.3 SpringBoot日志关系
(1) 排除其他日志框架
根据上面总结的要统一日志框架的使用,第一步要排除其他的日志框架,在SpringBoot的Maven依赖里可以清楚的看到SpringBoot排除了其他日志框架。
(2) 统一框架引入替换包
SpringBoot是使用了SLF4J+logback的日志框架组合,查看SpringBoot项目的Maven依赖关系可以看到SpringBoot的核心启动器 spring-boot-starter 引入了 spring-boot-starter-logging
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>2.4.0.RELEASE</version>
</dependency>
而 spring-boot-starter-logging 的Maven依赖主要引入了 logback-classic (包含了日志框架 Logback 的实现),log4j-to-slf4j (在 log4j 日志框架作者开发此框架的时候还没有想到使用日志抽象层进行开发,因此出现了 log4j 向 slf4j 转换的工具),jul-to-slf4j ( Java自带的日志框架转换为 slf4j).
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.13.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.7.30</version>
<scope>compile</scope>
</dependency>
</dependencies>
1.7.4 Spring Boot 的日志使用
日志级别从小到大为 trace < debug < info < warn < error 。 Spring Boot 默认日志级别 INFO.
Spring Boot 默认日志格式是
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
# %d{yyyy-MM-dd HH:mm:ss.SSS} 时间
# %thread 线程名称
# %-5level 日志级别从左显示5个字符宽度
# %logger{50} 类名
# %msg%n 日志信息加换行
1.7.5 自定义日志输出
可以直接在配置文件编写日志相关配置
# 日志配置
# 指定具体包的日志级别
logging.level.com.lagou=debug
# 控制台和日志文件输出格式
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
# 日志输出路径,默认文件spring.log
logging.file.path=spring.log
# logging.file.name=log.log
1.7.6 替换日志框架
因为 Log4j 日志框架已经年久失修,原作者都觉得写的不好,所以下面演示替换日志框架为 Log4j2 的 方式。根据 官网 我们 Log4j2 与 logging 需要二选一,因此修改 pom如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
第二部分 SpringBoot源码剖析
更新中。。。。。。
第三部分 SpringBoot数据访问
3.1 数据源自动配置源码剖析
3.1.1 数据源配置方式
1、pom依赖
<! -- 选择数据库驱动的库文件 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<! -- 配置spring-boot-starter-jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
2、配置数据库连接
application.properties中配置数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql:///springboot_h?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
# spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
3.1.2 连接池配置方式
SpringBoot提供了三种数据库连接池:
- HikariCP
- Commons DBCP2
- Tomcat JDBC Connection Pool
其中spring boot2.x版本默认使用HikariCP,maven中配置如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
如果不使用HikariCP,而改用Commons DBCP2
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<exclusion>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</exclusion>
</exclusions>
</dependency>
如果不使用HikariCP,而改用Tomcat JDBC Connection Pool
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<exclusion>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</exclusion>
</exclusions>
</dependency>
3.1.3 数据源自动配置
为什么说springboot默认使用的连接池类型是HikariCP,在哪指定的?
spring.factories中找到数据源的配置类
3.2 Druid连接池的配置
3.2.1 在pom.xml中引入druid数据源
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
3.2.2 在application.yml中引入druid的相关配置
spring:
datasource:
username: root
password: root
url: jdbc:mysql:///springboot_h?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver initialization-mode: always
# 使用druid数据源
type: com.alibaba.druid.pool.DruidDataSource
# 数据源其他配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
这是因为:如果单纯在yml文件中编写如上的配置,SpringBoot肯定是读取不到druid的相关配置的。因为它并不像我们原生的jdbc,系统默认就使用DataSourceProperties与其属性进行了绑定。所以我们应该编写一个类与其属性进行绑定
3.2.3 编写整合druid的配置类DruidConfig
public class DruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druid(){
return new DruidDataSource();
}
}
测试的时候,突然发现控制台报错了。经过查找发现是yml文件里的
filters: stat,wall,log4j
因为我们springBoot2.0以后使用的日志框架已经不再使用log4j了。此时应该引入相应的适配器。 我们可以在pom.xml文件上加入
<!--引入适配器-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
3.3 SpringBoot整合Mybatis
MyBatis 是一款优秀的持久层框架,Spring Boot官方虽然没有对MyBatis进行整合,但是MyBatis团队自行适配了对应的启动器,进一步简化了使用MyBatis进行数据的操作。
因为Spring Boot框架开发的便利性,所以实现Spring Boot与数据访问层框架(例如 MyBatis)的整合非常简单,主要是引入对应的依赖启动器,并进行数据库相关参数设置即可。
application.yml
spring:
datasource:
username: root
password: root
url: jdbc:mysql:///springboot_h?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.jdbc.Driver
# 使用druid数据源
type: com.alibaba.druid.pool.DruidDataSource
pom.xml
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
3.4 Mybatis自动配置源码分析
1、springboot项目最核心的就是自动加载配置,该功能则依赖的是一个注解 @SpringBootApplication中的@EnableAutoConfiguration
2、EnableAutoConfiguration主要是通过AutoConfigurationImportSelector类来加载。
以mybatis为例,*selector通过反射加载spring.factories中指定的java类,也就是加载 MybatisAutoConfiguration类(该类有Configuration注解,属于配置类)
更新中。。。。。。
3.5 SpringBoot + Mybatis实现动态数据源切换
3.5.1 动态数据源介绍
业务背景
电商订单项目分正向和逆向两个部分:其中正向数据库记录了订单的基本信息,包括订单基本信 息、订单商品信息、优惠卷信息、发票信息、账期信息、结算信息、订单备注信息、收货人信息 等;逆向数据库主要包含了商品的退货信息和维修信息。数据量超过500万行就要考虑分库分表和 读写分离,那么我们在正向操作和逆向操作的时候,就需要动态的切换到相应的数据库,进行相关 的操作。
解决思路
现在项目的结构设计基本上是基于MVC的,那么数据库的操作集中在dao层完成,主要业务逻辑在 service层处理,controller层处理请求。假设在执行dao层代码之前能够将数据源(DataSource) 换成我们想要执行操作的数据源,那么这个问题就解决了。
原理图
Spring内置了一个AbstractRoutingDataSource,它可以把多个数据源配置成一个Map,根据不同的key返回不同的数据源。
因为AbstractRoutingDataSource也是一个DataSource接口,应用程序可以先设置好key,访问数据库的代码就可以从AbstractRoutingDataSource拿到对应的一个真实的数据源,从而访问指定的数据库。
查看AbstractRoutingDataSource类:
/**
* Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()}
* 抽象 {@link javax.sql.DataSource} 路由 {@link #getConnection ()} 的实现
*
* calls to one of various target DataSources based on a lookup key. The latter is usually
* 根据查找键调用不同的目标数据之一。后者通常是
*
* (but not necessarily) determined through some thread-bound transactioncontext.
* (但不一定) 通过某些线程绑定事务上下文来确定。
* @author Juergen Hoeller
* @since 2.0.1
* @see #setTargetDataSources
* @see #setDefaultTargetDataSource
* @see #determineCurrentLookupKey()
*/
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
/**
* Specify the map of target DataSources, with the lookup key as key.
* 指定目标数据源的映射,查找键为键。
*
* The mapped value can either be a corresponding {@link javax.sql.DataSource}
* 映射的值可以是相应的{@link javax.sql.DataSource}
*
* instance or a data source name String (to be resolved via a {@link #setDataSourceLookup DataSourceLookup}).
* 实例或数据源名称字符串(要通过{@link #setDataSourceLookup DataSourceLookup})
*
* The key can be of arbitrary type; this class implements the generic lookup process only. The concrete key representation will be handled by {@link #resolveSpecifiedLookupKey(Object)} and {@link #determineCurrentLookupKey()}.
* 键可以是任意类型;此类仅实现一般查找过程。具体的键表示将由{@link#resolveSpecifiedLookupKey(Object)}和{@link#determineCurrentLookupKey()}处理。
*/
public void setTargetDataSources(Map<Object, Object> targetDataSources){
this.targetDataSources = targetDataSources;
}
......
/**
* Determine the current lookup key. This will typically be implemented to check a thread-bound transaction context. Allows for arbitrary keys. The returned key needs to match the stored lookup key type, as resolved by the {@link #resolveSpecifiedLookupKey} method.
* 确定当前的查找键。这通常会实现以检查线程绑定的事务上下文。允许任意键。返回的密钥需要与存储的查找密钥类型匹配, 如{@link #resolveSpecifiedLookupKey} 方法。
*/
protected abstract Object determineCurrentLookupKey();
}
上面源码中还有另外一个核心的方法,它需要一个Map,在方法注释中我们可以得知,这个Map存储的就是我 们配置的多个数据源的键值对。我们整理一下这个类切换数据源的运作方式,这个类在连接数据库 之前会执行determineCurrentLookupKey()方法,这个方法返回的数据将作为key去targetDataSources中查找相应的值,如果查找到相对应的DataSource,那么就使用此 DataSource获取数据库连接。
它是一个abstract类,所以我们使用的话,推荐的方式是创建一个类来继承它并且实现它的 determineCurrentLookupKey() 方法,这个方法介绍上面也进行了说明,就是通过这个方法进行数据源的切换。
3.5.2 具体实现
@Data
public class Product {
private Integer id;
private String name;
private Double price;
}
@Mapper
public interface ProductMapper {
@Select("select * from product")
public List<Product> findAllProductM();
@Select("select * from product")
public List<Product> findAllProductS();
}
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
public void findAllProductM(){
// 查询Master
List<Product> allProductM = productMapper.findAllProductM();
System.out.println(allProductM);
}
public void findAllProductS(){
// 查询Slave
List<Product> allProductS = productMapper.findAllProductS();
System.out.println(allProductS);
}
}
@RestController
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/findAllProductM")
public String findAllProductM() {
productService.findAllProductM();
return "master";
}
@GetMapping("/findAllProductS")
public String findAllProductS() {
productService.findAllProductS();
return "slave";
}
}
在application.properties中配置两个数据源
spring.druid.datasource.master.password=root
spring.druid.datasource.master.username=root
spring.druid.datasource.master.jdbc-url=jdbc:mysql://localhost:3306/product_master?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
spring.druid.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.druid.datasource.slave.password=root
spring.druid.datasource.slave.username=root
spring.druid.datasource.slave.jdbc-url=jdbc:mysql://localhost:3306/product_slave?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
spring.druid.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver
在SpringBoot的配置代码中,我们初始化两个数据源
@Configuration
public class MyDataSourceConfiguratioin {
Logger logger = LoggerFactory.getLogger(MyDataSourceConfiguratioin.class);
/**
* Master data source. */
@Bean("masterDataSource")
@ConfigurationProperties(prefix = "spring.druid.datasource.master")
DataSource masterDataSource() {
logger.info("create master datasource...");
return DataSourceBuilder.create().build();
}
/**
* Slave data source. */
@Bean("slaveDataSource")
@ConfigurationProperties(prefix = "spring.druid.datasource.slave")
DataSource slaveDataSource() {
logger.info("create slave datasource...");
return DataSourceBuilder.create().build();
}
}
然后,我们用Spring内置的RoutingDataSource,把两个真实的数据源代理为一个动态数据源
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return "masterDataSource";
}
}
对这个 RoutingDataSource ,需要在SpringBoot中配置好并设置为主数据源:
MyDataSourceConfiguratioin.java
@Bean
@Primary //设置为主数据源
DataSource primaryDataSource(@Autowired @Qualifier("masterDataSource") DataSource masterDataSource,
@Autowired @Qualifier("slaveDataSource") DataSource slaveDataSource){
logger.info("create routing datasource...");
Map<Object, Object> map = new HashMap<>();
map.put("masterDataSource", masterDataSource);
map.put("slaveDataSource", slaveDataSource);
RoutingDataSource routing = new RoutingDataSource();
routing.setTargetDataSources(map);
routing.setDefaultTargetDataSource(masterDataSource);
return routing;
}
现在, RoutingDataSource 配置好了,但是,路由的选择是写死的,即永远返 回"masterDataSource"
现在问题来了: 如何存储动态选择的key以及在哪设置key?
在Servlet的线程模型中,使用ThreadLocal存储key最合适,因此,我们编写一个 RoutingDataSourceContext ,来设置并动态存储key
public class RoutingDataSourceContext {
// holds data source key in thread local:
static final ThreadLocal<String> threadLocalDataSourceKey = new ThreadLocal<>();
public static String getDataSourceRoutingKey() {
String key = threadLocalDataSourceKey.get();
return key == null ? "masterDataSource" : key;
}
public RoutingDataSourceContext(String key) {
threadLocalDataSourceKey.set(key);
}
public void close() {
threadLocalDataSourceKey.remove();
}
}
然后,修改 RoutingDataSource ,获取key的代码
public class RoutingDataSource extends AbstractRoutingDataSource {
protected Object determineCurrentLookupKey() {
return RoutingDataSourceContext.getDataSourceRoutingKey();
}
}
这样,在某个地方,例如一个Controller的方法内部,就可以动态设置DataSource的Key
@GetMapping("/findAllProductM")
public String findAllProductM() {
String key = "masterDataSource";
RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext(key);
productService.findAllProductM();
return "master";
}
@GetMapping("/findAllProductS")
public String findAllProductS() {
String key = "slaveDataSource";
RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext(key);
productService.findAllProductS();
return "slave";
}
以上代码是可行的,但是,需要读数据库的地方,就需要加上一大段RoutingDataSourceContext ctx = …代码,使用起来十分不便。有没有方法可以简化呢?
仔细想想,Spring提供的声明式事务管理,就只需要一个@Transactional() 注解,放在某个Java方法上,这个方法就自动具有了事务。我们也可以编写一个类似的@RoutingWith(“slaveDataSource”) 注解,放到某个Controller的方法上,这个方法内部就自动选择了对应的数据源。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RoutingWith {
String value() default "master";
}
编译前需要添加一个Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
切面类
@Aspect
@Component
public class RoutingAspect {
@Around("@annotation(routingWith)")
public Object routingWithDataSource(ProceedingJoinPoint joinPoint, RoutingWith routingWith) throws Throwable {
String key = routingWith.value();
RoutingDataSourceContext ctx = new RoutingDataSourceContext(key);
return joinPoint.proceed();
}
}
注意方法的第二个参数RoutingWith是Spring传入的注解实例,我们根据注解的value()获取配置的 key。
改造controller
@RoutingWith("masterDataSource")
@GetMapping("/findAllProductM")
public String findAllProductM() {
// String key = "masterDataSource";
// RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext(key);
productService.findAllProductM();
return "master";
}
@RoutingWith("slaveDataSource")
@GetMapping("/findAllProductS")
public String findAllProductS() {
// String key = "slaveDataSource";
// RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext(key);
productService.findAllProductS();
return "slave";
}
第四部分 SpringBoot缓存深入
4.1 JSR107
JSR是Java Specification Requests 的缩写 ,Java规范请求,故名思议提交Java规范, JSR-107呢 就是关于如何使用缓存的规范,是java提供的一个接口规范,类似于JDBC规范,没有具体的实 现,具体的实现就是reids等这些缓存。
JSR107核心接口
Java Caching(JSR-107)定义了5个核心接口,分别是CachingProvider、CacheManager、Cache、Entry和Expiry。
-
CachingProvider(缓存提供者):创建、配置、获取、管理和控制多个CacheManager 。
-
CacheManager(缓存管理器):创建、配置、获取、管理和控制多个唯一命名的Cache, Cache存在于CacheManager的上下文中。一个CacheManager仅对应一个CachingProvider。
-
Cache(缓存):是由CacheManager管理的,CacheManager管理Cache的生命周期, Cache存在于CacheManager的上下文中,是一个类似map的数据结构,并临时存储以key为 索引的值。一个Cache仅被一个CacheManager所拥有。
-
Entry(缓存键值对):是一个存储在Cache中的key-value对。
-
Expiry(缓存时效):每一个存储在Cache中的条目都有一个定义的有效期。一旦超过这个 时间,条目就自动过期,过期后,条目将不可以访问、更新和删除操作。缓存有效期可以通 过ExpiryPolicy设置。
一个应用里面可以有多个缓存提供者(CachingProvider),一个缓存提供者可以获取到多个缓存管 理器(CacheManager),一个缓存管理器管理着不同的缓存(Cache),缓存中是一个个的缓存键值 对(Entry),每个entry都有一个有效期(Expiry)。缓存管理器和缓存之间的关系有点类似于数据库中 连接池和连接的关系。
使用JSR-107需导入的依赖
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
4.2 Spring的缓存抽象
Spring从3.1开始定义了org.springframework.cache.Cache 和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用Java Caching(JSR-107)注解简化我们进行缓存开发。
Spring Cache 只负责维护抽象层,具体的实现由自己的技术选型来决定。将缓存处理和缓存技术 解除耦合。
每次调用需要缓存功能的方法时,Spring会检查指定参数的指定的目标方法是否已经被调用过,如 果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次 调用直接从缓存中获取。
使用Spring缓存抽象时我们需要关注以下两点: 1 确定那些方法需要被缓存;2 缓存策略。
重要接口
- Cache: 缓存抽象的规范接口,缓存实现有:RedisCache、EhCache、 ConcurrentMapCache等。
- CacheManager: 缓存管理器,管理Cache的生命周期
4.3 Spring缓存使用
4.3.1 概念
概念/注解 | 作用 |
---|---|
Cache | 缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、 ConcurrentMapCache等 |
CacheManager | 缓存管理器,管理各种缓存(Cache)组件 |
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 保证方法被调用,又希望结果被缓存。 |
@EnableCaching | 开启基于注解的缓存 |
keyGenerator | 缓存数据时key生成策略 |
serialize | 缓存数据时value序列化策略 |
@Cacheable标注在方法上,表示该方法的结果需要被缓存起来,缓存的键由keyGenerator的 策略决定,缓存的值的形式则由serialize序列化策略决定(序列化还是json格式);标注上该注解之后,在缓存时效内再次调用该方法时将不会调用方法本身而是直接从缓存获取结果
@CachePut也标注在方法上,和@Cacheable相似也会将方法的返回值缓存起来,不同的是标注@CachePut的方法每次都会被调用,而且每次都会将结果缓存起来,适用于对象的更新
4.3.2 环境搭建
创建SpringBoot应用:选中Mysql、Mybatis、Web模块
数据库表
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`departmentName` varchar(255) DEFAULT NxQULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`lastName` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`gender` int(2) DEFAULT NULL,
`d_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/springboot_h
spring.datasource.username=root
spring.datasource.password=root
# 数据源配置:驱动可以不写,SpringBoot会根据连接自动判断
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#开启驼峰命名
mybatis.configuration.map-underscore-to-camel-case=true
# Logger日志,让控制台将Sql打印出来
logging.level.com.lagou.cache.mappers=debug
注解版Mybatis: 使用@MapperScan指定mapper接口所在的包
@Data
public class Employee {
private Integer id;
private String lastName;
private String email;
private Integer gender; //性别 1男 0女 private Integer dId;
}
@Data
public class Department {
private Integer id;
private String departmentName;
}
@SpringBootApplication
@MapperScan(basePackages = "com.lagou.cache.mappers")
public class SpringbootCacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootCacheApplication.class, args);
}
}
public interface EmployeeMapper {
@Select("SELECT * FROM employee WHERE id = #{id}")
public Employee getEmpById(Integer id);
@Insert("INSERT INTO employee(lastName,email,gender,d_id) VALUES(#{lastName},#{email},#{gender},#{dId})")
public void insertEmp(Employee employee);
@Update("UPDATE employee SET lastName = #{lastName},email = #{email},gender = #{gender},d_id = #{dId} WHERE id = #{id}")
public void updateEmp(Employee employee);
@Delete("DELETE FROM employee WHERE id = #{id}")
public void deleteEmpById(Integer id);
}
@Service
public class EmployeeService {
@Autowired
EmployeeMapper employeeMapper;
public Employee getEmpById(Integer id){
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
}
@RestController
public class EmployeeController {
@Autowired
EmployeeService employeeService;
@GetMapping("/emp/{id}")
public Employee getEmp(@PathVariable("id") Integer id){
return employeeService.getEmpById(id);
}
}
结论:当前还没有看到缓存效果,因为还没有进行缓存的相关设置
4.3.3 @Cacheable初体验
//启动类
@SpringBootApplication
@MapperScan(basePackages = "com.lagou.cache.mappers")
@EnableCaching //开启基于注解的缓存
public class SpringbootCacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootCacheApplication.class, args);
}
}
//在serive方法中添加注解
@Cacheable(cacheNames = {"emp"})
//将方法运行的结果进行缓存,以后再获取相同的数据时,直接从缓存中获取,不再调用方法
public Employee getEmpById(Integer id){
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
@Cacheable注解的属性
属性名 | 描述 |
---|---|
cacheNames/value | 指定缓存的名字,缓存使用CacheManager管理多个缓存组件 Cache,这些Cache组件就是根据这个名字进行区分的。对缓存的真 正CRUD操作在Cache中定义,每个缓存组件Cache都有自己唯一的 名字,通过cacheNames或者value属性指定,相当于是将缓存的键 值对进行分组,缓存的名字是一个数组,也就是说可以将一个缓存 键值对分到多个组里面 |
key | 缓存数据时的key的值,默认是使用方法参数的值,可以使用SpEL表 达式计算key的值 |
keyGenerator | 缓存的生成策略,和key二选一,都是生成键的,keyGenerator可自 定义 |
cacheManager | 指定缓存管理器(如ConcurrentHashMap、Redis等) |
cacheResolver | 和cacheManager功能一样,和cacheManager二选一 |
condition | 指定缓存的条件(满足什么条件时才缓存),可用SpEL表达式(如 #id>0,表示当入参id大于0时才缓存) |
unless | 否定缓存,即满足unless指定的条件时,方法的结果不进行缓存, 使用unless时可以在调用的方法获取到结果之后再进行判断(如 #result==null,表示如果结果为null时不缓存) |
sync | 是否使用异步模式进行缓存 |
注意⚠️
1、既满足condition又满足unless条件的也不进行缓存
2、使用异步模式进行缓存时(sync=true):unless条件将不被支持
可用的SpEL表达式见下表:
名字 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root object | 当前被调用的方法名 | #root.methodName |
method | root object | 当前被调用的方法 | #root.method.name |
target | root object | 当前被调用的目标对象 | #root.target |
targetClass | root object | 当前被调用的目标对象类 | #root.targetClass |
args | root object | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root object | 当前方法调用使用的缓存列表 (如@Cacheable(value= {“cache1”, “cache2”})),则有两 个cache | #root.caches[0].name |
argument name | evaluation context | 方法参数的名字,可以直接 # 参数名,也可以使用#p0或#a0 的形式,0代表参数的索引 | #iban、#a0、#p0 |
result | evaluation context | 方法执行后的返回值(仅当方法 执行之后的判断有效, 如"unless","cache put"的表 达式,"cache evict"的表达式 beforeInvocation=false) | #result |
4.4 缓存自动配置原理源码剖析
更新中。。。。。。
4.5 @Cacheable源码分析
我们在上述的两个方法上打上断点; debug运行springBoot; 访问getEmp接口;
我们会发现来到了 lookup方法这里,说明注解的执行在被注解的方法前,然后这里我们会返回 null;
我们放行到下一个注解会发现; 调用了put方法
添加了Cache; 然后我们第二次对getEmp接口发起请求我们会发现这一次缓存内容不再为null
@Cacheable运行流程:
1、方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定的名字获取(CacheManager 先获取相应的缓存,第一次获取缓存如果没有Cache组件会自动创建)。
2、去Cache中查找缓存的内容,使用的key默认就是方法的参数:
key默认是使用keyGenerator生成的,默认使用的是SimpleKeyGenerator
SimpleKeyGenerator生成key的默认策略:
如果没有参数: key = new SimpleKey();
如果有一个参数:key = 参数的值
如果有多个参数:key = new SimpleKey(params);
3、没有查到缓存就调用目标方法
4、将目标方法返回的结果放进缓存中
总结:@Cacheable标注的方法在执行之前会先检查缓存中有没有这个数据,默认按照参数的值 为key查询缓存,如果没有就运行方法并将结果放入缓存,以后再来调用时直接使用缓存中的数据。
核心:
1、使用CacheManager(ConcurrentMapCacheManager)按照名字得到 Cache(ConcurrentMapCache)组件
2、key使用keyGenerator生成,默认使用SimpleKeyGenerator
4.6 @CachePut&@CacheEvict&@CacheConfig
4.6.1 @CachePut
功能:既调用方法,又更新缓存数据,一般用于更新操作,在更新缓存时一定要和想更新的缓 存有相同的缓存名称和相同的key(可类比同一张表的同一条数据)
运行时机:1、先调用目标方法; 2、将目标方法的结果缓存起来
示例:
@CachePut(value = "emp",key = "#employee.id")
public Employee updateEmp(Employee employee){
employeeMapper.updateEmp(employee);
return employee;
}
@CachePut标注的方法总会被调用,且调用之后才将结果放入缓存,因此可以使用#result 获取到方法的返回值。
4.6.2 @CacheEvict
功能:缓存清除,清除缓存时要指明缓存的名字和key,相当于告诉数据库要删除哪个表中的 哪条数据,key默认为参数的值。
属性:
value/cacheNames:缓存的名字;
key:缓存的键;
allEntries:是否清除指定缓存中的所有键值对,默认为false,设置为true时会清除缓存中 的所有键值对,与key属性二选一使用
beforeInvocation: 在@CacheEvict注解的方法调用之前清除指定缓存,默认为false,即 在方法调用之后清除缓存,设置为true时则会在方法调用之前清除缓存(在方法调用之前还是之后 清除缓存的区别在于方法调用时是否会出现异常,若不出现异常,这两种设置没有区别,若出现异 常,设置为在方法调用之后清除缓存将不起作用,因为方法调用失败了)
示例:
@CacheEvict(value = "emp",key = "#id", beforeInvocation = true)
public void delEmp(Integer id){
employeeMapper.deleteEmpById(id);
}
4.6.3 @CacheConfig
功能:标注在类上,抽取缓存相关注解的公共配置,可抽取的公共配置有缓存名字、主键生成 器等(如注解中的属性所示)
示例 : 通过@CacheConfig的cacheNames 属性指定缓存的名字之后,该类中的其他缓存注解就不必再写value或者cacheName了,会使用该名字作为value或cacheName的值,当然也遵循就近原则。
@Service
@CacheConfig(cacheNames = "emp")
public class EmployeeService {
@Autowired
EmployeeMapper employeeMapper;
@Cacheable
public Employee getEmpById(Integer id) {
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
@CachePut(key = "#employee.id")
public Employee updateEmp(Employee employee) {
employeeMapper.updateEmp(employee);
return employee;
}
@CacheEvict(key = "#id", beforeInvocation = true)
public void delEmp(Integer id) {
employeeMapper.deleteEmpById(id);
}
}
4.7 基于Redis的缓存实现
SpringBoot默认开启的缓存管理器是ConcurrentMapCacheManager,创建缓存组件是ConcurrentMapCache,将缓存数据保存在一个个的ConcurrentHashMap<Object, Object>中。 开发时我们可以使用缓存中间件:redis、memcache、ehcache等,这些缓存中间件的启用很简
单——只要向容器中加入相关的bean就会启用,可以启用多个缓存中间件。
整合Redis:
1、引入Redis的starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
引入redis的starter之后,会在容器中加入redis相关的一些bean,其中有两个跟操作redis相关的:
RedisTemplate 和 StringRedisTemplate (用来操作字符串:key和value都是字符串)
template中封装了操作各种数据类型的操作(stringRredisTemplate.opsForValue()、 stringRredisTemplate.opsForList()等)
@Configuration
@ConditionalOnClass({JedisConnection.class, RedisOperations.class, Jedis.class}) @EnableConfigurationProperties({RedisProperties.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {}
@Configuration
protected static class RedisConfiguration {
protected RedisConfiguration() {}
@Bean
@ConditionalOnMissingBean(name = {"redisTemplate"})
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean({StringRedisTemplate.class})
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
//...
配置redis:只需要配置redis的主机地址(端口默认即为6379,因此可以不指定)
spring.redis.host=127.0.0.1
注意⚠️: 使用redis存储对象时,该对象必须可序列化(实现Serializable接口),否则会报错。
SpringBoot默认采用的是JDK的对象序列化方式,我们可以切换为使用JSON格式进行对象的序列化操作,这时需要我们自定义序列化规则(当然我们也可以使用Json工具先将对象转化为Json格式 之后再保存至redis,这样就无需自定义序列化)
4.8 自定义RedisCacheManager
4.8.1 Redis注解默认序列化机制
打开Spring Boot整合Redis组件提供的缓存自动配置类 RedisCacheConfiguration(org.springframework.boot.autoconfigure.cache包下的),查看该 类的源码信息,其核心代码如下
@Configuration
class RedisCacheConfiguration {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,ResourceLoader resourceLoader) {
RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(this.determineConfiguration(resourceLoader.getClassLoader()));
List<String> cacheNames = this.cacheProperties.getCacheNames();
if(!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet(cacheNames));
}
return (RedisCacheManager)this.customizerInvoker.customize(builder.build());
}
private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(ClassLoader classLoader){
if(this.redisCacheConfiguration != null) {
return this.redisCacheConfiguration;
} else {
Redis redisProperties = this.cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeValuesWith(SerializationPair.fromSerializer(newdkSerializationRedisSerializer(classLoader)));
//...
return config;
}
}
}
从上述核心源码中可以看出,RedisCacheConfiguration内部同样通过Redis连接工厂 RedisConnectionFactory定义了一个缓存管理器RedisCacheManager;同时定制 RedisCacheManager时,也默认使用了JdkSerializationRedisSerializer序列化方式。
如果想要使用自定义序列化方式的RedisCacheManager进行数据缓存操作,可以参考上述核心代 码创建一个名为cacheManager的Bean组件,并在该组件中设置对应的序列化方式即可。
4.8.2 自定义RedisCacheManager
在项目的Redis配置类RedisConfig中,按照上一步分析的定制方法自定义名为cacheManager的Bean组件
@Configurtion
public class RedisConfig{
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 分别创建String和JSON格式序列化对象,对缓存数据key和value进行转换
RedisSerializer<String> strSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 定制缓存数据序列化方式及时效
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(strSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jacksonSeial))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config).build();
return cacheManager;
}
}
上述代码中,在RedisConfig配置类中使用@Bean注解注入了一个默认名称为方法名的 cacheManager组件。在定义的Bean组件中,通过RedisCacheConfiguration对缓存数据的key和 value分别进行了序列化方式的定制,其中缓存数据的key定制为StringRedisSerializer(即String 格式),而value定制为了Jackson2JsonRedisSerializer(即JSON格式),同时还使用 entryTtl(Duration.ofDays(1))方法将缓存数据有效期设置为1天
完成基于注解的Redis缓存管理器RedisCacheManager定制后,可以对该缓存管理器的效果 进行测试(使用自定义序列化机制的RedisCacheManager测试时,实体类可以不用实现序列化接口)
第五部分 SpringBoot部署与监控
5.1 部署
目前,前后端分离的架构已成主流,而使用SpringBoot构建Web应用是非常快速的,项目发布到 服务器上的时候,只需要打成一个jar包,然后通过命令 : java -jar jar包名称即可启动服务了。
5.1.1 SpringBoot将项目打包成jar包 (官方推荐)
在pom.xml文件中导入Springboot的maven依赖
<!--将应用打包成一个可以执行的jar包-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
maven 执行package,完成以后 target 中会生成一个.jar包;
将jar包上传到服务器运行: java -jar spring-boot-mytest-0.0.1-SNAPSHOT.jar
5.1.2 war包
pom.xml配置修改
<!-- <packaging>jar</packaging> -->
<!-- 修改为 -->
<packaging>war</packaging>
<!--添加servlet-api的依赖,用来打war包 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!--最终打成war包,排除内置的tomcat-->
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
改造启动类:
如果是war包发布,需要增加SpringBootServletInitializer子类,并重写其configure方法, 或者将main函数所在的类继承SpringBootServletInitializer,并重写configure方法。
当时打包为war时上传到tomcat服务器中访问项目始终报404错就是忽略了这个步骤!!!
//这种改造方式也是官方比较推荐的方法
@SpringBootApplication
public class SpringBootMytestApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(SpringBootMytestApplication.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
// 注意这里要指向原先用main方法执行的Application启动类
return builder.sources(SpringBootMytestApplication.class);
}
}
maven package 打war包,使用外部Tomcat运行该 war文件(把 war 文件直接丢到 tomcat的webapps目录,启动 tomcat)
5.1.3 jar包和war包方式对比
1.SpringBoot项目打包时能打成 jar 与 war包,对比两种打包方式:
jar包更加简单方便,使用 java -jar xx.jar 就可以启动。所以打成 jar 包的最多。
war包可以部署到tomcat的 webapps中,随Tomcat的启动而启动。具体使用哪种方 式,应视应用场景而定。
2、打jar包时不会把src/main/webapp 下的内容打到jar包里 (你认为的打到jar包里面,路径是不行的会报404);打war包时会把src/main/webapp 下的内容打到war包里
3.打成什么文件包进行部署与项目业务有关,就像提供rest 服务的项目需要打包成 jar文件,用命令运行很方便。而有大量css、js、html,且需要经常改动的项目,打成 war 包去运行比较方便,因为改动静态资源可以直接覆盖,很快看到改动后的效果,这是 jar 包不能比的。
(例如: 项目打成 jar 包运行,一段时间后,前端要对其中某几个页面样式进行改动,使其更美观,那么改动几个css、html后,需要重新打成一个新的 jar 包,上传服务器并运行,这种改动频繁时很不友好,文件大时上传服务器很耗时,那么 war包就能免去这种烦恼,只要覆盖几个 css与html即可)。
5.1.4 多环境部署
在项目运行中,包括多种环境,例如线上环境prod(product)、开发环境dev(development)、测试环境test、提测环境qa、单元测试unitest等等。不同的环境需要进行不同的配置,从而在不同的场景中跑我们的程序。例如prod环境和dev环境通常需要连接不同的数据库、需要配置不同的日志输出配置。还有一些类和方法,在不同的环境下有不同的实现方式。
Spring Boot 对此提供了支持,一方面是注解@Profile,另一方面还有多资源配置文件。
@profile
@profile 注解的作用是指定类或方法在特定的 Profile 环境生效,任何 @Component 或 @Configuration 注解的类都可以使用 @Profile 注解。在使用DI来依赖注入的时候,能够根据 @profile 标明的环境,将注入符合当前运行环境的相应的bean。
使用要求:
@Component 或 @Configuration 注解的类可以使用 @profile;
@Profile 中需要指定一个字符串,约定生效的环境。
@profile 的使用位置
(1)@Prifile 修饰类
@Configuration
@Profile("prod")
public class JndiDataConfig {
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
(2) 修饰方法
@Configuration
public class AppConfig {
@Bean("dataSource")
@Profile("dev")
public DataSource standaloneDataSource() {
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql") .build();
}
@Bean("dataSource")
@Profile("prod")
public DataSource jndiDataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); }
}
}
(3) @Profile 修饰注解
支持定义在其他注解之上,以创建自定义场景注解。这样就创建了一个 @Dev 注 解,该注解可以标识bean使用于 @Dev 这个场景。后续就不再需要使用 @Profile(“dev”) 的方 式,这样即可以简化代码。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("prod")
public @interface Production {
}
profile激活
实际使用中,注解中标示了prod、test、qa等多个环境,运行时使用哪个profile由spring.profiles.active控制,以下说明2种方式:配置文件方式、命令行方式。
(1)配置文件方式激活profile
确定当前使用的是哪个环境,这边环境的值与application-prod.properties中-后面的值对应,这是SpringBoot约定好的。
在resources/application.properties中添加下面的配置。需要注意的是,spring.profiles.active的取值应该与@Profile注解中的标示保持一致。
spring:
profiles:
active: dev
(2)命令行方式激活profile
在打包后运行的时候,添加参数: java -jar spring-boot-config-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev;
多Profile的资源文件
除了@profile注解的可以标明某些方法和类具体在哪个环境下注入。springboot的环境隔离还可以使用多资源文件的方式,进行一些参数的配置。
(1)资源配置文件
Springboot的资源配置文件除了application.properties之外,还可以有对应的资源文件 application-{profile}.properties。
假设,一个应用的工作环境有:dev、test、prod
那么,我们可以添加 4 个配置文件:
- applcation.properties - 公共配置
- application-dev.properties - 开发环境配置
- application-test.properties - 测试环境配置
- application-prod.properties - 生产环境配置
不同的properties配置文件也可以是在 applcation.yml文件中来激活profile:
spring:
profiles:
active: dev
(2)效果演示
在controller层中的Sound.java中新建一个接口,返回配置文件中的信息:name和local。
@Controller
@RequestMapping("/sound")
public class Sound {
@Value("${com.name}") private String name;
@Value("${com.location}") private String location;
@RequestMapping("hello")
@ResponseBody
public String sound1(){
System.out.println(name + "hello Spring Boot, " +location);
return name + ",hello Spring Boot! " +location;
}
}
application.properties文件内容如下
## 多环境配置文件激活属性
spring.profiles.active=prod
application-dev.properties文件内容如下:
# 服务端口
server.port=1111
#可以定义一些自己使用的属性,然后通过@Value("${属性名}}")注解来加载对应的配置属性
com.name=DEV
com.location=Beijing
application-prod.properties文件内容如下:
server.port=2222
#可以定义一些自己使用的属性,然后通过@Value("${属性名}}")注解来加载对应的配置属性
com.name=Prod
com.location=Hubei
启动Springboot后,访问http://localhost:2222/sound/hello,则会有如下结果。如果此时访问http://localhost:1111/sound/hello则会无法访问,因为此时 spring.profiles.active=prod 激活的是prod环境,使用的是application-prod.properties中的配置。
更改application-dev.properties文件, spring.profiles.active=dev 激活dev环境。重启 Springboot则可以正常访问到http://localhost:1111/sound/hello。
5.2 监控
微服务的特点决定了功能模块的部署是分布式的,大部分功能模块都是运行在不同的机器上,彼此 通过服务调用进行交互,前后台的业务流会经过很多个微服务的处理和传递,出现了异常如何快速 定位是哪个环节出现了问题?
在这种情况下,微服务的监控显得尤为重要。springboot作为微服务框架,除了它强大的快速开 发功能外,还有就是它提供了actuator模块,引入该模块能够自动为springboot应用提供一系列 用于监控的端点。
5.2.1 Acturator
Actuator是spring boot的一个附加功能,可帮助你在应用程序生产环境时监视和管理应用程序。 可以使用HTTP的各种请求来监管,审计,收集应用的运行情况。Spring Boot Actuator提供了对单个 Spring Boot的监控,信息包含:应用状态、内存、线程、堆栈等等,比较全面的监控了Spring Boot应用的整个生命周期。特别对于微服务管理十分有意义。
Actuator 监控分成两类:原生端点和用户自定义端点;自定义端点主要是指扩展性,用户可以根据自己的实际应用,定义一些比较关心的指标,在运行期进行监控。
原生端点是在应用程序里提供众多 Web 接口,通过它们了解应用程序运行时的内部状况。原生端 点又可以分成三类:
- 应用配置类:可以查看应用在运行期的静态信息:例如自动配置信息、加载的springbean信息、yml 文件配置信息、环境信息、请求映射信息;
- 度量指标类:主要是运行期的动态信息,例如堆栈、请求链、一些健康指标、metrics 信息等;
- 操作控制类:主要是指 shutdown,用户可以发送一个请求将应用的监控功能关闭。
Actuator 提供了 13 个接口,具体如下表所示。
HTTP 方法 | 路径 | 描述 |
---|---|---|
GET | /auditevents | 显示应用暴露的审计事件 (比如认证进入、订单失败) |
GET | /beans | 描述应用程序上下文里全部的 Bean,以及它们的关系 |
GET | /conditions | 就是 1.0 的 /autoconfig ,提供一份自动配置生效的条件情 况,记录哪些自动配置条件通过了,哪些没通过 |
GET | /configprops | 描述配置属性(包含默认值)如何注入Bean |
GET | /env | 获取全部环境属性 |
GET | /env/{name} | 根据名称获取特定的环境属性值 |
GET | /flyway | 提供一份 Flyway 数据库迁移信息 |
GET | /liquidbase | 显示Liquibase 数据库迁移的纤细信息 |
GET | /health | 报告应用程序的健康指标,这些值由 HealthIndicator 的实 现类提供 |
GET | /heapdump | dump 一份应用的 JVM 堆信息 |
GET | /httptrace | 显示HTTP足迹,最近100个HTTP request/repsponse |
GET | /info | 获取应用程序的定制信息,这些信息由info打头的属性提供 |
GET | /logfile | 返回log file中的内容(如果 logging.file 或者 logging.path 被设置) |
GET | /loggers | 显示和修改配置的loggers |
GET | /metrics | 报告各种应用程序度量信息,比如内存用量和HTTP请求计数 |
GET | /metrics/{name} | 报告指定名称的应用程序度量值 |
GET | /scheduledtasks | 展示应用中的定时任务信息 |
GET | /sessions | 如果我们使用了 Spring Session 展示应用中的 HTTP sessions 信息 |
POST | /shutdown | 关闭应用程序,要求endpoints.shutdown.enabled设置为 true |
GET | /mappings | 描述全部的 URI路径,以及它们和控制器(包含Actuator端 点)的映射关系 |
GET | /threaddump | 获取线程活动的快照 |
使用Actuator功能与springBoot使用其他功能一样简单,只需要在pom.xml中添加如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
为了保证 actuator 暴露的监控接口的安全性,需要添加安全控制的依赖 spring-boot-starter-actuator依赖,访问应用监控端点时,都需要输入验证信息。Security 依赖,可以选择不加,不进行安全管理。
application.properties配置,配置完成之后,启动项目就可以继续验证各个监控功能了。
info.app.name=spring-boot-actuator
info.app.version= 1.0.0
info.app.test=test
#在SpringBoot2.x中为了安全期间,Actuator 只开放了两个端点 /actuator/health 和 /actuator/info。
#可以打开所有的监控点
management.endpoints.web.exposure.include=*
#也可以选择打开部分
#management.endpoints.web.exposure.include=beans,trace
#展示细节,除了always之外还有when-authorized、never,默认值是never
management.endpoint.health.show-details=always
#Actuator 默认所有的监控点路径都在/actuator/* ,当然如果有需要这个路径也支持定制。
#代表启用单独的url地址来监控SpringBoot应用,为了安全一般都启用独立的端口来访问后端的监控信息
#management.endpoints.web.base-path=/monitor
#启用接口关闭Spring Boot
management.endpoint.shutdown.enabled=true
5.2.2 属性
Actuator 几乎监控了应用涉及的方方面面,我们重点讲述一些经常在项目中常用的属性。
(1)health
health 主要用来检查应用的运行状态,这是我们使用最高频的一个监控点。通常使用此接口提醒我们应用实例的运行状态及应用不”健康“的原因,比如数据库连接、磁盘空间不够等。
默认情况下 health 的状态是开放的,添加依赖后启动项目,访问: http://localhost:8080/actuator/health 即可看到应用的状态。
health 通过合并几个健康指数检查应用的健康情况。Spring Boot Actuator 有几个预定义的健康 指标比如 DataSourceHealthIndicator , DiskSpaceHealthIndicator , MongoHealthIndicator , RedisHealthIndicator 等,它使用这些健康指标作为健康检查的一 部分。
举个例子,如果你的应用使用 Redis,RedisHealthindicator 将被当作检查的一部分; 如果使用MongoDB,那么MongoHealthIndicator 将被当作检查的一部分。
可以在配置文件中关闭特定的健康检查指标,比如关闭 redis 的健康检查:
management.health.redise.enabled=false
默认,所有的这些健康指标被当作健康检查的一部分。
(2)info
info 就是我们自己配置在配置文件中以 info 开头的配置信息,比如我们在示例项目中的配置是:
info.app.name=spring-boot-actuator
info.app.version= 1.0.0
info.app.test= test
启动示例项目,访问: http://localhost:8080/actuator/info 返回部分信息如下:
{
"app": {
"name": "spring-boot-actuator",
"version": "1.0.0",
"test":"test"
}
}
(3)beans
根据示例就可以看出,展示了 bean 的别名、类型、是否单例、类的地址、依赖等信息。
启动示例项目,访问: http://localhost:8080/actuator/beans 返回部分信息如下:
[
{
"context": "application:8080:management",
"parent": "application:8080",
"beans": [
{
"bean": "embeddedServletContainerFactory",
"aliases": [
],
"scope": "singleton",
"type": "org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletConta inerFactory",
"resource": "null",
"dependencies": [
]
},
{
"bean": "endpointWebMvcChildContextConfiguration",
"aliases": [
],
"scope": "singleton",
"type":"org.springframework.boot.actuate.autoconfigure.EndpointWebMvcChildContextCo nfiguration$$EnhancerBySpringCGLIB$$a4a10f9d",
"resource": "null",
"dependencies": [
]
}
]
}
]
(4)conditions
Spring Boot 的自动配置功能非常便利,但有时候也意味着出问题比较难找出具体的原因。使用 conditions 可以在应用运行时查看代码了某个配置在什么条件下生效,或者某个自动配置为什么没有生效。
启动示例项目,访问: http://localhost:8080/actuator/conditions 返回部分信息如下:
{
"positiveMatches": {
"DevToolsDataSourceAutoConfiguration": {
"notMatched": [
{
"condition": "DevToolsDataSourceAutoConfiguration.DevToolsDataSourceCondition",
"message": "DevTools DataSource Condition did not find a single DataSource bean"
}
],
"matched": [ ]
},
"RemoteDevToolsAutoConfiguration": {
"notMatched": [
{
"condition": "OnPropertyCondition",
"message": "@ConditionalOnProperty(spring.devtools.remote.secret) did not find property 'secret'"
}
],
"matched": [
{
"condition": "OnClassCondition",
"message": "@ConditionalOnClass found required classes 'javax.servlet.Filter','org.springframework.http.server.ServerHttpRequest'; @ConditionalOnMissingClass did not find unwanted class"
}
]
}
}
}
(5)heapdump
返回一个 GZip 压缩的 JVM 堆 dump
启动示例项目,访问: http://localhost:8080/actuator/heapdump 会自动生成一个JVM的堆文件 heapdump,我们可以使用 JDK 自带的 JVM 监控工具 VisualVM 打开此文件查看内存快照。 类似如下图:
(6)mappings
描述全部的 URI 路径,以及它们和控制器的映射关系
启动示例项目,访问: http://localhost:8080/actuator/mappings 返回部分信息如下:
{
"/**/favicon.ico": {
"bean": "faviconHandlerMapping"
},
"{[/hello]}": {
"bean": "requestMappingHandlerMapping",
"method": "public java.lang.String com.neo.controller.HelloController.index()"
},
"{[/error]}": {
"bean": "requestMappingHandlerMapping",
"method": "public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax. servlet.http.HttpServletRequest)"
}
}
(7)threaddump
/threaddump 接口会生成当前线程活动的快照。这个功能非常好,方便我们在日常定位问题的时 候查看线程的情况。 主要展示了线程名、线程ID、线程的状态、是否等待锁资源等信息。
启动示例项目,访问: http://localhost:8080/actuator/threaddump 返回部分信息如下:
[
{
"threadName": "http-nio-8088-exec-6",
"threadId": 49,
"blockedTime": -1,
"blockedCount": 0,
"waitedTime": -1,
"waitedCount": 2,
"lockName": "java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject@1630a 501",
"lockOwnerId": -1,
"lockOwnerName": null,
"inNative": false,
"suspended": false,
"threadState": "WAITING",
"stackTrace": [
{
"methodName": "park",
"fileName": "Unsafe.java",
"lineNumber": -2,
"className": "sun.misc.Unsafe",
"nativeMethod": true
},
...
{
"methodName": "run",
"fileName": "TaskThread.java",
"lineNumber": 61,
"className": "org.apache.tomcat.util.threads.TaskThread$WrappingRunnable",
"nativeMethod": false
}
...
],
"lockInfo": {
"className": "java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject",
"identityHashCode": 372286721
}
}
...
]
生产出现问题的时候,可以通过应用的线程快照来检测应用正在执行的任务。
(8)shutdown
开启接口优雅关闭 Spring Boot 应用,要使用这个功能首先需要在配置文件中开启
management.endpoint.shutdown.enabled=true
配置完成之后,启动示例项目,使用 curl 模拟 post 请求访问 shutdown 接口。(shutdown 接口默认只支持 post 请求)
curl -X POST "http://localhost:8080/actuator/shutdown"
{
"message": "Shutting down, bye..."
}
此时你会发现应用已经被关闭。
5.2.3 Spring Boot Admin
可视化后台管理系统
对于spring actuator而言,最大的缺点在于是以json形式来进行展示,为了更好的进行监控显示,我们来介绍一个更加方便的工具:spring boot admin。
Spring Boot Admin 是一个针对spring-boot的actuator接口进行UI美化封装的监控工具。他可以 返回在列表中浏览所有被监控spring-boot项目的基本信息比如:Spring容器管理的所有的bean、 详细的Health信息、内存信息、JVM信息、垃圾回收信息、各种配置信息(比如数据源、缓存列表 和命中率)等,Threads 线程管理,Environment 管理等。
利用springbootadmin进行监控的架构图
springbootadmin监控: 通俗点,就是我们如果有n个springboot业务系统需要监控的话,那么需要一个额外的springbootadmin应用来进行监控这些client,client和server之间需要做一点配置即可。
搭建Server端
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
</dependency>
applicaiton.yml
server:
port: 8081
启动类
@EnableAdminServer
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
搭建client端
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.1.0</version>
</dependency>
application.yml
server:
port: 8080
#自定义配置信息用于"/actuator/info"读取
info:
name: 老王
age: 100
phone: 110
#通过下面的配置启用所有的监控端点,默认情况下,这些端点是禁用的;
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
## 将Client作为服务注册到Server,通过Server来监听项目的运行情况
spring:
boot:
admin:
client:
url: http://localhost:8081
##application实例名
application:
name : spring-boot-admin-client
启动类
@RestController
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
@RequestMapping("/index") public String index() {
return "这是 index";
}
@RequestMapping("/home") public String home() {
return "这是 home";
}
}
查看 client 详细信息: