概述
先直接说我遇到的问题吧,Spring Boot应用启动失败:
ERROR | org.springframework.boot.web.embedded.tomcat.TomcatStarter | onStartup | 61 | - Error starting Tomcat context. Exception: org.springframework.beans.factory.UnsatisfiedDependencyException.
Message: Error creating bean with name 'jwtAuthorizationTokenFilter' defined in file [/Users/johnny/code/backend/rbac/rbac-provider/target/classes/com/aaaaa/rbac/modules/security/security/JwtAuthorizationTokenFilter.class]: Unsatisfied dependency expressed through constructor parameter 0;
nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jwtUserDetailsService': Injection of resource dependencies failed;
nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jwtPermissionService': Injection of resource dependencies failed;
nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'roleServiceImpl': Injection of resource dependencies failed;
nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.aaaaa.rbac.modules.system.service.mapper.RoleMapper' available:
expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@javax.annotation.Resource(shareable=true, lookup="", name="", description="", authenticationType=CONTAINER, type=java.lang.Object.class, mappedName="")}
PS:上面的报错日志经过硬换行(hard wrap)处理,方便阅读;IDEA打印的日志是经过软换行(soft wrap)显示的:
排查
Spring Boot启动失败不要太常见。
分析
分析一下这个启动报错日志:
- Spring启动时想注入
jwtAuthorizationTokenFilter
这个Bean,发现jwtAuthorizationTokenFilter
依赖于jwtUserDetailsService
; - 那就注入
jwtUserDetailsService
吧,发现jwtUserDetailsService
又依赖于jwtPermissionService
; -
jwtPermissionService
依赖于roleServiceImpl
; -
roleServiceImpl
又依赖于RoleMapper
; -
RoleMapper
依赖于RoleMapper
;
一开始以为是@Resource和@Autowired的差异导致应用启动失败。
于是把上面报错提到的Spring Bean的@Resource
注解换成@Autowired注解,还是同样的报错;
添加@Qualifier注解,还是同样的报错;
@Autowired
@Qualifier("roleServiceImpl")
private RoleService roleService;
经过上面这一番瞎折腾,发现解决不了问题,才静下心来分析Mapper类,此时才发现端倪:
import org.mapstruct.Mapper;
import org.mapstruct.ReportingPolicy;
@Mapper(componentModel = "spring", uses = {PermissionMapper.class, MenuMapper.class, DeptMapper.class}, unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface RoleMapper extends EntityMapper<RoleDTO, Role> {
}
RoleMapper
使用的@Mapper
注解并不是MyBatis提供的注解,而是MapStruct提供的注解。
另外附上EntityMapper源码:
public interface EntityMapper<D, E> {
E toEntity(D dto);
D toDto(E entity);
List<E> toEntity(List<D> dtoList);
List<D> toDto(List<E> entityList);
}
关于MapStruct的基础,可参考Java对象拷贝MapStruct。
看一下启动类:
@SpringBootApplication
@EnableTransactionManagement
@EnableDiscoveryClient
@EnableLdapRepositories("com.aaaaa.rbac.modules.ldap.repository")
@EnableSwagger2
public class AppRun {
public static void main(String[] args) {
SpringApplication.run(AppRun.class, args);
}
}
并没有配置扫描路径,那就在@SpringBootApplication
注解里增加扫描路径:
@SpringBootApplication(scanBasePackages = {"com.aaaaa.rbac"})
并没有解决问题,还是同样的报错。
MapStruct
上面提到xxxMapper
类使用MapStruct提供的@Mapper注解,并且显式指定生成Spring Component类:componentModel = "spring"
。
也就是说再执行mvn clean compile
后,应该可以在classpath路径下看到RoleMapperImpl.class
文件,pom文件里也引入过依赖:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.2.0.Final</version>
</dependency>
可是我在执行mvn clean compile
命令后,并没有看到aaaImpl.class
文件。
试过升级版本号,还是没有看到aaaImpl.class
文件。
于是又去瞎捣鼓,瞎排查,去排查编译问题。
理论上,应该生成Impl.class
文件,有这些文件,应用就可以启动成功。
尝试配置IDEA:
尝试过安装插件,并重启IDEA:
都没有看到编译生成的Impl.class文件,应用启动失败,还是同样的报错日志。
前前后后各种排查启动失败的问题,花费3~4个小时吧。
此时才想起来从头审视这个应用启动报错。
缘起
生产环境某个应用大量爆出如下ERROR级别日志:session ip change too many
查看到如下具体的报错信息,原来报错来源自alibaba druid这个开源组件:
等等!merchant服务代码我熟悉得很,并没有在pom.xml
文件里引入过druid啊。
查看Git提交日志,发现最近merchant服务引入另一个服务提供的Feign接口,即引入rbac-client
。借助于Maven Helper插件提供的Dependency Analyzer面板,查看到druid组件确实是在引入rbac-client
包后引入的。
maven module
rbac服务之前只有2个module:
<modules>
<module>rbac-common</module>
<module>rbac-provider</module>
</modules>
加上工程父pom.xml
文件,一共3个pom.xml
文件。
新增业务场景需要,其他应用(也就是上面报错的merchant应用)需要用到rbac里已有的某个模块功能,于是rbac服务新增一个rbac-client
模块,即maven module,即新增一个pom.xml
文件。
理论上工程根pom.xml
文件应该很轻量化:
<project>
<parent>
</parent>
<groupId>com.aaa.rbac</groupId>
<artifactId>rbac</artifactId>
<packaging>pom</packaging>
<version>1.0.0-SNAPSHOT</version>
<modules>
<module>rbac-common</module>
</modules>
<name>rbac</name>
<properties>
<user.version>1.0.0-SNAPSHOT</user.version>
</properties>
</project>
但是rbac这个服务却不是这样,在工程根pom.xml
文件里引入一大堆乱七八糟的依赖,rbac-client
作为其中的一个module,自然也就引入工程根pom.xml
文件里引入的一大堆依赖,这其中就包括引发session ip change too many
的druid组件。
真相
为了解决merchant服务爆出的druid错误日志。并没有考虑在merchant服务里的pom.xml
文件里,通过增加exclusions标签来排除druid依赖,而是考虑直接优化rbac服务的根pom.xml
文件。
也就是说,上面的应用启动报错日志,包括没有看到编译生成的aaImpl.class
文件,都是在我改动rbac服务的几个pom.xml
文件之后产生的。
ok,fine.
Actually, I’m not fine.
代码改动还在本地,没有提交。git stash
将所有改动暂存下来,即恢复到没有任何改动的clean状态。执行mvn clean compile
命令,终于看到生成如下aaMapperImpl.class
文件
编译器生成的类文件如下:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class RoleMapperImpl implements RoleMapper {
@Autowired
private PermissionMapper permissionMapper;
@Autowired
private MenuMapper menuMapper;
@Autowired
private DeptMapper deptMapper;
// 省略一大堆DTO和PO类转换方法
}
启动应用,当然可以启动成功,毕竟rbac服务在生产跑着呢。
这也印证之前的猜想,应用启动失败是因为没有找到aaMapperImpl.class
文件。
maven
知道原因后,rbac的pom文件还是得优化一下。执行git unstash
,或借助于IDEA提供的Git->Unstash Changes功能。
问题出在mapstruct-processor
核心依赖:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<scope>provided</scope>
</dependency>
rbac-provider
module依赖于rbac-common
module。
如果把mapstruct-processor
放在rbac-common module下的pom.xml
文件里,虽然rbac-provider有声明引入rbac-common,也就自然而然会引入mapstruct-processor
依赖,事实上通过Maven Helper插件也能看到rbac-provider module确实引入mapstruct-processor
依赖,但是应用启动就是失败。
如果把mapstruct-processor
依赖直接放在rbac-provider module的pom.xml
文件里,则应用启动成功。
What The Fuck???
所以问题是我搞不懂Maven啊?????
does not have an accessible empty constructor.
另外,在优化工程的pom.xml
文件时,也就是把之前放在根pom.xml
文件里的依赖分散到各个module下时。还出现一个编译问题:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile (default-compile) on project rbac-provider: Compilation failure
[ERROR] com.aaaaa.rbac.modules.system.service.dto.JobDTO does not have an accessible empty constructor.
也就是说编译都通不过,更谈何启动应用呢?
作为另一个比MapStruct更出名,使用率更高的编译器插件,Lombok远比MapStruct大名鼎鼎吧。rbac服务当然也在使用lombok。
如果在rbac-common里引入lombok,在rbac-provider里引入mapstruct-processor就会出现上面这个编译失败的问题。
关于Maven的迷思:
看来我是真的不会用Maven,不懂Maven啊???
反思
- 遇到问题一定不能混乱,不能
瞎百度 - 真的不懂Maven啊。