mike-neckのブログ

Java or Groovy or Swift or Golang

JAX-RS(Jersey) on ServiceTalk with Spring

2019 年の Java アドベントカレンダーの2日目のエントリーです。この前に書いた ServiceTalk の記事の続きです。今回は ServiceTalk の上で JAX-RS アプリケーションを動かします。

mike-neck.hatenadiary.com

github.com


前回に書いた通り Apple の ServiceTalk は Netty をベースに作られているネットワークフレームワークで、主に HTTP サーバー/クライアントを Netty レイヤーを意識することなく組み立てられるフレームワークです。 ServiceTalk にはデフォルトのルーティング機構があり、関数ハンドラーをパスに対して割り当てることで HTTP サーバーを構築できるなど、ここ最近のモダンな HTTP サーバーフレームワークと同じような仕組みが提供されています。


ただ、 ServiceTalk では JAX-RS モジュールが提供されており、 JAX-RS アプリケーション(より正確に言えば Jersey アプリケーション)を ServiceTalk の上で走らせられます。以下のレポジトリーの jax-rs-example というディレクトリーには JAX-RS だけを動かすサンプルがあります。

github.com

単純に 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-serverimplementation 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 の BeanFactorystatic なフィールドに保持しています。これは、 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 にはありません)


アプリケーションを起動した時の様子

f:id:mike_neck:20191126221804p:plain

実際に curl で呼んでみた

f:id:mike_neck:20191126221851p:plain


というわけで、 JAX-RS を ServiceTalk 上で Spring のサポートをつけつつ動かしてみました。