Kotlin из generateSequence () в flow (), но сгенерирован неправильный байт-код - PullRequest
1 голос
/ 02 ноября 2019

У меня есть функция, которая использует http-запрос для получения удаленной страницы. На этой странице может быть одна или ноль next ссылки на страницу. Если я хочу создать цепочку всех страниц, generateSequence - идеальное решение. Вот что я сделал:

Во-первых, есть две служебные функции:

fun getBlockingDocument(url: String): Document?, как следует из названия, это функция блокировки. Реализация просто отправляет HTTP-запрос и анализирует документ JSoup.

fun getNextIndexPage(doc: Document, url: String): String?, это также функция блокировки, но она не связана с сетью, она просто анализирует, чтобы получить следующую страницу, поэтомублокировка в порядке.

ОК, вот код последовательности:

val initUrl = // initial url

generateSequence(tools.getBlockingDocument(initUrl).let { initUrl to it }) { (url, doc) ->
  doc?.let {
    parser.getNextIndexPage(doc, url)
  }?.let { nextUrl ->
    nextIndexUrl to tools.getBlockingDocument(nextUrl)
  }
}.forEachIndexed { index, urlAndDoc ->
  val url = urlAndDoc.first
  logger.info("[{}] : {}", index, url)
}

Он работает хорошо и корректно объединяет все страницы.

Но что, если я изменюсетевой вызов функции приостановки? Это то, что я создал:

suspend fun getSuspendingDocument(url: String): Document?

Я не нашел похожих generateSequence примеров построителя как flow, поэтому я реализую так:

  @ExperimentalCoroutinesApi
  @Test
  fun testGetAllPagesByFlow() {
    val flow = flow<Pair<String, Document?>> {
      suspend fun generate(url: String) {
        tools.getSuspendingDocument(url)?.let { url to it }?.also { (url, doc) ->
          emit(url to doc)

          parser.getNextIndexPage(doc, url)?.also { nextUrl ->
            generate(nextUrl) // recursive
          }
        }
      }
      generate("http://...initial url here")
    } // flow

    runBlocking {
      flow.collectIndexed { index, urlAndDoc ->
        val url = urlAndDoc.first
        logger.info("[{}] : {}", index, url)
      }
    }
  }

Я использую рекурсивный вызов (fun generate()) до emit следующий URL, найденный на каждой странице. Я не уверен, что это идиоматический способ создания Flow, но я не нашел подобных кодов. Если у вас есть лучший идиоматичный способ, скажите, пожалуйста, большое спасибо!

В любом случае, я думаю, что это должно работать, но моя IDE (IntelliJ) жалуется wrong bytecode generated, чего я никогда раньше не видел.

Error:Kotlin: [Internal Error] org.jetbrains.kotlin.codegen.CompilationException: Back-end (JVM) Internal error: wrong bytecode generated
  @Lorg/jetbrains/annotations/Nullable;() // invisible
    // annotable parameter count: 1 (visible)
    // annotable parameter count: 1 (invisible)
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
   L1
    ALOAD 0
    GETFIELD destiny/data/FlowTest$testGetAllPagesByFlow$flow$1.p$ : Lkotlinx/coroutines/flow/FlowCollector;
    ASTORE 2
   L2
   L3
    LINENUMBER 44 L3
    NEW destiny/data/FlowTest$testGetAllPagesByFlow$flow$1$1
    DUP
    ALOAD 2
    ALOAD 3
    ACONST_NULL
    INVOKESPECIAL destiny/data/FlowTest$testGetAllPagesByFlow$flow$1$1.<init> (Lkotlinx/coroutines/flow/FlowCollector;Ldestiny/data/FlowTest$testGetAllPagesByFlow$flow$1$1;Lkotlin/coroutines/Continuation;)V
    ASTORE 3
   L4
   L5
    LINENUMBER 56 L5
    ALOAD 3
    CHECKCAST destiny/data/FlowTest$testGetAllPagesByFlow$flow$1$1
    ALOAD 0
    GETFIELD destiny/data/FlowTest$testGetAllPagesByFlow$flow$1.$initUrl : Ljava/lang/String;
    ALOAD 0
    ALOAD 0
    ALOAD 2
    PUTFIELD destiny/data/FlowTest$testGetAllPagesByFlow$flow$1.L$0 : Ljava/lang/Object;
    ALOAD 0
    ALOAD 3
    PUTFIELD destiny/data/FlowTest$testGetAllPagesByFlow$flow$1.L$1 : Ljava/lang/Object;
    ALOAD 0
    ICONST_1
    PUTFIELD destiny/data/FlowTest$testGetAllPagesByFlow$flow$1.label : I
    INVOKEVIRTUAL destiny/data/FlowTest$testGetAllPagesByFlow$flow$1$1.invoke (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
   L6
    DUP
    ALOAD 4
    IF_ACMPNE L7
   L8
    LINENUMBER 42 L8
    ALOAD 4
    ARETURN
   L9
    ALOAD 0
    GETFIELD destiny/data/FlowTest$testGetAllPagesByFlow$flow$1.L$1 : Ljava/lang/Object;
    CHECKCAST destiny/data/FlowTest$testGetAllPagesByFlow$flow$1$1
    ASTORE 3
    ALOAD 0
    GETFIELD destiny/data/FlowTest$testGetAllPagesByFlow$flow$1.L$0 : Ljava/lang/Object;
    CHECKCAST kotlinx/coroutines/flow/FlowCollector
    ASTORE 2
   L10
    ALOAD 1
    INVOKESTATIC kotlin/ResultKt.throwOnFailure (Ljava/lang/Object;)V
    ALOAD 1
   L7
    LINENUMBER 58 L7
    POP
   L11
   L12
    GETSTATIC kotlin/Unit.INSTANCE : Lkotlin/Unit;
    ARETURN
   L13
   L14
   L15
    NEW java/lang/IllegalStateException
    DUP
    LDC "call to 'resume' before 'invoke' with coroutine"
    INVOKESPECIAL java/lang/IllegalStateException.<init> (Ljava/lang/String;)V
    ATHROW
    RETURN
   L16
    LOCALVARIABLE $this$flow Lkotlinx/coroutines/flow/FlowCollector; L2 L14 2
    LOCALVARIABLE $fun$generate$1 Ldestiny/data/FlowTest$testGetAllPagesByFlow$flow$1$1; L4 L11 3
    LOCALVARIABLE this Ldestiny/data/FlowTest$testGetAllPagesByFlow$flow$1; L0 L13 0
    LOCALVARIABLE $result Ljava/lang/Object; L0 L13 1
    MAXSTACK = 5
    MAXLOCALS = 4
File being compiled at position: (42,46) in /destiny/data/core/src/test/java/destiny/data/FlowTest.kt
The root cause org.jetbrains.kotlin.codegen.CompilationException was thrown at: org.jetbrains.kotlin.codegen.TransformationMethodVisitor.visitEnd(TransformationMethodVisitor.kt:92)
    at org.jetbrains.kotlin.codegen.FunctionCodegen.endVisit(FunctionCodegen.java:990)
    at org.jetbrains.kotlin.codegen.FunctionCodegen.generateMethodBody(FunctionCodegen.java:487)
    at org.jetbrains.kotlin.codegen.FunctionCodegen.generateMethod(FunctionCodegen.java:260)
    at org.jetbrains.kotlin.codegen.FunctionCodegen.generateMethod(FunctionCodegen.java:176)
    ...

Это очень долго, остальные опущены.

Что не так с этим кодом?

А если рекурсивный путь не идеален, есть ли лучшее решение? (как generateSequence, это красиво). Спасибо.

Окружающая среда:

<kotlin.version>1.3.50</kotlin.version>
<kotlin.compiler.jvmTarget>1.8</kotlin.compiler.jvmTarget>
<dependency>
  <groupId>org.jetbrains.kotlinx</groupId>
  <artifactId>kotlinx-coroutines-core</artifactId>
  <version>1.3.2</version>
</dependency>
IntelliJ 2018.3.6

$ java -version
java version "11.0.3" 2019-04-16 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.3+12-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.3+12-LTS, mixed mode)

1 Ответ

2 голосов
/ 02 ноября 2019

Зачем вообще использовать рекурсию? Следующий код будет делать то же самое

val f: Flow<Pair<String, Document?>> = flow {
    var nextUrl: String? = url

    while (nextUrl != null) {
        val doc = tools.getSuspendingDocument(nextUrl)
        emit(url to doc)

        if (doc == null) break;

        nextUrl = parser.getNextIndexPage(doc, url)

    }
}
...