Google Cloud Endpoints(Java)を使ってみた
公式ドキュメント: 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メソッドで作用するようになる
https://t.co/D4Wfyijp2i によるとhttpMethodは指定しなければメソッド名から推定される処理されるべきhttpMethodが採用される。例えばgetが接頭辞とかについてればGETメソッドで処理されるみたいで、ついてなければPOSTっぽい。確証は無い
— kinjouj (@kinjou__j) 2015, 6月 20
んで、@ApiMethodのpathにはパスパラメーターを含むようにする事も可能。コメントしてるけどgetFugaのpathように指定する事が出来るが
- @Namedで引数に指定したパスパラメーターをメソッドの引数に適応出来る
- 但し、パスパラメーターをマッピングする場合には@Nullableと@DefaultValueを同時に併用する事は出来ない
一応念の為にやってみた所、「http://t.co/KOQe04U1g4.api.server.spi.config.validation.InvalidParameterAnnotationsException」っていう例外ぶっ飛ぶ
— kinjouj (@kinjou__j) 2015, 6月 23
てな感じで定義する事で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: 例外について
https://t.co/g6lOKpa5PF に書いてるけど、API側で例外ぶん投げた場合はその後にServiceUnavailableになると思うのでそれによるサービスの例外エラーが503になるとか
— kinjouj (@kinjou__j) 2015, 6月 21
ちょっと余談入ったけど、送出したいHTTPステータスに一致する例外をthrowすればそのHTTPステータスで処理されるんだと思われる。んでにServiceExceptionを継承した例外クラスを作ってその例外をぶっ飛ばせば例外クラスで指定されてるステータスなレスポンスが送出される
— kinjouj (@kinjou__j) 2015, 6月 21
「All HTTP 5xx status codes are converted to be HTTP 503 in the client response.」って書いとるがな
— kinjouj (@kinjou__j) 2015, 6月 21
っていう事でまとめると
- 特定の例外を送出する事でそれに対応したレスポンスが送出されるようになる
- 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をエンコード/デコードしているんじゃないかと
んまぁ詳しくはドキュメント読めって事で