学习 Spring Boot 时,使用 https://start.spring.io/ 创建示例时都非常顺利...但很多时候一旦自己按照业务要求创建多模块项目后就出现了 Spring Boot 项目正常启动了也没报任何的错误,但是访问 Controller 时就会报404。这到底是怎么回事呢?本文希望从各方面给你一个全景的解决视图。
阅读文章的过程中如果有任何疑问,欢迎添加笔者为好友,拉您进【七日书摘】微信交流群,一起交流技术,一起打造高质量的职场技术交流圈子,抱团取暖,共同进步。
项目目录结构不正确(@SpringBootApplication
注解的入口类未放置到正确的包名下)
比如创建了一个如下图的项目,
如上图中所示,项目的启动类在 com.saiyueze.chapter.simple2
中,此时启动 ChapterSimple2Application
可正常运行,并且也可以正常访问到com.saiyueze.chapter.simple2.web
这个包中的Controller
方法。
如果由于某些原因,启动类ChapterSimple2Application
被移动到了非 com.saiyueze.chapter.simple2
中,此时 ChapterSimple2Application
类可以正常启动,但是此时访问 com.saiyueze.chapter.simple2.web
中的任何Controller
方法都将返回 404 错误。
问题分析
此类情况是@SpringBootApplication
注解的入口类未在最外层包名下,因此造成 Spring Boot 项目时无法访问不在 @SpringBootApplication
注解的入口类所在的包名以及该包名下的所有子包中。
解决办法
可以直接把@SpringBootApplication
注解的入口类移动到最外层的包名中。
比如示例中 com.saiyueze.chapter.simple2
是最外层包名,此时可以把ChapterSimple2Application
移动到com.saiyueze.chapter.simple2
中即可解决。
当然也可以参考使用@ComponentScan
进行解决,可以在ChapterSimple2Application
中添加@ComponentScan
注解,并把最外层包名即com.saiyueze.chapter.simple2
添加到basePackages
中,以告诉 Spring Boot 项目启动时扫描该包及其子包中的类。
参考代码片段如下:
package com.saiyueze.chapter.simple2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = {"com.saiyueze.chapter.simple2"})
public class ChapterSimple2Application {
public static void main(String[] args) {
SpringApplication.run(ChapterSimple2Application.class, args);
}
}
使用 Maven 管理的多模块项目
使用 Maven 进行项目管理的工程中,通常会使用多 module 的架构设计,以满足项目的分层设计。
多 module 设计的项目,通常情况下,@SpringBootApplication
注解的入口类所在的包名和其他模块的包名都会存在差异,同时该包名也通常不是最所有模块最外层的包名,因此此时@SpringBootApplication
注解的入口类启动后,是无法访问其他模块的。
问题分析
由于多 module 项目,通常都会依据功能进行分模块设计,例如创建了如下实例结构项目:
mall-data //数据访问层
com.saiyueze.data //最外层包名
mall-service //业务逻辑层
conm.saiyueze.service //最外层包名
mall-portal //服务portal
com.saiyueze.web //最外层包名
MallPortalApplication.java //入口类
com.saiyueze.web.controller //controller包名
IndexController.java
在该示例中,mall-portal
会引入 mall-data
、mall-service
工程,并调用数据访问层和业务逻辑层代码。
其中默认情况下MallPortalApplication.java
作为入口类启动时一般会出现无法正确注册 mall-data
和mall-service
中的服务接口Bean.
错误信息一般如下:
Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'xxxxApplication': Unsatisfied dependency expressed through field 'xxxx'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'xxxx': Unsatisfied dependency expressed through field 'xxxx'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.xx.xx.xx' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
解决办法
在@SpringBootApplication
注解的入口类中添加@ComponentScan
注解,并把最外层或者模块最外层包名添加到 basePackages
中,以告诉 Spring Boot 项目启动时扫描 basePackages
中配置的包名及其子包中的类。
示例如下:
package com.saiyueze.portal;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = {"com.saiyueze.data","com.saiyueze.service"})
public class MallPortalApplication {
public static void main(String[] args) {
SpringApplication.run(MallPortalApplication.class, args);
}
}
特别说明:使用@ComponentScan
注解时 basePackages
中通配符问题
从文章中可以看出来,示例中的@ComponentScan(basePackages = {"com.saiyueze.data","com.saiyueze.service"})
中未使用通配符*
,同样当你在阅读其他文章的时候,一定会有人告诉你需要修改成@ComponentScan(basePackages = {"com.saiyueze.data.*","com.saiyueze.service.*"})
这样子,然后你又会发现一些其他的问题。而我文中提供的示例为什么又不会有后续问题呢??
抱着这样的疑问,咋们一起来看如下的示例:
配置一:`@ComponentScan(basePackages = {"com.saiyueze.chapter.simple2"})
配置二:@ComponentScan(basePackages = {"com.saiyueze.chapter.simple2.*"})
配置三:@ComponentScan(basePackages = {"com.saiyueze.chapter.simple2.**"})
要解决该问题,可以从ClassPathScanningCandidateComponentProvider.java
中的scanCandidateComponents
中我们可以找到这三个配置的异同性。
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
boolean traceEnabled = logger.isTraceEnabled();
boolean debugEnabled = logger.isDebugEnabled();
for (Resource resource : resources) {
if (traceEnabled) {
logger.trace("Scanning " + resource);
}
if (resource.isReadable()) {
try {
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
if (isCandidateComponent(sbd)) {
if (debugEnabled) {
logger.debug("Identified candidate component class: " + resource);
}
candidates.add(sbd);
}
else {
if (debugEnabled) {
logger.debug("Ignored because not a concrete top-level class: " + resource);
}
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not matching any filter: " + resource);
}
}
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to read candidate component class: " + resource, ex);
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not readable: " + resource);
}
}
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
}
return candidates;
}
在该方法中,我们重点看
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
这句代码,如果你进行 debug 时你会发现如下现象:
配置一的 packageSearchPath 值是:
classpath*:com.saiyueze.chapter.simple2/**/*.class
配置二的 packageSearchPath 值是:
classpath*:com.saiyueze.chapter.simple2/*/**/*.class
配置三的 packageSearchPath 值是:
classpath*:com.saiyueze.chapter.simple2/**/**/*.class
根据这个scanCandidateComponents
方法一路跟踪下去,你会发现**
匹配任意 .class 文件和包,而*
只能匹配包,因此无法扫描到包下的类,因此也就无法被Spring 容器管理。
------完------