记录 Commons DBCP testOnBorrow 的作用机制,从一点去分析数据库连接池获取的过程以及架构分层设计。
笔记内容会按照每层的作用,贯穿分析整个调用流程。
The indication of whether objects will be validated before being borrowed from the pool.
If the object fails to validate, it will be dropped from the pool, and we will attempt to borrow another.
testOnBorrow 不是 dbcp 定义的,是commons-pool 定义的。commons-pool 详细的定义了资源池使用的一套规范和运行流程。
/**
* Borrow an object from the pool. get object from 资源池
* @see org.apache.commons.pool2.impl.GenericObjectPool#borrowObject(long)
*/
public T borrowObject(final long borrowMaxWaitMillis) throws Exception {
PooledObject<T> p = null;
// if validation fails, the instance is destroyed and the next available instance is examined.
// This continues until either a valid instance is returned or there are no more idle instances available.
while (p == null) {
// If there is one or more idle instance available in the pool,
// then an idle instance will be selected based on the value of getLifo(), activated and returned.
p = idleObjects.pollFirst();
if (p != null) {
// 设置 testOnBorrow 就会进行可用性校验
if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) {
boolean validate = false;
Throwable validationThrowable = null;
try {
// 具体的校验实现由实现类完成。
// see org.apache.commons.dbcp2.PoolableConnectionFactory
validate = factory.validateObject(p);
} catch (final Throwable t) {
PoolUtils.checkRethrow(t);
validationThrowable = t;
}
if (!validate) {
try {
// 如果校验异常,会销毁该资源。
// obj is not valid and should be dropped from the pool
destroy(p);
destroyedByBorrowValidationCount.incrementAndGet();
} catch (final Exception e) {
// Ignore - validation failure is more important
}
p = null;
}
}
}
}
return p.getObject();
}
dbcp 是特定于管理数据库连接的资源池。
PoolableConnectionFactory is a PooledObjectFactory
PoolableConnection is a PooledObject
/**
* @see PoolableConnectionFactory#validateObject(PooledObject)
*/
@Override
public boolean validateObject(final PooledObject<PoolableConnection> p) {
try {
/**
* 检测资源池对象的创建时间,是否超过生存时间
* 如果超过 maxConnLifetimeMillis, 不再委托数据库连接进行校验,直接废弃改资源
* @see PoolableConnectionFactory#setMaxConnLifetimeMillis(long)
*/
validateLifetime(p);
// 委托数据库连接进行自我校验
validateConnection(p.getObject());
return true;
} catch (final Exception e) {
return false;
}
}
/**
* 数据库连接层的校验。具体到是否已关闭、是否与 server 连接可用
* @see Connection#isValid(int)
*/
public void validateConnection(final PoolableConnection conn) throws SQLException {
if(conn.isClosed()) {
throw new SQLException("validateConnection: connection closed");
}
conn.validate(_validationQuery, _validationQueryTimeout);
}
Spring 基于 Java 配置方式,在Bean 有依赖配置的情况下,可以直接写成方法调用。框架背后的原理(magic🎭)是怎样的?
Injecting Inter-bean Dependencies 有哪些使用误区?
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
// dependency is as simple as having one bean method call another
// 表面上是方法的直接调用,实际上是 Spring constructor injection
// beanTwo 方法的调用,背后是 Spring Bean 创建和初始化的过程。
return new BeanOne(beanTwo());
}
@Bean
public BeanTwo beanTwo() {
// 虽然是new instance, 但是多次调用 beanTwo 方法,得到的是同一个 instance
// singleton scope by default
return new BeanTwo();
}
}
有的开发者不理解 Inter-bean injection 的原理,理解为方法直接调用。会人工调用诸如 afterPropertiesSet 这样bean 初始化的方法,这样是没有必要的。
只有在 @Configuration 配置的类里,Inter-bean injection 才生效。
As of Spring 3.2, CGLIB classes have been repackaged under org.springframework.cglib
。 这些代码没有注释,需要去 CGLIB 查看。
@Configuration 类里的方法不能为private 或者 final,CGLIB 生成的继承类的规则限制。防止出现不生效的情况,Spring 会强制校验。
Configuration problem: @Bean method 'beanTwo' must not be private or final; change the method's modifiers to continue
容器启动时,对所有 @Configuration 注解的类进行动态代理(增强)。拦截类中的方法,对于 @Bean 注解的方法,会作为 factory-bean 方式对待,方法直接调用转化为 bean 获取的过程(get or create_and_get)。
动态代理使用 CGLIB 实现。
org.springframework.context.annotation.ConfigurationClassEnhancer 动态代理 @Configuration 类。
org.springframework.context.annotation.ConfigurationClassPostProcessor 发起代理(增强)的入口。postProcessBeanFactory
经过上述的分析,源码查看侧重在 net.sf.cglib.proxy.MethodInterceptor。
/**
* 拦截 @Bean 注解的方法,替换为 Bean 相关的操作(scoping and AOP proxying)。
* @see ConfigurationClassEnhancer
* @from org.springframework.context.annotation.ConfigurationClassEnhancer.BeanMethodInterceptor
*/
private static class BeanMethodInterceptor implements MethodInterceptor, ConditionalCallback {
@Override
@Nullable
public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs,
MethodProxy cglibMethodProxy) throws Throwable {
// 从proxy 获取BeanFactory。代理类有个属性 $$beanFactory 持有 BeanFactory 实例。
ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance);
String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod);
// Determine whether this bean is a scoped-proxy
if (BeanAnnotationHelper.isScopedProxy(beanMethod)) {
String scopedBeanName = ScopedProxyCreator.getTargetBeanName(beanName);
if (beanFactory.isCurrentlyInCreation(scopedBeanName)) {
beanName = scopedBeanName;
}
}
// To handle the case of an inter-bean method reference, we must explicitly check the
// container for already cached instances.
// 常规获取 Bean, beanFactory.getBean(beanName)
return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName);
}
}
bean 构造和初始化,使用 method 定义的方法来实现。
/**
* Read the given BeanMethod, registering bean definitions
* with the BeanDefinitionRegistry based on its contents.
* @from org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader
*/
private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
ConfigurationClass configClass = beanMethod.getConfigurationClass();
MethodMetadata metadata = beanMethod.getMetadata();
String methodName = metadata.getMethodName();
// 获取 method @Bean 注解配置
AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class);
Assert.state(bean != null, "No @Bean annotation attributes");
// Consider name and Register aliases...
ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata);
beanDef.setResource(configClass.getResource());
beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource()));
if (metadata.isStatic()) {
// static @Bean method 不依赖 configClass instance, 可以直接初始化为bean
if (configClass.getMetadata() instanceof StandardAnnotationMetadata) {
beanDef.setBeanClass(((StandardAnnotationMetadata) configClass.getMetadata()).getIntrospectedClass());
}
else {
beanDef.setBeanClassName(configClass.getMetadata().getClassName());
}
beanDef.setUniqueFactoryMethodName(methodName);
}
else {
// instance @Bean method 🎈🎈🎈
beanDef.setFactoryBeanName(configClass.getBeanName());
beanDef.setUniqueFactoryMethodName(methodName);
}
// beanDef.setAttribute...
this.registry.registerBeanDefinition(beanName, beanDefToRegister);
}
Spring 框架为了使用的方便,尽可能的隐藏了实现细节。让开发更加方便。
因为隐藏了技术细节,对于诸如上述的 Inter-bean dependency 配置方式,开发者可能会误解,会显示调用框架的接口。
这次分析AOP 的使用场景,又一次加深了动态代理的理解,眼前一亮。
通过AOP 方式对Bean-Method 代理,可以用 cache 使用的角度去理解。如果存在,从 beanFactory cache 获取并返回;如果不存在,则根据 Bean-Method 去创建bean, 并put 到beanFactory cache, 再返回。✨
CPU 使用高一般原因是出现代码死循环。
寻找到死循环的代码,就需要找对应的 stack。
匹配JVM stack,就需要查找CPU 使用率高的进程/线程。
遵循上述思路,总结排查步骤。
查找java 进程id
# 获取java 进程信息。 eg: 进程id = 12309
jps -v
查找 java 进程的线程 CPU 使用率
# -p 用于指定进程,-H 用于获取每个线程的信息
top -p 12309 -H
获取java 进程的 stack
jstack -l 12309 > stack.log
在项目开发过程中,需要给 web 请求统一添加 Header, 后端会根据是否有自定义Header 决定是否是超管权限。方便开发和线上调试。
通过使用 Chrome 插件,可以给每个 web 请求添加自定义 Header,并且不会影响正常工程代码。
插件名称:Modify-http-headers。
/**
* 后端示意代码
* 超管功能,登录PIN 替换,用于特殊场景的调试。 org.springframework.web.servlet.HandlerInterceptor
*/
@Slf4j
public class AuthInterceptor extends HandlerInterceptorAdapter {
/**
* 超管PIN header,仅用于调试
*/
private static final String SUPER_PIN_HEADER = "super-pin";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String superPin = request.getHeader(SUPER_PIN_HEADER);
// 如果Http 请求有自定义 Header, 并且当前用户属于超管, 可以进行登录用户替换。
if (StringUtils.isNotBlank(superPin) && inWhiteList(getCurrentPin(request))) {
AuthContext context = new AuthContext();
context.setPin(superPin);
setAuthContext(context);
}
return true;
}
}
代码中编写, Jquery/ axios 都支持拦截器。
可以根据不同的运行环境,决定是否添加自定义的Header。
不建议这种方式,对代码有污染,还有安全隐患。
Fiddler
非常方便的Http 拦截代理工具。
Chrome extensions
浏览器插件实现功能注入和增强。借助 Chrome 提供的丰富API,可以实现各种想要的特性。
Chrome Source Override
本地调试很方便 使用Chrome开发者工具overrides实现不同环境本地调试
Extensions are software programs, built on web technologies (such as HTML, CSS, and JavaScript) that enable users to customize the Chrome browsing experience.
enhance the browsing experience✨
插件开发教程 Chrome Extension development basics - Chrome Developers
API 能力总览 Chrome Extension development overview - Chrome Developers
官方案例 Extensions - Chrome Developers
❤注意事项
manifest.json 是必备文件,用来声明和定义插件。
根据插件的需求,Chrome 插件分为Content scripts、Service worker、The popup 形式。
permission 声明非常重要。使用对应的API 能力,需要在manifest声明对应的权限。
快速打开插件管理 chrome://extensions
可以使用console 和 开发者工具调试插件问题
🧡插件推荐的工程结构
Use the
chrome.webRequest
API to observe and analyze traffic and to intercept, block, or modify requests in-flight.上述案例拦截 web 请求,添加自定义 header 就是通过调用
chrome.webRequest
API 实现。
// 上述案例使用的插件 Modify-http-headers, 核心实现。
chrome.webRequest.onBeforeSendHeaders.addListener(function(details){
var headerInfo = JSON.parse(localStorage['salmonMHH']);
for (var i = 0; i < headerInfo.length; i++) {
details.requestHeaders.push({name: headerInfo[i].name, value: headerInfo[i].value});
}
return {requestHeaders: details.requestHeaders};
}, filter, ["blocking", "requestHeaders"]); "requestHeaders"]);
使用SpringBootTest 测试DAO 逻辑时,直接报错:java.lang.NoSuchMethodException: tk.mybatis.mapper.provider.base.BaseSelectProvider.<init>()
从异常日志分析,是 tk.mybatis 的增强方法初始化问题。可是,启动工程调用该DAO 方法,是正常执行的,那么问题是出在哪呢?
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = WebApplication.class)
public class ApplicationTests {
@Autowired
private DictionaryMapper dictionaryMapper;
@Test
public void baseMapperTest() {
int count = dictionaryMapper.selectCount(null);
Assert.assertTrue(count > 0);
}
}
同时,测试目录下,还有一个空的SpringBootApplication,主要是用于纯Spring 容器的一些逻辑测试(不加载RPC接口、DAO, 不初始化Redis 等)
/src/test/java/com/foo/EmptyApplication.java
@Slf4j
@SpringBootApplication(scanBasePackages = "com.foo.service", exclude = {RedisAutoConfiguration.class, MybatisAutoConfiguration.class, RPCAutoConfiguration.class})
public class EmptyApplication {
}
关于tk.mybatis 增强类初始化异常的问题,可以直接翻看之前的笔记可以解决。这里。
现在的问题是,只有在SpringBootTest 运行单测方法才会异常。推测是SpringBootTest 某种机制,导致MapperAutoConfiguration 没有loadClass 或者没有执行。
通过查看DEBUG 日志,发现MapperAutoConfiguration 正常load,只是因为不符合匹配条件,没有注册到Spring 容器,所以没有正常执行初始化。
MapperAutoConfiguration:
Did not match:
- @ConditionalOnBean (types: org.apache.ibatis.session.SqlSessionFactory; SearchStrategy: all) did not find any beans (OnBeanCondition)
通过源码,我们可以看到,明明指定了加载顺序,为什么会匹配失败?
@ConditionalOnBean(SqlSessionFactory.class)
@AutoConfigureAfter(MybatisAutoConfiguration.class)
public class MapperAutoConfiguration {
}
根据之前的Insight 笔记,可以直接跳到 org.springframework.boot.autoconfigure.EnableAutoConfigurationImportSelector#getCandidateConfigurations
通过断点调试分析,发现上述提到的EmptyApplication 也load 到容器中了,由于配置了exclusions,直接剔除了MybatisAutoConfiguration。容器中的EnableAutoConfiguration 组件集合是两个SpringBootApplication 扫描结果的合并。这样看来,确实是没法保障初始化顺序。
那么问题又来了,我们的SpringBootTest 明确指定了启动配置类,为什么EmptyApplication 也会掺和进来?
通过源码分析,springboot 是以WebApplication 作为启动类。
但是,由于EmptyApplication 属于 Configuration,在初始化过程中,正常加载到容器中。这时候容器中就存在两个 SpringBootApplication。自动配置等组件也是这两个 Application 共同扫描的集合。
SpringBootApplication 声明并启用了EnableAutoConfiguration。
EnableAutoConfiguration 是通过EnableAutoConfigurationImportSelector 来实现的。
上述EnableAutoConfiguration 加载就是EnableAutoConfigurationImportSelector 执行扫描导入实现的。
/**
* Build and validate a configuration model based on the registry of
* Configuration classes.
* @see org.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitions
*/
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
// Parse each @Configuration class
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);
do {
// parse 过程中,引入EmptyApplication。由于EmptyApp 指定exclude MybatisAutoConfiguration,在两次排序合并后,添加到了最后。打乱了 EnableAutoConfiguration 的顺序。
parser.parse(candidates);
parser.validate();
}
}
最复杂的问题,原因总是很简单。
这个问题与环境没有关系,与 SpringBootTest 也没有关系,是 SpringBoot 加载机制的问题。
工程里尽量不要写多个 SpringBootApplication ,避免不必要的麻烦。目前使用的版本:spring-boot-autoconfigure-1.5.10.RELEASE