2019 年の Java アドベントカレンダーの2日目のエントリーです。この前に書いた ServiceTalk の記事の続きです。今回は ServiceTalk の上で JAX-RS アプリケーションを動かします。
前回に書いた通り Apple の ServiceTalk は Netty をベースに作られているネットワークフレームワークで、主に HTTP サーバー/クライアントを Netty レイヤーを意識することなく組み立てられるフレームワークです。 ServiceTalk にはデフォルトのルーティング機構があり、関数ハンドラーをパスに対して割り当てることで HTTP サーバーを構築できるなど、ここ最近のモダンな HTTP サーバーフレームワークと同じような仕組みが提供されています。
ただ、 ServiceTalk では JAX-RS モジュールが提供されており、 JAX-RS アプリケーション(より正確に言えば Jersey アプリケーション)を ServiceTalk の上で走らせられます。以下のレポジトリーの jax-rs-example というディレクトリーには JAX-RS だけを動かすサンプルがあります。
単純に JAX-RS を動かすだけなら、 ServiceTalk のサンプルにもあるので、ここではもう一つ進んだ使い方をします。
Jersey2 には HK2 という DI フレームワークがのっていますが、これで DI するかわりに、 Spring で DI します。
gradle のスクリプト
plugins { id 'org.springframework.boot' version '2.2.1.RELEASE' id 'io.spring.dependency-management' version '1.0.8.RELEASE' id 'java' } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = JavaVersion.VERSION_13 ext { servicetalkVersion = '0.20.0' } repositories { mavenCentral() } dependencies { implementation ( 'org.springframework.boot:spring-boot-starter', "org.glassfish.jersey.inject:jersey-hk2:2.28", 'org.glassfish.hk2:spring-bridge:2.6.1', "io.servicetalk:servicetalk-http-netty:${servicetalkVersion}", "io.servicetalk:servicetalk-annotations:${servicetalkVersion}", "io.servicetalk:servicetalk-data-jackson-jersey:${servicetalkVersion}", "io.servicetalk:servicetalk-http-router-jersey:${servicetalkVersion}", ) testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } } test { useJUnitPlatform() }
もし、アプリケーションを ResourceConfig
で設定したい場合は、 org.glassfish.jersey.core:jersey-server
を implementation
configuration に指定する必要があります。 servicetalk-http-router-jersey
が compile スコープではなく、 runtime スコープで jersey-server
を取り込んでいるためです。
Application
クラス
Application
を継承したクラスで JAX-RS アプリケーションの設定をします
public class JaxRsApplication extends Application { private static final Logger logger = LoggerFactory.getLogger(JaxRsApplication.class); static BeanFactory beanFactory; @Inject public JaxRsApplication(ServiceLocator serviceLocator) { logger.info("initializing resource-config: {}", serviceLocator.getName()); SpringBridge.getSpringBridge().initializeSpringBridge(serviceLocator); SpringIntoHK2Bridge springIntoHK2Bridge = serviceLocator.getService(SpringIntoHK2Bridge.class); springIntoHK2Bridge.bridgeSpringBeanFactory(beanFactory); } @Override public Set<Class<?>> getClasses() { return Set.of(TimeResource.class); } }
Spring の BeanFactory
を static
なフィールドに保持しています。これは、 JAX-RS の起動中にどうしても Spring から差し込むことができなかったので、 servicetalk 起動前に Spring から受け取ったものをこのフィールドに設定して、 HK2 との連携設定をするためです。
SpringBootApplication
クラス
@SpringBootApplication public class App { private static final Logger logger = LoggerFactory.getLogger(App.class); public static void main(String[] args) { SpringApplication.run(App.class, args); } @Bean CommandLineRunner commandLineRunner(ApplicationContext applicationContext) { JaxRsApplication.beanFactory = applicationContext; return args -> { logger.info("starting server on https://localhost:8080"); HttpServers.forPort(8080) .listenStreamingAndAwait( new HttpJerseyRouterBuilder() .buildStreaming(JaxRsApplication.class)) // (1) .awaitShutdown(); }; } @Bean DateTimeFormatter dateTimeFormatter() { return new DateTimeFormatterBuilder() .parseCaseInsensitive() .append(DateTimeFormatter.ISO_LOCAL_DATE) .appendLiteral('T') .appendValue(ChronoField.HOUR_OF_DAY, 2) .appendLiteral(':') .appendValue(ChronoField.MINUTE_OF_HOUR, 2) .appendLiteral(':') .appendValue(ChronoField.SECOND_OF_MINUTE, 2) .appendOffsetId() .toFormatter(); } @Bean ZoneId zoneId(@Value("${timezone}") String timezone) { return ZoneId.of(timezone); } @Bean Clock clock(ZoneId zoneId) { return Clock.system(zoneId); } }
このクラスで リソースクラスに差し込むオブジェクトを生成しています。(1) の箇所では Application
クラスのインスタンスを渡すことも可能ですが、その場合 ServiceLocator
を自分で作る必要があるにもかかわらず、設定途中で Jersey が作る ServiceLocator
とは別物になってしまうため、インスタンスではなくクラスを渡したほうがよいです。
リソースクラス
@Path("/time") public class TimeResource { private static final Logger logger = LoggerFactory.getLogger(TimeResource.class); private final DateTimeFormatter dateTimeFormatter; private final Clock clock; @Inject public TimeResource(DateTimeFormatter dateTimeFormatter, Clock clock) { this.dateTimeFormatter = dateTimeFormatter; this.clock = clock; } @GET @Produces("application/json") public Response getTime() { OffsetDateTime now = OffsetDateTime.now(clock); Time time = new Time(clock.getZone().getId(), now.format(dateTimeFormatter)); logger.info("new request: {}", time); return Response.ok(time).build(); } }
このクラスのコンストラクターは @Autowired
ではなく(HK2 が認識できない) 、 @Inject
で修飾する必要があります。(Spring はコンストラクターインジェクションを自動で認識する気の利いた機能がありますが、HK2 にはありません)
アプリケーションを起動した時の様子
実際に curl で呼んでみた
というわけで、 JAX-RS を ServiceTalk 上で Spring のサポートをつけつつ動かしてみました。