在上一篇文章 《Spring Boot (三): ORM 框架 JPA 与连接池 Hikari》 https://www.geekdigging.com/2019/09/19/2405775053/ 我们介绍了 JPA 与连接池 Hikari 的整合使用, 在国内使用比较多的连接池还有一个是阿里开源的 Druid . 本篇文章我们就来聊一聊 Druid 的一些使用姿势.
1. Druid 是什么?
我们先来看一下官方的回答:
Druid 是 Java 语言中最好的数据库连接池. Druid 能够提供强大的监控和扩展功能.
说 Druid 是 Java 语言中最好的数据库连接池, 这个笔者个人觉得有些吹牛了, 至少在性能上和我们上一篇介绍的 Hikari 是没得比的, 相关的性能测试在网上能找到很多, 笔者这边就不列举了. 但是 Druid 在其他的一些方面就做的比较出色了, 功能非常丰富:
可以监控数据库访问性能, Druid 内置提供了一个功能强大的 StatFilter 插件, 能够详细统计 SQL 的执行性能, 这对于线上分析数据库访问性能有帮助.
数据库密码加密. 直接把数据库密码写在配置文件中, 这是不好的行为, 容易导致安全问题. DruidDruiver 和 DruidDataSource 都支持 PasswordCallback .
SQL 执行日志, Druid 提供了不同的 LogFilter , 能够支持 Common-Logging , Log4j 和 JdkLog , 你可以按需要选择相应的 LogFilter , 监控你应用的数据库访问情况.
扩展 JDBC , 如果你要对 JDBC 层有编程的需求, 可以通过 Druid 提供的 Filter 机制, 很方便编写 JDBC 层的扩展插件.
2. Spring Boot 应用中如何使用
目前 Druid 官方为我们提供了两种使用依赖方式, 一种是基于传统 Java 工程提供的依赖包, maven 坐标如下:
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>druid</artifactId>
- <version>1.1.20</version>
- </dependency>
还有一种是基于 Spring Boot 提供的依赖包, maven 坐标如下:
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>druid-spring-boot-starter</artifactId>
- <version>1.1.20</version>
- </dependency>
下面的这种依赖包除了包含了上面的那种 Druid 基础包, 还包含了 Spring Boot 自动配置的依赖包以及 sl4j-API , 我们在 Spring Boot 中使用 Druid , 当然是推荐各位读者使用第二种方式引入依赖.
3. 工程实战
3.1 创建父工程 spring-boot-jpa-druid
父工程 pom.xml 如下:
代码清单: spring-boot-jpa-druid/pom.xml
- <?xml version="1.0" encoding="UTF-8"?>
- <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>2.1.8.RELEASE</version>
- <relativePath/> <!-- lookup parent from repository -->
- </parent>
- <groupId>com.springcloud</groupId>
- <artifactId>spring-boot-jpa-druid</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <name>spring-boot-jpa-druid</name>
- <description>spring-boot-jpa-druid</description>
- <properties>
- <druid.version>1.1.20</druid.version>
- <java.version>1.8</java.version>
- </properties>
- <dependencies>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-jpa</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <dependency>
- <groupId>MySQL</groupId>
- <artifactId>MySQL-connector-java</artifactId>
- <scope>runtime</scope>
- </dependency>
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>druid-spring-boot-starter</artifactId>
- <version>${druid.version}</version>
- </dependency>
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <optional>true</optional>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </dependency>
- </dependencies>
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
- </build>
- </project>
笔者这里使用的 Druid 依赖包是
druid-spring-boot-starter
, 版本号为 1.1.20.
3.2 数据库密码不加密的配置文件 application-pass.YAML 如下:
代码清单: spring-boot-jpa-druid/src/main/resources/application-pass.YAML
- spring:
- datasource:
- type: com.alibaba.druid.pool.DruidDataSource
- url: jdbc:MySQL://192.168.0.128:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false
- username: root
- password: 123456
- driverClassName: com.MySQL.cj.jdbc.Driver
- druid:
- # 连接池的配置信息
- # 初始化时建立物理连接的个数
- initial-size: 3
- # 连接池最小连接数
- min-idle: 3
- # 连接池最大连接数
- max-active: 20
- # 获取连接时最大等待时间, 单位毫秒
- max-wait: 60000
- # 申请连接的时候检测, 如果空闲时间大于 timeBetweenEvictionRunsMillis, 执行 validationQuery 检测连接是否有效.
- test-while-idle: true
- # 既作为检测的间隔时间又作为 testWhileIdel 执行的依据
- time-between-connect-error-millis: 60000
- # 销毁线程时检测当前连接的最后活动时间和当前时间差大于该值时, 关闭当前连接
- min-evictable-idle-time-millis: 30000
- # 用来检测连接是否有效的 sql 必须是一个查询语句
- # MySQL 中为 select 'x'
- # oracle 中为 select 1 from dual
- validation-query: select 'x'
- # 申请连接时会执行 validationQuery 检测连接是否有效, 开启会降低性能, 默认为 true
- test-on-borrow: false
- # 归还连接时会执行 validationQuery 检测连接是否有效, 开启会降低性能, 默认为 true
- test-on-return: false
- # 是否缓存 preparedStatement,mysql5.5 + 建议开启
- pool-prepared-statements: true
- # 当值大于 0 时 poolPreparedStatements 会自动修改为 true
- max-pool-prepared-statement-per-connection-size: 20
- # 合并多个 DruidDataSource 的监控数据
- use-global-data-source-stat: false
- # 配置扩展插件
- filters: stat,wall,slf4j
- # 通过 connectProperties 属性来打开 mergeSql 功能; 慢 SQL 记录
- connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
- # 定时输出统计信息到日志中, 并每次输出日志会导致清零 (reset) 连接池相关的计数器.
- time-between-log-stats-millis: 300000
- # 配置 DruidStatFilter
- Web-stat-filter:
- enabled: true
- url-pattern: '/*'
- exclusions: '*.js,*.gif,*.jpg,*.bmp,*.png,*.CSS,*.ico,/druid/*'
- # 配置 DruidStatViewServlet
- stat-view-servlet:
- # 是否启用 StatViewServlet(监控页面)默认值为 false(考虑到安全问题默认并未启动, 如需启用建议设置密码或白名单以保障安全)
- enabled: true
- url-pattern: '/druid/*'
- # IP 白名单(没有配置或者为空, 则允许所有访问)
- allow: 127.0.0.1,192.168.0.1
- # IP 黑名单 (存在共同时, deny 优先于 allow)
- deny: 192.168.0.128
- # 禁用 html 页面上的 "Reset All" 功能
- reset-enable: false
- # 登录名
- login-username: admin
- # 登录密码
- login-password: admin
相关配置的含义已经写在注释中了, 这里有一点要讲一下, 当我们要配置统计信息(包括监控信息)
time-between-log-stats-millis
输出至日志中, 合并多个 DruidDataSource 的监控数据
use-global-data-source-stat
不可开启, 否则启动会报错.
spring.datasource.druid.filters
: 因为 Druid 的扩展是通过 Filter 插件的形式来开启的, 这里我们开启了 stat 和 wall , 这俩个分别为监控和防御 SQL 注入攻击. Druid 还提供了一些其他默认的 Filter , 如下表:
Filter 类名 | 别名 |
---|---|
default | com.alibaba.druid.filter.stat.StatFilter |
stat | com.alibaba.druid.filter.stat.StatFilter |
mergeStat | com.alibaba.druid.filter.stat.MergeStatFilter |
encoding | com.alibaba.druid.filter.encoding.EncodingConvertFilter |
log4j | com.alibaba.druid.filter.logging.Log4jFilter |
log4j2 | com.alibaba.druid.filter.logging.Log4j2Filter |
slf4j | com.alibaba.druid.filter.logging.Slf4jLogFilter |
commonlogging | com.alibaba.druid.filter.logging.CommonsLogFilter |
wall | com.alibaba.druid.wall.WallFilter |
从名称上可以看出来, 主要是一些编码和日志的相关 Filter .
3.3 数据库密码加密
在生产环境中, 直接在配置文件中暴露明文密码是一件非常危险的事情, 出于两点考虑: 对外, 即使应用服务被入侵, 数据库还是安全的; 对内, 生产环境的数据库密码理论上应该只有 dba 知道, 但是代码都是在代码仓库中放着的, 如果密码没有加密, 每次发布前 dba 都需要手动修改配置文件后再进行打包编译.
首先, 我们需要生成数据库密码的密文, 需要在命令行中执行如下命令:
java -cp druid-1.0.16.jar com.alibaba.druid.filter.config.ConfigTools you_password
输出如下:
- privateKey:MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAh12hnaZuMe76Yb4pi7ogSAEMOcavmz7Blo8DYxeipxeZQhnrXngxc0gAQ6ORlofLWtDm6S7bI7wfDT2EFy/2DwIDAQABAkABMRjYK3vy4pi/vY3eFhBssd2qsI4hPsczjSTJfY7IC9Dc1f7g0axTM6Cx68tRUwv0rSnUiJ5EcDEhuD0JusSZAiEAwX1HpCTq8QgBV1WriHQC7Cd/9Qqp1V4yJeA/jdvXhbsCIQCzGS6wdTQCXDZKLvjRLeSUyTmmIqV/wckqdnpMUZ2BvQIgBIamr1tBt6OlTGKvoYB9NQLzhkrakCgk6ifltK7IytMCIBIbf67zipiafhqt+RYdD7lDRwLXCeiKzS3v4JmKvuP5AiEAr+zqD6sdXv7rWjqu50n+LXbWtNP/M4JzzO1mJOHEhoE=
- publicKey:MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAIddoZ2mbjHu+mG+KYu6IEgBDDnGr5s+wZaPA2MXoqcXmUIZ6154MXNIAEOjkZaHy1rQ5uku2yO8Hw09hBcv9g8CAwEAAQ==
- password:Y464AerH8tabxQg5DlkUej6gQ64KY73ahgiPyaB0vguLBLjUEEkVu6VBueiXxcnMfVjh1Nbd+lJNUTnS1a3/xg==
这里我们需要将生成的公钥 publicKey 和密码 password 加入配置文件中, application-decrypt.YAML 如下:
代码清单: spring-boot-jpa-druid/src/main/resources/application-decrypt.YAML
- spring:
- datasource:
- type: com.alibaba.druid.pool.DruidDataSource
- url: jdbc:MySQL://192.168.0.128:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false
- username: root
- # 加密后密文, 原密码为 123456
- password: Y464AerH8tabxQg5DlkUej6gQ64KY73ahgiPyaB0vguLBLjUEEkVu6VBueiXxcnMfVjh1Nbd+lJNUTnS1a3/xg==
- driverClassName: com.MySQL.cj.jdbc.Driver
- druid:
- filter:
- config:
- enabled: true
- connection-properties: config.decrypt=true;config.decrypt.key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAIddoZ2mbjHu+mG+KYu6IEgBDDnGr5s+wZaPA2MXoqcXmUIZ6154MXNIAEOjkZaHy1rQ5uku2yO8Hw09hBcv9g8CAwEAAQ==
- # 剩余配置省略
已省略部分配置, 有需要的读者可以访问 GitHub 仓库获取.
3.4 配置文件 application.YAML 如下:
代码清单: spring-boot-jpa-druid/src/main/resources/application.YAML
server: port: 8080 spring: application: name: spring-boot-jpa-druid profiles: active: decrypt jpa: database: MySQL show-sql: true generate-ddl: true database-platform: org.hibernate.dialect.MySQL5InnoDBDialect hibernate: ddl-auto: update properties: hibernate: format_sql: true
其余的测试代码同上一篇文章 《Spring Boot (三): ORM 框架 JPA 与连接池 Hikari》 https://www.geekdigging.com/2019/09/19/2405775053/ , 有兴趣的读者可以访问 GitHub 仓库获取, 笔者这里就不一一列举了.
4. 测试
我们在主配置文件中, 选择密码加密的配置文件启动, 将 spring.profiles.active 配置为 decrypt , 点击启动, 可以看到工程正常启动, 查看控制台输出日志, 其中有这么一句:
2019-09-22 21:21:54.501 INFO 16972 --- [-Log-1465691120] c.a.d.p.DruidDataSourceStatLoggerImpl : {"url":"jdbc:mysql://192.168.0.128:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false","dbType":"mysql","name":"DataSource-1465691120","activeCount":0,"poolingCount":3,"poolingPeak":3,"poolingPeakTime":"2019-09-22 21:21:54","connectCount":0,"closeCount":0,"physicalConnectCount":3}
可以看到, 我们配置的监控信息输出会在系统启动的时候先输出一次, 我们在配置文件中配置的是每 5 分钟输出一次, 等十分钟看一下控制台的输出信息, 结果如下:
2019-09-22 21:26:54.503 INFO 16972 --- [-Log-1465691120] c.a.d.p.DruidDataSourceStatLoggerImpl : { "url":"jdbc:mysql://192.168.0.128:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false","dbType":"mysql","name":"DataSource-1465691120","activeCount":0,"activePeak":1,"activePeakTime":"2019-09-22 21:21:54","poolingCount":3,"poolingPeak":3,"poolingPeakTime":"2019-09-22 21:21:54","connectCount":2,"closeCount":2,"connectionHoldTimeHistogram":[0,0,2] } 2019-09-22 21:31:54.505 INFO 16972 --- [-Log-1465691120] c.a.d.p.DruidDataSourceStatLoggerImpl : { "url":"jdbc:mysql://192.168.0.128:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false","dbType":"mysql","name":"DataSource-1465691120","activeCount":0,"poolingCount":3,"connectCount":0,"closeCount":0 } 2019-09-22 21:36:54.505 INFO 16972 --- [-Log-1465691120] c.a.d.p.DruidDataSourceStatLoggerImpl : { "url":"jdbc:mysql://192.168.0.128:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false","dbType":"mysql","name":"DataSource-1465691120","activeCount":0,"poolingCount":3,"connectCount":0,"closeCount":0 }
从时间上可以看出, 确实是每 5 分钟会输出一次.
打开浏览器访问: http://localhost/ :8080/druid/ , 查看 Druid 监控页面, 结果如图:
我们可以进行一些接口测试, 在查看监控页面, 可以看到所有的 SQL 都正常记录, 如图:
同时, 我们看一下后台的日志打印, 是否正常打出记录的日志, 截取打印部分, 如下:
2019-09-22 21:51:54.506 INFO 16972 --- [-Log-1465691120] c.a.d.p.DruidDataSourceStatLoggerImpl : {"url":"jdbc:mysql://192.168.0.128:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false","dbType":"mysql","name":"DataSource-1465691120","activeCount":0,"activePeak":1,"activePeakTime":"2019-09-22 21:47:28","poolingCount":3,"poolingPeak":3,"poolingPeakTime":"2019-09-22 21:47:28","connectCount":4,"closeCount":4,"executeCount":4,"commitCount":4,"pstmtCacheHitCount":2,"pstmtCacheMissCount":2,"startTransactionCount":4,"transactionHistogram":[0,1,2,1],"connectionHoldTimeHistogram":[0,1,0,3],"sqlList":[{"sql":"insert into user (age, nick_name, id) values (?, ?, ?)","executeCount":2,"executeMillisMax":1,"executeMillisTotal":2,"executeHistogram":[1,1],"executeAndResultHoldHistogram":[1,1],"concurrentMax":1,"updateCount":2,"updateCountMax":1,"updateHistogram":[0,2],"inTransactionCount":2},{"sql":"select usermodel0_.id as id1_0_, usermodel0_.age as age2_0_, usermodel0_.nick_name as nick_nam3_0_ from user usermodel0_ order by usermodel0_.id desc","executeCount":2,"executeMillisMax":3,"executeMillisTotal":4,"executeHistogram":[0,2],"executeAndResultHoldHistogram":[2],"concurrentMax":1,"fetchRowCount":4,"fetchRowCountMax":2,"fetchRowHistogram":[0,2],"inTransactionCount":2}]}
可以看到, 日志中打印了我们执行的 SQL 相关的信息, 和我们在监控页面看到的信息完全一致.
至此, 测试成功, 篇幅原因, 一些测试过程未列出, 各位感兴趣的读者朋友可以自己动手尝试一下.
5. 示例代码
示例代码 - GitHub
示例代码 - Gitee
6. 参考
《Druid 官方文档》 https://github.com/alibaba/druid/wiki
来源: http://www.tuicool.com/articles/ruYB3en