当前位置:网站首页>WTF? The code in beta environment doesn't agree with each other and fails to start

WTF? The code in beta environment doesn't agree with each other and fails to start

2020-12-06 07:05:40 osc_ el4lxnyg

background

In the process of doing projects recently , One of my changes has been posted to beta The environment , But in the process of dealing with another problem, I found that , This change can't be started in the domestic environment in advance , It feels weird , It has arrived beta How can we say that if you can't start a change, you can't start it ? Through unremitting efforts , Finally, the problem was solved , Share it here .

Problem phenomenon

Startup time , Report the following error :
WTF? stay beta If the code of the environment doesn't agree with each other, it fails to start
Error description

It can be seen from the error report that MybatisAutoConfiguration Instantiation in sqlSessionTemplate A null pointer exception was reported .

To find the essence

Seeing this mistake , My first reaction was , Let's take a look at MybatisAutoConfiguration How to instantiate sqlSessionTemplate Of , Which line of code caused the null pointer , The code is as follows :

@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(); //  This line of null pointer 
    if (executorType != null) {
      return new SqlSessionTemplate(sqlSessionFactory, executorType);
    } else {
      return new SqlSessionTemplate(sqlSessionFactory);
    }
  }
  ... //  Remove the irrelevant code for solving this problem 
}

After debugging, it is found that the null pointer is reported because MybatisAutoConfiguration Class properties The attribute is null. So why can the deployment backbone start successfully ?

Switch the code to master, The break point in the null pointer line , And then restart , Found that the start-up process is over , They didn't get into the breakpoint , That's what the code shouldn't have done, but it was misexecuted , Well , The next step is to locate why this method will be executed .

First, you can see that the execution of this method depends on @ConditionalOnMissingBean annotation , Then take a look at the source code of this annotation :

@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;

}

You can see from the source code that the annotation is derived from @Conditional Annotated , rely on OnBeanCondition.class Medium matches To decide how to return @ConditionalOnMissingBean Whether the method in which the annotation is located will be executed ( Or whether the class where the annotation is located will be loaded ).

So the next key is to find out why OnBeanCondition.class Medium matches Method unexpectedly returned true. Source code is as follows :

@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;

    ... //  Remove the irrelevant code for solving this problem 
}

It is found that there is no one of its own in this class matchs Method , It's inherited from SpringBootCondition, The method is defined as follows :

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);
    ... //  Remove the irrelevant code for solving this problem 
}

thus it can be seen , The core lies in OnBeanCondition Class SpringBootCondition Class getMatchOutcome 了 . The source code of this method is :

@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); //  The breakpoint is in this line 
            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());
    }
    ... //  Remove the irrelevant code for solving this problem 
}

Then it's easy , Debugging to see why the method returned true Chant . the reason being that ConditionalOnMissingBean This phenomenon caused by annotations , So it's natural that the breakpoint should be in metadata.isAnnotated(ConditionalOnMissingBean.class.getName()) The expression holds if In the sentence .

Debug my change branch and find the following figure :

WTF? stay beta If the code of the environment doesn't agree with each other, it fails to start
Debug screenshot
There is declaringClassName by org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration also returnTypeName by org.mybatis.spring.SqlSessionTemplate Of metadata. At the same time spring In the context of SqlSessionTemplate Type of bean, So it goes back ConditionOutcome.match(matchMessage.toString()); It's equivalent to the above matchs Method returns true, So we started instantiating sqlSessionTemplate, Then there's the null pointer .

That's when I was thinking , Why is the trunk code started in Spring You can find this in the context of SqlSessionTemplate Type of Bean Well ? Cut the code back to the trunk , And then debug again ! During debugging, it was found that , There's no backbone code running at all declaringClassName by org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration also returnTypeName by org.mybatis.spring.SqlSessionTemplate Of metadata. WTF? This can be ? Now the question is what branch code will load MybatisAutoConfiguration The class . Let's go on to investigate .

Why this class will be loaded ?

We know that SpringBoot in xxxAutoConfiguration It's usually by @EnableAutoConfiguration To deal with , To load into Spring In the context of , The source code of the annotation is as follows :

@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 {};

}

You can see that the annotation is supported by @Import Annotations will all match the configuration conditions of bean Define loading to IoC Container of . The key is EnableAutoConfigurationImportSelector.class Class is in , This kind of source code is as follows :

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;
    }
    ... //  Remove irrelevant code         
}

The above three methods are related to the problem in this class , It can be seen that this class is aided by SpringFactoriesLoader To load all qualified configuration classes to Spring Of IOC In the container .

elegant SpringFactoriesLoader

SpringFactoriesLoader Belong to Spring A framework specific extension ( Its function and usage are similar to Java Of SPI programme :java.util.ServiceLoader), Its main function is from the specified configuration file META-INF/spring.factories Medium load configuration ,spring.factories It's a very classic java properties file , The format of the content is Key=Value form , It's just that Key as well as Value All very special , by Java The complete class name of the class (Fully qualified name), such as :

com.zhf.service.DemoService=com.zhf.service.impl.DemoServiceImpl,com.zhf.service.impl.DemoServiceImpl2

then Spring The framework can act as Key To find the corresponding type name list ,SpringFactories Source code is as follows :

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) {
        ...
    }
    ...
}

about @EnableAutoConfiguraion Come on ,SpringFactoriesLoader Its purpose is slightly different from its original intention , It is intended to provide SPI Expand , And in the @EnableAutoConfiguration In this scene , It provides more support for the function of configuration search , That is to say, according to @EnableAutoConfiguration The complete class name of org.springframework.boot.autoconfigure.EnableAutoConfiguration As Key To get a set of corresponding @Configuration class .

In conclusion ,@EnableAutoConfiguration The principle of automatic configuration is :SpringFactoriesLoader from classpath Search for all META-INF/spring.fatories file , And Key[org.springframework.boot.autoconfigure.EnableAutoConfiguration] Corresponding Value The configuration item is instantiated as the corresponding annotation by reflection @Configuration Of JavaConfig Formal IoC Container configuration class , Then summarize to the current used IoC In the container .

The truth

got it MybatisAutoConfiguration How to load a class , That's a simple question , Find the reference of the class directly on the change branch , stay mybatis-spring-boot-autoconfigure Of jar Bag META-INF/spring.factories In file , Find the following definition :

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

At the same time, we found that it was named mybatis-spring-boot-autoconfigure Of jar A bag is a quilt mybatis-spring-boot-starter Introduced into the system as an indirect dependency , The truth of the problem is that the project was introduced by mistake mybatis-spring-boot-starter rely on , Remove the dependency , The problem is solved perfectly !

WTF? stay beta If the code of the environment doesn't agree with each other, it fails to start

Ah Hao said
Learned ? Please give me a coffee ~

版权声明
本文为[osc_ el4lxnyg]所创,转载请带上原文链接,感谢
https://chowdera.com/2020/12/202012060705352093.html