ClassNotFoundException 当“实现”用于库的库依赖时

Posted

技术标签:

【中文标题】ClassNotFoundException 当“实现”用于库的库依赖时【英文标题】:ClassNotFoundException when "implementation" is used for library dependency of a library 【发布时间】:2018-09-08 04:58:24 【问题描述】:

我刚刚创建了一个库并上传到 bintray 和 jcenter。

在我的测试应用中,这个库被添加为一个模块:

implementation project(':dropdownview')

一切都很好。

库模块上传到jcenter后,我用这个代替:

implementation 'com.asksira.android:dropdownview:0.9.1

然后当库尝试调用依赖于另一个库的方法时,会发生运行时错误

Caused by: java.lang.ClassNotFoundException: Didn't find class "com.transitionseverywhere.TransitionSet" on path: DexPathList[[zip file "/data/app/com.asksira.dropdownviewdemo-6fj-Q2LdwKQcRAnZHd2jlw==/base.apk"],nativeLibraryDirectories=[/data/app/com.asksira.dropdownviewdemo-6fj-Q2LdwKQcRAnZHd2jlw==/lib/arm64, /system/lib64, /system/vendor/lib64]]

(我关注 this guide 发布库。我在使用相同方法之前发布了 3 个库,它们都运行良好;但这是我第一次在我自己的库中包含另一个 3rd 方库依赖项.)

编译与实现

然后我尝试将我的库的第 3 方库依赖项从

implementation 'com.andkulikov:transitionseverywhere:1.7.9'

compile 'com.andkulikov:transitionseverywhere:1.7.9'

(请注意,这不是应用程序对我的库的依赖,而是我的库对另一个库的依赖)

然后再次上传到 0.9.2 版本的 bintray。

implementation 'com.asksira.android:dropdownview:0.9.2

这次成功了?!

我的问题

这是 Android Studio / Gradle 的某种错误(但谷歌说他们将在 2018 年底之前删除 compile...),还是我做错了什么?

v0.9.1的完整源代码可以在here找到。

请注意,我没有直接从app 访问任何方法到TransitionsEverywhere。具体来说,ClassNotFoundException 在我点击DropDownView 时发生,DropDownView 调用expand() 这是public 内部方法。

更多信息

为了消除其他因素,以下是我在将implementation 更改为compile 之前尝试过的事情,都没有运气:

    清理并重建 卸载应用 + 清理并重新构建 使应用程序成为 MultiDexApplication 即时运行已被禁用

【问题讨论】:

当你使用实现的时候,你还必须添加你的库的依赖库。所以在这种情况下,你必须在你的应用模块中添加这些依赖库。 @Chandrakanth 你确定是这样吗?据我所知,implementation 仅表示应用程序本身无法访问依赖项的依赖关系。不然怎么能完全取代被弃用的compile @SiraLam 看看这个。 ***.com/questions/44493378/… 当您使用实现时,这意味着 lib 仅适用于正确的代码。构建代码时它不会编译。当您使用编译时,这意味着在构建项目时,lib 代码也会在您的项目中构建。现在,当您在运行时遇到错误时,代码存在但依赖库不存在。希望你现在已经清楚了。 @WarrenFaith 哦,已经一年了。现在我确切地知道为什么 - 当您的项目具有使用实现包含依赖项 B 的依赖项 A 时,您的项目将看不到依赖项 B。但是由于我已经通过在线存储库包含了依赖项 A,所以您项目的 gradle 不知道依赖项 A 正在请求依赖项 B。因此,如果您从项目中请求依赖项 B,它将解决;但最好的方法是在您的库中使用api 而不是implementation。简而言之,imo,每个模块都应该使用api。只有***项目应该使用implementation 【参考方案1】:

我现在遇到了完全相同的问题,根据您的评论,我真的怀疑这是否应该是这样。我的意思是用api 替换库中的所有implementation 对干净的抽象没有意义。如果不需要,甚至有时甚至不应该被允许使用,我为什么要将我的库使用的依赖项暴露给消费者/应用程序。

我还检查了生成的 APK 确实包含它抱怨找不到的类。

由于我之前遇到了依赖问题,我记得我自己改进了为库生成的 POM。

在我改进之前,生成的 pom 是这样的:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>tld.yourdomain.project</groupId>
    <artifactId>library-custom</artifactId>
    <version>1.2.0-SNAPSHOT</version>
    <packaging>aar</packaging>
    <dependencies/>
</project>

我使用以下脚本添加依赖项,并基于 implementationapi 为它们添加了正确的范围 (based on that nice info)

apply plugin: 'maven-publish'

task sourceJar(type: Jar) 
    from android.sourceSets.main.java.srcDirs
    archiveClassifier = "sources"


task listDependencies() 
    // Curious, "implementation" also contains "api"...
    configurations.implementation.allDependencies.each  dep -> println "Implementation: $dep" 
    configurations.api.allDependencies.each  dep -> println "Api: $dep" 


afterEvaluate 
    publishing 
        publications 
            mavenAar(MavenPublication) 
                groupId libraryGroupId
                artifactId libraryArtefactId
                version versionName

                artifact sourceJar
                artifact bundleReleaseAar

                pom.withXml 
                    def dependenciesNode = asNode().appendNode('dependencies')
                    configurations.api.allDependencies
                            .findAll  dependency -> dependency.name != "unspecified" 
                            .each  dependency ->
                        addDependency(dependenciesNode.appendNode('dependency'), dependency, "compile")
                    

                    configurations.implementation.allDependencies
                            .findAll  dependency -> !configurations.api.allDependencies.contains(dependency) 
                            .findAll  dependency -> dependency.name != "unspecified" 
                            .each  dependency ->
                        addDependency(dependenciesNode.appendNode('dependency'), dependency, "runtime")
                    
                
            
        

        repositories 
            maven 
                def snapshot = "http://repo.yourdomainname.tld/content/repositories/snapshots/"
                def release = "http://repo.yourdomainname.tld/content/repositories/releases/"
                url = versionName.endsWith("-SNAPSHOT") ? snapshot : release
                credentials 
                    username nexusUsername
                    password nexusPassword
                
            
        
    


def addDependency(dependencyNode, dependency, scope) 
    dependencyNode.appendNode('groupId', dependency.group)
    dependencyNode.appendNode('artifactId', dependency.name)
    dependencyNode.appendNode('version', dependency.version)
    dependencyNode.appendNode('scope', scope)

你需要了解的关键部分:

如果您不定义范围,将假定为“编译” implementation 依赖也包含 api 的,只需运行任务 listDependencies() 即可查看输出 在runtime 范围内,API 在应用程序/消费者中不可用,但它是类路径的一部分。这样消费者就不能直接访问这些依赖项,只能通过您自己的库提供的方法使这些依赖项“不可见”,但它们将成为类路径的一部分,因此当这些“不可见”依赖项的类是时,应用程序不会崩溃由类加载器加载。

上面的脚本现在生成以下 pom:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>tld.yourdomain.project</groupId>
    <artifactId>library-custom</artifactId>
    <version>1.2.0-SNAPSHOT</version>
    <packaging>aar</packaging>
    <dependencies>
        <dependency>
            <groupId>tld.dependency</groupId>
            <artifactId>android-sdk</artifactId>
            <version>1.2.3</version>
            <scope>compile</scope> <!-- From api -->
        </dependency>
        <dependency>
            <groupId>tld.dependency.another</groupId>
            <artifactId>another-artifact</artifactId>
            <version>1.2.3</version>
            <scope>runtime</scope> <!-- From implementation -->
        </dependency>
        <!-- and much more -->
    </dependencies>
</project>

总结一下:

api 提供类,使消费者也可以访问依赖项 implementation 也提供类,但不会使消费者可以访问依赖项,但在定义的 runtime 范围内,它仍然是类路径的一部分,使类加载器知道这些类在运行时可用

编辑

如果您要进行大量更改并测试快照,请确保已为它们禁用缓存。将此添加到您的根 build.gradle 文件中:

allprojects 
    configurations.all() 
        // to make sure SNAPSHOTS are fetched again each time
        resolutionStrategy.cacheDynamicVersionsFor 0, 'seconds'
        resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
    
    // more stuff here

【讨论】:

老实说,我认为您在这个主题上的知识绝对比我多得多,而且从我的角度来看,很少有 Android 开发人员能够真正理解您的解决方案(他们中的大多数人能够了解您的关键点并进行总结)。所以我只想问你是否认为需要这样做实际上是 Gradle 的糟糕设计? 相反。我正在为我调查未知领域,我真的认为我错过了自动和正确生成 POM 条目的点。所以我所做的主要是解决我缺少的信息 @WarrenFaith 我包含了脚本来生成你提到的 pom 文件。我仍然不断收到同样的错误?这事有进一步更新吗?几周以来我一直坚持这一点 @Kaveri 您是否检查过您生成的 pom 文件现在是否包含依赖项?还要检查 gradle 缓存是否不使用旧版本(缓存默认设置为 24h)。 (检查底部我的答案的编辑)

以上是关于ClassNotFoundException 当“实现”用于库的库依赖时的主要内容,如果未能解决你的问题,请参考以下文章

当dex分包遇上NoClassDefFoundError&ClassNotFoundException

InflateException 和 ClassNotFoundException 导致应用程序崩溃

ClassNotFoundException:oracle.jdbc.driver.OracleDriver

Android 组件 ClassNotFoundException

ClassNotFoundException / mysql jdbc驱动程序[重复]

Databricks 连接 java.lang.ClassNotFoundException