之前的这篇文章介绍了Druid的连接池管理实现,但除了基本的连接管理,Druid还支持监控,代理等功能,便于开发人员分析数据库操作的一些性能指标等。其主要使用Filter机制来实现,本文将介绍下其中的Filter实现。
从Filter接口中,可以看出,Filter主要重新定义了来自Connection,Statement,ResultSet等对象的接口,并且均通过对应的Proxy对象来替代,每个接口中还带有一个FilterChain参数,主要是为了支持Filter链,如:
public interface Filter extends Wrapper {
/**
* 初始化Filter
*/
void init(DataSourceProxy dataSource);
/**
* 销毁Filter
*/
void destroy();
// Connection相关方法
/**
* 创建一个连接
*/
ConnectionProxy connection_connect(FilterChain chain, Properties info) throws SQLException;
/**
* 创建一个Statement代理对象
*/
StatementProxy connection_createStatement(FilterChain chain, ConnectionProxy connection) throws SQLException;
// ...
// Statement相关方法
/**
* 执行查询操作
*/
ResultSetProxy statement_executeQuery(FilterChain chain, StatementProxy statement, String sql) throws SQLException;
/**
* 执行更新操作
*/
int statement_executeUpdate(FilterChain chain, StatementProxy statement, String sql) throws SQLException;
// ...
// ResultSet相关方法
/**
* 下一条记录
*/
boolean resultSet_next(FilterChain chain, ResultSetProxy resultSet) throws SQLException;
/**
* 关闭ResultSet
*/
void resultSet_close(FilterChain chain, ResultSetProxy resultSet) throws SQLException;
// ...
}
同Filter类似,FilterChain也是定义了Connection,Statement,ResultSet等对象的相关接口,只是在实际调用这些对象前,会先执行一遍配置的Filter链。
public interface FilterChain {
/**
* 创建Connection代理
*/
ConnectionProxy connection_connect(Properties info) throws SQLException;
/**
* 创建Statement代理
*/
StatementProxy connection_createStatement(ConnectionProxy connection) throws SQLException;
/**
* 创建PrepareStatement代理
*/
PreparedStatementProxy connection_prepareStatement(ConnectionProxy connection, String sql) throws SQLException;
// ...
}
使用Druid时,一旦配置filters,则在执行数据库操作时,均将使用代理对象(如ConnectionProxy,StatementProxy,ResultSetProxy等),下面将分别描述在使用Filter时,Druid不同的表现。
// DruidDataSource.getConnection()
/**
* 获取数据库连接
*/
public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
// 尝试初始化数据源
init();
if (filters.size() > 0) {
// 经过filter中获取连接
// 这里是每次获取连接,都需要构造Filter链,即执行各Filter
FilterChainImpl filterChain = new FilterChainImpl(this);
return filterChain.dataSource_connect(this, maxWaitMillis);
} else {
// 直接获取连接
return getConnectionDirect(maxWaitMillis);
}
}
// FilterChainImpl
public DruidPooledConnection dataSource_connect(DruidDataSource dataSource, long maxWaitMillis) throws SQLException {
if (this.pos < filterSize) {
// 还有未执行的Filter,则先执行Filter.dataSource_getConnection()
DruidPooledConnection conn = nextFilter().dataSource_getConnection(this, dataSource, maxWaitMillis);
return conn;
}
return dataSource.getConnectionDirect(maxWaitMillis);
}
Druid内部默认提供了一些可选的Filter,可见META-INF/druid-filter.properties中:
druid.filters.default=com.alibaba.druid.filter.stat.StatFilter
druid.filters.stat=com.alibaba.druid.filter.stat.StatFilter
druid.filters.mergeStat=com.alibaba.druid.filter.stat.MergeStatFilter
druid.filters.counter=com.alibaba.druid.filter.stat.StatFilter
druid.filters.encoding=com.alibaba.druid.filter.encoding.EncodingConvertFilter
druid.filters.log4j=com.alibaba.druid.filter.logging.Log4jFilter
druid.filters.log4j2=com.alibaba.druid.filter.logging.Log4j2Filter
druid.filters.slf4j=com.alibaba.druid.filter.logging.Slf4jLogFilter
druid.filters.commonlogging=com.alibaba.druid.filter.logging.CommonsLogFilter
druid.filters.commonLogging=com.alibaba.druid.filter.logging.CommonsLogFilter
druid.filters.wall=com.alibaba.druid.wall.WallFilter
druid.filters.config=com.alibaba.druid.filter.config.ConfigFilter
假设需要使用StatFilter(主要作一些连接池相关的监听通知的操作)和Slf4jLogFilter(增加一些Connection,Statement,ResultSet等相关的日志),只需在配置数据源时设置即可:
datasource.setFilters("stat,slf4j");
// StatFilter.dataSource_getConnection()
public DruidPooledConnection dataSource_getConnection(FilterChain chain, DruidDataSource dataSource,
long maxWaitMillis) throws SQLException {
// 调用下一个Filter,即LogFilter
DruidPooledConnection conn = chain.dataSource_connect(dataSource, maxWaitMillis);
if (conn != null) {
// 设置连接的连接时间
conn.setConnectedTimeNano();
// 触发一些打开连接的事件
StatFilterContext.getInstance().pool_connection_open();
}
return conn;
}
// StatFilterContext.pool_connection_open()
// 通知配置的监听器
public void pool_connection_open() {
for (int i = 0; i < listeners.size(); ++i) {
StatFilterContextListener listener = listeners.get(i);
listener.pool_connect();
}
}
// LogFilter.dataSource_getConnection()
public DruidPooledConnection dataSource_getConnection(FilterChain chain, DruidDataSource dataSource,
long maxWaitMillis) throws SQLException {
// 调用下一个Filter,即LogFilter
DruidPooledConnection conn = chain.dataSource_connect(dataSource, maxWaitMillis);
// 获取物理连接
ConnectionProxy connection = (ConnectionProxy) conn.getConnectionHolder().getConnection();
if (connectionConnectAfterLogEnable && isConnectionLogEnabled()) {
// 记录获取到连接的日志
connectionLog("{conn-" + connection.getId() + "} pool-connect");
}
return conn;
}
由上述可知,这些配置的Filter会在获取到连接后再被执行,并且Filter的执行顺序与配置的的顺序相反,即Slf4jLogFilter更先执行。
Druid内置了一些常用的Filter,下面将介绍游戏i饿。
ConfigFilter主要负责从外部获取数据源配置,如:
// ConfigFilter.init()
public void init(DataSourceProxy dataSourceProxy) {
if (!(dataSourceProxy instanceof DruidDataSource)) {
LOG.error("ConfigLoader only support DruidDataSource");
}
DruidDataSource dataSource = (DruidDataSource) dataSourceProxy;
Properties connectinProperties = dataSource.getConnectProperties();
Properties configFileProperties = loadPropertyFromConfigFile(connectinProperties);
// 判断是否需要解密,如果需要就进行解密行动
boolean decrypt = isDecrypt(connectinProperties, configFileProperties);
if (configFileProperties == null) {
// 没有配置文件
if (decrypt) {
// 解密密码(RSA)
decrypt(dataSource, null);
}
return;
}
if (decrypt) {
// 解密密码(RSA), 并配置数据源
decrypt(dataSource, configFileProperties);
}
try {
// 从配置文件中设置数据源配置
DruidDataSourceFactory.config(dataSource, configFileProperties);
} catch (SQLException e) {
throw new IllegalArgumentException("Config DataSource error.", e);
}
}
ConfigFilter常见的配置方式为:
<!-- 从本地文件读取配置 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="filters" value="config" />
<property name="connectionProperties" value="config.file=file:///home/admin/druid-pool.properties" />
</bean>
<!-- 从HTTP文件读取配置 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="filters" value="config" />
<property name="connectionProperties" value="config.file=http://127.0.0.1/druid-pool.properties" />
</bean>
启用ConfigFilter的加解密功能:
# 生成公私钥
java -cp druid-{version}.jar com.alibaba.druid.filter.config.ConfigTools plain_password
<!-- 配置加密后的密码,及公钥 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="url" value="jdbc:derby:memory:spring-test;create=true" />
<property name="username" value="sa" />
<property name="password" value="${encryted_password}" />
<property name="filters" value="config" />
<property name="connectionProperties" value="config.decrypt=true;config.decrypt.key=${publickey}" />
</bean>
WallFilter的功能是防御SQL注入攻击。它是基于SQL语法分析,理解其中的SQL语义,然后做处理的,智能,准确,误报率低。对于SQL注入这类问题,通过常见的SQL占位符便可防止,但对于一些其他诸如SQL变量,表的控制,则可以通过WallFilter来处理,WallFilter初始化时,会预先加载一些被禁止的变量,表等,如:
// WallFilter.init()
public synchronized void init(DataSourceProxy dataSource) {
if (JdbcUtils.MYSQL.equals(dbType) || //
JdbcUtils.MARIADB.equals(dbType) || //
JdbcUtils.H2.equals(dbType)) {
if (config == null) {
// 构建WallFilter配置对象, 默认配置在目录META-INF/druid/wall/mysql下
config = new WallConfig(MySqlWallProvider.DEFAULT_CONFIG_DIR);
}
// 针对MYSQL, MARIADB, H2的WallFilter内部实现
provider = new MySqlWallProvider(config);
}
// ... 其他数据库类型
}
public WallConfig(String dir){
this.dir = dir;
this.init();
}
public final void init() {
loadConfig(dir);
}
/**
* 加载配置
*/
public void loadConfig(String dir) {
// 逐行加载配置文件,包括jar包中的对应resource
// 禁止访问的变量
loadResource(this.denyVariants, dir + "/deny-variant.txt");
// 禁止访问的Schema,如information_schema,mysql等
loadResource(this.denySchemas, dir + "/deny-schema.txt");
// 禁止使用的函数,如load_file等
loadResource(this.denyFunctions, dir + "/deny-function.txt");
// 禁止访问的表,没有提供默认配置,开发人员可以自己定义
loadResource(this.denyTables, dir + "/deny-table.txt");
// 禁止访问的对象,没有提供默认配置,开发人员可以自己定义
loadResource(this.denyObjects, dir + "/deny-object.txt");
// 只读的表
loadResource(this.readOnlyTables, dir + "/readonly-table.txt");
// 允许使用的函数
loadResource(this.permitFunctions, dir + "/permit-function.txt");
// 允许访问的表,没有提供默认配置,开发人员可以自己定义
loadResource(this.permitTables, dir + "/permit-table.txt");
// 允许访问的Schema
loadResource(this.permitSchemas, dir + "/permit-schema.txt");
// 允许访问的变量
loadResource(this.permitVariants, dir + "/permit-variant.txt");
}
因此,之后在执行SQL相关的操作时,则会先对SQL进行校验,如:
// WallFilter.connection_prepareStatement
public PreparedStatementProxy connection_prepareStatement(FilterChain chain, ConnectionProxy connection,
String sql, int autoGeneratedKeys) throws SQLException {
String dbType = connection.getDirectDataSource().getDbType();
// WallFilter ThreadLocal上下文
WallContext.create(dbType);
try {
// 检测SQL
// 合法放入白名单缓存,非法放入黑名单缓存
sql = check(sql);
PreparedStatementProxy stmt = chain.connection_prepareStatement(connection, sql, autoGeneratedKeys);
setSqlStatAttribute(stmt);
return stmt;
} finally {
WallContext.clearContext();
}
}
WallFilter相关的详细配置可见这里。
EncodingConvertFilter主要用于当客户端编码不一致时,使用较少,除非一些遗留问题,应保证客户端与服务端使用的编码一致。
<!-- 仅需配置clientEncoding与serverEncoding -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
// ...
<property name="filters" value="encoding" />
<property name="connectionProperties" value="clientEncoding=UTF-8;serverEncoding=ISO-8859-1" />
</bean>
StatFilter的功能是主要是用于统计监控信息。比如,通常开发人员会比较关注的慢查询问题,可以用StatFilter来处理,详细配置可见这里:
<bean id="stat-filter" class="com.alibaba.druid.filter.stat.StatFilter">
<property name="slowSqlMillis" value="10000" />
<property name="logSlowSql" value="true" />
</bean>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="filters" value="slf4j" />
<property name="proxyFilters">
<list>
<ref bean="stat-filter" />
</list>
</property>
</bean>
Druid内置提供了四种LogFilter(Log4jFilter、Log4j2Filter、CommonsLogFilter、Slf4jLogFilter),这样开发人员就可以自由配置需要在哪些JDBC操作上进行日志记录,比如数据源,数据库连接等,详细配置可见这里:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
...
<property name="proxyFilters">
<list>
<ref bean="logFilter"/>
</list>
</property>
</bean>
<bean id="logFilter" class="com.alibaba.druid.filter.logging.Slf4jLogFilter">
<property name="dataSourceLogEnabled" value="false" />
<property name="connectionLogEnabled" value="false" />
<property name="statementLogEnabled" value="false" />
<property name="resultSetLogEnabled" value="false" />
</bean>
WebStatFilter用于采集web-jdbc关联监控的数据,即通过Servlet容器中的Filter来作相关的收集及监控,详细配置可见这里:
# web.xml
<filter>
<filter-name>DruidWebStatFilter</filter-name>
<filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class>
<init-param>
# 经常需要排除一些不必要的url,比如.js,/jslib/等等
<param-name>exclusions</param-name>
<param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>DruidWebStatFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
实现自己的Filter很简答,只需继承一个Filter适配器(FilterAdapter),重写感兴趣的方法即可:
public class MyFilter extends FilterAdapter{
// override some methods
}
以上,则是有关Druid的Filter机制,也可以说是拦截器机制。