一、问题描述
在一次偶然通过idea运行gateway dev分支代码的时候,遇到服务启动失败的异常,但是同样的dev代码在dev环境上面使用时正常的。但是在本地idea中启动就失败,项目无法正常启动,以下是报错异常信息(考虑到信息安全,只展示以下关键信息)。
shell
Caused by: java.lang.NoSuchFieldError: az_glo
at com.anet.ops.scd.Cmbot.<clinit>(Cmbot.java:26)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:264)
at com.sun.proxy.$Proxy177.<clinit>(Unknown Source)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at java.lang.reflect.Proxy.newProxyInstance(Proxy.java:739)
at feign.ReflectiveFeign.newInstance(ReflectiveFeign.java:65)
at feign.Feign$Builder.target(Feign.java:268)
at org.springframework.cloud.openfeign.DefaultTargeter.target(DefaultTargeter.java:30)
at org.springframework.cloud.openfeign.FeignClientFactoryBean.getTarget(FeignClientFactoryBean.java:451)
at org.springframework.cloud.openfeign.FeignClientFactoryBean.getObject(FeignClientFactoryBean.java:402)
at org.springframework.cloud.openfeign.FeignClientsRegistrar.lambda$registerFeignClient$0(FeignClientsRegistrar.java:235)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1249)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1191)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
... 42 common frames omitted
二、问题分析
通过异常可以看到是Cmbot这个类中引入了不存在az_glo变量导致服务启动失败。通过idea反编译看到确实类中az_glo变量报红。
顺着调用链点开SRrp看到,确实使用的SRrp中没有az_glo这个变量。但是同时在依赖树中注意到s-library这个包还有一个高版本。
打开高版本jar中的SRrp可以看到有az_glo这个变量,那么到这里就能知道服务启动失败的原因是因为依赖冲突了,这里引用了低版本的jar所以找不到变量。
执行gradle dependencies命令查看依赖树分析。由于解析内容太长这里按照不同构建类型来进行展示。
compileClasspath
shell
compileClasspath - Compile classpath for source set 'main'.
+--- com.anet.ops:c-ient:1.9.177
+--- com.anet.ops:commons:1.10.165
| +--- com.anet.ops:s-library:0.1.2
productionRuntimeClasspath
shell
productionRuntimeClasspath
+--- project :gateway:mbg
| +--- com.anet.ops:commons:1.10.165
| | +--- com.anet.ops:s-library:0.1.2 -> 0.1.9
runtimeClasspath
shell
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project :gateway:mbg
| +--- com.anet.ops:commons:1.10.165
| | +--- com.anet.ops:s-library:0.1.2 -> 0.1.9
结果对比
通过对比三者的结果发现。
- 在compileClasspath中使用的时低版本的0.1.2的s-library
- 在runtimeClasspath中由于其他模块也依赖了s-library的更高版本,所以整个项目都使用了更高的版本
从这里可以看到,低版本的s-library只是在编译阶段会使用到,正常运行构建好的jar使用的是高版本,这里就解释了为什么同样的代码idea启动报错,dev环境上却能正常运行。因为idea里面我们是直接编译运行的,因此编译阶段使用了低版本的jar导致出错。到这里我们已经知道了为什么本地idea启动报错,dev环境上却能正常运行的问题。
但是紧接着就是另一问题,为什么编译阶段没有采用gradle默认策略中更高版本依赖呢?
三、implementation和api的区别
接着上面的问题,为什么compileClasspath中有没有使用高版本依赖呢?这个要从gradle中使用不同的引入依赖方式来定了。我们代码中都是通过implementation的方式来引入依赖,没有使用api来引入依赖(使用implementation可以减少依赖暴露,节省构建时间)。
这是因为他们两者在gradle中的处理方式不同gradle中implementation和api的区别。
-
依赖可见性
implementation
:当你使用implementation
声明一个依赖时,这个依赖只会对当前模块可见,不会传递给依赖当前模块的其他模块。这意味着如果当前模块 A 依赖于模块 B,而模块 B 使用了implementation
声明一个库,那么这个库对模块 A 是不可见的。api
:相反,当你使用api
声明一个依赖时,这个依赖不仅对当前模块可见,而且会传递给依赖当前模块的其他模块。这意味着如果模块 B 使用了api
声明一个库,那么这个库对模块 A 也是可见的。
-
编译时依赖传递行为:
implementation
:使用implementation
声明的依赖项只会在编译时对当前模块生效,不会传递给其他模块。这意味着如果模块 A 依赖于模块 B,并且模块 B 使用implementation
声明一个库,那么库不会对模块 A 可见。api
:使用api
声明的依赖项会在编译时和运行时都对当前模块和依赖当前模块的其他模块生效。
-
构建速度和可维护性:
- 使用
implementation
可以降低构建系统的负担,因为它会限制传递性依赖的范围,从而减少了重新编译的需要。 - 使用
api
可以提供更广泛的依赖传递,但可能会增加构建时间,因为任何直接或间接依赖当前模块的模块都可能会受到影响。
- 使用
一般来说,推荐使用 implementation
来声明依赖项,除非你确实需要将某个库暴露给其他模块使用,这时可以使用 api
。这样做有助于减少依赖传递的复杂性,提高构建速度,并提供更好的封装性。
到这里就能明白为什么会出现complieClasspath中使用了低版本,并且对应的其他c-ient依赖的s-library不在dependency树里面了。
四、处理问题
问题已经分析清楚之后,我们处理版本有很多,可以采用force或者dependencymanagement强制限制版本,或者exclude排除依赖,但是考虑到项目实际运行,最终还是要继续使用高版本的jar,所以我们不会采取以上方式处理,我们已经通过dependencies命令发现了是因为commons版本太低导致使用了低版本的s-library,那么我们只需要升级commons依赖版本即可。
shell
implementation 'com.anet.ops:commons:1.10.225'
五、总结
到这里我们了解到了
- gradle默认采用高版本来解决依赖冲突。
- 在编译阶段implementation生命的依赖,不会将依赖传递给其他外部模块,因此在编译阶段可能会出现低版本没有被覆盖的问题,运行时则不会(这就解释了为什么本地idea运行失败,打包运行没有问题)。