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启动失败不要太常见。

分析

分析一下这个启动报错日志:

  1. Spring启动时想注入jwtAuthorizationTokenFilter这个Bean,发现jwtAuthorizationTokenFilter依赖于jwtUserDetailsService
  2. 那就注入jwtUserDetailsService吧,发现jwtUserDetailsService又依赖于jwtPermissionService
  3. jwtPermissionService依赖于roleServiceImpl
  4. roleServiceImpl又依赖于RoleMapper
  5. 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)

这尼玛不就和上面相同的问题吗?

但是使用上面的方法,并不能解决问题。

最后解决问题的方法:

  1. 找到设置选项:File | Settings | Build, Execution, Deployment | Compiler | User-local build process vm options(overrides Shared options)
  2. 添加-Djps.track.ap.dependencies=false

再次尝试启动应用,成功。

java: Couldn't retrieve @Mapper annotation

报错同样出现在Build Output里:
在这里插入图片描述
解决方法:移除springfox-swagger2里的mapstruct相关依赖。

参考Couldn't retrieve @Mapper annotation

反思

  1. 遇到问题一定不能慌乱,不能瞎百度 ,瞎Google,瞎尝试。时间就在尝试中浪费,注意力就在分析这个研究那个中分散,可能差一点就摸到门了。
  2. 一定要先分析,排除和问题无关的因素。
  3. 说起来容易,排查起问题来思路肯定容易混乱,尤其是前前后后遇到上文提到的5类报错。
  4. 真的不懂Maven啊。
  5. 问题是和MapStruct、Lombok、Maven有关。解决问题不难,难的是彻底搞透原理。
posted @ 2024-08-21 17:25  johnny233  阅读(35)  评论(0编辑  收藏  举报