नमस्ते! मेरा नाम यूरी व्लाद है, मैं Badoo पर एक एंड्रॉइड डेवलपर हूं और शुद्ध कोटलिन पर रेकिटिव लाइब्रेरी - रिएक्टिव एक्सटेंशन्स बनाने में भाग लेता हूं ।
किसी भी पुस्तकालय को जब भी संभव हो बाइनरी संगतता का निरीक्षण करना चाहिए। यदि निर्भरता में लाइब्रेरी के विभिन्न संस्करण असंगत हैं, तो परिणाम रनटाइम में क्रैश हो जाएगा। हम इस तरह की समस्या का सामना कर सकते हैं, उदाहरण के लिए, जब एमवीआईसी को रिकेटिव समर्थन जोड़ते हैं ।

इस लेख में, मैं आपको संक्षेप में बताऊंगा कि बाइनरी संगतता क्या है और कोटलिन के लिए इसकी विशेषताएं क्या हैं, साथ ही साथ जेटब्रेन में इसका समर्थन कैसे किया जाता है, और अब बेत में।
कोटलीन बाइनरी संगतता समस्या
मान लीजिए कि हमारे पास एक अद्भुत पुस्तकालय com.sample:lib:1.0
इस वर्ग के साथ:
data class A(val a: Int)
इसके आधार पर, हमने एक दूसरा पुस्तकालय com.sample:lib-extensions:1.0
। इसकी निर्भरताओं में com.sample:lib:1.0
। com.sample:lib:1.0
। उदाहरण के लिए, इसमें कक्षा A
लिए एक कारखाना विधि शामिल है:
fun createA(a: Int = 0): A = A(a)
अब हम अपने पुस्तकालय com.sample:lib:2.0
का नया संस्करण जारी करेंगे com.sample:lib:2.0
निम्नलिखित परिवर्तन के साथ:
data class A(val a: Int, val b: String? = null)
कोटलिन की पूरी तरह से संगत परिवर्तन, है ना? डिफ़ॉल्ट पैरामीटर के साथ, हम कंस्ट्रक्शन val a = A(a)
का उपयोग जारी रख सकते हैं, लेकिन केवल अगर सभी निर्भरता पूरी तरह से recompiled हैं। डिफ़ॉल्ट पैरामीटर जेवीएम का हिस्सा नहीं हैं और विशेष सिंथेटिक कंस्ट्रक्टर A
द्वारा लागू किए जाते हैं, जिसमें मापदंडों के वर्ग के सभी क्षेत्र शामिल हैं। मावेन रिपॉजिटरी से निर्भरता प्राप्त करने के मामले में, हम उन्हें पहले से ही इकट्ठा कर लेते हैं और उन्हें फिर से जोड़ नहीं सकते हैं।
com.sample:lib
का नया संस्करण com.sample:lib
, और हम इसे तुरंत अपनी परियोजना से जोड़ते हैं। हम अप टू डेट रहना चाहते हैं! नई सुविधाएँ, नए सुधार, नए कीड़े !
dependencies { implementation 'com.sample:lib:2.0' implementation 'com.sample:lib-extensions:1.0' }
और इस मामले में, हम रनटाइम में दुर्घटनाग्रस्त हो जाते हैं। createA
में createA
फ़ंक्शन क्लास
कंस्ट्रक्टर को एक पैरामीटर के साथ कॉल करने का प्रयास करेगा, लेकिन createA
में ऐसा कोई नहीं है। एक ही समूह और नाम के साथ सभी आश्रितों में से, ग्रैडल उसी का चयन करेगा जिसका सबसे हाल का संस्करण है और इसे विधानसभा में शामिल करें।
सबसे अधिक संभावना है, आप पहले से ही अपनी परियोजनाओं में द्विआधारी असंगति का सामना कर चुके हैं। निजी तौर पर, मुझे यह तब आया जब मैंने अपने एप्लिकेशन AndroidX पर माइग्रेट किए।
आप Gvsmirnov उपयोगकर्ता द्वारा "उदाहरण में और न केवल" द्विआधारी संगतता लेखों में द्विआधारी संगतता के बारे में अधिक पढ़ सकते हैं। ग्रहण रचनाकारों से और जावा व्हर्टन द्वारा हाल ही में प्रकाशित लेख "कोटलिन में सार्वजनिक एपीआई चुनौतियां" ।
बाइनरी संगतता सुनिश्चित करने के तरीके
ऐसा लगता है कि आपको केवल संगत परिवर्तन करने की कोशिश करने की आवश्यकता है। उदाहरण के लिए, नए फ़ील्ड्स को जोड़ते समय एक डिफ़ॉल्ट मान के साथ कंस्ट्रक्टर जोड़ें, नए पैरामीटर के साथ एक विधि को ओवरराइड करके नए मापदंडों को जोड़ें आदि, लेकिन गलती करना हमेशा आसान होता है। इसलिए, एक ही पुस्तकालय के दो अलग-अलग संस्करणों की द्विआधारी संगतता की जांच के लिए विभिन्न उपकरण बनाए गए, जैसे:
- जावा एपीआई अनुपालन परीक्षक
- Clirr
- Revapi
- Japicmp
- Japitools
- jour
- Japi-चेकर
- SigTest
वे दो JAR फाइलें लेते हैं और परिणाम देते हैं: वे कितने संगत हैं।
हालांकि, हम कोटलिन पुस्तकालय विकसित कर रहे हैं, जो अब तक केवल कोटलिन से उपयोग करने के लिए समझ में आता है। इसका मतलब है कि हमें हमेशा 100% संगतता की आवश्यकता नहीं है, उदाहरण के लिए, internal
कक्षाओं के लिए। यद्यपि वे बायटेकोड में सार्वजनिक हैं, कोटलिन कोड के बाहर उनके उपयोग की संभावना नहीं है। इसलिए, द्विआधारी संगतता बनाए रखने के लिए, kotlin-stdlib JetBrains एक बाइनरी संगतता परीक्षक का उपयोग करता है। मूल सिद्धांत यह है: संपूर्ण सार्वजनिक API का एक डंप JAR फ़ाइल से बनाया गया है और फ़ाइल में लिखा गया है। यह फ़ाइल आगे की सभी जाँचों के लिए एक आधार रेखा (संदर्भ) है, और यह इस तरह दिखता है:
public final class kotlin/coroutines/ContinuationKt { public static final fun createCoroutine (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; public static final fun createCoroutine (Lkotlin/jvm/functions/Function2;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; public static final fun startCoroutine (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)V public static final fun startCoroutine (Lkotlin/jvm/functions/Function2;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)V }
स्रोत कोड में परिवर्तन करने के बाद, बेसलाइन लाइब्रेरी को पुनर्जीवित किया जाता है, वर्तमान एक के साथ तुलना में, और बेसलाइन में कोई भी परिवर्तन दिखाई देने पर चेक विफल हो जाता है। इन बदलावों को पास करके -Doverwrite.output=true
जा सकता है -Doverwrite.output=true
। बाइनरी संगत परिवर्तन होने पर भी एक त्रुटि उत्पन्न होगी। बेसलाइन को समय पर अपडेट करने और पुल अनुरोध में सीधे इसके बदलाव देखने के लिए यह आवश्यक है।
बाइनरी संगतता सत्यापनकर्ता
आइए देखें कि यह उपकरण कैसे काम करता है। बाइनरी संगतता JVM (बाइटकोड) स्तर पर प्रदान की जाती है और भाषा स्वतंत्र होती है। बाइनरी संगतता (और इसके विपरीत) को तोड़ने के बिना, कोटलिन के साथ जावा वर्ग कार्यान्वयन को बदलना संभव है।
पहले आपको यह समझने की आवश्यकता है कि पुस्तकालय में कौन सी कक्षाएं हैं। हमें याद है कि वैश्विक कार्यों और स्थिरांक के लिए भी, फ़ाइल नाम और प्रत्यय Kt
के साथ एक वर्ग बनाया जाता है, उदाहरण के लिए, ContinuationKt
। सभी वर्गों को प्राप्त करने के लिए, हम JDK से JarFile
क्लास का उपयोग करते हैं, प्रत्येक कक्षा को पॉइंटर्स प्राप्त करते हैं और उन्हें org.objectweb.asm.tree.ClassNode
पास करते हैं। यह कक्षा हमें कक्षा की दृश्यता, उसके तरीकों, क्षेत्रों और टिप्पणियों से अवगत कराएगी।
val jar = JarFile("/path/to/lib.jar") val classStreams = jar.classEntries().map { entry -> jar.getInputStream(entry) } val classNodes = classStreams.map { it.use { stream -> val classNode = ClassNode() ClassReader(stream).accept(classNode, ClassReader.SKIP_CODE) classNode } }
कोटलिन, संकलन करते समय, अपने @Metadata
रनटाइम एनोटेशन को प्रत्येक वर्ग में जोड़ता है ताकि kotlin-reflect
को kotlin-reflect
में परिवर्तित होने से पहले kotlin-reflect
वर्ग के kotlin-reflect
को पुनर्स्थापित कर सके। यह इस तरह दिखता है:
@Metadata( mv = {1, 1, 16}, bv = {1, 0, 3}, k = 1, d1 = {"\u0000 \n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\b\n\u0002\b\u0006\n\u0002\u0010\u000b\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0000\b\u0086\b\u0018\u00002\u00020\u0001B\r\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0002\u0010\u0004J\t\u0010\u0007\u001a\u00020\u0003HÆ\u0003J\u0013\u0010\b\u001a\u00020\u00002\b\b\u0002\u0010\u0002\u001a\u00020\u0003HÆ\u0001J\u0013\u0010\t\u001a\u00020\n2\b\u0010\u000b\u001a\u0004\u0018\u00010\u0001HÖ\u0003J\t\u0010\f\u001a\u00020\u0003HÖ\u0001J\t\u0010\r\u001a\u00020\u000eHÖ\u0001R\u0011\u0010\u0002\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\u0005\u0010\u0006¨\u0006\u000f"}, d2 = {"Lcom/sample/A;", "", "a", "", "(I)V", "getA", "()I", "component1", "copy", "equals", "", "other", "hashCode", "toString", "", "app_release"} )
ClassNode
@Metadata
से @Metadata
एनोटेशन प्राप्त कर सकते हैं और इसे KotlinClassHeader
में पार्स कर KotlinClassHeader
। आपको इसे मैन्युअल रूप से करना होगा, क्योंकि kotlin-reflect
पता नहीं है कि ऑब्जेक्टवेब एएसएम के साथ कैसे काम किया जाए।
val ClassNode.kotlinMetadata: KotlinClassMetadata? get() { val metadata = findAnnotation("kotlin/Metadata", false) ?: return null val header = with(metadata) { KotlinClassHeader( kind = get("k") as Int?, metadataVersion = (get("mv") as List<Int>?)?.toIntArray(), bytecodeVersion = (get("bv") as List<Int>?)?.toIntArray(), data1 = (get("d1") as List<String>?)?.toTypedArray(), data2 = (get("d2") as List<String>?)?.toTypedArray(), extraString = get("xs") as String?, packageName = get("pn") as String?, extraInt = get("xi") as Int? ) } return KotlinClassMetadata.read(header) }
kotlin.Metadata को internal
से ठीक से संभालने के लिए आवश्यक है, क्योंकि यह बाइटकोड में मौजूद नहीं है। internal
कक्षाओं और कार्यों में परिवर्तन पुस्तकालय उपयोगकर्ताओं को प्रभावित नहीं कर सकता है, हालांकि वे बायटेकोड के संदर्भ में एक सार्वजनिक एपीआई हैं।
Kotlin.Metadata से आप companion object
बारे में पता लगा सकते हैं। यहां तक कि अगर आप इसे निजी घोषित करते हैं, तब भी इसे सार्वजनिक स्थैतिक क्षेत्र Companion
में संग्रहीत किया जाएगा, जिसका अर्थ है कि यह क्षेत्र द्विआधारी संगतता की आवश्यकता के अंतर्गत आता है।
class CompositeException() { private companion object { } }
public final static Lcom/badoo/reaktive/base/exceptions/CompositeException$Companion; Companion @Ljava/lang/Deprecated;()
आवश्यक एनोटेशन में, यह उन वर्गों और विधियों के लिए @PublishedApi
ध्यान देने योग्य है जो सार्वजनिक inline
फ़ंक्शंस में उपयोग किए जाते हैं। इस तरह के कार्यों का शरीर उनके कॉल के स्थानों में रहता है, जिसका अर्थ है कि उनमें कक्षाएं और विधियां द्विआधारी संगत होनी चाहिए। जब आप ऐसे कार्यों में गैर-सार्वजनिक वर्गों और विधियों का उपयोग करने का प्रयास करते हैं, तो कोटलिन कंपाइलर @PublishedApi
त्रुटि को @PublishedApi
देगा और उन्हें @PublishedApi
एनोटेशन के साथ चिह्नित करने की पेशकश @PublishedApi
।
fun ClassNode.isPublishedApi() = findAnnotation("kotlin/PublishedApi", includeInvisible = true) != null
द्विआधारी संगतता का समर्थन करने के लिए वर्ग वंशानुक्रम और इंटरफेस के कार्यान्वयन के पेड़ महत्वपूर्ण हैं। उदाहरण के लिए, हम क्लास से कुछ इंटरफ़ेस नहीं हटा सकते। और माता-पिता वर्ग और लागू करने योग्य इंटरफेस प्राप्त करना बहुत सरल है।
val supertypes = listOf(classNode.superName) - "java/lang/Object" + classNode.interfaces.sorted()
Object
सूची से हटा Object
है, क्योंकि इसे ट्रैक करने का कोई मतलब नहीं है।
सत्यापनकर्ता के अंदर बहुत सारे अतिरिक्त अतिरिक्त कोटलिन-विशिष्ट चेक होते हैं: Interface$DefaultImpls
माध्यम से Interface$DefaultImpls
में डिफ़ॉल्ट तरीकों की जाँच करना, when
ऑपरेटर के काम करने के लिए $WhenMappings
कक्षाओं की अनदेखी करना, और अन्य।
इसके बाद, आपको सभी ClassNode
MethodNode
और उनके MethodNode
और FieldNode
प्राप्त करने की FieldNode
। वर्गों, उनके क्षेत्रों और विधियों के हस्ताक्षर से, हमें ClassBinarySignature
, FieldBinarySignature
और MethodBinarySignature
, जिन्हें प्रोजेक्ट में स्थानीय रूप से घोषित किया जाता है। वे सभी MemberBinarySignature
इंटरफेस को लागू करते हैं, जो कि isEffectivelyPublic
पद्धति का उपयोग करके अपनी सार्वजनिक दृश्यता निर्धारित करने में सक्षम हैं और एक पठनीय प्रारूप val signature: String
isEffectivelyPublic
हस्ताक्षर में अपने हस्ताक्षर प्रदर्शित करते हैं val signature: String
।
classNodes.map { with(it) { val metadata = kotlinMetadata val mVisibility = visibilityMapNew[name] val classAccess = AccessFlags(effectiveAccess and Opcodes.ACC_STATIC.inv()) val supertypes = listOf(superName) - "java/lang/Object" + interfaces.sorted() val memberSignatures = ( fields.map { with(it) { FieldBinarySignature(JvmFieldSignature(name, desc), isPublishedApi(), AccessFlags(access)) } } + methods.map { with(it) { MethodBinarySignature(JvmMethodSignature(name, desc), isPublishedApi(), AccessFlags(access)) } } ).filter { it.isEffectivelyPublic(classAccess, mVisibility) } ClassBinarySignature(name, superName, outerClassName, supertypes, memberSignatures, classAccess, isEffectivelyPublic(mVisibility), metadata.isFileOrMultipartFacade() || isDefaultImpls(metadata) } }
ClassBinarySignature
सूची प्राप्त करने के बाद, आप इसे dump(to: Appendable)
का उपयोग करके किसी फ़ाइल या मेमोरी में लिख सकते हैं और इसकी तुलना बेसलाइन से कर सकते हैं, जो RuntimePublicAPITest
परीक्षण में होती है:
class RuntimePublicAPITest { @[Rule JvmField] val testName = TestName() @Test fun kotlinStdlibRuntimeMerged() { snapshotAPIAndCompare("../../stdlib/jvm/build/libs", "kotlin-stdlib") } private fun snapshotAPIAndCompare( basePath: String, jarPattern: String, publicPackages: List<String> = emptyList(), nonPublicPackages: List<String> = emptyList() ) { val base = File(basePath).absoluteFile.normalize() val jarFile = getJarPath(base, jarPattern, System.getProperty("kotlinVersion")) println("Reading binary API from $jarFile") val api = getBinaryAPI(JarFile(jarFile)).filterOutNonPublic(nonPublicPackages) val target = File("reference-public-api") .resolve(testName.methodName.replaceCamelCaseWithDashedLowerCase() + ".txt") api.dumpAndCompareWith(target) }
एक नई आधार रेखा बनाने से, हमें एक पठनीय प्रारूप में परिवर्तन मिलता है, उदाहरण के लिए, इस प्रतिबद्ध में :
public static final fun flattenObservable (Lcom/badoo/reaktive/single/Single;)Lcom/badoo/reaktive/observable/Observable; } + public final class com/badoo/reaktive/single/MapIterableKt { + public static final fun mapIterable (Lcom/badoo/reaktive/single/Single;Lkotlin/jvm/functions/Function1;)Lcom/badoo/reaktive/single/Single; + public static final fun mapIterableTo (Lcom/badoo/reaktive/single/Single;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lcom/badoo/reaktive/single/Single; + } public final class com/badoo/reaktive/single/MapKt {
अपनी परियोजना में एक सत्यापनकर्ता का उपयोग करना
उपयोग अत्यंत सरल है। binary-compatibility-validator
को अपनी परियोजना में कॉपी करें और इसके build.gradle
बदलें। build.gradle
और RuntimePublicAPITest
:
plugins { id("org.jetbrains.kotlin.jvm") } dependencies { implementation(Deps.asm) implementation(Deps.asm.tree) implementation(Deps.kotlinx.metadata.jvm) testImplementation(Deps.kotlin.test.junit) } tasks.named("test", Test::class) {
हमारे मामले में, RuntimePublicAPITest
फ़ाइल के परीक्षण कार्यों में से एक इस तरह दिखता है:
@Test fun reaktive() { snapshotAPIAndCompare("../../reaktive/build/libs", "reaktive-jvm") }
अब प्रत्येक पुल अनुरोध के लिए, चलाएँ ./gradlew :tools:binary-compatibility:test -Pbinary-compatibility-override=false
और डेवलपर्स को समय पर आधारभूत फ़ाइलों को अपडेट करने के लिए मजबूर करें।
मरहम में उड़ना
हालांकि, इस दृष्टिकोण में कुछ बुरे बिंदु हैं।
सबसे पहले, हमें आधारभूत फ़ाइलों में परिवर्तन का स्वतंत्र रूप से विश्लेषण करना चाहिए। हमेशा उनके परिवर्तन बाइनरी असंगति को जन्म नहीं देते हैं। उदाहरण के लिए, यदि आप एक नया इंटरफ़ेस लागू करते हैं, तो आपको आधार रेखा में ऐसा अंतर मिलता है:
- public final class com/test/A { + public final class com/test/A : Comparable {
दूसरे, ऐसे उपकरण जो इसके लिए अभिप्रेत नहीं हैं, उनका उपयोग किया जाता है। कुछ फ़ाइल को डिस्क पर लिखने के रूप में टेस्ट का साइड इफेक्ट नहीं होना चाहिए, जो बाद में एक ही परीक्षण द्वारा उपयोग किया जाएगा, और इससे भी अधिक, पर्यावरण चर के माध्यम से इसके लिए मापदंडों को पारित करना। इस टूल को ग्रैगल प्लगइन में उपयोग करना और किसी कार्य का उपयोग करके आधार रेखा बनाना बहुत अच्छा होगा। लेकिन मैं वास्तव में सत्यापनकर्ता में अपने दम पर कुछ बदलना चाहता हूं, ताकि बाद में कोटलिन रिपॉजिटरी से अपने सभी परिवर्तनों को खींचना आसान हो, क्योंकि भविष्य में भाषा में नए निर्माण दिखाई दे सकते हैं जिन्हें समर्थन की आवश्यकता है।
अच्छी तरह से और तीसरे, केवल जेवीएम समर्थित है।
निष्कर्ष
बाइनरी संगतता परीक्षक का उपयोग करके , आप बाइनरी संगतता प्राप्त कर सकते हैं और इसके राज्य में परिवर्तन के लिए समय पर प्रतिक्रिया कर सकते हैं। परियोजना में इसका उपयोग करने के लिए, केवल दो फ़ाइलों को बदलना और परीक्षणों को हमारे CI से जोड़ना आवश्यक था। इस समाधान के कुछ नुकसान हैं, लेकिन यह अभी भी उपयोग करने के लिए काफी सुविधाजनक है। अब Reaktive उसी तरह से JVM के लिए द्विआधारी संगतता बनाए रखने की कोशिश करेगा जिस तरह से JetBrains कोटलिन स्टैंडर्ड लाइब्रेरी के लिए करता है।
आपका ध्यान के लिए धन्यवाद!