Open6

Java の ScopedValue の使いどころ

nakamura_tonakamura_to

Java 24 でプレビュー版の ScopedValue は、子クラスから親クラス(インターフェース)に情報を渡す時に便利かもしれない。

以下は「デフォルトメソッドをオーバーライドした子供のメソッド」から名前と呼び出し回数を「親のデフォルトメソッド」に伝える例。

import java.util.concurrent.atomic.AtomicInteger;

void main() {
  var myClass = new MyClass();
  myClass.doSomething();
  myClass.doSomething();
  myClass.doSomething();
}

interface MyInterface {
  default void doSomething() {
    var context = MethodContext.current();
    System.out.printf("%s: %d%n", context.name(), context.count());
  }
}

@SuppressWarnings("preview")
class MyClass implements MyInterface {

  private final AtomicInteger counter = new AtomicInteger();

  @Override
  public void doSomething() {
    var context = new MethodContext("doSomething", counter.incrementAndGet());
    ScopedValue.where(MethodContext.METHOD_CONTEXT, context)
            .run(MyInterface.super::doSomething);
  }
}

@SuppressWarnings("preview")
record MethodContext(String name, int count) {
  public static ScopedValue<MethodContext> METHOD_CONTEXT = ScopedValue.newInstance();

  static MethodContext current() {
    return METHOD_CONTEXT.get();
  }
}
nakamura_tonakamura_to

main メソッドの実行結果は次のとおり

doSomething: 1
doSomething: 2
doSomething: 3
nakamura_tonakamura_to

Doma では、 Dao の実装クラスで定義されたデータソースを取得するために次のようなコードが必要である。

@Dao
public interface EmployeeDao {
    default void doSomething() {
        var config = Config.get(this);
        var dataSource = config.getDataSource();
    }
}

ScopedValue を使うと次のように書き換えられる。

@Dao
public interface EmployeeDao {
    default void doSomething() {
        var config = Config.get();
        var dataSource = config.getDataSource();
    }
}

this を渡す必要がなくなるだけだが、 get メソッドの定義は次の通り Object を受け取るので ScopedValue を使った方が安全と言える。

  static Config get(Object provider) {
    if (provider instanceof ConfigProvider) {
      ConfigProvider p = (ConfigProvider) provider;
      return p.getConfig();
    }
    throw new DomaIllegalArgumentException(
        "provider", Message.DOMA2218.getMessage("provider", ConfigProvider.class.getName()));
  }

nakamura_tonakamura_to

this を渡す以外だと Hibernate Data Repositories や Jakarta Data のようにアクセサメソッドを用意しないといけない。
https://docs.jboss.org/hibernate/orm/6.6/repositories/html_single/Hibernate_Data_Repositories.html#resource-accessor-method

Doma でもしやるならこうなるだろう。

@Dao
public interface EmployeeDao {
    default void doSomething() {
        var config = getConfig();
        var dataSource = config.getDataSource();
    }

    Config getConfig();
}

しかし、getConfig メソッドが外部に公開されてしまうため好ましくない。

nakamura_tonakamura_to

ここで述べた ScopedValue の使い方は、子クラスをアノテーションプロセッサーなどで自動生成するときに特に効果的だと思う。

nakamura_tonakamura_to

ScopedValue の正式に Java の標準になったら Doma に MethodContext のような record を導入してユーティリティ的なメソッドを用意するかもしれない。

@Dao
public interface EmployeeDao {
    default void doSomething() {
        MethodContext context = MethodContext.current();
        Config config = context.config();
        DataSource dataSource = context.dataSource();
        QueryDsl queryDsl = context.queryDsl();
        Method method = context.method();
    }
}