mike-neckのブログ

Java or Groovy or Swift or Golang

Grizzly2上でJAX-RSを走らせつつ、staticなコンテンツのサーバーにもなるようにしようとしてハマって解決できてない件

こんにちわ、みけです。

諸事情があって、というよりは、

ちょっとやってみたいなと思ったので、

herokuでGrizzly2+JAX-RS(Jersey)を動かすアプリケーションを

書いています。

この組み合わせはよくあるというか、

mavenarchetypeに登録されているので、

簡単にコードを書くことができます。

public class Main {

    private final Logger LOG = LoggerFactory.getLogger(Main.class);

    private static final int DEFAULT_PORT = 8080;

    private final URI uri;

    private final boolean runOnHeroku;

    public static void main(String... args) {
        ExtendedOptional<String> env = ExtendedOptional.ofNullable(System.getenv("PORT"));
        env.ifPresent(p -> new Main(p).start())
            .ifNotPresent(() -> new Main().start());
    }

    public Main(String port) {
        this.uri = UriBuilder.fromUri("http://0.0.0.0").port(Integer.parseInt(port)).build();
        this.runOnHeroku = true;
    }

    public Main() {
        this.uri = UriBuilder.fromUri("http://localhost").port(DEFAULT_PORT).build();
        this.runOnHeroku = false;
    }

    public void start() {
        ResourceConfig config = new ResourceConfig().package("org.mikeneck.jaxrs.app");
        HttpServer httpServer = GrizzlyHttpServerFactory.createHttpServer(this.uri, config, false);
        try {
            httpServer.start();
            String msg = String.format("Jersey and Grizzly application started with WADL" +
                    " available at %sapplication.wadl", uri);
            LOG.info(msg);
            if (!runOnHeroku) {
                LOG.info("hit enter to stop server.");
                System.in.read();
            } else {
                while (true) {
                    System.in.read();
                }
            }
        } catch (IOException e) {
            LOG.error("server stopped by an error", e);
            throw new ProcessingException(LocalizationMessages.FAILED_TO_START_SERVER(e.getMessage()), e);
        } finally {
            httpServer.shutdownNow();
        }
    }
}

ここで、使ってるExtendedOptional<T>というのは

Optional<T>を少し改造したクラスで、

Optional<T>メソッドifPresent(Consumer<T>)

戻り値をvoidからExtendedOptional<T>にして、

さらにメソッドifNotPresent(VoidConsumer)を加えたものです。

ここに、次のようなリソースのクラスを付け加えます。

package org.mikeneck.jaxrs.app;

import javax.ws.rs.*;

import static javax.ws.rs.core.MediaType.TEXT_PLAIN;

@Path("hello")
public class Hello {

    @GET
    @Produces(TEXT_PLAIN)
    public String text(@QueryParam("q") @DefaultValue("world") String q) {
        return "hello, " + q;
    }
}

これを、ローカルで走らせた時に、http://localhost:8080/helloにアクセスすると、

hello, worldと返ってきます。



本題

このアプリケーションサーバーにスタティックなコンテンツを返す機能を

取り付けたいと思います。

アプリケーションをコンパイルしてjarにした後でも、

コンテンツを返すことができるように、

org.glassfish.grizzly.http.server.CLStaticHttpHandlerを使います。

修正したMainクラスは次のとおり(一部)になります。

    public void start() {
        ResourceConfig config = new ResourceConfig().package("org.mikeneck.jaxrs.app");
        HttpServer httpServer = GrizzlyHttpServerFactory.createHttpServer(this.uri, config, false);
        httpServer.getServerConfiguration().addHttpHandler(
                new CLStaticHttpHandler(getClass().getClassLoader(), "/public/"));
        try {
            httpServer.start();
            String msg = String.format("Jersey and Grizzly application started with WADL" +
                    " available at %sapplication.wadl", uri);
            LOG.info(msg);
            if (!runOnHeroku) {
                LOG.info("hit enter to stop server.");
                System.in.read();
            } else {
                while (true) {
                    System.in.read();
                }
            }
        } catch (IOException e) {
            LOG.error("server stopped by an error", e);
            throw new ProcessingException(LocalizationMessages.FAILED_TO_START_SERVER(e.getMessage()), e);
        } finally {
            httpServer.shutdownNow();
        }
    }

このプロジェクトは次のようなパスの構成になっています。

src
├main
└resources
  ├public
  │  └index.html
  └logback.groovy

そしてこの変更のおかげで、

http://localhost:8080/にアクセスすると、

publicの下にあるindex.htmlが返ってくるようになります。

困ったこと

これは、これでよいのですが、

http://localhost:8080/helloにアクセスすると、

404が返ってきてしまうわけです。


原因

ソースコードを読みます。

org.glassfish.grizzly.http.server.CLStaticHttpHandler

152行目のprotected boolean handle(String resourcePath, Request request, Response response)

メソッドで、リクエストをさばきます。

173行目からのforループでクラスローダーからリソースを探します。

        for (int i = 0; i < docRootsLocal.length; i++) {
            String docRoot = docRootsLocal[i];
            final String fullPath = docRoot + resourcePath;
            url = classLoader.getResource(fullPath);
            if (url == null) {
                url = classLoader.getResource(fullPath + "/index.html");
            }
            if (url != null) {
                break;
            }
        }

(一部省略)

ここで見つかったurlに対して、

ディレクトリーのチェック、プロトコルのチェックを行って、

リソースが存在するか確認した後、booleanの変数foundtrueが設定されます。

この後の256行目では次のようになっています。

        if (!found) {
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.log(Level.FINE, "Resource not found {0}", resourcePath);
            }
            return false;
        }

なんと、org.glassfish.grizzly.http.server.CLStaticHttpHandlerでは、

リソースが見つからなかった時にfalseを返していた!


いや、いいんですけどね(´・ω・`)

次のhandlerが適切に処理してくれれば…

CLStaticHttpHandler#handleをコールしているのが、

StaticHttpHandlerBase#serviceですので、

今度はこちらを読みます。

    @Override
    public void service(final Request request, final Response response)
            throws Exception {
        final String uri = getRelativeURI(request);
        if (uri == null || !handle(uri, request, response)) {
            onMissingResource(request, response);
        }
    }

先ほどのCLStaticHttpHandler#handlefalseを返していましたので、

onMissingResourceに進みます。

    protected void onMissingResource(final Request request, final Response response)
            throws Exception {
        response.sendError(404);
    }

見事に404返してますね(´・ω・`)


ということで、誰かどうすればよいのかエロイ人教えて

なお、次のようなのは断る。

  • Servlet使う(いや、まあ、これでもいいんですけどね。楽だし)