在使用spring boot
的项目中, 自己会去编写一些controller
和service
. 通常情况下,我们想要spring
帮我们装载到容器中在类上面使用@Controller
、@Service
、@Component
、@Configuration
等注解,并且保证这个些类都在扫描包或则在其子包下就可以了。默认的扫描package
就是启动类所在的包或则其子包。比如,我启动类的全类路径为org.example.AppStartup
那只要在包org.example
下,或则在其子包org.example.*
下的所有带自动加载注解的类都会被扫描进Spring
容器。
当然你也可以使用注解@SpringBootApplication
的scanBasePackages
属性指定你想要扫描的包。
以上方法应该对使用过spring-boot
的同学来说应该没问毛病,也都是废话。但是如果有一天你按照上面的操作,结果这些controller
、service
… 都被被自动加载了, 你启动了应用发现controller
不能访问,报404
,慌不慌?
其实我遇到这个问题的时候慌的一匹, 因为网上讲的问题全是,上面我讲的那堆。根本解决不到问题。
首先,一般情况下是遇不到这类问题的,spring
低一些的版本应该也是没有的,我用的spring framework
的版本相对来说比较高5.3.9
,spring-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.";
}
}
是不是很意外?
据说在springframework
的5.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.ClassPathScanningCandidateComponentProvider
的setResourceLoader
加载了该值,如下:
@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
重启项目,你会发现工程完全可以自动加载默认包下的Controller
、service
了。
当然更暴力的方式是把涉及的jar
文件中的META-INF/spring.components
文件直接给干掉。这种方式感觉其实可行性非常差。
其实本文并没有讲解META-INF/spring.components
本身的一些用法,我也不太清楚。我们很多常规项目应该也不会遇到类似的问题。本项目主要是使用了CAS-Server才引入了该问题。当然如果有大牛指正问题更好。