以前说过druid的连接池和spring-boot-starter实现,今天来看下它里面的Filter。它的Filter是采用责任链模式,这种模式十分适合拓展,用户自己自己定义各种Filter,实现接口逻辑,然后利用SPI或者配置文件在初始化的时候加载到DataSource的filter集合里,后面在做操作的时候就会执行你的逻辑进行增强。这块今天就看了半个小时,大概记录下。
Filter实现
责任链模式一般都会有一个FilterChain,它会持有所有的filter,然后会在它的方法里面去调用所有filter的具体实现。
所以在这里面有两个角色,filterChain和filter,前者持有后者,它们在Druid里面都有各自的接口实现,名字就是FilterChain和Filter,然后当你去看这两个接口定义时可以发现,它里面的方法特别多,足足有1000多行,切入点给的特别细,具体有哪些没细看(就是太多了,懒得看),后面等到用的时候再看吧。
Filter在Druid里面有一些默认实现,现成的你可以直接拿来用比如StatFilter,WallFilter,EncodingConvertFilter等,你自己也可以自定义Filter,利用SPI机制在启动的时候就会被加载到jvm,后面执行。
FilterChain在Drui里面只有唯一的实现FilterChainImpl,所有有关filter的执行都是从这里进行调用的。
Filter调用流程
初始化
初始化其实就是filter被加载到filters集合里面的过程,它的实际调用地方很多,在init里面有两个地方:
private void initFromSPIServiceLoader() {
if (loadSpifilterSkip) {
return;
}
if (autoFilters == null) {
List<Filter> filters = new ArrayList<Filter>();
ServiceLoader<Filter> autoFilterLoader = ServiceLoader.load(Filter.class);
for (Filter filter : autoFilterLoader) {
AutoLoad autoLoad = filter.getClass().getAnnotation(AutoLoad.class);
if (autoLoad != null && autoLoad.value()) {
filters.add(filter);
}
}
autoFilters = filters;
}
for (Filter filter : autoFilters) {
if (LOG.isInfoEnabled()) {
LOG.info("load filter from spi :" + filter.getClass().getName());
}
addFilter(filter);
}
}
private void initFromWrapDriverUrl() throws SQLException {
if (!jdbcUrl.startsWith(DruidDriver.DEFAULT_PREFIX)) {
return;
}
DataSourceProxyConfig config = DruidDriver.parseConfig(jdbcUrl, null);
this.driverClass = config.getRawDriverClassName();
LOG.error("error url : '" + jdbcUrl + "', it should be : '" + config.getRawUrl() + "'");
this.jdbcUrl = config.getRawUrl();
if (this.name == null) {
this.name = config.getName();
}
for (Filter filter : config.getFilters()) {
addFilter(filter);
}
}
前者是从SPI加载自定义filter,后者是加载一个jdbc的转码filter,避免乱码(网上查的,没验证过)。
还有就是DruidAbstractDataSource里面的setFilters方法:
public void setFilters(String filters) throws SQLException {
if (filters != null && filters.startsWith("!")) {
filters = filters.substring(1);
this.clearFilters();
}
this.addFilters(filters);
}
这个是读取配置文件的filter,可以多个用逗号隔开。它的核心逻辑是在FilterManager的loadFilter方法里面,有兴趣可以自己看看。
至此所有的filter都被加载到了filters集合里。
调用
调用接口实现很多,我以其中用的比较多的获取连接为例,其他也是类似。
当连接池初始化之后要建立物理连接,会调用DruidAbstractDataSource里的createPhysicalConnection方法:
public Connection createPhysicalConnection(String url, Properties info) throws SQLException {
Connection conn;
if (getProxyFilters().isEmpty()) {
conn = getDriver().connect(url, info);
} else {
FilterChainImpl filterChain = createChain();
conn = filterChain.connection_connect(info);
recycleFilterChain(filterChain);
}
createCountUpdater.incrementAndGet(this);
return conn;
}
当检查到有filter的时候(getProxyFilters()这个方法就是返回的filters集合),会调用DruidDriver的connect方法,然后调用DataSourceProxyImpl的connect方法:
public ConnectionProxy connect(Properties info) throws SQLException {
this.properties = info;
PasswordCallback passwordCallback = this.config.getPasswordCallback();
if (passwordCallback != null) {
char[] chars = passwordCallback.getPassword();
String password = new String(chars);
info.put("password", password);
}
NameCallback userCallback = this.config.getUserCallback();
if (userCallback != null) {
String user = userCallback.getName();
info.put("user", user);
}
FilterChain chain = new FilterChainImpl(this);
return chain.connection_connect(info);
}
注意看该方法的最后两行,会构件一个FilterChainImpl(前面说的FilterChain的唯一实现),然后调用它的connection_connect方法。
这个方法干了啥呢?如下:
public ConnectionProxy connection_connect(Properties info) throws SQLException {
if (this.pos < filterSize) {
return nextFilter()
.connection_connect(this, info);
}
Driver driver = dataSource.getRawDriver();
String url = dataSource.getRawJdbcUrl();
Connection nativeConnection = driver.connect(url, info);
if (nativeConnection == null) {
return null;
}
return new ConnectionProxyImpl(dataSource, nativeConnection, info, dataSource.createConnectionId());
}
很多人可能一开始看不懂这个代码干了啥,因为有点绕,代码基础不太行的看着会有点蒙。
pos是当前filterChain的指针,filterSize是整个filter的数量。nextFilter()就是拿到当前位置的filter然后将指针后移。connection_connect是调用Filter实现类的方法。
它这么写会形成一个类似套娃的效果,整个filterChain返回的ConnectionProxy其实是多有filter功能“包裹“起来的功能合集连接。你可以想象成一个洋葱,最里层是实际连接,然后每外面一层都是filter添加的额外逻辑。
说着有点抽象,举个例子你就懂了。
比如我现在配置了连个filter,StatFilter和EncodingConvertFilter。
下面是两个Filter的connection_connect方法实现,前面是StatFilter,后面是EncodingConvertFilter:
public ConnectionProxy connection_connect(FilterChain chain, Properties info) throws SQLException {
ConnectionProxy connection = null;
long startNano = System.nanoTime();
long startTime = System.currentTimeMillis();
long nanoSpan;
long nowTime = System.currentTimeMillis();
JdbcDataSourceStat dataSourceStat = chain.getDataSource().getDataSourceStat();
dataSourceStat.getConnectionStat().beforeConnect();
try {
connection = chain.connection_connect(info);
nanoSpan = System.nanoTime() - startNano;
} catch (SQLException ex) {
dataSourceStat.getConnectionStat().connectError(ex);
throw ex;
}
dataSourceStat.getConnectionStat().afterConnected(nanoSpan);
if (connection != null) {
JdbcConnectionStat.Entry statEntry = getConnectionInfo(connection);
dataSourceStat.getConnections().put(connection.getId(), statEntry);
statEntry.setConnectTime(new Date(startTime));
statEntry.setConnectTimespanNano(nanoSpan);
statEntry.setEstablishNano(System.nanoTime());
statEntry.setEstablishTime(nowTime);
statEntry.setConnectStackTrace(new Exception());
dataSourceStat.getConnectionStat().setActiveCount(dataSourceStat.getConnections().size());
}
return connection;
}
public ConnectionProxy connection_connect(FilterChain chain, Properties info) throws SQLException {
ConnectionProxy conn = chain.connection_connect(info);
CharsetParameter param = new CharsetParameter();
param.setClientEncoding(info.getProperty(CharsetParameter.CLIENTENCODINGKEY));
param.setServerEncoding(info.getProperty(CharsetParameter.SERVERENCODINGKEY));
if (param.getClientEncoding() == null || "".equalsIgnoreCase(param.getClientEncoding())) {
param.setClientEncoding(clientEncoding);
}
if (param.getServerEncoding() == null || "".equalsIgnoreCase(param.getServerEncoding())) {
param.setServerEncoding(serverEncoding);
}
conn.putAttribute(ATTR_CHARSET_PARAMETER, param);
conn.putAttribute(ATTR_CHARSET_CONVERTER,
new CharsetConvert(param.getClientEncoding(), param.getServerEncoding()));
return conn;
}
当你调用filterChain的connection_connect方法时,会经历下面几步:
- 第一次进入filterChain的connection_connect方法,filterSize是2,这时候pos是0,会走if逻辑,然后调用StatFilter的connection_connect方法。
- 执行StatFilter逻辑,前面逻辑不用管,是一些监控的逻辑,然后走到try的
connection = chain.connection_connect(info);
语句时,会再次调用filterChain的connection_connect方法。(注意这条语句后面的方法都没执行,等待下面的方法返回结果) - 第二次进入filterChain的connection_connect方法,filterSize是2,这时候pos是1,还是会走if逻辑,继续调用EncodingConvertFilter的connection_connect方法。
- 执行EncodingConvertFilter逻辑,直接第一句ConnectionProxy conn = chain.connection_connect(info);就会在调用filterChain的connection_connect方法。(注意这条语句后面的方法都没执行,等待下面的方法返回结果)
- 第三次进入filterChain的connection_connect方法,filterSize是2,这时候pos是2,这次会走后面的逻辑,生成一个物理连接返回。
- 回到EncodingConvertFilter里执行后续逻辑,返回“包裹“EncodingConvertFilter逻辑的连接。
- 回到StatFilter里执行后续逻辑,返回“包裹“EncodingConvertFilter和StatFilter逻辑的连接。
- 最后实际用到的所有连接都是这个“包裹“之后的连接。
这里可以看到包在越外层的代码前置方法越先执行,后置方法越后执行。现在的Druid里面的filter是无序的,如果多个filter之间要有顺序,现在是不支持的貌似,你应该要自己去写处理逻辑。
其实如果你刷题刷的多的话,这块其实就是一个递归的实现。而且还是尾递归。
总结
经过上面的讲解,相信你会比较清晰的了解filter的执行流程,如果你觉得理解的不太清楚。
可以思考一下在Druid里面,如果你自定义的filter在其他filter之前,且没有调用chain.connection_connect(info)
会发生啥?
想明白这个你也就知道FilterAdapter的作用了。