ABOUT ME

Today
Yesterday
Total
  • Spring Data R2DBC + Jasync 사용시 AbstractMethodError가 발생하는 경우
    TECH/Spring 2022. 10. 31. 18:50

    Spring Data R2DBC 2.7.1에서 데이터베이스 드라이버를 Jasync (mysql) 사용했을 때에 저장할 경우 AbstractMethodError 에러가 발생함을 확인하였다.

     

    에러 발생 환경

    • Spring Data R2DBC 2.7.1
    • Jasync 2.0.1

    에러 로그

    2022-07-06 11:20:53.459 ERROR 7370 --- [netty-thread-18] reactor.core.publisher.Operators         : Operator called default onErrorDropped
    
    java.lang.AbstractMethodError: Method com/github/jasync/r2dbc/mysql/JasyncInsertSyntheticMetadata.getColumnMetadatas()Ljava/util/List; is abstract
      at com.github.jasync.r2dbc.mysql.JasyncInsertSyntheticMetadata.getColumnMetadatas(JasyncInsertSyntheticMetadata.kt) ~[jasync-r2dbc-mysql-2.0.8.jar:na]
      at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
      at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
      at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
      at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
      at org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:282) ~[spring-core-5.3.21.jar:5.3.21]
      at org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:266) ~[spring-core-5.3.21.jar:5.3.21]
      at org.springframework.data.r2dbc.convert.RowMetadataUtils.getColumnMetadata(RowMetadataUtils.java:69) ~[spring-data-r2dbc-1.5.1.jar:1.5.1]
      at org.springframework.data.r2dbc.convert.RowMetadataUtils.containsColumn(RowMetadataUtils.java:46) ~[spring-data-r2dbc-1.5.1.jar:1.5.1]
      at org.springframework.data.r2dbc.convert.MappingR2dbcConverter.extractGeneratedIdentifier(MappingR2dbcConverter.java:658) ~[spring-data-r2dbc-1.5.1.jar:1.5.1]
      at org.springframework.data.r2dbc.convert.MappingR2dbcConverter.potentiallySetId(MappingR2dbcConverter.java:643) ~[spring-data-r2dbc-1.5.1.jar:1.5.1]
      at org.springframework.data.r2dbc.convert.MappingR2dbcConverter.lambda$populateIdIfNecessary$2(MappingR2dbcConverter.java:630) ~[spring-data-r2dbc-1.5.1.jar:1.5.1]
      at com.github.jasync.r2dbc.mysql.JasyncResult.map$lambda-0(JasyncResult.kt:35) ~[jasync-r2dbc-mysql-2.0.8.jar:na]
      at reactor.core.publisher.MonoSupplier.call(MonoSupplier.java:86) ~[reactor-core-3.4.19.jar:3.4.19]
      at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:405) ~[reactor-core-3.4.19.jar:3.4.19]
      at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:539) ~[reactor-core-3.4.19.jar:3.4.19]
      at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122) ~[reactor-core-3.4.19.jar:3.4.19]
      at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79) ~[reactor-core-3.4.19.jar:3.4.19]
      at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122) ~[reactor-core-3.4.19.jar:3.4.19]
      at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:282) ~[reactor-core-3.4.19.jar:3.4.19]
      at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:863) ~[reactor-core-3.4.19.jar:3.4.19]
      at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1816) ~[reactor-core-3.4.19.jar:3.4.19]
      at reactor.core.publisher.MonoCompletionStage.lambda$subscribe$0(MonoCompletionStage.java:83) ~[reactor-core-3.4.19.jar:3.4.19]
      at java.base/java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:863) ~[na:na]
      at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:841) ~[na:na]
      at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510) ~[na:na]
      at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2147) ~[na:na]
      at com.github.jasync.sql.db.util.FutureUtilsKt.success(FutureUtils.kt:16) ~[jasync-pool-2.0.8.jar:na]
      at com.github.jasync.sql.db.mysql.MySQLConnection.succeedQueryPromise$lambda-12(MySQLConnection.kt:359) ~[jasync-mysql-2.0.8.jar:na]
      at java.base/java.util.Optional.ifPresent(Optional.java:178) ~[na:na]
      at com.github.jasync.sql.db.mysql.MySQLConnection.succeedQueryPromise(MySQLConnection.kt:358) ~[jasync-mysql-2.0.8.jar:na]
      at com.github.jasync.sql.db.mysql.MySQLConnection.onOk(MySQLConnection.kt:227) ~[jasync-mysql-2.0.8.jar:na]
      at com.github.jasync.sql.db.mysql.codec.MySQLConnectionHandler.channelRead0(MySQLConnectionHandler.kt:119) ~[jasync-mysql-2.0.8.jar:na]
      at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:99) ~[netty-transport-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:327) ~[netty-codec-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:299) ~[netty-codec-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) ~[netty-transport-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) ~[netty-transport-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166) ~[netty-transport-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:722) ~[netty-transport-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:658) ~[netty-transport-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:584) ~[netty-transport-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:496) ~[netty-transport-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) ~[netty-common-4.1.78.Final.jar:4.1.78.Final]
      at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.78.Final.jar:4.1.78.Final]
      at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
    

    com/github/jasync/r2dbc/mysql/JasyncInsertSyntheticMetadata.getColumnMetadatas 호출로인해 발생한 에러로 추정된다.

     

    에러 분석

    문제가 발생한 Jasync 코드를 intellij에서 java로 decompile 해보았다.

    @NotNull
       public Iterable getColumnMetadatas() {
          return (Iterable)CollectionsKt.listOf(this);
       }
    

    리턴 타입이 일치하지 않는다 (incompatible) 는 에러를 발견할 수 있었다.

     

    이에 decompile된 코드가 아닌 실제 코드를 확인해보았다.

    // <https://github.com/jasync-sql/jasync-sql/blob/master/r2dbc-mysql/src/main/java/JasyncInsertSyntheticMetadata.kt#L32>
    internal class JasyncInsertSyntheticMetadata(private val generatedKeyName: String) : RowMetadata, ColumnMetadata {
      ...
        override fun getColumnMetadatas(): Iterable {
            return listOf(this)
        }
      ...
    
    • JasyncInsertSyntheticMetadata 클래스는 R2DBC의 RowMetadata 와 ColumnMetadata 를 상속하여 구현한 클래스이다.
    • 에러가 발생한 getColumnMetadatas 는 RowMetadata 의 메서드를 오버라이드한 것이다.
    • 반환 타입이 Iterable<ColumnMetadata> 임을 주목하자

    Spring Data R2DBC pom.xml 확인

    # https://github.com/spring-projects/spring-data-r2dbc/blob/1.5.1/pom.xml#L32
        <r2dbc-spi-test.version>0.9.1.RELEASE</r2dbc-spi-test.version>

    Spring Data R2DBC 2.7.1에서는 R2DBC SPI 0.9.1 버전을 사용한다.

     

    R2DBC SPI 0.9.1 버전 확인

    https://r2dbc.io/spec/0.9.1.RELEASE/api/

    문제가 발생한 메서드의 스펙을 확인해보았다.

    List 를 반환하도록 되어 있다.

     

    Jasync의 R2DBC SPI 버전 확인

    다시 Jasync로 돌아가 R2DBC SPI의 버전을 확인해보았다.

    # <https://github.com/jasync-sql/jasync-sql/blob/2.0.8/gradle.properties>
    R2DBC_SPI_VERSION=0.8.0.RELEASE
    

    Spring Data R2DBC에서 사용하는 R2DBC 버전인 0.9.1 보다 낮은 버전인 0.8.0 버전을 사용하고 있음을 확인할 수 있었다.

    그런데, R2DBC 0.9대로 올라가면서 getColumnMetadatas의 List를 반환하도록 변경되었고, 이로 인해 발생한 문제이다.

    변경 commit

    https://github.com/r2dbc/r2dbc-spi/commit/47e25176a1e94736c004d20cd04315b463ddc289#diff-b35758593cc5cefd44b1b593b7f2fab1d07c0e160974a7d69a4bcce855bfcf44

    위와 관련하여 jasync에 해당 이슈를 등록 (https://github.com/jasync-sql/jasync-sql/issues/296)하였고, 최근 버전(2.1.1)에 이슈가 해결되어 이제는 정상적으로 동작한다.

     

    요약

    • R2DBC SPI 0.8.0 버전을 사용하는 Jasync에서는 Iterable을 반환하지만, R2DBC SPI 0.9.1을 사용하는 Spring Data R2DBC 2.7.1에서는 List 타입이 반환되기를 기대하므로 에러가 발생했다. 즉, jasync와 Spring Data R2DBC 에서 사용하는 R2DBC SPI 버전이 상이하여 발생한 에러이다.
    • 따라서, Spring Data R2DBC 2.7.1 사용시 AbstractMethodError 가 발생했다면
      • R2DBC SPI 버전 0.8.0과 Spring Data R2DBC 1.4.5 버전(R2DBC SPI 버전 0.8.0 사용)을 사용하거나 (spring-boot-starter-data-r2dbc:2.6.9)
      • 최신 SPI 버전을 사용하는 Jasync 버전 2.1.1 사용한다 (권장)
    •  
Designed by Tistory.