Google Cloud Endpoints(Java)を使ってみた

2015-06-24T17:03:46+09:00 Java Google App Engine JavaScript

公式ドキュメント: https://cloud.google.com/appengine/docs/java/endpoints

今までGoogle App Engineを使ってWeb API的なのを定義する際、Slim3を使ってOAuthServiceで認証情報を取得してJSONでレスポンス返す的な事を一から実装してたりとかしてたけど、今時そんな事しなくてもGoogle Cloud Endpointsを使う事で同等な事をさらっと出来るようになってるらしい

っていう事でドキュメントを読みつつやってみた

環境構築

buildscript {
    repositories {
        mavenCentral()
    }

    dependencies {
        classpath 'com.google.appengine:gradle-appengine-plugin:1.9.21'
    }
}

repositories {
    mavenCentral()
}

apply {
    plugin "war"
    plugin "appengine"
}

dependencies {
    compile "javax.servlet:servlet-api:2.5"

    // javax.injectの解決に必要
    compile "javax.inject:javax.inject:1"

    // javax.annotationの解決に必要
    compile "com.google.code.findbugs:jsr305:3.0.0"

    compile "com.google.appengine:appengine-api-1.0-sdk:1.9.22"
    compile "com.google.appengine:appengine-endpoints:1.9.22"

    providedRuntime "com.google.appengine:appengine-api-stubs:1.9.22"

    testCompile "junit:junit:4.11"
    testCompile "org.hamcrest:hamcrest-all:1.3"
    testCompile "com.google.appengine:appengine-testing:1.9.22"
}

// daemon=trueにするとテスト時にdaemonしたままテスト実行する
// appengineRunだけを普通に動かす事できなくなるので注意
test {
    appengine {
        daemon = true
    }
    dependsOn += ['appengineRun']
}

んまぁこれだけ。テストする際に実際にサーバーを動かしてURLFetchServiceを使って結果をテストするので

WebApi.java

基本的には@Apiと@ApiMethodを使うのだけど、@Apiで共通項目だけを設定したクラスを用意しておいて、一部実際のAPI実装クラス上で特有する設定部分は@ApiClassで定義するのがよろしい模様。っていうか複数の@Apiを持つクラスを定義して同一のnameを持つ場合に設定項目が異なるとエラーになるので

package sample;

import com.google.api.server.spi.config.Api;

@Api(name = "sample")
public class WebApi {
}

っていうように@Apiを使ったクラスをベースとしたのを定義しておいて、クラスを継承するか@ApiReferenceでこのクラスを指定するなどの方式を取れば基底となる@Apiを継承して定義出来るような感じっぽい

HogeApi.java

{% raw %}
package sample;

import javax.annotation.Nullable;
import javax.inject.Named;

import com.google.api.server.spi.config.ApiReference;
import com.google.api.server.spi.config.ApiMethod;
import com.google.api.server.spi.config.DefaultValue;

@ApiReference(WebApi.class)
public class HogeApi {
    @ApiMethod(path = "hoge", httpMethod = "GET")
    public Sample getSay(@Named("name") @Nullable @DefaultValue("hoge") String name) {
        Sample sample = new Sample();
        sample.setName(name);

        return sample;
    }

    /*
    @ApiMethod(path = "fuga/{name}", httpMethod = "GET")
    public Sample getFuga(@Named("name") @Nullable @DefaultValue("fuga") String name) {
        return getSay(name);
    }
    */
}
{% endraw %}

っていうように@ApiMethodを使ってAPIを定義出来る。その際にhttpMethodを指定しないような場合にはそのメソッド名から特定されたhttpMethodが自動で設定される模様。例えば上記のgetSayのような場合であればhttpMethod="GET"しなくてもGETメソッドで作用するようになる

んで、@ApiMethodのpathにはパスパラメーターを含むようにする事も可能。コメントしてるけどgetFugaのpathように指定する事が出来るが

  • @Namedで引数に指定したパスパラメーターをメソッドの引数に適応出来る
  • 但し、パスパラメーターをマッピングする場合には@Nullableと@DefaultValueを同時に併用する事は出来ない

てな感じで定義する事でAPI定義することが出来る。まだ設定が足りないので動かす事は出来ないけど、実際サーバーを動かすと [http://localhost:8080/_ah/api/explorer] っていうようなURLにアクセスしてAPI定義上から上記のgetSayを動かすと

っていうようなレスポンス結果が得られるようになる

SampleApi.java

package sample;

import java.util.Arrays;
import java.util.List;
import javax.inject.Named;

import com.google.api.server.spi.ServiceException;
import com.google.api.server.spi.config.ApiClass;
import com.google.api.server.spi.config.ApiMethod;
import com.google.api.server.spi.config.ApiReference;
import com.google.api.server.spi.response.UnauthorizedException;

import com.google.appengine.api.users.User;

@ApiClass(clientIds = { "省略" })
public class SampleApi extends WebApi {

    @ApiMethod(path = "say")
    public List<Sample> getSay(Sample sample) {
        return Arrays.asList(sample);
    }

    @ApiMethod(path = "user")
    public User getUser(User user) throws ServiceException {
        if (user == null) {
            throw new UnauthorizedException("not found user");
        }

        return user;
    }
}

まずgetSayから、引数にオブジェクトを持つ場合クエリーからの内容を元にオブジェクトのインスタンスを生成して注入してくれる模様。但し@Namedを指定して利用できるクラスはプリミティブ型と一部のオブジェクト型のみサポートされているだけなので、自前のクラスを@Namedを使って注入する事は出来ないっぽい

んでgetUser、引数にcom.google.appengine.users.Userを持つ事で@Api及び@ApiClass等で指定しているclientIdsによるOAuth2認証を利用してユーザー情報を取得出来るようになる。ちなみにOAuth2関係無く認証情報が取得出来ないようなケースの場合にはnullが入る模様。で上記ではUnauthorizedExceptionをスローしているのでレスポンス結果は

{
  "error" : {
    "message" : "not found user",
    "code" : 401,
    "errors" : [ {
      "domain" : "global",
      "reason" : "required",
      "message" : "not found user"
    } ]
  }
}

っていうようなレスポンス結果が得られるようになる

んまぁここまでがざっくりとしたAPI実装部分。動かすには設定(web.xml)が必要

web.xml

<?xml version="1.0" ?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5">

    <servlet>
        <servlet-name>SystemServiceServlet</servlet-name>
        <servlet-class>com.google.api.server.spi.SystemServiceServlet</servlet-class>
        <init-param>
            <param-name>services</param-name>
            <param-value>sample.SampleApi</param-value>
        </init-param>
    </servlet>

    <servlet-mapping>
        <servlet-name>SystemServiceServlet</servlet-name>
        <url-pattern>/_ah/spi/*</url-pattern>
    </servlet-mapping>

</web-app>

っていうようにservicesに@Api及び@ApiClassで定義しているAPI実装クラスを指定する必要がある模様で

んまぁここまで設定を行った上でサーバーを起動して http://localhost:8080/_ah/api/explorer にアクセスすると

ていうようなAPIが定義されそれぞれ適切なリクエストを行う事で結果をJSON等で取得出来るようになる

Google API Clieng Library for JavaScriptを使ってAPIをコールする

<html>
  <head>
    <script src="app.js"></script>
  </head>
  <body>
    <div id="user"></div>
    <script src="https://apis.google.com/js/client.js?onload=init"></script>
  </body>
</html>

みたいなHTMLでapp.jsで

const CLIENT_ID = "省略";
const SCOPE = "https://www.googleapis.com/auth/userinfo.email";

function init() {
  var callback = function() {
    gapi.auth.authorize(
      {
        client_id: CLIENT_ID,
        scope: SCOPE,
        immediate: false
      },
      function(res) {
        gapi.client.sample.sampleApi.getUser().execute(function(user) {
          document.querySelector("#user").innerText = "Hello: " + user.nickname;
        });
      }
    );
  };

  gapi.client.load('sample', 'v1', callback, "//" + window.location.host + "/_ah/api");
}

のような感じでgapiを使ってAPI定義をインポートして利用する事も出来る模様(ちなみに https://kinjouj-test.appspot.com にてこの記事投稿時点では見れるので)

っていうような感じで使う事が出来る

んまぁそんな感じで今時のGoogle App Engineを使ってWeb APIを実装するような場合であればGoogle Cloud Endpointsを利用する事も出来るって事で

余談1: 例外について

っていう事でまとめると

  • 特定の例外を送出する事でそれに対応したレスポンスが送出されるようになる
  • nullを返すと204(No Content?)になる
  • 3xxとははすべて404になる?
  • 500とかは503で変換される
  • 自前の例外を実装する場合にはcom.google.api.server.spi.ServiceExceptionを継承したクラスを定義する事でその例外を送出した場合に対応するレスポンスを送出する事が可能

っていうような感じになる

余談2: ApiResourcePropertyに関して

APIのレスポンスを出す際にオブジェクトのこのプロパティは出したくないとかそういうような制御をする事も出来る

package sample;

import java.io.Serializable;

import com.google.api.server.spi.config.AnnotationBoolean;
import com.google.api.server.spi.config.ApiResourceProperty;

public class Sample implements Serializable {

    private int id;
    private String name;

    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

っていうような@ApiResourcePropertyでignoredを指定する事でこのプロパティを出力しない等の設定を定義する事が出来る。又、nameパラメーターでこのプロパティを出力する際の名前等も変更する事が可能っぽい

余談3: @ApiTransformerについて

ようなオブジェクトとかをJSONとかに変換や解析するような仕組み等をオブジェクトに組み込む機能かと。まったく検証してないので詳しくは書かないけど、デフォルトとかだとjacksonを使ってJSONをエンコード/デコードしているんじゃないかと

んまぁ詳しくはドキュメント読めって事で

sedで改行を含めて削除する angular.js directiveのbindToController