Druid的Filter机制
2016 年 06 月 20 日
db

    之前的这篇文章介绍了Druid的连接池管理实现,但除了基本的连接管理,Druid还支持监控代理等功能,便于开发人员分析数据库操作的一些性能指标等。其主要使用Filter机制来实现,本文将介绍下其中的Filter实现

  • Filter接口定义

  • Filter接口中,可以看出,Filter主要重新定义了来自ConnectionStatementResultSet等对象的接口,并且均通过对应的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也是定义了ConnectionStatementResultSet等对象的相关接口,只是在实际调用这些对象前,会先执行一遍配置的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;
    
        // ...
     }
        
  • Filter实现

  • 使用Druid时,一旦配置filters,则在执行数据库操作时,均将使用代理对象(如ConnectionProxyStatementProxyResultSetProxy等),下面将分别描述在使用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(增加一些ConnectionStatementResultSet等相关的日志),只需在配置数据源时设置即可:

    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更先执行。

  • 内置的一些Filter

  • Druid内置了一些常用的Filter,下面将介绍游戏i饿。

  • ConfigFilter

  • ConfigFilter主要负责从外部获取数据源配置,如:

    从配置文件中读取配置;
    从远程http文件中读取配置;
    为数据库密码提供加密功能。
    // 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

  • 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

  • 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的功能是主要是用于统计监控信息。比如,通常开发人员会比较关注的慢查询问题,可以用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>
        
  • LogFilter

  • Druid内置提供了四种LogFilter(Log4jFilterLog4j2FilterCommonsLogFilterSlf4jLogFilter),这样开发人员就可以自由配置需要在哪些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

  • 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很简答,只需继承一个Filter适配器(FilterAdapter),重写感兴趣的方法即可:

    public class MyFilter extends FilterAdapter{
        // override some methods
    }
        
  • 总结

  • 以上,则是有关Druid的Filter机制,也可以说是拦截器机制

好人,一生平安。