Druid实现简单数据库连接池监控

1. Druid简单介绍

   Druid是阿里巴巴开源的数据库连接池,号称是Java语言中最好的数据库连接池,能够提供强大的监控和扩展功能。
   GitHub地址:https://github.com/alibaba/druid

优点:
   ① 可以监控数据库访问性能,Druid内置提供了一个功能强大的StatFilter插件,能够详细统计SQL的执行性能,这对于线上分析数据库访问性能有帮助。
   ② 替换DBCP和C3P0,Druid提供了一个高效、功能强大、可扩展性好的数据库连接池。
   ③ 数据库密码加密。直接把数据库密码写在配置文件中,这是不好的行为,容易导致安全问题。DruidDriver和DruidDataSource都支持PasswordCallback。
   ④ SQL执行日志,Druid提供了不同的LogFilter,能够支持Common-Logging、Log4j和JdkLog,你可以按需要选择相应的LogFilter,监控你应用的数据库访问情况。
   ⑤ 扩展JDBC,如果你要对JDBC层有编程的需求,可以通过Druid提供的Filter-Chain机制,很方便编写JDBC层的扩展插件。

2. druid基础配置

2.1 创建基本的SpringBoot项目

此次使用的是基于Mysql与Postgresql的双数据源搭建的demo。(双数据源搭建查看上一篇博客内容)

2.2 导入jar

   之后导入druid.jar。Druid 0.1.18之后版本都发布到maven中央仓库中,所以你只需要在项目的pom.xml中加上dependency就可以了。

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
<!--druid-demo所需jar,本demo使用的是log4j来做日志记录-->
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>

<!--需要将Spring-boot中去掉logback的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

<!--日志-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

2.3 设置druid相关配置

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
spring:
# MySQL connection config
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/yq_mysql?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false
username: root
password: admin
# Druid 数据源专用配置
# 初始化大小,最小,最大
initialSize: 3
minIdle: 5
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 30000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
maxEvictableIdleTimeMillis: 900000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打开PSCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙,使用的日志组件为log4j2
filters: stat,wall,log4j2
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录.合并多个DruidDataSource的监控数据
#useGlobalDataSourceStat: true
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=10000
# Thymeleaf configurations
thymeleaf:
mode: HTML
cache: false
servlet.content-type: text/html
encoding: UTF-8
# jpa configurations
jpa:
#配置指明在程序启动的时候要删除并且创建实体类对应的表。这个参数很危险,因为他会把对应的表删除掉然后重建。所以千万不要在生成环境中使用。只有在测试环境中,一开始初始化数据库结构的时候才能使用一次。过后使用update
#hibernate.ddl-auto: create
hibernate.ddl-auto: update
#默认的存储引擎切换为 InnoDB
database-platform: org.hibernate.dialect.MySQL57InnoDBDialect
# 配置在日志中打印出执行的 SQL 语句信息。
show-sql: true
pgdatasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/yq_pgsql?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: postgres
password: admin
servlet:
multipart:
max-file-size: 100MB
max-request-size: 200MB

2.4 配置Druid监控统计功能

   基于Druid的Filter-Chain扩展机制,Druid提供了3个非常有用的具有监控统计功能的Filter:

StatFilter用于统计监控信息;
WallFilter基于SQL语义分析来实现防御SQL注入攻击;
LogFilter 用于输出JDBC执行的日志。

如果在项目中需要使用Druid提供的这些监控统计功能,可以通过以下两种途径进行配置。
①方式一:基于Servlet 注解的配置
   对于使用Servlet 3.0的项目,在启动类上加上注解@ServletComponentScan 启用Servlet自动扫描,并在自定义的DruidStatViewServlet/DruidStatFilter 上分别加上注解@WebServlet/@WebFilter 使其能够被自动发现。

DruidStatViewServlet.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* druid数据源状态监控.
*/
import com.alibaba.druid.support.http.StatViewServlet;

import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;

@WebServlet(urlPatterns = "/druid/*",initParams = {
//IP白名单 没有配置或者为空则允许所有访问
@WebInitParam(name="allow",value = "127.0.0.1"),
// IP黑名单 存在共同时,deny优先于allow
@WebInitParam(name = "deny", value = "192.168.1.10"),
// 用户名
@WebInitParam(name = "loginUsername", value = "root"),
// 密码
@WebInitParam(name = "loginPassword", value = "123"),
// 禁用HTML页面上的“Reset All”功能
@WebInitParam(name = "resetEnable", value = "false")
})
public class DruidStatViewServlet extends StatViewServlet {
}

DruidStatFilter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* druid过滤器
* /**
* WebStatFilter用于采集web-jdbc关联监控的数据。
* 属性filterName声明过滤器的名称,可选
* 属性urlPatterns指定要过滤 的URL模式,也可使用属性value来声明.(指定要过滤的URL模式是必选属性)
*/
import com.alibaba.druid.support.http.WebStatFilter;

import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;

@WebFilter(filterName = "druidWebStatFilter",urlPatterns = "/*",initParams = {
// 忽略资源
@WebInitParam(name = "exclusions", value = "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*")
})
public class DruidStatFilter extends WebStatFilter {
}

之后在启动类加上@ServletComponentScan:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.web.servlet.ServletComponentScan;

//暂时关闭Spring 自带的Security用户认证
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
@ServletComponentScan
public class DruidApplication {

public static void main(String[] args) {
SpringApplication.run(DruidApplication.class, args);
}

}

②方式二
   使用Spring的注解@Bean对自定义的ServletFilter进行注册,Servlet使用ServletRegistrationBean进行注册,Filter使用FilterRegistrationBean进行注册。

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
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;

@SpringBootConfiguration
public class DruidMonitorConfig {
@Bean
public ServletRegistrationBean servletRegistrationBean() {
System.out.println("init Druid Monitor Servlet ...");
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(),
"/druid/*");
// IP白名单
servletRegistrationBean.addInitParameter("allow", "127.0.0.1");
// IP黑名单(共同存在时,deny优先于allow)
servletRegistrationBean.addInitParameter("deny", "192.168.1.10");
// 控制台管理用户
servletRegistrationBean.addInitParameter("loginUsername", "root");
servletRegistrationBean.addInitParameter("loginPassword", "123");
// 是否能够重置数据 禁用HTML页面上的“Reset All”功能
servletRegistrationBean.addInitParameter("resetEnable", "false");
return servletRegistrationBean;
}

@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}

此demo采用的第二种方式(注释启动类的//@ServletComponentScan)。

3. Druid使用log4j2进行日志输出

3.1 pom.xml中springboot版本依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!--Spring-boot中去掉logback的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

<!--日志-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

<!--数据库连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.6</version>
</dependency>

3.2 log4j2.xml文件中的日志配置(完整,可直接拷贝使用)

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
112
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="OFF">
<appenders>

<Console name="Console" target="SYSTEM_OUT">
<!--只接受程序中DEBUG级别的日志进行处理-->
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="[%d{HH:mm:ss.SSS}] %-5level %class{36} %L %M - %msg%xEx%n"/>
</Console>

<!--处理DEBUG级别的日志,并把该日志放到logs/debug.log文件中-->
<!--打印出DEBUG级别日志,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
<RollingFile name="RollingFileDebug" fileName="./logs/debug.log"
filePattern="logs/$${date:yyyy-MM}/debug-%d{yyyy-MM-dd}-%i.log.gz">
<Filters>
<ThresholdFilter level="DEBUG"/>
<ThresholdFilter level="INFO" onMatch="DENY" onMismatch="NEUTRAL"/>
</Filters>
<PatternLayout
pattern="[%d{yyyy-MM-dd HH:mm:ss}] %-5level %class{36} %L %M - %msg%xEx%n"/>
<Policies>
<SizeBasedTriggeringPolicy size="500 MB"/>
<TimeBasedTriggeringPolicy/>
</Policies>
</RollingFile>

<!--处理INFO级别的日志,并把该日志放到logs/info.log文件中-->
<RollingFile name="RollingFileInfo" fileName="./logs/info.log"
filePattern="logs/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log.gz">
<Filters>
<!--只接受INFO级别的日志,其余的全部拒绝处理-->
<ThresholdFilter level="INFO"/>
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL"/>
</Filters>
<PatternLayout
pattern="[%d{yyyy-MM-dd HH:mm:ss}] %-5level %class{36} %L %M - %msg%xEx%n"/>
<Policies>
<SizeBasedTriggeringPolicy size="500 MB"/>
<TimeBasedTriggeringPolicy/>
</Policies>
</RollingFile>

<!--处理WARN级别的日志,并把该日志放到logs/warn.log文件中-->
<RollingFile name="RollingFileWarn" fileName="./logs/warn.log"
filePattern="logs/$${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log.gz">
<Filters>
<ThresholdFilter level="WARN"/>
<ThresholdFilter level="ERROR" onMatch="DENY" onMismatch="NEUTRAL"/>
</Filters>
<PatternLayout
pattern="[%d{yyyy-MM-dd HH:mm:ss}] %-5level %class{36} %L %M - %msg%xEx%n"/>
<Policies>
<SizeBasedTriggeringPolicy size="500 MB"/>
<TimeBasedTriggeringPolicy/>
</Policies>
</RollingFile>

<!--处理error级别的日志,并把该日志放到logs/error.log文件中-->
<RollingFile name="RollingFileError" fileName="./logs/error.log"
filePattern="logs/$${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log.gz">
<ThresholdFilter level="ERROR"/>
<PatternLayout
pattern="[%d{yyyy-MM-dd HH:mm:ss}] %-5level %class{36} %L %M - %msg%xEx%n"/>
<Policies>
<SizeBasedTriggeringPolicy size="500 MB"/>
<TimeBasedTriggeringPolicy/>
</Policies>
</RollingFile>

<!--druid的日志记录追加器-->
<RollingFile name="druidSqlRollingFile" fileName="./logs/druid-sql.log"
filePattern="logs/$${date:yyyy-MM}/api-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}] %-5level %L %M - %msg%xEx%n"/>
<Policies>
<SizeBasedTriggeringPolicy size="500 MB"/>
<TimeBasedTriggeringPolicy/>
</Policies>
</RollingFile>
</appenders>

<loggers>
<root level="DEBUG">
<appender-ref ref="Console"/>
<appender-ref ref="RollingFileInfo"/>
<appender-ref ref="RollingFileWarn"/>
<appender-ref ref="RollingFileError"/>
<appender-ref ref="RollingFileDebug"/>
</root>

<!--记录druid-sql的记录-->
<logger name="druid.sql.Statement" level="debug" additivity="false">
<appender-ref ref="druidSqlRollingFile"/>
</logger>
<logger name="druid.sql.Statement" level="debug" additivity="false">
<appender-ref ref="druidSqlRollingFile"/>
</logger>

<!--log4j2 自带过滤日志-->
<Logger name="org.apache.catalina.startup.DigesterFactory" level="error" />
<Logger name="org.apache.catalina.util.LifecycleBase" level="error" />
<Logger name="org.apache.coyote.http11.Http11NioProtocol" level="warn" />
<logger name="org.apache.sshd.common.util.SecurityUtils" level="warn"/>
<Logger name="org.apache.tomcat.util.net.NioSelectorPool" level="warn" />
<Logger name="org.crsh.plugin" level="warn" />
<logger name="org.crsh.ssh" level="warn"/>
<Logger name="org.eclipse.jetty.util.component.AbstractLifeCycle" level="error" />
<Logger name="org.hibernate.validator.internal.util.Version" level="warn" />
<logger name="org.springframework.boot.actuate.autoconfigure.CrshAutoConfiguration" level="warn"/>
<logger name="org.springframework.boot.actuate.endpoint.jmx" level="warn"/>
<logger name="org.thymeleaf" level="warn"/>
</loggers>
</configuration>

4.3 配置application.yml

1
2
3
4
5
6
7
8
9
10
11
# 配置日志输出
spring:
datasource:
druid:
filter:
slf4j:
enabled=true
statement-create-after-log-enabled=false
statement-close-after-log-enabled=false
result-set-open-after-log-enabled=false
result-set-close-after-log-enabled=false

4. 测试与运行

4.1 相关实体类创建

   相关数据库表、数据创建,以及相关dao,service,controller创建。(本demo采用上一个博客的相关类)
   系统结构目录如下:

4.2 测试

swagger上查询结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
{
"id": 1,
"name": "游强",
"account": "yq",
"password": "admin"
},
{
"id": 2,
"name": "秦音",
"account": "qy",
"password": "admin"
}
]

然后访问:http://localhost:8081/druid/

大概就是下面这样的图:

控制日志记录如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2020-06-18 00:11:15.948  INFO 34916 --- [nio-8081-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-06-18 00:11:15.948 INFO 34916 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-06-18 00:11:15.957 INFO 34916 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 8 ms
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@68197e8b] was not registered for synchronization because synchronization is not active
2020-06-18 00:11:16.030 INFO 34916 --- [nio-8081-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
JDBC Connection [org.postgresql.jdbc.PgConnection@5d7d2a16] will not be managed by Spring
==> Preparing: select * from yq_user where id = ?
==> Parameters: 1(Long)
<== Columns: id, name, account, password
<== Row: 1, 游强, yq, admin
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@68197e8b]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7f98edcf] was not registered for synchronization because synchronization is not active
2020-06-18 00:11:16.691 INFO 34916 --- [nio-8081-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-2} inited
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@12d550e0] will not be managed by Spring
==> Preparing: select * from yq_user where id = ?
==> Parameters: 2(Long)
<== Columns: id, name, account, password
<== Row: 2, 秦音, qy, admin
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7f98edcf]

5. druid数据库密码加密

   本demo只对主数据源MySQL进行了密码加密,对从数据源密码步骤类似。

5.1 密码加密

5.1.1 通过druid-1.0.18.jar提供的ConfigTools工具对密码进行加密:

   在druid所在的目录下打开cmd窗口:

1
2
3
4
5
6
7
#格式:java -cp druid.jar com.alibaba.druid.filter.config.ConfigTools you_password
F:\Download>java -cp druid-1.1.9.jar com.alibaba.druid.filter.config.ConfigTools admin
privateKey:MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAwB/rXIuWe5IHbIZOC9+VB4iRuBow0ca3pztVSIYtJjgkCSbWVN4fWzjfeO54DY9GKhkIufCTE/T+Fgci+UjtRQIDAQABAkBh2uAiDubioYoueGmgGozpfWHbB1v+PNylzM6vVcgBQouOqd0T1/tqs1IxCATSt+vvJ5BKsbgRPWO+nJAHqJyBAiEA+M3jVoGlsHA65j1JlqqPuHk7MXNIhwlajNEYq9F2l00CIQDFrmI9XZDZp0wpm6Vy9YGSuqAvLdqcwMh7LY2g3+jh2QIgDIBj5OvcxHHPM9RuhyiI0i8dP03YnhhlOWAkSjXbLJ0CIQCtsscbyMVomrovrVY5p0PNnDL4gcAgEL2YjrRt8ZF+MQIgOjRYMqQxMLKIzS1JBwjxK3X54xVbyLwgm0DNKlrXJ48=
publicKey:MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAMAf61yLlnuSB2yGTgvflQeIkbgaMNHGt6c7VUiGLSY4JAkm1lTeH1s433jueA2PRioZCLnwkxP0/hYHIvlI7UUCAwEAAQ==
password:UDwkTJKXPXEIPjlYFnLKNJ5Tq5mWmEV5yLuENS8LqdAsb5QavQoM6jF3gQKgxmTX5GcXz5Ncxw5XknVQyab3kA==

F:\Download>
5.1.2 配置文件配置加密解密参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
datasource:
name: MySQL
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/yq_mysql?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false
username: root
# 修改密码为密文
password: UDwkTJKXPXEIPjlYFnLKNJ5Tq5mWmEV5yLuENS8LqdAsb5QavQoM6jF3gQKgxmTX5GcXz5Ncxw5XknVQyab3kA==
# 公钥
publickey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAMAf61yLlnuSB2yGTgvflQeIkbgaMNHGt6c7VUiGLSY4JAkm1lTeH1s433jueA2PRioZCLnwkxP0/hYHIvlI7UUCAwEAAQ==
# 配置 connection-properties,启用ConfigFilter解密密码,以及配置公钥${publickey}
connection-properties: config.decrypt=true;config.decrypt.key=${spring.datasource.publickey};password=${spring.datasource.password}
druid:
filter:
config:
# 启动ConfigFilter
enabled: true
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙,在原来的filters后面添加config,使用逗号分隔
filters: stat,wall,log4j2,config

   需要用到生成的 publicKey 和 password。而privateKey私钥,用于生成密文密码用,不用管。

5.2 配置ConfigFilter

5.2.1 配置文件从本地文件系统中读取

   SpringBoot默认是从本项目中的soureces目录中读取,故如果是使用本地application.yml文件中的配置参数,下面的代码可以忽略不添加。而且某些配置SpringBoot会自动注入,不需要手动设置。

1
2
3
4
5
6
7
8
9
10
11
try {
Properties properties = new Properties();
properties.load(getClass().getResourceAsStream("/application.yml"));
datasource.setName(properties.getProperty("name"));
datasource.setUsername(properties.getProperty("username"));
datasource.setPassword(properties.getProperty("password"));
datasource.setDriverClassName(properties.getProperty("driverClassName"));
datasource.setUrl(properties.getProperty("url"));
} catch (IOException e) {
e.printStackTrace();
}
5.2.2 配置文件从远程http服务器中读取
1
2
3
4
5
6
7
8
9
10
11
try {
Properties properties = new Properties();
properties.load(getClass().getResourceAsStream("http://127.0.0.1/application.yml"));
datasource.setName(properties.getProperty("name"));
datasource.setUsername(properties.getProperty("username"));
datasource.setPassword(properties.getProperty("password"));
datasource.setDriverClassName(properties.getProperty("driverClassName"));
datasource.setUrl(properties.getProperty("url"));
} catch (IOException e) {
e.printStackTrace();
}

   这种配置方式,使得一个应用集群中,多个实例可以从同一个地方读取配置,集中配置,集中修改,部署更简单。

5.2.3 通过jvm启动参数来使用ConfigFilter

   DruidDataSource支持jvm启动参数配置filters,所以可以:

1
java -Ddruid.filters=./config/application.yml

5.3 手动密码加密解密

   5.1中的加密是通过外部cmd命令生成的密码,5.2是使用druid封装好的解密工具对密文进行解密。但其实也可以通过自定义的代码来实现加密解密。
   其实druid的加密解密都是通过ConfigToolsTest.java文件中的encrypt和decrypt方法来实现的。故只要单独调用这两个方法,即可实现自定义加密解密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws Exception {
String password = "admin";
//使用ConfigTools生成一组公私钥
String[] keyPair = ConfigTools.genKeyPair(512);
//获取公钥
System.out.println("privateKey:" + keyPair[0]);
//获取私钥
System.out.println("publicKey:" + keyPair[1]);
//通过公钥+密码明文,对密码进行加密
System.out.println("password:" + ConfigTools.encrypt(keyPair[0], password));

//然后通过上面得到的密码密文+公钥,解密密码获得明文
String pwd = "n2giOipoUromAsrEUFApiG15LOKwvyp+5tumwlnQz2NiMs/vtqmkzkAb5cQmACqIKTEFhBBu4GuR/ggtdbYxPw==";
String pub = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANNhfjPiJkaOd07kp0F9ut92rwMWNO6zRkHHCBNiGpIPr6OKG9OvDcnsTyQ4d9hVINIIRwd+NQ4NF76AgijZ7r0CAwEAAQ==";
String decrypt = ConfigTools.decrypt(pub, pwd);
System.out.println(decrypt);
}

   可以将手动生成的密码密文与公钥写入配置文件,然后在创建dataSource这个bean的时候,手动解密写入dataSource中。

5.4 相关问题记录

5.4.1 配置 connection-properties 失误

druid官网上的配置是:

1
connection-properties: config.decrypt=true;config.decrypt.key=${publickey}

但这样配置会报下面的错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
2020-06-18 14:41:39.925 ERROR 6388 --- [nio-8081-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.lang.IllegalArgumentException: Failed to decrypt.
### The error may exist in file [D:\work\code\qinyin\druid\target\classes\mapper\MysqlUserMapper.xml]
### The error may involve com.example.druid.mapper.mysqlMapper.MysqlUserMapper.getOne
### The error occurred while executing a query
### Cause: java.lang.IllegalArgumentException: Failed to decrypt.] with root cause

java.lang.IllegalArgumentException: Illegal character $
at com.alibaba.druid.util.Base64.base64toInt(Base64.java:174) ~[druid-1.1.9.jar:1.1.9]
at com.alibaba.druid.util.Base64.base64ToByteArray(Base64.java:140) ~[druid-1.1.9.jar:1.1.9]
at com.alibaba.druid.util.Base64.base64ToByteArray(Base64.java:107) ~[druid-1.1.9.jar:1.1.9]
at com.alibaba.druid.filter.config.ConfigTools.getPublicKey(ConfigTools.java:94) ~[druid-1.1.9.jar:1.1.9]
at com.alibaba.druid.filter.config.ConfigFilter.getPublicKey(ConfigFilter.java:223) ~[druid-1.1.9.jar:1.1.9]
at com.alibaba.druid.filter.config.ConfigFilter.decrypt(ConfigFilter.java:195) ~[druid-1.1.9.jar:1.1.9]

   目前不清楚是自己跟官网上的其他配置有出入还是官网错误,暂时没找到原因。但我使用下面的配置,就可以正常访问或查询数据:

1
connection-properties: config.decrypt=true;config.decrypt.key=${spring.datasource.publickey};password=${spring.datasource.password}
5.4.2 配置不写入配置文件,而是写入代码中

   如果connection-properties: config.decrypt=true;config.decrypt.key=${spring.datasource.publickey};password=${spring.datasource.password}这个配置不写入配置文件,而是在代码中体现:

1
2
3
4
5
6
7
8
9
try {
Properties properties = new Properties();
properties.load(getClass().getResourceAsStream("/application.yml"));
//config.decrypt=true;config.decrypt.key=${spring.datasource.publickey};password=${spring.datasource.password}
String connectionProperties = "config.decrypt=true;" + "config.decrypt.key=" + publickey + ";" + "password=" + password;
datasource.setConnectionProperties(connectionProperties);
} catch (IOException e) {
e.printStackTrace();
}

会报下面的错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2020-06-18 14:38:14.990 ERROR 24840 --- [reate-248705782] com.alibaba.druid.pool.DruidDataSource   : create connection SQLException, url: jdbc:mysql://localhost:3306/yq_mysql?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false, errorCode 1045, state 28000

java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:965) ~[mysql-connector-java-5.1.47.jar:5.1.47]
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3978) ~[mysql-connector-java-5.1.47.jar:5.1.47]
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3914) ~[mysql-connector-java-5.1.47.jar:5.1.47]
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:871) ~[mysql-connector-java-5.1.47.jar:5.1.47]
at com.mysql.jdbc.MysqlIO.proceedHandshakeWithPluggableAuthentication(MysqlIO.java:1714) ~[mysql-connector-java-5.1.47.jar:5.1.47]
at com.mysql.jdbc.MysqlIO.doHandshake(MysqlIO.java:1224) ~[mysql-connector-java-5.1.47.jar:5.1.47]
at com.mysql.jdbc.ConnectionImpl.coreConnect(ConnectionImpl.java:2199) ~[mysql-connector-java-5.1.47.jar:5.1.47]
at com.mysql.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:2230) ~[mysql-connector-java-5.1.47.jar:5.1.47]
at com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2025) ~[mysql-connector-java-5.1.47.jar:5.1.47]
at com.mysql.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:778) ~[mysql-connector-java-5.1.47.jar:5.1.47]
at com.mysql.jdbc.JDBC4Connection.<init>(JDBC4Connection.java:47) ~[mysql-connector-java-5.1.47.jar:5.1.47]
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.8.0_191]
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:1.8.0_191]
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.8.0_191]
at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[na:1.8.0_191]
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) ~[mysql-connector-java-5.1.47.jar:5.1.47]

6. 问题记录

如果在项目中使用的是log4j而不是log4j2,那么可能会出现如下警告:

6.1 log4j警告

在项目运行过程中,有log4j配置缺失警告:

1
2
log4j:WARN No appenders could be found for logger (druid.sql.Connection).
log4j:WARN Please initialize the log4j system properly.

   该警告不影响项目正常运行,但如果想要消除警告,有多种方式:
   ①手动在resources目录下创建log4j.properties配置文件来指定相关log4j参数(网上找的答案,但手动操作过,发现没有效果。也有说是因为SpringBoot中含有logback这个依赖,需要将该依赖解除,自定义的log4j.properties配置才会生效。但也亲自操作过,也没有效果,可能是操作过程中有失误)。
   ②强制在启动类中设置日志缺失环境

1
2
3
4
public static void main(String[] args) {
BasicConfigurator.configure(); //自动快速地使用缺省Log4j环境
SpringApplication.run(DruidApplication.class, args);
}

   虽然这种方式解决log4j的警告,但个人感觉日志显示的级别过于详细。且只对主数库MySQL进行详细记录,但对PostgreSQL只是简单的记录。对该日志级别设置暂时没找到方法。

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
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@f91ab56] was not registered for synchronization because synchronization is not active
2020-06-17 22:08:06.988 INFO 32188 --- [nio-8081-exec-6] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
JDBC Connection [org.postgresql.jdbc.PgConnection@136e7f5c] will not be managed by Spring
==> Preparing: select * from yq_user where id = ?
==> Parameters: 1(Long)
<== Columns: id, name, account, password
<== Row: 1, 游强, yq, admin
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@f91ab56]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1428c0e1] was not registered for synchronization because synchronization is not active
0 [http-nio-8081-exec-6] DEBUG druid.sql.Connection - {conn-110001} connected
7 [http-nio-8081-exec-6] DEBUG druid.sql.Connection - {conn-110002} connected
13 [http-nio-8081-exec-6] DEBUG druid.sql.Connection - {conn-110003} connected
2020-06-17 22:08:07.569 INFO 32188 --- [nio-8081-exec-6] com.alibaba.druid.pool.DruidDataSource : {dataSource-2} inited
15 [http-nio-8081-exec-6] DEBUG druid.sql.Connection - {conn-110003} pool-connect
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@2713ea39] will not be managed by Spring
==> Preparing: select * from yq_user where id = ?
134 [http-nio-8081-exec-6] DEBUG druid.sql.Statement - {conn-110003, pstmt-120000} created. select * from yq_user where id = ?
==> Parameters: 2(Long)
143 [http-nio-8081-exec-6] DEBUG druid.sql.Statement - {conn-110003, pstmt-120000} Parameters : [2]
143 [http-nio-8081-exec-6] DEBUG druid.sql.Statement - {conn-110003, pstmt-120000} Types : [BIGINT]
143 [http-nio-8081-exec-6] DEBUG druid.sql.Statement - {conn-110003, pstmt-120000} executed. 5.6849 millis. select * from yq_user where id = ?
148 [http-nio-8081-exec-6] DEBUG druid.sql.ResultSet - {conn-110003, pstmt-120000, rs-150000} open
149 [http-nio-8081-exec-6] DEBUG druid.sql.ResultSet - {conn-110003, pstmt-120000, rs-150000} Header: [id, name, account, password]
149 [http-nio-8081-exec-6] DEBUG druid.sql.ResultSet - {conn-110003, pstmt-120000, rs-150000} Result: [2, 秦音, qy, admin]
<== Columns: id, name, account, password
<== Row: 2, 秦音, qy, admin
<== Total: 1
150 [http-nio-8081-exec-6] DEBUG druid.sql.ResultSet - {conn-110003, pstmt-120000, rs-150000} closed
150 [http-nio-8081-exec-6] DEBUG druid.sql.Statement - {conn-110003, pstmt-120000} clearParameters.
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1428c0e1]
151 [http-nio-8081-exec-6] DEBUG druid.sql.Connection - {conn-110003} pool-recycle
900023 [Druid-ConnectionPool-Destroy-486994287] DEBUG druid.sql.Connection - {conn-110001} closed
900024 [Druid-ConnectionPool-Destroy-486994287] DEBUG druid.sql.Connection - {conn-110002} closed
960025 [Druid-ConnectionPool-Destroy-486994287] DEBUG druid.sql.Connection - {conn-110003} closed

6.2 log4j日志配置文件示例

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
#此句为定义名为stdout的输出端是哪种类型,可以是
#org.apache.log4j.ConsoleAppender(控制台),
#org.apache.log4j.FileAppender(文件),
#org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件),
#org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
#org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
#此句为定义名为stdout的输出端的layout是哪种类型,可以是
#org.apache.log4j.HTMLLayout(以HTML表格形式布局),
#org.apache.log4j.PatternLayout(可以灵活地指定布局模式),
#org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串),
#org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
#如果使用pattern布局就要指定的打印信息的具体格式ConversionPattern,打印参数如下:
#%m 输出代码中指定的消息
#%p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL
#%r 输出自应用启动到输出该log信息耗费的毫秒数
#%c 输出所属的类目,通常就是所在类的全名
#%t 输出产生该日志事件的线程名
#%n 输出一个回车换行符,Windows平台为“rn”,Unix平台为“n”
#%d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921
#%l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。
#[QC]是log信息的开头,可以为任意字符,一般为项目简称。
#比如:输出的信息:#[TS] DEBUG [main] AbstractBeanFactory.getBean(189) | Returning cached instance of singleton bean 'MyAutoProxy'
log4j.appender.stdout.layout.ConversionPattern= [QC] %p [%t] %C.%M(%L) | %m%n

#定义名为R的输出端的类型为每天产生一个日志文件。
log4j.appender.R=org.apache.log4j.RollingFileAppender
#此句为定义名为R的输出端的文件名为D:\\Tomcat 5.5\\logs\\qc.log可以自行修改。
log4j.appender.R.File=D:/work/code/qinyin/druid/src/main/resources/druid.log
#log4j.appender.R.File=${catalina.home}/logs/ddoMsg.log
log4j.appender.R.MaxFileSize=1024KB
log4j.appender.R.MaxBackupIndex=100
#与log4j.appender.stdout.layout属性相同
log4j.appender.R.layout=org.apache.log4j.PatternLayout
#与log4j.appender.stdout.layout.ConversionPattern属性相同
log4j.appender.R.layout.ConversionPattern= %d{yyyy-MM-dd HH:mm:ss} %5p %c %t: - %m%n

#INFO WARN ERROR DEBUG
#此句为将等级为INFO的日志信息输出到stdout和R这两个目的地,stdout和R的定义在下面的代码,可以任意起名。(此文本设置的是:stdout为控制台输出,R为文本输出)
# 等级可分为OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL,如果配置OFF则不打出任何信息,
# #如果配置为INFO这样只显示INFO, WARN, ERROR的log信息,而DEBUG信息不会被显示,
log4j.rootLogger=OFF, stdout, R
org.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog

#指定com.example.druid包下的所有类的等级为DEBUG。
log4j.logger.com.example.druid =OFF
#这两句是把这两个包下出现的错误的等级设为ERROR,如果项目中没有配置EHCache,则不需要这两句。
log4j.logger.com.opensymphony.oscache=OFF
log4j.logger.net.sf.navigator=OFF
#这句是displaytag的包。(QC问题列表页面所用)
log4j.logger.org.displaytag=OFF
#此句为Spring的包。
log4j.logger.org.springframework=OFF
#此两句是hibernate的包。
log4j.logger.org.hibernate.ps.PreparedStatementCache=OFF
log4j.logger.org.hibernate=OFF

7. 知识扩展

7.1 线程池介绍

   Java中已经提供了创建线程池的一个类:Executor,而我们创建时,一般使用它的子类:ThreadPoolExecutor

1
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize,  
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

   这是其中最重要的一个构造方法,这个方法决定了创建出来的线程池的各种属性,下面依靠一张图来更好的理解线程池和这几个参数:

   从图中,可以看出,线程池中的corePoolSize就是线程池中的核心线程数量,这几个核心线程,只是在没有用的时候,也不会被回收;
   maximumPoolSize就是线程池中可以容纳的最大线程的数量;
   keepAliveTime就是线程池中除了核心线程之外的其他的最长可以保留的时间,因为在线程池中,除了核心线程即使在无任务的情况下也不能被清除,其余的都是有存活时间的,意思就是非核心线程可以保留的最长的空闲时间;
   util就是计算这个时间的一个单位;
   workQueue就是等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出);
   threadFactory就是创建线程的线程工厂;
   handler是一种拒绝策略,我们可以在任务满了知乎,拒绝执行某些任务。

线程池的执行流程又是怎样的呢?

   从图中可以看出,任务进来时,首先执行判断,判断核心线程是否处于空闲状态,如果不是,核心线程就先就执行任务,如果核心线程已满,则判断任务队列是否有地方存放该任务,若果有,就将任务保存在任务队列中,等待执行,如果满了,在判断最大可容纳的线程数,如果没有超出这个数量,就开创非核心线程执行任务,如果超出了,就调用handler实现拒绝策略。

handler的拒绝策略:
   第一种AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满
   第二种DisCardPolicy:不执行新任务,也不抛出异常
   第三种DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行
   第四种CallerRunsPolicy:直接调用execute来执行当前任务

四种常见的线程池:
   CachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。
   SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。
   SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。
   FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程

参考链接:
Springboot+Druid搭建简单demo
如何在Spring Boot中配置数据库密码加密?