WTF?在beta环境的代码一言不合就启动失败啦

背景

最近在做项目的过程中,我的一个变更已经发布到beta环境了,但是我在处理另一个问题的过程中发现,这个变更在预发的国内环境也莫名其妙启动不起来了,感觉很是诡异,已经到beta的变更怎么能说启动不起来就启动不起来呢?经过不懈的努力,最后终于把这个问题给解决了,在这里分享给大家。

问题的现象

启动时,报如下错误:
WTF?在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语句里。

调试我的变更分支发现如下图:

WTF?在beta环境的代码一言不合就启动失败啦
调试截图
存在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依赖,去掉该依赖,问题得到完美解决!

WTF?在beta环境的代码一言不合就启动失败啦

阿豪说
学到了?请我喝杯咖啡吧~

上一篇:数据卷与持久卷


下一篇:k8s中controller-manager相关的yaml文件