spring boot不加载本地写的类到容器中

问题描述

在使用spring boot的项目中, 自己会去编写一些controllerservice. 通常情况下,我们想要spring帮我们装载到容器中在类上面使用@Controller@Service@Component@Configuration等注解,并且保证这个些类都在扫描包或则在其子包下就可以了。默认的扫描package就是启动类所在的包或则其子包。比如,我启动类的全类路径为org.example.AppStartup那只要在包org.example下,或则在其子包org.example.*下的所有带自动加载注解的类都会被扫描进Spring容器。
当然你也可以使用注解@SpringBootApplicationscanBasePackages属性指定你想要扫描的包。
以上方法应该对使用过spring-boot的同学来说应该没问毛病,也都是废话。但是如果有一天你按照上面的操作,结果这些controllerservice… 都被被自动加载了, 你启动了应用发现controller不能访问,报404,慌不慌?
其实我遇到这个问题的时候慌的一匹, 因为网上讲的问题全是,上面我讲的那堆。根本解决不到问题。

问题重现

首先,一般情况下是遇不到这类问题的,spring低一些的版本应该也是没有的,我用的spring framework的版本相对来说比较高5.3.9spring-boot的版本也高2.5.4。问题产生的根本原因也是因为我用了一个cas框架,我把他弄到了spring-boot中,作为依赖把他加载起来。然后在maven里面加入依赖:

<dependency>
   <groupId>org.apereo.casgroupId>
    <artifactId>cas-server-core-api-authenticationartifactId>
    <version>6.4.3version>
    <exclusions>
        <exclusion>
            <groupId>org.apereo.casgroupId>
            <artifactId>cas-server-core-api-configuration-modelartifactId>
        exclusion>
    exclusions>
dependency>
<dependency>
    <groupId>org.apereo.casgroupId>
    <artifactId>cas-server-core-api-configuration-modelartifactId>
    <version>6.4.3version>
dependency>

然后启动org.example.AppStartup

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AppStartup {
    public static void main(String[] args) {
        try {
            // 这个就是@Accessors(chain = true)
            //new CasConfigurationProperties().setAudit(null).setAcme()
            SpringApplication.run(AppStartup.class, args);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

最后你会发现工程里面的org.example.controller.HelloController 并不能通过http://localhost:8080/hello访问。HelloController如下:

package org.example.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @RequestMapping("/hello")
    public String hello() {
        return "hell world , nice to meet you.";
    }
}

是不是很意外?

问题原因

据说在springframework5.0版本中,引入了一个META-INF/spring.components文件,在自动装配注解的时候,这玩意会作为索引被加载到加载过程中。如果spring容器在classpath检测到了该文件,他就会按照这个的逻辑进行装配了beandefine了。也就是说他不在会扫描AppStartup.main所在包以及其所在子包的配置注解了。所以问题就产生了。代码逻辑如下:
类:org.springframework.context.annotation.ClassPathBeanDefinitionScanner

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
// ............ 省略了不少代码
// 这个packages默认就是AppStartup.main所在的包路径
        for (String basePackage : basePackages) {

            Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
// .........省略了很多代码           
        return beanDefinitions;
    }
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
// 就是这个 this.componentsIndex 使得不会去默认扫package了。
    if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
        return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
    } else {
        return scanCandidateComponents(basePackage);
    }
}
private Set<BeanDefinition> addCandidateComponentsFromIndex(
  CandidateComponentsIndex index,
   String basePackage
 ) {
        Set<BeanDefinition> candidates = new LinkedHashSet<>();
        try {
            Set<String> types = new HashSet<>();
            for (TypeFilter filter : this.includeFilters) {
            // ..............................  自从这个index里面去加载了。
                types.addAll(index.getCandidateTypes(basePackage, stereotype));
            }
            // ..............................  还有很多代码
        return candidates;
    }

问题就此算是找了。

解决办法

问题解决其实也是比较简单的。我们只需要找到这个this.componentsIndex的来源就好了。在类org.springframework.context.annotation.ClassPathScanningCandidateComponentProvidersetResourceLoader加载了该值,如下:

 @Override
    public void setResourceLoader(@Nullable ResourceLoader resourceLoader) {
        this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
        this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
        ///  加载
        this.componentsIndex = CandidateComponentsIndexLoader.loadIndex(this.resourcePatternResolver.getClassLoader());
    }

真实的操作也在org.springframework.context.annotation.CandidateComponentsIndexLoader类中。
真实执行的操作在其方法doLoadIndex(ClassLoader classLoader)中。方法体如下.

 @Nullable
    private static CandidateComponentsIndex doLoadIndex(ClassLoader classLoader) {
        if (shouldIgnoreIndex) {
            return null;
        }

        try {
            Enumeration<URL> urls = classLoader.getResources(COMPONENTS_RESOURCE_LOCATION);
            if (!urls.hasMoreElements()) {
                return null;
            }
            List<Properties> result = new ArrayList<>();
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
                result.add(properties);
            }
            if (logger.isDebugEnabled()) {
                logger.debug("Loaded " + result.size() + "] index(es)");
            }
            int totalCount = result.stream().mapToInt(Properties::size).sum();
            return (totalCount > 0 ? new CandidateComponentsIndex(result) : null);
        } catch (IOException ex) {
            throw new IllegalStateException("Unable to load indexes from location [" +
                    COMPONENTS_RESOURCE_LOCATION + "]", ex);
        }
    }

可以看出在进来的时候判断shouldIgnoreIndex, 如果true直接就返回null了,也就是说我们得继续搞这个shouldIgnoreIndex,看他是怎么弄成true的.

private static final boolean shouldIgnoreIndex = SpringProperties.getFlag(IGNORE_INDEX);

这个IGNORE_INDEX

public static final String IGNORE_INDEX = "spring.index.ignore";

直接从配置文件中获取的。
继续看SpringProperties是从spring.properties中获取的.

private static final String PROPERTIES_RESOURCE_LOCATION = "spring.properties";

从过上面的分析可以得到结论, 我们的第一种解决办法就是在工程的resource目录下面加一个spring.properties文件。在文件里面增加一个配置项目:

spring.index.ignore=true

重启项目,你会发现工程完全可以自动加载默认包下的Controllerservice了。

当然更暴力的方式是把涉及的jar文件中的META-INF/spring.components文件直接给干掉。这种方式感觉其实可行性非常差。
spring boot不加载本地写的类到容器中_第1张图片

总结

其实本文并没有讲解META-INF/spring.components本身的一些用法,我也不太清楚。我们很多常规项目应该也不会遇到类似的问题。本项目主要是使用了CAS-Server才引入了该问题。当然如果有大牛指正问题更好。

你可能感兴趣的:(杂项,spring,boot,容器,spring)