背景
最近在做项目的过程中,我的一个变更已经发布到beta环境了,但是我在处理另一个问题的过程中发现,这个变更在预发的国内环境也莫名其妙启动不起来了,感觉很是诡异,已经到beta的变更怎么能说启动不起来就启动不起来呢?经过不懈的努力,最后终于把这个问题给解决了,在这里分享给大家。
问题的现象
启动时,报如下错误:
错误描述
从报错中看出是MybatisAutoConfiguration中在实例化sqlSessionTemplate时报了空指针异常。
抽丝剥茧找本质
看到这个错误,我的第一反应是,先看下MybatisAutoConfiguration类中是如何实例化sqlSessionTemplate的,是哪行代码导致了空指针,代码如下:
@Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnBean(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration {
private static Log log = LogFactory.getLog(MybatisAutoConfiguration.class);
@Autowired
private MybatisProperties properties;
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType(); // 这行空指针
if (executorType != null) {
return new SqlSessionTemplate(sqlSessionFactory, executorType);
} else {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
... // 去除解本问题无关的代码
}
经调试发现报空指针是因为MybatisAutoConfiguration类的properties属性为null。那为什么部署主干可以成功启动呢?
将代码切换到master,在空指针这行打断点,然后重启,发现启动过程完毕了,都没有进到断点中,那就是这段代码本来不该执行的却被错误执行了,那好嘛,接下来就是定位为啥这个方法会被执行就可以了。
首先可以看到这个方法的执行依赖于@ConditionalOnMissingBean注解,那接着看下这个注解的源码吧:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnMissingBean {
Class<?>[] value() default {};
String[] type() default {};
Class<?>[] ignored() default {};
String[] ignoredType() default {};
Class<? extends Annotation>[] annotation() default {};
String[] name() default {};
SearchStrategy search() default SearchStrategy.ALL;
}
从源码中可以看出该注解是派生自@Conditional注解的,依赖OnBeanCondition.class中的matches方法的返回来决定@ConditionalOnMissingBean注解所在的方法是否会被执行(或者该注解所在的类是否会被加载)。
那接下来的关键就是找出为啥OnBeanCondition.class中的matches方法意外返回了true。源码如下:
@Order(Ordered.LOWEST_PRECEDENCE)
public class OnBeanCondition extends SpringBootCondition implements ConfigurationCondition {
private static final String[] NO_BEANS = {};
public static final String FACTORY_BEAN_OBJECT_TYPE = BeanTypeRegistry.FACTORY_BEAN_OBJECT_TYPE;
... // 去除解本问题无关的代码
}
发现该类中被没有自己的matchs方法,而是继承自SpringBootCondition,方法定义如下:
public abstract class SpringBootCondition implements Condition {
@Override
public final boolean matches(ConditionContext context,
AnnotatedTypeMetadata metadata) {
String classOrMethodName = getClassOrMethodName(metadata);
try {
ConditionOutcome outcome = getMatchOutcome(context, metadata);
logOutcome(classOrMethodName, outcome);
recordEvaluation(context, classOrMethodName, outcome);
return outcome.isMatch();
}
catch (NoClassDefFoundError ex) {
throw new IllegalStateException(
"Could not evaluate condition on " + classOrMethodName + " due to "
+ ex.getMessage() + " not "
+ "found. Make sure your own configuration does not rely on "
+ "that class. This can also happen if you are "
+ "@ComponentScanning a springframework package (e.g. if you "
+ "put a @ComponentScan in the default package by mistake)",
ex);
}
catch (RuntimeException ex) {
throw new IllegalStateException(
"Error processing condition on " + getName(metadata), ex);
}
}
public abstract ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata);
... // 去除解本问题无关的代码
}
由此可见,核心就落在了在OnBeanCondition类中实现的SpringBootCondition类的抽象方法getMatchOutcome了。该方法的实现源码为:
@Order(Ordered.LOWEST_PRECEDENCE)
public class OnBeanCondition extends SpringBootCondition implements ConfigurationCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
StringBuilder matchMessage = new StringBuilder();
if (metadata.isAnnotated(ConditionalOnBean.class.getName())) {
BeanSearchSpec spec = new BeanSearchSpec(context, metadata,
ConditionalOnBean.class);
List<String> matching = getMatchingBeans(context, spec);
if (matching.isEmpty()) {
return ConditionOutcome
.noMatch("@ConditionalOnBean " + spec + " found no beans");
}
matchMessage.append("@ConditionalOnBean ").append(spec)
.append(" found the following ").append(matching);
}
if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
BeanSearchSpec spec = new SingleCandidateBeanSearchSpec(context, metadata,
ConditionalOnSingleCandidate.class);
List<String> matching = getMatchingBeans(context, spec);
if (matching.isEmpty()) {
return ConditionOutcome.noMatch(
"@ConditionalOnSingleCandidate " + spec + " found no beans");
}
else if (!hasSingleAutowireCandidate(context.getBeanFactory(), matching)) {
return ConditionOutcome.noMatch("@ConditionalOnSingleCandidate " + spec
+ " found no primary candidate amongst the" + " following "
+ matching);
}
matchMessage.append("@ConditionalOnSingleCandidate ").append(spec)
.append(" found a primary candidate amongst the following ")
.append(matching);
}
if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
BeanSearchSpec spec = new BeanSearchSpec(context, metadata,
ConditionalOnMissingBean.class); // 断点打在这一行
List<String> matching = getMatchingBeans(context, spec);
if (!matching.isEmpty()) {
return ConditionOutcome.noMatch("@ConditionalOnMissingBean " + spec
+ " found the following " + matching);
}
matchMessage.append(matchMessage.length() == 0 ? "" : " ");
matchMessage.append("@ConditionalOnMissingBean ").append(spec)
.append(" found no beans");
}
return ConditionOutcome.match(matchMessage.toString());
}
... // 去除解本问题无关的代码
}
那接下来就简单了,就调试看为啥该方法返回了true呗。因为是ConditionalOnMissingBean注解造成的这个现象,所以断点位置很自然就应该在metadata.isAnnotated(ConditionalOnMissingBean.class.getName())表达式成立的if语句里。
调试我的变更分支发现如下图:
调试截图
存在declaringClassName为org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration并且returnTypeName为org.mybatis.spring.SqlSessionTemplate的metadata。同时此时spring的上下文当中没有SqlSessionTemplate类型的bean,所以返回了ConditionOutcome.match(matchMessage.toString());也就相当于上面的matchs方法返回了true,所以就开始实例化sqlSessionTemplate,随后就出现了空指针。
这个时候我就在想,为啥主干代码的启动的时候在Spring的上下文当中就找到了这个SqlSessionTemplate类型的Bean呢?把代码切回主干,然后再一次调试!调试过程中发现,主干代码运行时根本就不存在declaringClassName为org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration并且returnTypeName为org.mybatis.spring.SqlSessionTemplate的metadata。 WTF?这都可以?那现在问题又演变成为啥分支代码会加载MybatisAutoConfiguration类了。让我们接着来排查。
该类为啥会被加载?
我们知道在SpringBoot中xxxAutoConfiguration一般都是由@EnableAutoConfiguration注解来处理,从而加载到Spring的上下文当中的,该注解的源码如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
可以看到该注解借助@Import注解将所有符合配置条件的bean定义加载到IoC容器的。那关键就在EnableAutoConfigurationImportSelector.class类中了,该类源码如下:
public class EnableAutoConfigurationImportSelector
implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware,
BeanFactoryAware, EnvironmentAware, Ordered {
@Override
public String[] selectImports(AnnotationMetadata metadata) {
if (!isEnabled(metadata)) {
return NO_IMPORTS;
}
try {
AnnotationAttributes attributes = getAttributes(metadata);
List<String> configurations = getCandidateConfigurations(metadata,
attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(metadata, attributes);
configurations.removeAll(exclusions);
configurations = sort(configurations);
recordWithConditionEvaluationReport(configurations, exclusions);
return configurations.toArray(new String[configurations.size()]);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
/**
* Return the class used by {@link SpringFactoriesLoader} to load configuration
* candidates.
* @return the factory class
*/
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
... // 去除无关代码
}
该类中跟问题相关的就是上面三个方法了,可见该类是借助SpringFactoriesLoader来实现加载所有符合条件的配置类到Spring的IOC容器中。
优雅的SpringFactoriesLoader
SpringFactoriesLoader属于Spring框架专属的一种扩展方案(其功能和使用方式类似于Java的SPI方案:java.util.ServiceLoader),它的主要功能就是从指定的配置文件META-INF/spring.factories中加载配置,spring.factories是一个非常经典的java properties文件,内容格式是Key=Value形式,只不过这Key以及Value都非常特殊,为Java类的完整类名(Fully qualified name),比如:
com.zhf.service.DemoService=com.zhf.service.impl.DemoServiceImpl,com.zhf.service.impl.DemoServiceImpl2
然后Spring框架就可以根据某个类型作为Key来查找对应的类型名称列表了,SpringFactories源码如下:
public abstract class SpringFactoriesLoader {
private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);
/**
* The location to look for factories.
* <p>Can be present in multiple JAR files.
*/
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) {
...
}
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
...
}
...
}
对于@EnableAutoConfiguraion来说,SpringFactoriesLoader的用途和其本意稍微不同,它本意是为了提供SPI扩展,而在@EnableAutoConfiguration这个场景下,它更多的是提供了一种配置查找的功能的支持,也就是根据@EnableAutoConfiguration的完整类名org.springframework.boot.autoconfigure.EnableAutoConfiguration作为Key来获取一组对应的@Configuration类。
总结来说,@EnableAutoConfiguration能实现自动配置的原理就是:SpringFactoriesLoader从classpath中搜寻所有META-INF/spring.fatories文件,并将其中Key[org.springframework.boot.autoconfigure.EnableAutoConfiguration]对应的Value配置项通过反射的方式实例化为对应的标注了@Configuration的JavaConfig形式的IoC容器配置类,然后汇总到当前使用的IoC容器中。
真相大白
知道了MybatisAutoConfiguration类的加载方式,那问题就很简单了,就直接在变更分支上查找该类的引用,在mybatis-spring-boot-autoconfigure的jar包的META-INF/spring.factories文件中,找到如下定义:
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
同时发现名为mybatis-spring-boot-autoconfigure的jar包是被mybatis-spring-boot-starter作为间接依赖引入到系统中,那问题的真相就是项目被误引入了mybatis-spring-boot-starter依赖,去掉该依赖,问题得到完美解决!
阿豪说
学到了?请我喝杯咖啡吧~