51

Spring Boot (四): Druid 连接池密码加密与监控

 4 years ago
source link: https://www.tuicool.com/articles/ruYB3en
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

iQRfM3E.png!web

在上一篇文章 《Spring Boot (三): ORM 框架 JPA 与连接池 Hikari》 我们介绍了 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 ,当然是推荐各位读者使用第二种方式引入依赖。

eYFj6r2.png!web

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.yml 如下:

代码清单:spring-boot-jpa-druid/src/main/resources/application-pass.yml

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 插件的形式来开启的,这里我们开启了 statwall ,这俩个分别为监控和防御 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.yml 如下:

代码清单:spring-boot-jpa-druid/src/main/resources/application-decrypt.yml

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.yml 如下:

代码清单:spring-boot-jpa-druid/src/main/resources/application.yml

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》 ,有兴趣的读者可以访问 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 监控页面,结果如图:

BVNvAn3.png!web

我们可以进行一些接口测试,在查看监控页面,可以看到所有的 SQL 都正常记录,如图:

mQZVNvr.png!web

6f6zIbM.png!web

Z32Yneu.png!web

同时,我们看一下后台的日志打印,是否正常打出记录的日志,截取打印部分,如下:

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 官方文档》

jqAJvaM.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK