- 投稿日:2021-01-11T23:40:08+09:00
【Java】AtcorderのABC-188に参加しました。
こんばんは。初投稿です。よろしくお願いします。
2021/1/10に、AtcorderのABCコンテストに参加しまして、レートが付きました。
https://atcoder.jp/users/ishikawaryou何とかC問題まで時間内に解くことができました。
僕はもともとはCOBOL技術者でJavaは完全独学ですが、時間はかかれどC問題まで解けた自分を褒めてあげようと思います。これからもコツコツ取り組んで、茶色を目指したいと思います!
同じく灰色レベルで頑張っている方はお互いがんばっていきましょう。
- 投稿日:2021-01-11T21:24:16+09:00
GraphQLサーバーをJavaで実装してみる with Netflix DGS
NetflixのTech blogに書かれていたGraphQL FrameworkがOSSとして公開されていたので、さっそく使ってみました。
- How Netflix Scales its API with GraphQL Federation (Part 1)
- How Netflix Scales its API with GraphQL Federation (Part 2)
Netflix DGS
- Documentation: https://netflix.github.io/dgs/
- DGS = Domain Graph Service
- Netflixが開発したGraphQLサーバーを構築するためのフレームワーク
- GraphQL Java とSpring Boot上で動作
- 大規模GraphQLサービスの構築を想定していて、Apollo Federationを実装
- Spring Securityをサポート
- スキーマからのCode generationをサポート
実装する機能を決める
比較のため、以前書いた記事 GraphQLサーバーをJavaで実装してみると同じ機能(Query、Mutation、Subscription)を実装してみます。機能のユースケースは以下の通り。
- Query: 本の一覧を取得する。
- Query: 本のIDを指定して、特定の本を取得する。
- Mutation: 新しい本を登録する。
- Subscription: 新たに登録された本を通知する。
Federationを実装したかったのですが、現時点ではFederationに関するドキュメントが見つからず、特にGateway部分の実装がよくわかりません。ドキュメントが公開されたら実装してみることにします。
プロジェクトを構成する
プロジェクトはひな型はSpring Initializerで適当に作成します。必要なdependenciesは後から追加するので、ここではLombokのみ追加します。
アプリケーションを作成する
build.gradle
以下のように変更します。
build.gradleplugins { id 'org.springframework.boot' version '2.4.1' id 'io.spring.dependency-management' version '1.0.10.RELEASE' id 'java' // Added for "api". id 'java-library' // Added for code generation from scheme. id 'com.netflix.dgs.codegen' version '4.0.10' } sourceCompatibility = '11' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() // Added to fix error "Could not find com.apollographql.federation:federation-graphql-java-support". jcenter() } dependencies { api 'com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter:latest.release' // Added for subscription. implementation 'com.netflix.graphql.dgs:graphql-dgs-subscriptions-websockets-autoconfigure:latest.release' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' } // Added for code generation from scheme. generateJava { schemaPaths = ["${projectDir}/src/main/resources/schema"] // Set package name of generated code. packageName = 'sandbox.dgs' // Set "false" not to generate client code. generateClient = false }schema.graphqls
GraphQLのスキーマを定義します。
schema.graphqls
はsrc/main/resources/schema
以下に登録します。schema.graphqlstype Query { bookById(id: ID): [Book]! books: [Book]! } type Book { id: ID name: String pageCount: Int } type Mutation { registerBook ( id: ID name: String pageCount: Int ): Book } type Subscription { subscribeBooks: Book }Java Model
Code generation plugin を使って、スキーマからJavaのクラスを作成します。
$ ./gradlew generateJava $ ls -l build/generated/sandbox/dgs total 8 -rw-r--r-- 1 xxxxxxx yyyyy 942 Jan 11 19:48 DgsConstants.java drwxr-xr-x 4 xxxxxxx yyyyy 128 Jan 11 19:48 types $ ls -l build/generated/sandbox/dgs/types total 16 -rw-r--r-- 1 xxxxxxx yyyyy 2223 Jan 11 19:48 Book.java -rw-r--r-- 1 xxxxxxx yyyyy 1487 Jan 11 19:48 Subscription.javaBookクラスが作成できました。
DGS Component
次に、Query、Mutation、そしてSubscriptionを実装したServiceクラス(DGS component)を作成します。
- クラスに
@DgsComponent
を付与します。- メソッドに
@DgsData
を付与し、parentType
にはスキーマのtype name、field
にはfield nameを設定します。- fieldの引数は、
@InputArgument
もしくはDataFetchingEnvironment
を使用して取得します。- subscriptionのreturn値は
reactive-streams
のPublisher
を返します。
DataProvider
とIBookProcessor
は独自に作成したクラスです。以下の処理を行います。
- DataProvider : データ提供クラス。すべてのBookのList、または指定されたbookIdのBookを返す。
- IBookProcessor : 本の登録をイベントとして登録(emit)し、イベントのPublisherを発行(publish)する。
BookService.java@AllArgsConstructor @DgsComponent public class BookService { private final DataProvider dataProvider; private final IBookProcessor bookProcessor; @DgsData(parentType = "Query", field = "books") public List<Book> books() { return dataProvider.books(); } @DgsData(parentType = "Query", field = "bookById") public List<Book> books(@InputArgument("id") String id) { if (id == null || id.isEmpty()) { return dataProvider.books(); } return List.of(dataProvider.bookById(id)); } @DgsData(parentType = "Mutation", field = "registerBook") public Book registerBook(DataFetchingEnvironment dataFetchingEnvironment) { final String id = dataFetchingEnvironment.getArgument("id"); final String name = dataFetchingEnvironment.getArgument("name"); final int pageCount = dataFetchingEnvironment.getArgument("pageCount"); final Book book = new Book(id, name, pageCount); dataProvider.books().add(book); // Emit an event for subscription. bookProcessor.emit(book); return book; } @DgsData(parentType = "Subscription", field = "subscribeBooks") public Publisher<Book> subscribeBooks() { return bookProcessor.publish(); } }アプリケーションを実行する
作成したアプリケーションを実行します。GraphiQLが付属されているので、SpringBootのアプリケーションを起動し、エンドポイント(
http://localhost:8080/graphiql
)にブラウザでアクセスします。Subscriptionだけエラーになりました。debugログを見てみると、以下のエラーが出力されていました。
2021-01-11 21:05:52.305 DEBUG 70696 --- [nio-8080-exec-2] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing ["Trying to execute subscription on /graphql. Use /subscriptions instead!"] 2021-01-11 21:05:52.305 DEBUG 70696 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Completed 400 BAD_REQUESTSubscriptionの場合は、
/subscriptions
にリクエストする必要があるようです。
GraphiQLのリクエスト先を変更する方法がわからなかったので、以下のようなクライアントコードを実装して試したところ、うまく動きました。main.tsimport { WebSocketLink } from "@apollo/client/link/ws"; import { SubscriptionClient } from 'subscriptions-transport-ws'; import { ApolloClient, InMemoryCache } from "@apollo/client"; import gql from 'graphql-tag'; import * as $ from 'jquery'; const GRAPHQL_ENDPOINT = 'ws://localhost:8080/subscriptions'; const client = new SubscriptionClient(GRAPHQL_ENDPOINT, { reconnect: true, }); const link = new WebSocketLink(client); const apolloClient = new ApolloClient({ link: link, cache: new InMemoryCache() }); const asGql = gql` subscription BookSubscription { subscribeBooks { id, name } } ` const s = apolloClient.subscribe({ query: asGql }) s.subscribe({ next: ({ data }) => { const result = document.getElementById("result"); $("#result").append(JSON.stringify(data)); $("#result").append("<br>"); } });
- 投稿日:2021-01-11T17:19:53+09:00
gradleプロジェクトでリリースバージョンとリリースノートを自動作成
背景
Javaプロジェクトを開発している時にいくつか課題があり、それぞれ以下のように解決しました。
- リリースのたびに
build.gradle
のversion
を書き換える必要がある -> gradleプロジェクトでバージョンを自動指定- リリースノートの作成が手間 -> GitHubのリリースノートの自動作成
上記をもう少し自動化してみようというのがこの記事の目的となります。前提として、bashコマンドを用いたスクリプトを実行するため、bashが利用できる環境であることが条件となります(
macOS Big Sur
とAmazon Linux 2
では動作確認済み)。事前準備
grenコマンドのインストール
gren-release-notesパッケージのインストールの手順に沿ってgrenコマンドが利用できるようにします。
build.gradleの設定
nebulaプラグインを利用するため、gradleプロジェクトでバージョンを自動指定を参考に
build.gradle
を設定します。GitHub CLIのインストール
GitHub CLIは、GitHub上でリリースドラフトを作成するために利用します。以下の手順でインストールと設定をします。
- 利用する環境に合わせて、Installationを参考にインストールします。
- 先ほどと同様に、個人アクセストークンを作成します。権限は
repo
とread:org
にチェックを入れます。- 一度だけgh auth loginを実行して認証します。
echo {個人アクセストークンの文字列} | gh auth login --with-token
を実行します。上記設定の後、GitHubリポジトリに移動して
gh repo view
を実行し、結果が表示されたらOKです。スクリプトの実行
https://github.com/kazntree/nebula-chain にある
release.sh
をリリースしたいリポジトリ内の任意の場所に置いて実行すると、新しいバージョンでリリースして、さらにプルリクエストを基にリリースノートを作成します。$ ./script/release.sh Do you want to release and publish? [y/n] y release start. Inferred project: nebula-chain-test, version: 0.4.0 > Task :release Tagging repository as 0.4.0 Pushing changes in [0.4.0] to origin BUILD SUCCESSFUL in 7s 11 actionable tasks: 10 executed, 1 up-to-date https://github.com/kazntree/nebula-chain-test/releases/tag/0.4.0 Getting gren config from local file /home/ec2-user/github/nebula-chain-test/.grenrc.yml ? - Generate release notes: =================================== ✔ Releases found: 4 ✔ Tags found: 0.4.0, 0.3.0 ✔ Pull Requests found: 1 ✔ 0.4.0 has been successfully updated! See the results here: https://github.com/kazntree/nebula-chain-test/releases/tag/0.4.0メジャーバージョンを上げたい場合は
release.sh -Prelease.scope=major
を、パッチバージョンを上げたい場合はrelease.sh -Prelease.scope=patch
を指定します。release.shの説明
release.sh
で行っていることは、以下のとおりです。
- 事前準備が終わっているか確認する(コマンドの存在など)
- コマンド実行の確認(
y
で実行)- nebulaプラグインを利用して、新しいバージョンをリリース
- GitHub CLIを利用して、GitHub上で新しいリリースノートを作成
- grenコマンドを利用して、前回リリースから取り込まれたプルリクエスト一覧をリリースノートへ上書き
- 投稿日:2021-01-11T16:11:37+09:00
Java × Spring Boot で GraphQL クライアントを実装する
はじめに
現在、長い間 API 設計のデファクトとなっていた REST から、GraphQL や gRPC といった新しいものを採用する API が続々と登場しています。
GitHub API v4 が GraphQL を採用しているというのを知っている方も多いかと思います。今後も、GraphQL のような新しい設計を採用する API が増えていくことが予想されますが、従来の REST API アプリケーションに GraphQL のデータソースとの接続を容易にできるかというのは気になる点です。
そこで、今回は GraphQL API をデータソースとして叩くような GraphQL クライアント機能をよくある JVM 環境のアプリケーションにて実装してみたいと思います。
( GraphQL API サーバー自体を実装するわけではありません)GraphQL 初心者の方にも理解しやすい様に、用語の説明はリンクまたは文章にて記載しているので、入門向けでもあります。
サーバー側の構築
サーバー側を一から実装していくのは面倒なので、ありものを使うことにします。
今回はポケモンを題材にした GraphQL API の GraphQL Pokémon を使っていきたいと思います
他の有名どころだと先述した GitHub API v4 や スターウォーズ (SWAPI GraphQL) などがあります。
GitHub API でもよかったのですが、認証が絡んできて多少脱線しそうなため、見送っています。GraphQL Pokémon のデプロイ
非常に簡単なので、サーバー側のデプロイ方法を先述しておきます
# GitHubからクローン $ git clone git@github.com:lucasbento/graphql-pokemon.git $ cd graphql-pokemon # build & run $ yarn $ yarn run build $ yarn start GraphQL-Pokemon started on http://localhost:5000/GraphiQL で簡単 API 仕様把握
- 作者が GraphiQL インターフェースを Web上 でも提供してくれています
https://graphql-pokemon.now.sh
- 現在は上記のURLはサーバーダウンしているようなので、Issue#15 で紹介されている下記URLをお試しください
- もしくは、localで立ち上げていればそちらでも見れます
下記は、GraphiQL の画面キャプチャです。
右の Docs から型の要素情報を確認しつつ、欲しい query を簡単に作ることができます。
キャプチャの Query の例は、151番目までのカントー地方に生息するポケモンの図鑑番号・名前・タイプの情報を返却するものです。(英語名ですが )
詳しい GraphQL クエリについての情報は公式ドキュメントを見てみることをオススメします。
Queries and Mutations | GraphQLクライアント側の構築
JVM 環境で多く採用されている Spring Boot アプリケーション上で GraphQL サーバーを叩くような Web アプリケーションを作っていきます
- Runtime
- JVM
- Language
- Java 11
- もちろん Kotlin や Scala などでも構いません
- その場合、適宜コードは読み替えてください
- Dependency Manager
- Gradle
- Framework
- Spring Boot
- JVM 上で動く Web アプリケーションを簡単に構築するために使用します
- Apollo Android
- 後ほど詳細を説明します
Project 作成
- Spring Initializr を使って Spring Boot アプリケーションのひな形をつくっていきます
- 最下部の Java バージョンはお使いの環境のものを利用してください
- Dependencies に REST API を作るための Spring Web を追加してください
Apollo の導入
これから、Project を開いて実装を行っていくのですが、GraphQL API を叩くために必要な依存パッケージ(ライブラリ)を導入していきます
- Gradle を用いて、GraphQL クライアントライブラリである apollo-android を導入します
- apollo-android は文字通り Android 向けの GraphQL クライアントライブラリです
- ただ、説明にもあるとおり JVM 上であれば利用することができるのでサーバーサイドでも用いることが可能です
? A strongly-typed, caching GraphQL client for Android and the JVM
- build.gradle に Apollo Android 関連の依存ライブラリを追加します
% git diff diff --git a/build.gradle b/build.gradle index 1325946..269b7a7 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'org.springframework.boot' version '2.4.1' id 'io.spring.dependency-management' version '1.0.10.RELEASE' id 'java' + id 'com.apollographql.apollo' version '2.5.2' } group = 'com.example' @@ -14,6 +15,9 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'com.apollographql.apollo:apollo-runtime:2.5.2' + // 同期通信を簡単に書くために RxJava で書けるライブラリも import しています + implementation 'com.apollographql.apollo:apollo-rx3-support:2.5.2' testImplementation 'org.springframework.boot:spring-boot-starter-test' }なぜ Apollo を使うの?
Apollo を利用するのは、GraphQL のスキーマファーストという原理を大いに享受できるメリットがあるからです。
strongly-typed
という説明にもあるとおり、Apollo はサーバ側のスキーマ情報を用いて API レスポンスの各言語のオブジェクトクラスを自動生成してくれます。
※ Apollo は、Java 版の apollo-android のほか、Node.js 版 (Web) や Swift 版 (iOS) など主要なクライアント言語に対応しています。この自動生成が従来の REST とは大きく異なるところです。
従来は、サーバ側の API 仕様ドキュメントに基づいて、クライアント側で一からその定義通りに実装をしていました。
しかし、このレスポンスフィールドは必須のフィールドなのか?、型はなんなのか?、Null になり得るか?などの詳細を密に確認しながら進めなければなりませんでした。
さらに、API によって担当者が異なると、各々の流儀にしたがって作られた仕様書を読まなければなりませんでした。場合によっては API ドキュメントは存在せず、クライアントエンジニアが実際のサーバ側のコード(言語)を読む必要があるかもしれません。
その場合も、言語仕様を熟知していないとミスリードが生まれてしまう危険が大いにあるといえます。GraphQL は GraphQL 自体に型があり、Null 安全 (null safe) です。
利用できるリソースはすべてサーバ側のスキーマ情報に型とともに厳格に記述されているため、その型通りに生成されたオブジェクトを利用することは、すなわちコミュニケーションによる仕様のロストがないことを保証してくれます。クライアントエンジニアは GraphQL のみを知っていれば、裏側が Java で書かれていようが Kotlin で書かれていようが、Ruby on Rails で書かれていようがその言語仕様を熟知する必要がないということです。
(逆に言えば、GraphQL を用いる場合は クライアント / サーバエンジニア 双方に GraphQL の学習コストはかかります。とはいえ、プログラミング言語を1つでも習得しているような人にとって、GraphQL の学習コストはそこまで大きくはないでしょう)GraphQL API リクエスト実装
さて、ここからが本編です!
Apollo の公式ドキュメントを参考に実装していきますので、詳しい説明や補足などはこちらをご覧ください。
Get started with Java - Client (Android) - Apollo GraphQL Docsschama.json の作成
サーバー側のスキーマ情報の定義ファイルをプロジェクトに追加します。
これがないと、自動生成の恩恵を受けることができないので必須の工程となります。
apollo-android の plugin に schema.json を生成してくれるための便利コマンドがあるので、それを実行して作っていきます。
先ほど、ローカルの5000番ポートに立ち上げておいた GraphQL Pokémon のエンドポイントを指定して schema.json を作成します。# 先に出力先のフォルダを作っておく $ mkdir -p src/main/graphql/graphqlpokemon # schema.json作成 $ ./gradlew downloadApolloSchema \ --endpoint="http://localhost:5000/graphql/endpoint" \ --schema="src/main/graphql/graphqlpokemon/schema.json" BUILD SUCCESSFUL in 616ms 1 actionable task: 1 executedGraphQL Query を追加する
次に、必要なクエリ情報をまた別の定義ファイルに書いていきます。
これは、先ほどの GraphiQL で試した Query (151番目までのポケモンの情報)を使っていきましょう。src/main/graphql/graphqlpokemon/KantoPokemons.graphqlquery KantoPokemons { pokemons(first: 151) { number, name, types } }Apollo による Query モデルクラスの自動生成
上記まで出来たら、
strongly-typed
の恩恵を受けるために、Apollo の機能を使って GraphQL Query に必要なモデルクラスを自動生成してもらいましょう!$ ./gradlew generateApolloSources BUILD SUCCESSFUL in 868ms 4 actionable tasks: 2 executed, 2 up-to-dateすると、下記の様に勝手に build ディレクトリ内に必要なモデルクラスが出来上がっているのがわかると思います。
また、上記の様に必ずしも手動で生成用の task を実行する必要はなく、 build タスクに依存して勝手に実行されるので、コンパイルすれば必ず最新の状態となります。すばらしい
はい、この3ステップ(実質自動生成なので2ステップ)で事前準備は完了です!とても簡単ですね。
Graph Query も GraphiQL で試したものを使っているので、基本的にコピペをミスらなければ確実に正しい構文で書いてあることが保証されているというのも大きなメリットかなと思います。Repository の実装
やっとですが、Spring Boot アプリケーション側の実装となります。
GraphQL クライアント実装にフォーカスしているため、データソース取得が行えれば良いので、Spring Boot の Repository のみを実装していきます。
今回は上述のクエリの情報を返却する様な形で、カントー地方のポケモンの基本情報を取得するという機能を実装していきたいと思います。src/main/java/com/example/graphqlclientspring/PokemonRepository.javapackage com.example.graphqlclientspring; import com.apollographql.apollo.ApolloClient; import com.apollographql.apollo.api.Response; import com.apollographql.apollo.rx3.Rx3Apollo; import graphqlpokemon.KantoPokemonsQuery; import org.springframework.stereotype.Repository; import java.util.List; @Repository public class PokemonRepository { public List<KantoPokemonsQuery.Pokemon> fetchKantoPokemons() { final var apolloClient = ApolloClient.builder() .serverUrl("http://localhost:5000/graphql/endpoint") .build(); final var query = KantoPokemonsQuery.builder().build(); final var apolloQueryCall = apolloClient.query(query); // ブロッキング処理を簡単に書くため、Rx3Apollo を利用 return Rx3Apollo.from(apolloQueryCall) .map(Response::getData) .map(KantoPokemonsQuery.Data::pokemons) .blockingFirst(); } }上記のコードを見てもわかる通り、たった数行 Apollo の機能を使うだけで実装ができてしまいます。
Controller の実装
最後に、上記の Repository を叩くためのエントリーポイントを作ります。
特に難しいことはしておらず、Apollo が生成してくれるクラスはそのままではシリアライズができないので、 json 形式で返却できる様に POJO クラスを作成しそこへの詰め替えを行なっているというだけのシンプルな実装です。src/main/java/com/example/graphqlclientspring/PokemonController.javapackage com.example.graphqlclientspring; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.stream.Collectors; @RestController public class PokemonController { private final PokemonRepository pokemonRepository; public PokemonController(PokemonRepository pokemonRepository) { this.pokemonRepository = pokemonRepository; } @GetMapping("/pokemons") public ResponseEntity<List<PokemonResponseEntity>> kantoPokemons() { // 取得と出力用オブジェクトへの変換 final var pokemons = pokemonRepository.fetchKantoPokemons() .stream() .map(pokemon -> new PokemonResponseEntity(pokemon.number(), pokemon.name(), pokemon.types())) .collect(Collectors.toList()); return ResponseEntity.ok(pokemons); } // json 出力用 POJO public static class PokemonResponseEntity { public final String number; public final String name; public final List<String> types; public PokemonResponseEntity(String number, String name, List<String> types) { this.number = number; this.name = name; this.types = types; } } }動作確認
では、出来上がったコードを Run して動作を確認しましょう。
$ ./gradlew bootRun
別窓で実行 or ブラウザで開いてみましょう。(jq は json の整形用のコマンドです)
下記の様に151番目までのポケモンの情報が取得できたと思います# Spring Boot アプリケーションのデフォルトポートは 8080 $ curl -s "http://localhost:8080/pokemons" | jq [ { "number": "001", "name": "Bulbasaur", "types": [ "Grass", "Poison" ] }, { "number": "002", "name": "Ivysaur", "types": [ "Grass", "Poison" ] }, { "number": "003", "name": "Venusaur", "types": [ "Grass", "Poison" ] }, { "number": "004", "name": "Charmander", "types": [ "Fire" ] }, { "number": "005", "name": "Charmeleon", "types": [ "Fire" ] },まとめ
だいぶ長くなってしまいましたが、以上が GraphQL API のデータソースを叩くクライアント処理の実装でした。
もし、役に立った場合は LGTM やコメントをいただけると励みになります
最後までお読みいただき、ありがとうございましたGitHub リポジトリ
下記に commit していますので、ご参考にどうぞ。
- 投稿日:2021-01-11T16:11:37+09:00
Java × Spring Boot × Apollo で GraphQL クライアントを実装する
はじめに
現在、長い間 API 設計のデファクトとなっていた REST から、GraphQL や gRPC といった新しいものを採用する API が続々と登場しています。
GitHub API v4 が GraphQL を採用しているというのを知っている方も多いかと思います。今後も、GraphQL のような新しい設計を採用する API が増えていくことが予想されますが、従来の REST API アプリケーションに GraphQL のデータソースとの接続を容易にできるかというのは気になる点です。
そこで、今回は GraphQL API をデータソースとして叩くような GraphQL クライアント機能をよくある JVM 環境のアプリケーションにて実装してみたいと思います。
( GraphQL API サーバー自体を実装するわけではありません)GraphQL 初心者の方にも理解しやすい様に、用語の説明はリンクまたは文章にて記載しているので、入門向けでもあります。
サーバー側の構築
サーバー側を一から実装していくのは面倒なので、ありものを使うことにします。
今回はポケモンを題材にした GraphQL API の GraphQL Pokémon を使っていきたいと思います
他の有名どころだと先述した GitHub API v4 や スターウォーズ (SWAPI GraphQL) などがあります。
GitHub API でもよかったのですが、認証が絡んできて多少脱線しそうなため、見送っています。GraphQL Pokémon のデプロイ
非常に簡単なので、サーバー側のデプロイ方法を先述しておきます
# GitHubからクローン $ git clone git@github.com:lucasbento/graphql-pokemon.git $ cd graphql-pokemon # build & run $ yarn $ yarn run build $ yarn start GraphQL-Pokemon started on http://localhost:5000/GraphiQL で簡単 API 仕様把握
- 作者が GraphiQL インターフェースを Web上 でも提供してくれています
https://graphql-pokemon.now.sh
- 現在は上記のURLはサーバーダウンしているようなので、Issue#15 で紹介されている下記URLをお試しください
- もしくは、localで立ち上げていればそちらでも見れます
下記は、GraphiQL の画面キャプチャです。
右の Docs から型の要素情報を確認しつつ、欲しい query を簡単に作ることができます。
キャプチャの Query の例は、151番目までのカントー地方に生息するポケモンの図鑑番号・名前・タイプの情報を返却するものです。(英語名ですが )
詳しい GraphQL クエリについての情報は公式ドキュメントを見てみることをオススメします。
Queries and Mutations | GraphQLクライアント側の構築
JVM 環境で多く採用されている Spring Boot アプリケーション上で GraphQL サーバーを叩くような Web アプリケーションを作っていきます
- Runtime
- JVM
- Language
- Java 11
- もちろん Kotlin や Scala などでも構いません
- その場合、適宜コードは読み替えてください
- Dependency Manager
- Gradle
- Framework
- Spring Boot
- JVM 上で動く Web アプリケーションを簡単に構築するために使用します
- Apollo Android
- 後ほど詳細を説明します
Project 作成
- Spring Initializr を使って Spring Boot アプリケーションのひな形をつくっていきます
- 最下部の Java バージョンはお使いの環境のものを利用してください
- Dependencies に REST API を作るための Spring Web を追加してください
Apollo の導入
これから、Project を開いて実装を行っていくのですが、GraphQL API を叩くために必要な依存パッケージ(ライブラリ)を導入していきます
- Gradle を用いて、GraphQL クライアントライブラリである apollo-android を導入します
- apollo-android は文字通り Android 向けの GraphQL クライアントライブラリです
- ただ、説明にもあるとおり JVM 上であれば利用することができるのでサーバーサイドでも用いることが可能です
? A strongly-typed, caching GraphQL client for Android and the JVM
- build.gradle に Apollo Android 関連の依存ライブラリを追加します
% git diff diff --git a/build.gradle b/build.gradle index 1325946..269b7a7 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'org.springframework.boot' version '2.4.1' id 'io.spring.dependency-management' version '1.0.10.RELEASE' id 'java' + id 'com.apollographql.apollo' version '2.5.2' } group = 'com.example' @@ -14,6 +15,9 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'com.apollographql.apollo:apollo-runtime:2.5.2' + // 同期通信を簡単に書くために RxJava で書けるライブラリも import しています + implementation 'com.apollographql.apollo:apollo-rx3-support:2.5.2' testImplementation 'org.springframework.boot:spring-boot-starter-test' }なぜ Apollo を使うの?
Apollo を利用するのは、GraphQL のスキーマファーストという原理を大いに享受できるメリットがあるからです。
strongly-typed
という説明にもあるとおり、Apollo はサーバ側のスキーマ情報を用いて API レスポンスの各言語のオブジェクトクラスを自動生成してくれます。
※ Apollo は、Java 版の apollo-android のほか、Node.js 版 (Web) や Swift 版 (iOS) など主要なクライアント言語に対応しています。この自動生成が従来の REST とは大きく異なるところです。
従来は、サーバ側の API 仕様ドキュメントに基づいて、クライアント側で一からその定義通りに実装をしていました。
しかし、このレスポンスフィールドは必須のフィールドなのか?、型はなんなのか?、Null になり得るか?などの詳細を密に確認しながら進めなければなりませんでした。
さらに、API によって担当者が異なると、各々の流儀にしたがって作られた仕様書を読まなければなりませんでした。場合によっては API ドキュメントは存在せず、クライアントエンジニアが実際のサーバ側のコード(言語)を読む必要があるかもしれません。
その場合も、言語仕様を熟知していないとミスリードが生まれてしまう危険が大いにあるといえます。GraphQL は GraphQL 自体に型があり、Null 安全 (null safe) です。
利用できるリソースはすべてサーバ側のスキーマ情報に型とともに厳格に記述されているため、その型通りに生成されたオブジェクトを利用することは、すなわちコミュニケーションによる仕様のロストがないことを保証してくれます。クライアントエンジニアは GraphQL のみを知っていれば、裏側が Java で書かれていようが Kotlin で書かれていようが、Ruby on Rails で書かれていようがその言語仕様を熟知する必要がないということです。
(逆に言えば、GraphQL を用いる場合は クライアント / サーバエンジニア 双方に GraphQL の学習コストはかかります。とはいえ、プログラミング言語を1つでも習得しているような人にとって、GraphQL の学習コストはそこまで大きくはないでしょう)GraphQL API リクエスト実装
さて、ここからが本編です!
Apollo の公式ドキュメントを参考に実装していきますので、詳しい説明や補足などはこちらをご覧ください。
Get started with Java - Client (Android) - Apollo GraphQL Docsschama.json の作成
サーバー側のスキーマ情報の定義ファイルをプロジェクトに追加します。
これがないと、自動生成の恩恵を受けることができないので必須の工程となります。
apollo-android の plugin に schema.json を生成してくれるための便利コマンドがあるので、それを実行して作っていきます。
先ほど、ローカルの5000番ポートに立ち上げておいた GraphQL Pokémon のエンドポイントを指定して schema.json を作成します。# 先に出力先のフォルダを作っておく $ mkdir -p src/main/graphql/graphqlpokemon # schema.json作成 $ ./gradlew downloadApolloSchema \ --endpoint="http://localhost:5000/graphql/endpoint" \ --schema="src/main/graphql/graphqlpokemon/schema.json" BUILD SUCCESSFUL in 616ms 1 actionable task: 1 executedGraphQL Query を追加する
次に、必要なクエリ情報をまた別の定義ファイルに書いていきます。
これは、先ほどの GraphiQL で試した Query (151番目までのポケモンの情報)を使っていきましょう。src/main/graphql/graphqlpokemon/KantoPokemons.graphqlquery KantoPokemons { pokemons(first: 151) { number, name, types } }Apollo による Query モデルクラスの自動生成
上記まで出来たら、
strongly-typed
の恩恵を受けるために、Apollo の機能を使って GraphQL Query に必要なモデルクラスを自動生成してもらいましょう!$ ./gradlew generateApolloSources BUILD SUCCESSFUL in 868ms 4 actionable tasks: 2 executed, 2 up-to-dateすると、下記の様に勝手に build ディレクトリ内に必要なモデルクラスが出来上がっているのがわかると思います。
また、上記の様に必ずしも手動で生成用の task を実行する必要はなく、 build タスクに依存して勝手に実行されるので、コンパイルすれば必ず最新の状態となります。すばらしい
はい、この3ステップ(実質自動生成なので2ステップ)で事前準備は完了です!とても簡単ですね。
Graph Query も GraphiQL で試したものを使っているので、基本的にコピペをミスらなければ確実に正しい構文で書いてあることが保証されているというのも大きなメリットかなと思います。Repository の実装
やっとですが、Spring Boot アプリケーション側の実装となります。
GraphQL クライアント実装にフォーカスしているため、データソース取得が行えれば良いので、Spring Boot の Repository のみを実装していきます。
今回は上述のクエリの情報を返却する様な形で、カントー地方のポケモンの基本情報を取得するという機能を実装していきたいと思います。src/main/java/com/example/graphqlclientspring/PokemonRepository.javapackage com.example.graphqlclientspring; import com.apollographql.apollo.ApolloClient; import com.apollographql.apollo.api.Response; import com.apollographql.apollo.rx3.Rx3Apollo; import graphqlpokemon.KantoPokemonsQuery; import org.springframework.stereotype.Repository; import java.util.List; @Repository public class PokemonRepository { public List<KantoPokemonsQuery.Pokemon> fetchKantoPokemons() { final var apolloClient = ApolloClient.builder() .serverUrl("http://localhost:5000/graphql/endpoint") .build(); final var query = KantoPokemonsQuery.builder().build(); final var apolloQueryCall = apolloClient.query(query); // ブロッキング処理を簡単に書くため、Rx3Apollo を利用 return Rx3Apollo.from(apolloQueryCall) .map(Response::getData) .map(KantoPokemonsQuery.Data::pokemons) .blockingFirst(); } }上記のコードを見てもわかる通り、たった数行 Apollo の機能を使うだけで実装ができてしまいます。
Controller の実装
最後に、上記の Repository を叩くためのエントリーポイントを作ります。
特に難しいことはしておらず、Apollo が生成してくれるクラスはそのままではシリアライズができないので、 json 形式で返却できる様に POJO クラスを作成しそこへの詰め替えを行なっているというだけのシンプルな実装です。src/main/java/com/example/graphqlclientspring/PokemonController.javapackage com.example.graphqlclientspring; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.stream.Collectors; @RestController public class PokemonController { private final PokemonRepository pokemonRepository; public PokemonController(PokemonRepository pokemonRepository) { this.pokemonRepository = pokemonRepository; } @GetMapping("/pokemons") public ResponseEntity<List<PokemonResponseEntity>> kantoPokemons() { // 取得と出力用オブジェクトへの変換 final var pokemons = pokemonRepository.fetchKantoPokemons() .stream() .map(pokemon -> new PokemonResponseEntity(pokemon.number(), pokemon.name(), pokemon.types())) .collect(Collectors.toList()); return ResponseEntity.ok(pokemons); } // json 出力用 POJO public static class PokemonResponseEntity { public final String number; public final String name; public final List<String> types; public PokemonResponseEntity(String number, String name, List<String> types) { this.number = number; this.name = name; this.types = types; } } }動作確認
では、出来上がったコードを Run して動作を確認しましょう。
$ ./gradlew bootRun
別窓で実行 or ブラウザで開いてみましょう。(jq は json の整形用のコマンドです)
下記の様に151番目までのポケモンの情報が取得できたと思います# Spring Boot アプリケーションのデフォルトポートは 8080 $ curl -s "http://localhost:8080/pokemons" | jq [ { "number": "001", "name": "Bulbasaur", "types": [ "Grass", "Poison" ] }, { "number": "002", "name": "Ivysaur", "types": [ "Grass", "Poison" ] }, { "number": "003", "name": "Venusaur", "types": [ "Grass", "Poison" ] }, { "number": "004", "name": "Charmander", "types": [ "Fire" ] }, { "number": "005", "name": "Charmeleon", "types": [ "Fire" ] },まとめ
だいぶ長くなってしまいましたが、以上が GraphQL API のデータソースを叩くクライアント処理の実装でした。
もし、役に立った場合は LGTM やコメントをいただけると励みになります
最後までお読みいただき、ありがとうございましたGitHub リポジトリ
下記に commit していますので、ご参考にどうぞ。
- 投稿日:2021-01-11T14:26:25+09:00
(Android) DataBindingを使ってテキストを動的に変更する
はじめに
前回は DataBindingを使って静的テキストを表示させてみるを紹介しました。
今回はこれに少し手を加えて、ボタンをクリックするとテキストが切り替わるアプリを実装します。
クリック後、データを変更するためのインターフェースを定義
SampleEventHandlers.javapublic interface SampleEventHandlers { void onChangeClick(View view); }レイアウトにボタンを追加/クリックイベントと紐付ける
activity_main.xml<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Binding Objects --> <data> <variable name="user" type="com.example.databinding2.User" /> <!-- handlersという名前(任意)で、ハンドラー(インターフェース)が設定される --> <variable name="handlers" type="com.example.databinding2.SampleEventHandlers" /> </data> <!-- View --> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.name}" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.email}" /> <Button android:id="@+id/button_change" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Change" android:onClick="@{handlers.onChangeClick}" /> <!-- ボタンとSampleEventHandlers.javaのonChangeClickを紐付ける --> </LinearLayout> </layout>モデルクラスに値を変更するメソッドを追加(未完成 理由は後述)
User.javapublic class User { private String name; private String email; public User(String name, String email) { this.name = name; this.email = email; } public String getName() { return name; } public String getEmail() { return email; } // セッターを追加 public void setName(String name) { this.name = name; } // セッターを追加 public void setEmail(String email) { this.email = email; } }DataBinding処理(クリックイベント)を追加
MainActivity.javapublic class MainActivity extends AppCompatActivity implements SampleEventHandlers{ private User user = new User("Taro", "taro@test.com"); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Bindingのインスタンスを取得 ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main); // xmlのuserとMainActivityのuserを紐付ける binding.setUser(user); // xmlのhandlersにMainActivityのonChangeClick()を紐付ける binding.setHandlers(this); } // buttonをクリックしたときのイベント処理 @Override public void onChangeClick(View view) { if (user.getName().equals("Taro")) { user.setName("Jiro"); user.setEmail("jiro@test.com"); Log.d("DEBUG", user.getName()); } else { user.setName("Taro"); user.setEmail("taro@test.com"); } } }アプリビルドしてみるが。。。
これを実行するとログには"Jiro"と表示されるが、画面に表示される文字は"Taro"のままとなってしまう。なぜ?
どうやらプロパティの変更をViewに通知する仕組みが必要みたいで、オブジェクトを監視できるようにUserモデルを修正する必要があるとのこと。
モデルクラスを修正(プロパティの変更をViewに通知する)
Point
- BaseObservableを継承する
- getメソッドにBindableを追加
- 監視用の定数BR.name,BR.emailが生成できるようになる
- setメソッド内にnotifyPropertyChanged(BR.name)を追加
- レイアウト側からBR.nameに対応するgetName()が呼ばれるようになる
User.javapublic class User extends BaseObservable { private String name; private String email; public User(String name, String email) { this.name = name; this.email = email; } @Bindable public String getName() { return name; } @Bindable public String getEmail() { return email; } public void setName(String name) { this.name = name; notifyPropertyChanged(BR.name); } public void setEmail(String email) { this.email = email; notifyPropertyChanged(BR.email); } }参考サイト
https://qiita.com/Omoti/items/a83910a990e64f4dbdf1#step0-%E5%B0%8E%E5%85%A5
ありがとうございました
- 投稿日:2021-01-11T14:03:35+09:00
gradleプロジェクトでバージョンを自動指定
背景
Javaプロジェクトを開発している時に、今まではリリースのたびにbuild.gradleのversionを書き換えていました。こういう感じのやつですね。
version = '1.2.0'この運用だと、以下の点で問題がありました。
- リリースの度にbuild.gradleに対して変更が必要。場合によってはコンフリクトの原因にもなる。
- リリース後に再びbuild.gradleのversionにバージョンを上げて-SNAPSHOTを付ける必要がある(例:1.2.0をリリースしたら、1.3.0-SNAPSHOTに変更する)。これを忘れてpublishすると、リリースしたバージョンが上書きされてしまうので。
- 新しいブランチで開発中に、連携テストをするため暫定的にリリースしたい場合、build.gradleでユニークなバージョンを指定してpublishする必要がある。
- ローカルにコミット漏れがある状態でリリースしてしまったことがある。
最初、
build.gradle
内でgitコマンドを利用できるgrgitプラグインを利用して簡単なスクリプトを書こうと思ったのですが、コード量が膨らみそうになりました。そこで、自前のプラグインを書こうか考えていたところ、nebula-releaseプラグインというものを見つけました。非常に機能が豊富そうだったので、これを利用してこの問題を解決することにしました。build.gradleの設定
nebula-releaseプラグインに関する説明はnebula-release-pluginリポジトリにあります。プラグインの適用は、build.gradleに以下の設定を追加します。設定は他にも色々あるのですが、まずはこれだけ。
plugins { id 'nebula.release' version '15.3.1' }ビルドしてみる
開発時
まずは
./gradlew build
を実行してビルドしてみます。すると、以下のファイルが生成されました。$ ls build/libs/ nebula-test-0.1.0-dev.3+618c605.jarこの命名については、Tasks Providedに記載されており、プロジェクト名の後ろに
<major>.<minor>.<patch>-dev.#+<hash>
が付いています。#
が前回リリースからのコミット数、hash
がコミットのハッシュ値の一部となっているため、コンフリクトを起こすことはまずありません。また、コミットされていないファイルがある場合は、nebula-test-0.1.0-dev.3.uncommitted+618c605.jar
のようにuncommitted
が自動的に付与され、この状態ではリリースできないようになっています。本番環境ではなく開発環境にデプロイしたいような場合に、先ほどのユニークなバージョンではなくSNAPSHOTバージョンでビルドしたい場合は、
./gradlew build snapshot
を実行します。$ ls build/libs/ nebula-test-0.1.0-SNAPSHOT.jarリリース時
リリース時には、
final
パラメタを追加します。こうすると、「プロジェクト名+バージョン」のみが付いたファイル名が生成され、現在のバージョンでタグ付けされます。$ ./gradlew final Inferred project: nebula-test, version: 0.1.0 > Task :release Tagging repository as v0.1.0 Pushing changes in [v0.1.0] to origin BUILD SUCCESSFUL in 6s 7 actionable tasks: 4 executed, 3 up-to-date $ $ ls build/libs/ nebula-test-0.1.0.jar次に再度
./gradlew build
を実行すると、次のマイナーバージョンで生成されます。$ ls build/libs/ nebula-test-0.2.0-dev.0+726f7c6.jarメジャーバージョン・パッチバージョンのリリース
特にパラメタを指定せずにリリースする場合はマイナーバージョンを上げてリリースされますが、メジャーバージョンやパッチバージョンを上げたい場合は、それぞれ以下のようにパラメタを指定します。
- メジャーバージョン:
./gradlew final -Prelease.scope=major
- 例:0.3.0 -> 1.0.0
- パッチバージョン:
./gradlew final -Prelease.scope=patch
- 例:0.3.0 -> 0.3.1
その他メモ
nebulaプラグインを使っていて気付いた点を共有します。
リリース時にpublishも行う
build.gradle
に以下の行を追加して、依存関係を加えます。tasks.release.dependsOn tasks.publish複数のサブプロジェクトから成る場合はこのような感じで。
tasks.release.dependsOn ":sub1:publish", ":sub2:publish", ...タグ名のプレフィックスを除く
./gradlew final
でリリースする際、通常はv1.2.0
のように先頭にv
が付くのですが、これを付けたくない場合はbuild.gradle
に以下の行を追加します。project.release.tagStrategy.prefixNameWithV = falseAWS CodeBuildとの連携
nebulaプラグインはgitの履歴から最新のタグを探すため、必ず全てのgitの履歴が必要となります。もしAWS CodeBuildを利用している場合は、ソースの設定の「Gitのクローンの深さ」で
Full
を指定することで正しくバージョンを取得することができます。
- 投稿日:2021-01-11T13:40:05+09:00
Javaインストール後の実行環境テスト
最終確認
インストール、Version確認後
VSCordでフォルダを作成し、期待する結果が出力されるまでテストする
Javaの環境構築はこちら実行環境テスト
# テスト用ディレクトリを作成 $ mkdir -p ~/java/test # ディレクトリへ移動 $ cd ~/java/test # テスト用のファイルを作成 $ touch test.java # テスト用のファイルを編集 $ vi test.java//test.java class Main { public static void main(String[] args) { String msg = ""; msg += "Hello "; msg += "World!"; System.out.println(msg); } }# 作成したディレクトリへ移動 $ cd ~/java/hello:wqで保存
実行する
$ java test.javaHello World実行環境テストはクリア!
- 投稿日:2021-01-11T13:28:06+09:00
Javaの環境構築
環境構築に必要なもの
Java、VisualStudioCode、Javaの拡張機能の3つをインストール
❶Javaのインストール
//ターミナルを開く $ javac -version //JDKがインストールされているか確認バージョンが出てこなかったらJavaインストールページへ
一覧から選択後、インストール
バージョンのアップデート
//バージョンが古い $ javac -version javac 11.0.9.1//インストール後 $ javac -version javac 15.0.1最新のバージョンになっていない
//インストール済みのJavaを確認 /usr/libexec/java_home -V Matching Java Virtual Machines (2): 15.0.1, x86_64: "Java SE 15.0.1" /Library/Java/JavaVirtualMachines/jdk-15.0.1.jdk/Contents/Home 11.0.9.1, x86_64: "AdoptOpenJDK 11" /Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home❷VisualStudioCodeのインストール
インストール後
左のバーを(アクティビティバー)
四角のアイコンをExtensions
ここで、拡張機能をインストールできるVisualStudioCodeを開く 表示が英語になっているので、日本語に変更 「Japanese Language Pack for Visual Studio Code」と検索 「Japanese」と入力すると、候補が出てくる インストールをクリック インストールが終わったらVisualStudioCodeを再起動 表示が日本語になる❸Javaの拡張機能のインストール
VisualStudioCodeのextensionsで 「Java Extension Packvscjava.vscode-java-pack」を検索して、インストール環境構築が完了したので次回は実行環境テスト!
- 投稿日:2021-01-11T13:27:37+09:00
Javaについて触れてみる
Javaとは
C++やC言語を元として開発されたプログラミング言語(コンパイラ言語) Googleが開発に用いている“Google三大言語(Java、C++、Python)”の1つ開発できるもの
・Webアプリ・Webサービス ・デスクトップアプリ ・スマホアプリ ・組み込み系 ・IoT...RubyやPHPは初心者向けで比較的簡単に扱え、小規模向けでもあるが
Javaの場合は少々複雑だが、非常に幅広い分野のプログラムを作ることができるJavaのメリット
・実行環境を選ばない ・実行速度が速く安定性がある ・セキュリティ性が高い ・拡張性・再利用性がある ・メモリー自動管理機能Javaのデメリット
・オブジェクト指向に慣れる必要がある ・小規模なプログラムやシステムの場合、逆に時間がかかるカッコいいスローガン
Write once, run anywhere (一度プログラムを書いてしまえば、どのコンピューターでも動く)次回は環境構築を行います!!
- 投稿日:2021-01-11T13:16:40+09:00
HotSpot VMについて
この記事は何?
Javaの内部とかに全然詳しくないプログラマがHotSpot VMについて調べた話
経緯
- Stringの内部実装について調べる
@HotSpotIntrinsicCandidate
<- なんだァ?てめェ……- HotSpot VMなるものが関係してるらしいので調べる
ざっくり言うと
HotSpot VM: HotSpotを実装したJVM
HotSpot: オラクルのJVM高速化記述非常に粗くまとめると「よく使われる部分をキャッシュしておき、Javaの実行を早くする技術」のこと
経験的に少数の処理がよく呼び出され、そのほかはあんまり呼ばれない(例えば処理全体の2割が8割の実行時間を占める、など)。そのためその2割をキャッシュしておけば早くなる、と言うことらしい
もうちょっとだけ詳しく
Javaを実行する際、人間の書いたJavaコードはバイトコード(JVM語)に変換され、そのJVM語をJVMインタプリタが解釈してプログラムが実行される。
HotSpotがなければヒープ領域にあるJavaコードをJVMが逐一解釈して処理が実行されるが、これでは処理速度が遅いと言う問題がある。
HotSpotは同じバイトコードがJVMに何度も呼ばれた場合、その部分を機械語にコンパイルしてキャッシュしておく。こうして、プログラムの中のよく呼び出される部分の処理が高速化される。詳細は以下の参考記事
https://www.atmarkit.co.jp/fjava/special/jvmhistory/jvmhistory01.html
https://www.atmarkit.co.jp/ait/articles/0403/11/news096.html
https://ja.wikipedia.org/wiki/HotSpot
- 投稿日:2021-01-11T13:15:23+09:00
デメテルの法則
デメテルの法則に沿わない、ダメな例
// NG。以下のようなコードを書くべきではない class A { private B b; public B getB() { return b; } } class B { public void hoge() { System.out.println("hoge"); } } class Test { public static void main(String[] args) { new A().getB().hoge(); } }デメテルの法則に沿う、良い例
// OK。以下のようなコードが望ましい class A { private B b; public void hoge() { b.hoge(); } } class B { public void hoge() { System.out.println("hoge"); } } class Test { public static void main(String[] args) { new A().hoge(); } }なぜデメテルの法則に沿うようにするとよいのか?
例えば、クラスBのメソッド名が「hoge」から「fuga」に変わった場合、デメテルの法則が適用されている方のコードはTestクラスの中のコードを変更する必要はない。しかしデメテルの法則が適用されていない方のコードは、Testクラスのコードも変更する必要がある。(つまり、変更箇所が増える)
class A { private B b; public void hoge() { // ↓ここを変えた b.fuga(); } } class B { // ↓ここを変えた public void fuga() { System.out.println("fuga"); } } class Test { public static void main(String[] args) { new A().hoge(); } }
- 投稿日:2021-01-11T09:15:59+09:00
【Java】Mapのキーと値のペアを拡張for文を使って取得する
はじめに
Map のキーと値のペアを
Map.entrySet
を使って取得する方法です。
Map のキーと値を取得する方法は他にもkeySet
やvalues
があります。
これらはキー・値のどちらかを取得するだけですが、entrySet
を使うとキーと値のペアを取得することが出来ます。entry.Set の使い方
Mapに値を設定
Map<String,String> animal = new HashMap<>(); animal.put("monkey", "猿"); animal.put("dog", "犬"); animal.put("cat", "猫");Mapのキーと値のペアを取得
以下の処理ではentrySetメソッドを使用してキーと値のペアを取得し、getKeyメソッドでキーを、getValueメソッドで値を取得しています。
for (Map.Entry<String, String> animalNameInJapanese : animal.entrySet()) { System.out.println(animalName.getKey() + "は日本語で" + animalName.getValue() + "です。"); }出力
monkeyは日本語で猿です。 dogは日本語で犬です。 catは日本語で猫です。参照
- 投稿日:2021-01-11T07:26:22+09:00
RhinoでJavaの中からJavaScriptを利用する 2021年版
JavaVM で JavaScript エンジンを動かして、DSLとして JavaScript を利用したいようなケースは結構あったはずだが、JavaVM に標準で搭載されているJavaScriptエンジン Nashorn はすでに Java11 で非推奨となっていて、プロダクションで使っている勢にとっては不安の種となっている。
Nashorn 非推奨の理由は、最新の ECMAScript 仕様に追随しきれないというもの。
確かにモダンなESを使いたいモチベーションはあるが、それが足枷となってJVMそのものの進化のスピードが律速されてしまうのであれば、非推奨としてJVM本体の進化のペースとは切り離すという判断は合理的。
ではどうしたらいいのか。Alt JVM として開発されている他言語対応JVM GraalVM を使うというのが第一の選択肢である。GraalVM に関しては色々なところで触れられているのでここでは触れない。
実運用上は VM を変更して運用することはないとはいえ、Javaのプログラムが特定のJVMでないと動かないというのは、なんとなく残念な感じがしてしまう。ここで試してみるのは、Nashorn 以前の JS on JVM を実現するスタンダードである Rhino である。
https://github.com/mozilla/rhino
久しぶりに Rhino のリポジトリを見てみると、最新の 1.7.13 が2020年9月にリリースされており、今でもちゃんとメンテナンスされていることがわかる。
ES2015 との互換性テーブルを見てみると、なかなか苦労している様子ではあるが、互換性のレベルも徐々に向上しているので今後にも期待が持てそうだ。
ScriptEngine で Rhino を使う
Rhino 1.7.13 で、JVM の ScriptEngine インタフェースに対応している。なるほど Nashorn をそのまま置き換えられるようになったのだ。
Rhino で ScriptEngine インタフェースを利用するには、従来の rhino.jar だけではなく rhino-engine.jar が必要となる。
そのため pom.xml に以下の依存関係を記述する。(Mavenの場合)
<!-- https://mvnrepository.com/artifact/org.mozilla/rhino --> <dependency> <groupId>org.mozilla</groupId> <artifactId>rhino</artifactId> <version>1.7.13</version> </dependency> <!-- https://mvnrepository.com/artifact/org.mozilla/rhino-engine --> <dependency> <groupId>org.mozilla</groupId> <artifactId>rhino-engine</artifactId> <version>1.7.13</version> </dependency>最も単純に JavaScript を実行するには以下のようなコードになる。エンジン名に
rhino
を指定するだけでそのまま利用可能だ。ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName("rhino"); try { scriptEngine.eval("function add(a, b) { return a + b }"); Number v = (Number) scriptEngine.eval("add(13, 17)"); System.out.println(v); } catch (ScriptException e) { e.printStackTrace(); }ただ Nashorn の場合は、上記コードの戻り値の
Number
の実際の型はDouble
だったが、Rhinoの場合はLong
に変わっている。細かいところではいろいろと差異がありそうだ。Javaオブジェクトと相互運用してみる
JavaScript の中から Java のオブジェクトを利用するためには、JavaScript のスコープにオブジェクトをセットして呼び出す必要がある。
Map<String, Object> map = new HashMap<>(); map.put("a", 10); map.put("b", 20); map.put("console", new MyConsole()); SimpleBindings bindings = new SimpleBindings(map); scriptEngine .eval("c = a + b;" + "a += b;" + "console.log('a=' + a + ', b=' + b + ', c=' + c);", bindings);ここで
MyConsole
は自分で作成した以下のようなクラスだ。public class MyConsole { public void log(Object arg) { System.out.println(String.valueOf(arg)); } }結果は
a=30, b=20, c=30となり期待するとおりとなった。
生で Rhino を使う
1.7.12 以前の Rhino と同様に、ScriptEngine インタフェースを経由せずにそのまま生で Rhino を使うことも可能だ。
この場合のコードは以下のようになる。
Context context = new ContextFactory().enterContext(); Scriptable globalScope = context.initStandardObjects(); Script script = null; try (Reader reader = new FileReader(jsSourceFile)) { script = context.compileReader(reader, jsSourceFile.getName(), 1, null); } catch (IOException | EvaluatorException e) { e.printStackTrace(System.err); System.exit(1); } ScriptableObject.putProperty(globalScope, "a", 10); ScriptableObject.putProperty(globalScope, "b", 20); ScriptableObject.putProperty(globalScope, "console", new MyConsole()); // run global scope context script.exec(context, globalScope);文字列の比較に注意
いくつか JavaScript で Java のオブジェクトを操作するプログラムを書いてみたところ以下のケースに遭遇した。
var javaObject = someObject.getValue(); // javaObject は java.lang.String である console.log(javaObject); // "OK" if (javaObject === 'OK') { // false // JavaScriptの文字列リテラルと同一とみなされない console.log('==='); } else if (javaObject == 'OK') { // false // == でもダメ console.log('=='); } else if ('OK'.equals(javaObject)) { // true console.log('equals'); }この結果は
equals
になる。Java の文字列と JavaScript の文字列は別のものなのである。
これは注意が必要だ。追記
context.getWrapFactory().setJavaPrimitiveWrap(false);
を呼び出すことでこのあたりの挙動が変更できるとのこと。
まとめ
Rhino は着々と進化しつづけている。2021年においても Java アプリケーションにおける DSL として JavaScript を採用することにおいて不安はない。
- 投稿日:2021-01-11T02:52:17+09:00
Dockerを用いて掲示板を作った1
これは何
DockerとWebアプリ作成の勉強のため,この本の第3章をDockerコンテナの上で実装してみた際のメモ.
ファイル構成
$ tree . . ├── Dockerfile └── testbbs └── WEB-INF ├── classes ├── src │ ├── Message.java │ ├── PostBBS.java │ └── ShowBBS.java └── web.xml 4 directories, 5 filesDockerfile
DockerfileFROM tomcat:8.5.54-jdk11-adoptopenjdk-hotspot WORKDIR /usr/local/tomcat/webapps/ RUN mkdir -p ./testbbs COPY ./testbbs ./testbbs/ RUN javac -classpath $CATALINA_HOME/lib/servlet-api.jar -d ./testbbs/WEB-INF/classes ./testbbs/WEB-INF/src/*.javaJavaファイル
参考にしてる本と同じ.
Message.javaimport java.util.*; public class Message { public static ArrayList<Message> messageList = new ArrayList<Message>(); String title; String handle; String message; Date date; Message (String title, String handle, String message) { this.title = title; this.handle = handle; this.message = message; this.date = new Date(); } }PostBBS.javaimport java.io.*; import javax.servlet.http.*; public class PostBBS extends HttpServlet { @Override public void doPost(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException, IOException { request.setCharacterEncoding("UTF-8"); Message newMessage = new Message(request.getParameter("title"), request.getParameter("handle"), request.getParameter("message")); Message.messageList.add(0, newMessage); response.sendRedirect("/testbbs/ShowBBS"); } }ShowBBS.javaimport java.io.*; import javax.servlet.http.*; public class ShowBBS extends HttpServlet { private String espaceHTML (String src) { return src.replace ("&", "&").replace("<", "<") .replace (">", ">").replace ("\"", """) .replace ("'", "'"); } @Override public void doGet (HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html; charset=UTF-8"); PrintWriter out = response.getWriter(); out.println("<html>"); out.println("<head>"); out.println("<title>テスト掲示板</title>"); out.println("</head>"); out.println("<body>"); out.println("<h1>テスト掲示板</h1>"); out.println("<form action='/testbbs/PostBBS' method='post'>"); out.println("タイトル:<input type='text' name='title' size='60'>"); out.println("<br />"); out.println("ハンドル名:<input type='text' name='handle'>"); out.println("<br />"); out.println("<textarea name='message' rows='4' cols='60'></textarea>"); out.println("<br />"); out.println("<input type='submit' />"); out.println("</form>"); out.println("<hr />"); for (Message message: Message.messageList) { out.println("<p> 『" + espaceHTML(message.title) + "』 "); out.println(espaceHTML(message.handle) + " さん "); out.println(espaceHTML(message.date.toString()) + "</p>"); out.println("<p>"); out.println(espaceHTML(message.message).replace("\r\n", "<br />")); out.println("</p><hr />"); } out.println("</body>"); out.println("</html>"); } }web.xmlファイル
これも本と同じ.
web.xml<web-app xmlns="http://xmlns.jcp.org/xml/nx/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1" metadata-complete="true"> <servlet> <servlet-name>ShowBBS</servlet-name> <servlet-class>ShowBBS</servlet-class> </servlet> <servlet> <servlet-name>PostBBS</servlet-name> <servlet-class>PostBBS</servlet-class> </servlet> <servlet-mapping> <servlet-name>ShowBBS</servlet-name> <url-pattern>/ShowBBS</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>PostBBS</servlet-name> <url-pattern>/PostBBS</url-pattern> </servlet-mapping> </web-app>ビルドして実行
$ docker build -t henacat . Sending build context to Docker daemon 11.78kB Step 1/6 : FROM tomcat:8.5.54-jdk11-adoptopenjdk-hotspot ---> 66317f378ae0 Step 2/6 : RUN apt-get update && apt-get install -y wget ---> Using cache ---> 871dd39a71cc Step 3/6 : WORKDIR /usr/local/tomcat/webapps/ ---> Using cache ---> 19d29e246ff6 Step 4/6 : RUN mkdir -p ./testbbs ---> Using cache ---> c2765cdc59e3 Step 5/6 : COPY ./testbbs ./testbbs/ ---> Using cache ---> dbd09272e03b Step 6/6 : RUN javac -classpath $CATALINA_HOME/lib/servlet-api.jar -d ./testbbs/WEB-INF/classes ./testbbs/WEB-INF/src/*.java ---> Using cache ---> 41e02fb5b101 Successfully built 41e02fb5b101 Successfully tagged henacat:latest $ docker run -d -p 8080:8080 -it henacat $ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 249799090cb1 henacat "catalina.sh run" About a minute ago Up About a minute 0.0.0.0:8080->8080/tcp elegant_ishizakahttp://localhost:8080/testbbs/ShowBBS にアクセスすると,ちゃんと掲示板が表示される.
ハマったこと
最初に使ったDockerイメージはtomcat:10.0.0-jdk11-adoptopenjdk-hotspot だったが,これだとコンパイルが通らなかった.
どうもサーブレットのためのパッケージが見つからないらしい.
クラスパスを間違えたかなと思い色々変えてみたがコンパイルエラーは直らず,tomcatの問題かと思いドキュメントを見にいくと,tomcat-10からどうもパッケージ名が変わったらしい.
tomcat-8 では,javax.servlet.httpだったのが,tomcat-10では,jakarta.servlet.httpとなっており,そりゃパッケージ見つからんってエラー吐かれるよなと.
と言うわけで,本と同じtomcat-8のDockerイメージを用いることで無事解決.参考文献