20210111のJavaに関する記事は15件です。

【Java】AtcorderのABC-188に参加しました。

こんばんは。初投稿です。よろしくお願いします。

2021/1/10に、AtcorderのABCコンテストに参加しまして、レートが付きました。
https://atcoder.jp/users/ishikawaryou

何とかC問題まで時間内に解くことができました。
僕はもともとはCOBOL技術者でJavaは完全独学ですが、時間はかかれどC問題まで解けた自分を褒めてあげようと思います。

これからもコツコツ取り組んで、茶色を目指したいと思います!
同じく灰色レベルで頑張っている方はお互いがんばっていきましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GraphQLサーバーをJavaで実装してみる with Netflix DGS

NetflixのTech blogに書かれていたGraphQL FrameworkがOSSとして公開されていたので、さっそく使ってみました。

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.gradle
plugins {
    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.graphqlssrc/main/resources/schema以下に登録します。

schema.graphqls
type 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.java

Bookクラスが作成できました。

DGS Component

次に、Query、Mutation、そしてSubscriptionを実装したServiceクラス(DGS component)を作成します。

  • クラスに@DgsComponentを付与します。
  • メソッドに@DgsDataを付与し、parentTypeにはスキーマのtype name、fieldにはfield nameを設定します。
  • fieldの引数は、@InputArgumentもしくはDataFetchingEnvironmentを使用して取得します。
  • subscriptionのreturn値はreactive-streamsPublisherを返します。

DataProviderIBookProcessorは独自に作成したクラスです。以下の処理を行います。

  • 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)にブラウザでアクセスします。

  • Query: 本の一覧を取得する。
    list.png

  • Query: 本のIDを指定して、特定の本を取得する。
    byId.png

  • Mutation: 新しい本を登録する。
    mutation.png

  • Subscription: 新たに登録された本を通知する。
    subscription.png

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_REQUEST

Subscriptionの場合は、/subscriptionsにリクエストする必要があるようです。
GraphiQLのリクエスト先を変更する方法がわからなかったので、以下のようなクライアントコードを実装して試したところ、うまく動きました。

main.ts
import { 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>");
  }
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gradleプロジェクトでリリースバージョンとリリースノートを自動作成

背景

Javaプロジェクトを開発している時にいくつか課題があり、それぞれ以下のように解決しました。

上記をもう少し自動化してみようというのがこの記事の目的となります。前提として、bashコマンドを用いたスクリプトを実行するため、bashが利用できる環境であることが条件となります(macOS Big SurAmazon Linux 2では動作確認済み)。

事前準備

grenコマンドのインストール

gren-release-notesパッケージのインストールの手順に沿ってgrenコマンドが利用できるようにします。

build.gradleの設定

nebulaプラグインを利用するため、gradleプロジェクトでバージョンを自動指定を参考にbuild.gradleを設定します。

GitHub CLIのインストール

GitHub CLIは、GitHub上でリリースドラフトを作成するために利用します。以下の手順でインストールと設定をします。

  1. 利用する環境に合わせて、Installationを参考にインストールします。
  2. 先ほどと同様に、個人アクセストークンを作成します。権限はreporead:orgにチェックを入れます。
  3. 一度だけ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コマンドを利用して、前回リリースから取り込まれたプルリクエスト一覧をリリースノートへ上書き
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Java × Spring Boot で GraphQL クライアントを実装する

はじめに

現在、長い間 API 設計のデファクトとなっていた REST から、GraphQL や gRPC といった新しいものを採用する API が続々と登場しています。
GitHub API v4 が GraphQL を採用しているというのを知っている方も多いかと思います。

今後も、GraphQL のような新しい設計を採用する API が増えていくことが予想されますが、従来の REST API アプリケーションに GraphQL のデータソースとの接続を容易にできるかというのは気になる点です。
そこで、今回は GraphQL API をデータソースとして叩くような GraphQL クライアント機能をよくある JVM 環境のアプリケーションにて実装してみたいと思います。
:warning: GraphQL API サーバー自体を実装するわけではありません)

GraphQL 初心者の方にも理解しやすい様に、用語の説明はリンクまたは文章にて記載しているので、入門向けでもあります。

サーバー側の構築

サーバー側を一から実装していくのは面倒なので、ありものを使うことにします。
今回はポケモンを題材にした GraphQL API の GraphQL Pokémon を使っていきたいと思います:zap:

他の有名どころだと先述した GitHub API v4 や スターウォーズ (SWAPI GraphQL) などがあります。
GitHub API でもよかったのですが、認証が絡んできて多少脱線しそうなため、見送っています。

GraphQL Pokémon のデプロイ

非常に簡単なので、サーバー側のデプロイ方法を先述しておきます

  • ローカル環境で構築します
    • 事前に Node.js & yarn の導入が必要です(こちらの導入手順は割愛します)
# 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 の画面キャプチャです。
右の Docs から型の要素情報を確認しつつ、欲しい query を簡単に作ることができます。
キャプチャの Query の例は、151番目までのカントー地方に生息するポケモンの図鑑番号・名前・タイプの情報を返却するものです。(英語名ですが :sweat_smile:
image.png

詳しい 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 を追加してください

image.png

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 を使うの? :thinking:

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 Docs

schama.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 executed

GraphQL Query を追加する

次に、必要なクエリ情報をまた別の定義ファイルに書いていきます。
これは、先ほどの GraphiQL で試した Query (151番目までのポケモンの情報)を使っていきましょう。

src/main/graphql/graphqlpokemon/KantoPokemons.graphql
query 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 タスクに依存して勝手に実行されるので、コンパイルすれば必ず最新の状態となります。すばらしい :clap:

はい、この3ステップ(実質自動生成なので2ステップ)で事前準備は完了です!とても簡単ですね。
Graph Query も GraphiQL で試したものを使っているので、基本的にコピペをミスらなければ確実に正しい構文で書いてあることが保証されているというのも大きなメリットかなと思います。

Repository の実装

やっとですが、Spring Boot アプリケーション側の実装となります。
GraphQL クライアント実装にフォーカスしているため、データソース取得が行えれば良いので、Spring Boot の Repository のみを実装していきます。
今回は上述のクエリの情報を返却する様な形で、カントー地方のポケモンの基本情報を取得するという機能を実装していきたいと思います。

src/main/java/com/example/graphqlclientspring/PokemonRepository.java
package 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.java
package 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番目までのポケモンの情報が取得できたと思います :tada:

# 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 やコメントをいただけると励みになります :pray:
最後までお読みいただき、ありがとうございました :bow:

GitHub リポジトリ

下記に commit していますので、ご参考にどうぞ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Java × Spring Boot × Apollo で GraphQL クライアントを実装する

はじめに

現在、長い間 API 設計のデファクトとなっていた REST から、GraphQL や gRPC といった新しいものを採用する API が続々と登場しています。
GitHub API v4 が GraphQL を採用しているというのを知っている方も多いかと思います。

今後も、GraphQL のような新しい設計を採用する API が増えていくことが予想されますが、従来の REST API アプリケーションに GraphQL のデータソースとの接続を容易にできるかというのは気になる点です。
そこで、今回は GraphQL API をデータソースとして叩くような GraphQL クライアント機能をよくある JVM 環境のアプリケーションにて実装してみたいと思います。
:warning: GraphQL API サーバー自体を実装するわけではありません)

GraphQL 初心者の方にも理解しやすい様に、用語の説明はリンクまたは文章にて記載しているので、入門向けでもあります。

サーバー側の構築

サーバー側を一から実装していくのは面倒なので、ありものを使うことにします。
今回はポケモンを題材にした GraphQL API の GraphQL Pokémon を使っていきたいと思います:zap:

他の有名どころだと先述した GitHub API v4 や スターウォーズ (SWAPI GraphQL) などがあります。
GitHub API でもよかったのですが、認証が絡んできて多少脱線しそうなため、見送っています。

GraphQL Pokémon のデプロイ

非常に簡単なので、サーバー側のデプロイ方法を先述しておきます

  • ローカル環境で構築します
    • 事前に Node.js & yarn の導入が必要です(こちらの導入手順は割愛します)
# 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 の画面キャプチャです。
右の Docs から型の要素情報を確認しつつ、欲しい query を簡単に作ることができます。
キャプチャの Query の例は、151番目までのカントー地方に生息するポケモンの図鑑番号・名前・タイプの情報を返却するものです。(英語名ですが :sweat_smile:
image.png

詳しい 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 を追加してください

image.png

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 を使うの? :thinking:

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 Docs

schama.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 executed

GraphQL Query を追加する

次に、必要なクエリ情報をまた別の定義ファイルに書いていきます。
これは、先ほどの GraphiQL で試した Query (151番目までのポケモンの情報)を使っていきましょう。

src/main/graphql/graphqlpokemon/KantoPokemons.graphql
query 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 タスクに依存して勝手に実行されるので、コンパイルすれば必ず最新の状態となります。すばらしい :clap:

はい、この3ステップ(実質自動生成なので2ステップ)で事前準備は完了です!とても簡単ですね。
Graph Query も GraphiQL で試したものを使っているので、基本的にコピペをミスらなければ確実に正しい構文で書いてあることが保証されているというのも大きなメリットかなと思います。

Repository の実装

やっとですが、Spring Boot アプリケーション側の実装となります。
GraphQL クライアント実装にフォーカスしているため、データソース取得が行えれば良いので、Spring Boot の Repository のみを実装していきます。
今回は上述のクエリの情報を返却する様な形で、カントー地方のポケモンの基本情報を取得するという機能を実装していきたいと思います。

src/main/java/com/example/graphqlclientspring/PokemonRepository.java
package 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.java
package 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番目までのポケモンの情報が取得できたと思います :tada:

# 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 やコメントをいただけると励みになります :pray:
最後までお読みいただき、ありがとうございました :bow:

GitHub リポジトリ

下記に commit していますので、ご参考にどうぞ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(Android) DataBindingを使ってテキストを動的に変更する

はじめに

前回は DataBindingを使って静的テキストを表示させてみるを紹介しました。
今回はこれに少し手を加えて、ボタンをクリックするとテキストが切り替わるアプリを実装します。

クリック後、データを変更するためのインターフェースを定義

SampleEventHandlers.java
public 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.java
public 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.java
public 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.java
public 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
ありがとうございました

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 = false

AWS CodeBuildとの連携

nebulaプラグインはgitの履歴から最新のタグを探すため、必ず全てのgitの履歴が必要となります。もしAWS CodeBuildを利用している場合は、ソースの設定の「Gitのクローンの深さ」でFullを指定することで正しくバージョンを取得することができます。
depth.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.java
Hello World

実行環境テストはクリア!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaの環境構築

環境構築に必要なもの

Java、VisualStudioCode、Javaの拡張機能の3つをインストール

❶Javaのインストール

//ターミナルを開く
$ javac -version  //JDKがインストールされているか確認

バージョンが出てこなかったらJavaインストールページへ

一覧から選択後、インストール

image.png

バージョンのアップデート

//バージョンが古い
$ 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のインストール

VisualStudioCodeインストールページ
image.png

インストール後

左のバーを(アクティビティバー)
四角のアイコンをExtensions
ここで、拡張機能をインストールできる

VisualStudioCodeを開く
表示が英語になっているので、日本語に変更
「Japanese Language Pack for Visual Studio Code」と検索
「Japanese」と入力すると、候補が出てくる
インストールをクリック
インストールが終わったらVisualStudioCodeを再起動
表示が日本語になる

❸Javaの拡張機能のインストール

VisualStudioCodeのextensionsで
「Java Extension Packvscjava.vscode-java-pack」を検索して、インストール

環境構築が完了したので次回は実行環境テスト!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaについて触れてみる

Javaとは

C++やC言語を元として開発されたプログラミング言語(コンパイラ言語)
Googleが開発に用いている“Google三大言語(Java、C++、Python)”の1つ

開発できるもの

・Webアプリ・Webサービス
・デスクトップアプリ
・スマホアプリ
・組み込み系
・IoT...

RubyやPHPは初心者向けで比較的簡単に扱え、小規模向けでもあるが
Javaの場合は少々複雑だが、非常に幅広い分野のプログラムを作ることができる

Javaのメリット

・実行環境を選ばない
・実行速度が速く安定性がある
・セキュリティ性が高い
・拡張性・再利用性がある
・メモリー自動管理機能

Javaのデメリット

・オブジェクト指向に慣れる必要がある
・小規模なプログラムやシステムの場合、逆に時間がかかる

カッコいいスローガン

Write once, run anywhere
(一度プログラムを書いてしまえば、どのコンピューターでも動く)

次回は環境構築を行います!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

デメテルの法則

デメテルの法則に沿わない、ダメな例

// 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();
  }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Java】Mapのキーと値のペアを拡張for文を使って取得する

はじめに

Map のキーと値のペアをMap.entrySetを使って取得する方法です。
Map のキーと値を取得する方法は他にもkeySetvaluesがあります。
これらはキー・値のどちらかを取得するだけですが、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は日本語で猫です。

参照

Map Java Platform SE8 #entrySet

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 を採用することにおいて不安はない。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerを用いて掲示板を作った1

これは何

DockerとWebアプリ作成の勉強のため,この本の第3章をDockerコンテナの上で実装してみた際のメモ.

ファイル構成

$ tree .
.
├── Dockerfile
└── testbbs
    └── WEB-INF
        ├── classes
        ├── src
        │   ├── Message.java
        │   ├── PostBBS.java
        │   └── ShowBBS.java
        └── web.xml

4 directories, 5 files

Dockerfile

Dockerfile
FROM 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/*.java

Javaファイル

参考にしてる本と同じ.

Message.java
import 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.java
import 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.java
import java.io.*;
import javax.servlet.http.*;

public class ShowBBS extends HttpServlet {
    private String espaceHTML (String src) {
        return src.replace ("&", "&amp;").replace("<", "&lt;")
            .replace (">", "&gt;").replace ("\"", "&quot;")
            .replace ("'", "&#39;");
    }

    @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) + "』&nbsp; &nbsp;");
            out.println(espaceHTML(message.handle) + " さん&nbsp;&nbsp;");
            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_ishizaka

http://localhost:8080/testbbs/ShowBBS にアクセスすると,ちゃんと掲示板が表示される.

Screen Shot 2021-01-11 at 2.42.20.png

ハマったこと

最初に使ったDockerイメージはtomcat:10.0.0-jdk11-adoptopenjdk-hotspot だったが,これだとコンパイルが通らなかった.
どうもサーブレットのためのパッケージが見つからないらしい.
クラスパスを間違えたかなと思い色々変えてみたがコンパイルエラーは直らず,tomcatの問題かと思いドキュメントを見にいくと,tomcat-10からどうもパッケージ名が変わったらしい.
tomcat-8 では,javax.servlet.httpだったのが,tomcat-10では,jakarta.servlet.httpとなっており,そりゃパッケージ見つからんってエラー吐かれるよなと.
と言うわけで,本と同じtomcat-8のDockerイメージを用いることで無事解決.

参考文献

前橋和弥「Webサーバを作りながら学ぶ基礎からのWebアプリケーション開発入門」技術評論社 (2016)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む