MapStruct+Maven+Lombok问题NoSuchBeanDefinitionException、does not have an accessible empty constructo排查
概述
先直接说我遇到的问题吧,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"})
并没有解决问题,还是同样的报错。
没看到aaaImpl.class
文件
上面提到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就会出现上面这个编译失败的问题。也就是说,lombok
关于Maven的迷思:
看来我是真的不会用Maven,不懂Maven啊???
java: Internal error in the mapping processor: java.lang.NullPointerException
mvn clean compile
执行成功,但是应用启动报错:
java: Internal error in the mapping processor: java.lang.NullPointerException
at org.mapstruct.ap.internal.processor.DefaultVersionInformation.createManifestUrl(DefaultVersionInformation.java:180)
at org.mapstruct.ap.internal.processor.DefaultVersionInformation.openManifest(DefaultVersionInformation.java:151)
at org.mapstruct.ap.internal.processor.DefaultVersionInformation.getLibraryName(DefaultVersionInformation.java:127)
at org.mapstruct.ap.internal.processor.DefaultVersionInformation.getCompiler(DefaultVersionInformation.java:120)
at org.mapstruct.ap.internal.processor.DefaultVersionInformation.fromProcessingEnvironment(DefaultVersionInformation.java:98)
at org.mapstruct.ap.internal.processor.DefaultModelElementProcessorContext.<init>(DefaultModelElementProcessorContext.java:59)
at org.mapstruct.ap.MappingProcessor.processMapperElements(MappingProcessor.java:222)
at org.mapstruct.ap.MappingProcessor.process(MappingProcessor.java:162)
at org.jetbrains.jps.javac.APIWrappers$ProcessorWrapper.process(APIWrappers.java:157)
at org.jetbrains.jps.javac.JavacMain.compile(JavacMain.java:238)
如上报错日志有删减及美化换行处理,另外这个启动报错日志并不是出现在IDEA的console里,而是出现在Build Output里:
并且有个WARN提示:
java: You aren't using a compiler supported by lombok, so lombok will not work and has been disabled.
Your processor is: com.sun.proxy.$Proxy25
Lombok supports: sun/apple javac 1.6, ECJ
解决WARN的方法是:使用较新的lombok版本。
解决ERROR报错的方法是:使用较新的MapStruct版本。
参考intellij-idea-mapstruct-java-internal-error-in-the-mapping-processor-java-lang。
NullPointerException: Cannot invoke "java.net.URL.toExternalForm()" because "resource" is null
具体的报错信息为:
Internal error in the mapping processor: java.lang.NullPointerException: Cannot invoke "java.net.URL.toExternalForm()" because "resource" is null
at org.mapstruct.ap.internal.processor.DefaultVersionInformation.createManifestUrl(DefaultVersionInformation.java:180)
at org.mapstruct.ap.internal.processor.DefaultVersionInformation.openManifest(DefaultVersionInformation.java:151)
参考stackoverflow。才发现工程使用的JDK版本不对。应该使用JDK 11(公司的Git工程),而我在使用JDK17(个人的demo示例工程)。
切换IDEA使用JDK 11后,还是报错:
java: Internal error in the mapping processor: java.lang.NullPointerException
at org.mapstruct.ap.internal.processor.DefaultVersionInformation.createManifestUrl(DefaultVersionInformation.java:180)
at org.mapstruct.ap.internal.processor.DefaultVersionInformation.openManifest(DefaultVersionInformation.java:151)
这尼玛不就和上面相同的问题吗?
但是使用上面的方法,并不能解决问题。
最后解决问题的方法:
- 找到设置选项:File | Settings | Build, Execution, Deployment | Compiler | User-local build process vm options(overrides Shared options)
- 添加
-Djps.track.ap.dependencies=false
再次尝试启动应用,成功。
java: Couldn't retrieve @Mapper annotation
报错同样出现在Build Output里:
解决方法:移除springfox-swagger2里的mapstruct相关依赖。
参考Couldn't retrieve @Mapper annotation。
反思
- 遇到问题一定不能慌乱,不能
瞎百度,瞎Google,瞎尝试。时间就在尝试中浪费,注意力就在分析这个研究那个中分散,可能差一点就摸到门了。 - 一定要先分析,排除和问题无关的因素。
- 说起来容易,排查起问题来思路肯定容易混乱,尤其是前前后后遇到上文提到的5类报错。
- 真的不懂Maven啊。
- 问题是和MapStruct、Lombok、Maven有关。解决问题不难,难的是彻底搞透原理。