20201129のJavaに関する記事は30件です。

Java abstractキーワード

1,abstractとは

abstractクラスは、インスタンス化することができませんが、サブクラス(子クラス)を持つことができます。
abstractメソッドは、実装しない宣言です、

2,ソースコード

Main.java
public class Main {

    public static void main(String[] args) {

        // abstract = abstract classes cannot be instantiated, but they can have a subclass
        //            abstract methods are declared without an implementation
        Car car = new Car();
        car.go();
    }
}

スーパークラス(親クラス)には、abstractがついているのでインスタンス化できません?

Vehicle.java
public abstract class Vehicle {

    abstract void go();
}

親クラスに、abstractメソッドがあるので、必ずオーバーライドする必要があります?

Car.java
public class Car extends Vehicle {

    @Override
    void go() {
        System.out.println("The driver is driving the car.");
    }
}

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

Java superキーワード

1,superキーワードとは

スーパークラス(親クラス)のオブジェクトを参照するキーワードです。

2,ソースコード

Main.java
public class Main {

    public static void main(String[] args) {

        // super = keyword refers to the superclass (parent) of an object
        //         very similar to the "this" keyword

        Hero hero1 = new Hero("Batman", 42, "$$$");
        Hero hero2 = new Hero("Superman", 43, "everything");

        System.out.println(hero1.name);
        System.out.println(hero1.age);
        System.out.println(hero1.power);

        System.out.println(hero2.toString());

    }

}

Personクラス(スーパークラス)?

Person.java
public class Person {

    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String toString() {
        return this.name + "\n" + this.age + "\n";
    }

}

Personクラスを継承しているHeroクラス?

Hero.java
public class Hero extends Person {

    String power;

    public Hero(String name, int age, String power) {
        super(name, age);

        this.power = power;
    }

    public String toString() {
        return super.toString() + this.power;
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Groovyの最新動向について紹介してみる

これは、NTTテクノクロス Advent Calendar 2020の初日の記事です。

はじめに

こんにちは。NTTテクノクロスの中野です。

結構前になってしまいますが、会社ブログでGroovy/Grailsの記事を書いてました。ちなみに、この社外ブログでの最新の執筆記事は、2020/10/02に公開したNTT Performance Tuning Challenge 2020 で優勝してきたです(ドヤァ)。このコンテストでもGo言語版を選択したように、最近は業務でもGo言語を使うことが多いのですが、もちろん今もGroovy/Grailsが大好きです。

というわけで、今日は久しぶりにGroovyの情報を紹介していきたいと思います。

Groovy 3.0 がリリースされました!

もう少しで1年経ちそうなくらいなので「何をいまさら」な感じですが、今年の2020/02/10にGroovy 3.0がリリースされています。主なポイントを以下に紹介します。

Javaとのシンタックス互換性の飛躍的向上

Daniel Sun氏の活躍で実装されたParrotパーサーによって、Javaとのシンタックス互換性が一気に高まりました。

元々Groovy 2.x系でもJava 7以前との互換性はかなり高く、Java 7のソースコードをほぼ修正なしでGroovyのソースコードとして扱えたのですが、Java 8以降に採用されたシンタックスへの追随がかなりビハインド状態で、「Javaシンタックスとの親和性」について声高にアピールしづらいもやもや期間が長く続いていました。この状況が、Groovy 3.0のParrotパーサーによって、ついに解消されたわけです。

新しくサポートされたシンタックスをいくつか紹介すると...

  • Groovyのクロージャを書く時に、Javaのラムダ記法がつかるようになった
  • for文の初期化式としてカンマ区切りで複数の変数の初期化が書けるようになった
    • (例): for(int i = 0, j = 10; i < j; i++, j--) {..}
  • Javaと同じ配列の初期化記法が使えるようになった
    • (例): new int[]{1, 2, 3}
  • do-while文が使えるようになった
  • try-with-resourcesが使えるようになった
  • スコープを分けるブロックとして機能するだけの波括弧が使えるようになった
    • 以前は、クロージャ記法なのかブロックなのかが曖昧になるため、エラーとしていた
  • メソッド参照が使えるようになった
  • などなど
  • ※ 動作可能なサンプルコードがみたい方はこちらのGistを参照のこと

これで「GroovyはJavaシンタックスとの親和性が高い」と胸を張っていえるようになりましたね。

とはいえ、JavaはJavaでどんどん改善が進んでいて新しいシンタックスが増えていっているので、いたちごっこはまだまだ続くわけですが、新しいParrotパーサーではそういった新しいシンタックスへの追随も柔軟に対応できるようなので、それほど悲観的になる必要はないでしょう。

なお、Groovy 3.0には従来のレガシーパーサーも同梱されているので、以前の挙動を望む場合には自分で切り替えることもできます(Groovy 4.0で削除予定)。

JPMS(Jigsaw)対応

JPMS(Jigsaw)では、モジュールごとに個別のパッケージ名を付ける必要があります。元々Groovyには独自の「モジュール」がありましたが、これはJPMSが要求するようなパッケージ分離性を満たしていませんでした。JPMS要件を満たすためには、多数のクラスを元とは異なるパッケージに移動する必要がありました。しかし、いきなり「移動」してしまうと、既存のGroovyソースコードがGroovy 3.0でコンパイル/実行できなくなってしまいます。そこで、Groovy 3.0では移行期間として、ひとまず「移動」ではなく「コピー」することになりました。

  • 実際にコピーされたクラスの例
    • groovy.util.XmlParsergroovy.xml.XmlParser
    • groovy.util.XmlSlurpergroovy.xml.XmlSlurper
    • groovy.util.GroovyTestCasegroovy.test.GroovyTestCase

Groovy 3.0では、groovy.util.XmlParserなどの古いパッケージ側のクラスも残されていますが、これはGroovy 4.0で削除される予定です。今から実装する場合は、新しいパッケージ側のクラスを使うようにしましょう。

というわけで、実際にJPMS準拠となるのはGroovy 4.0からの予定です。Groovy 3.0の段階ではまだJPMS準拠のモジュールとしては使えません。従来通り、クラスパスに配置する必要があります。

Groovy 4.0 はどうなる?

今のところリリース時期は未定のようですが、次のメジャーバージョンであるGroovy 4.0の開発が着々と進められています。

ちなみにアルファ版(4.0.0-alpha-1)はすでに公開されていて、SDKMAN!を使っていれば、以下のコマンドで簡単にインストールして試すことができます。

> sdk install groovy 4.0.0-alpha-1

さて、Groovy 4.0でのおもな変更点をピックアップして紹介していきましょう。

JPMS(Jigsaw)対応

前述したとおり、Groovy 3.0で仕込んでおいたJPMS対応の大詰めです。互換性のためにGroovy 3.0で残してあった旧パッケージ側のクラス(または旧パッケージそのもの)がすべて削除されます。古いパッケージのクラスを使っているソースコードは、新しいパッケージの方を使うように修正する必要があります。これによって、JPMS準拠のモジュールとしてGroovyを利用できるようになります(のはず)。

レガシーパーサーの削除

Parrotパーサーのみが同梱されて、以前のレガシーパーサーは削除されます。

Maven/GradleのグループID変更

かなり前になりますが、GroovyはCodehaus1というホスティングサービスを使ってホスティングしていた時期があります。その時期に付けられたorg.codehaus.groovyというMaven/GradleのグループIDがいまだにそのまま使われ続けていたのですが、ついにというかやっとというか、org.apache.groovyに変更されます。ビルドスクリプトなどの依存関係定義で、Groovyのバージョンを4に変更するときには、忘れずにグループIDも修正しましょう。

新機能: 組み込みの型チェッカー

従来からGroovyの静的型付けチェックには、選択的に特定の型チェックを弱めて動的なコードを通しやすくしたり、逆にむしろJavaよりも厳しくチェックしたりする機能があったようです(そうだっけ?)。一部では使われていたもののあまり広まってなかったので、普及のために使えそうなものを組み込みで用意してみたよ、ということのようです。

たとえば、以下のコードには問題があるのですが、どこかわかりますか?

def whenIs2020Over() {
    def newYearsEve = '2020-12-31'
    def matcher = newYearsEve =~ /(\d{4})-(\d{1,2})-(\d{1,2}/  // --[1]
    //...
}

そうですね。[1]の部分の正規表現の末尾で閉じ丸括弧が漏れているため、実行するとPatternSyntaxExceptionがスローされてしまいます。従来であれば、これは実行時エラーとしてしか検出できませんが、Groovy 4.0からは以下のようにアノテーションを付与すると...

import groovy.transform.TypeChecked

@TypeChecked(extensions = 'groovy.typecheckers.RegexChecker')
def whenIs2020Over() {
    def newYearsEve = '2020-12-31'
    def matcher = newYearsEve =~ /(\d{4})-(\d{1,2})-(\d{1,2}/
    //...
}

コンパイル時に検出できるようになります。

1 compilation error:
[Static type checking] - Bad regex: Unclosed group near index 26
(\d{4})-(\d{1,2})-(\d{1,2}
 at line: 6, column: 19

これは、オプションのgroovy-typecheckersモジュールの機能として提供されます。

新機能: 組み込みのマクロメソッド

Groovy 2.5でGroovy Macroという機能が導入されました。C/C++のマクロのように、グローバル関数のようなものをソースコードに書いておくと、AST変換によって実コードが展開されてコンパイルされる、という仕組みなのですが、こっちも普及のためにいくつか使えそうなものを標準で用意してみることにしたよ、という話のようです。

その一つとして、昔ながらのプリントデバッグを支援するマクロが用意されています。たとえば、次のようにいくつかの変数を定義したとします。

def num = 42
def list = [1, 2, 3]
def range = 0..5
def string = 'foo'

ここで、デバッグ目的でこれらをプリントしたいとします。

println "num=$num, list=$list, range=$range, string=$string"

と頑張って書けば、

num=42, list=[1, 2, 3], range=[0, 1, 2, 3, 4, 5], string=foo

と出力できますが、ちょっとタイピング量が多すぎる感じがありますね。

新しく追加されたNVマクロメソッドを使ってみます。

println NV(num, list, range, string)

これを実行すると、次のように出力されます。

num=42, list=[1, 2, 3], range=[0, 1, 2, 3, 4, 5], string=foo

変数名だけ列挙していけば良いので、たしかにちょっと手軽になった感じがします。

同じ系統で、NVINVDという2つのバリエーションがあります。それぞれの違いは、値の文字列化の方式です。

  • NV: 値の文字列化にtoString()を使う
  • NVI: 値の文字列化にGroovy JDKのinspect()を使う
  • NVD: 値の文字列化にGroovy JDKのdump()を使う

さきほどのrange変数を出力して違いをみてみると、こんな感じです。

println NV(range)
//=> range=[0, 1, 2, 3, 4, 5]

println NVI(range)
//=> range=0..5

println NVD(range)
//=> range=<groovy.lang.IntRange@14 from=0 to=5 reverse=false inclusive=true modCount=0>

これは、オプションのgroovy-macro-libraryモジュールの機能として提供されます。

新機能: JavaShell (incubating)

Groovyのソースコードを動的にコンパイルして実行するGroovyShellというクラスが元々ありますが、これのJava版として、Javaのソースコードを動的にコンパイルして実行するJavaShellが追加されます。たとえば、JDK 14で追加されたrecord機能を使った文字出力内容を検証するためのGroovyコードは次のようにかけます。

import org.apache.groovy.util.JavaShell

def opts = ['--enable-preview', '--release', '14']
def src = 'record Coord(int x, int y) {}'
Class coordClass = new JavaShell().compile('Coord', opts, src)
assert coordClass.newInstance(5, 10).toString() == 'Coord[x=5, y=10]'

また、標準バンドルされているGroovy Consoleで、エディタ上のソースコードをJavaとして実行するための「Run as Java」メニューが追加されます。

新機能: POJO Annotation (incubating)

これは相当に野心的・実験的な機能です。

基本的に、GroovyでPOJOのようなクラスを書いたとしても、コンパイルしたクラスの実行時にはGroovyのJARファイルが必要になります。Groovyのクラスが持つ動的性質を実現するために、専用の下回りの機構が必要となるためです。これに対して、クラスに新しい@POJOアノテーション(と既存の@CompileStatic)をつけることで、そのクラスの動的性質に関わるすべてをクラスファイルに組み込んでしまい、実行時にGroovyのJARファイルがなくてもピュアJavaの世界でそのまま実行できるようにしてしまおう、というのがこの新機能になります。

もし、これが実用化できると、Groovyをプリプロセッサとして利用できる可能性がでてきます。Lombokに似ていますが、Groovyの言語パワーを活かせるため、より高度な応用が期待できそうです。

ただし、徐々に改善していけるという見込みはあるらしいものの、現状でGroovyのすべての動的性質をサポートできているわけではないので、まだまだincubating状態が続きそうな気配です。

新機能: recordライクなクラス (incubating)

Java 14で、プレビュー機能としてrecordが導入されました。これを受けて、Groovyでもrecordライクなクラスを書くためのサポートが追加されます。

元々Groovyでは、Groovy Beansとして、Java Beansを強力に拡張する機能が用意されています。たとえば、こんな感じです。

class Book {
    String title
    String author
}

// Mapを受け取るコンストラクタが自動生成されている
def book = new Book(title: "プログラミングGROOVY", author: "..., 中野")

// getterが自動生成されている
assert book.getTitle() == "プログラミングGROOVY"

// setterが自動生成されている
book.setTitle("JavaからGroovyへ")
assert book.getTitle() == "JavaからGroovyへ"

// インスタンス変数への直接参照のようにみえるがgetterを経由している
assert book.title == "プログラミングGROOVY"

// インスタンス変数への直接代入のようにみえるがsetterを経由している
book.title = "JavaからGroovyへ"
assert book.title == "JavaからGroovyへ"

また、Groovyには複数のAST変換アノテーションを集約して1つのアノテーションとして再利用可能にするメタアノテーションという仕組みがあります。たとえば、クラスの不変性を実現するための@Immutableアノテーションを1つ指定するだけで、次に列挙するサブアノテーションが持っている各機能がAST変換によって付与されます。

// @Immutableとしてのベース
@ImmutableBase

// クラスを不変(immutable)にするための各種サブアノテーション
@ImmutableOptions
@PropertyOptions(propertyHandler = ImmutablePropertyHandler)
@KnownImmutable

// toString()を自動生成する
@ToString(cache = true, includeSuperProperties = true)

// equals()とhashCode()を自動生成する
@EqualsAndHashCode(cache = true)

// インスタンス変数の定義順で受け取るコンストラクタを自動生成する
@TupleConstructor(defaults = false)

// Mapを受け取るコンストラクタを自動生成する
@MapConstructor

さて、Java 14のrecordクラスは、このGroovyの@Immutableを付与したクラスとある程度類似しているのですが、完全には一致していません。とはいえ、Groovyのメタアノテーションを使えば、recordとの差分を調整した新しい@RecordTypeアノテーションを追加するのは比較的簡単です。これには、以下のようなサブアノテーションが集約されます。

// @RecordTypeとしてのベース
@RecordBase

// ピュアJavaな世界で実行可能なクラスファイルを生成する(前項で説明)
@POJO

// クラスを不変(immutable)にするための各種サブアノテーション(@Immutableと共通)
@ImmutableOptions
@PropertyOptions(propertyHandler = ImmutablePropertyHandler)
@KnownImmutable

// toString()を自動生成する(@Immutableと共通)
@ToString(cache = true, includeNames = true)

// equals()とhashCode()を自動生成する(@Immutableと共通)
@EqualsAndHashCode(cache = true, useCanEqual = false)

// インスタンス変数の定義順で受け取るコンストラクタを自動生成する(@Immutableと共通)
@TupleConstructor(defaults = false)

// Mapを受け取るコンストラクタを自動生成する(@Immutableと共通)
@MapConstructor

これを使った例を見てみましょう。

import groovy.test.GroovyAssert

@groovy.transform.RecordType
class Book {
    String title
    String author
}

// インスタンス変数の定義順で受け取るコンストラクタが自動生成されている
def book = new Book("プログラミングGROOVY", "..., 中野")

// インスタンス変数名と同名のgetterが自動生成されている
assert book.title() == "プログラミングGROOVY"

// Groovy Beansとしてのgetter/setterは生成されない
// (最後まで一通り実行できるように、期待する例外がスローされたかどうかをチェック
//  するGroovyのアサーション機能を使っている)
GroovyAssert.shouldFail(MissingMethodException) {
    book.getTitle()
    //=> Caught: groovy.lang.MissingMethodException: No signature of method: Book.getTitle() ...
}
GroovyAssert.shouldFail(MissingMethodException) {
    book.setTitle("JavaからGroovyへ")
    //=> Caught: groovy.lang.MissingMethodException: No signature of method: Book.setTitle()
}

// インスタンス変数への代入は禁止されている
// (Groovy BeansとしてのsetTitle()は生成されていないため、これはインスタンス変数への直接代入を意味する)
GroovyAssert.shouldFail(ReadOnlyPropertyException) {
    book.title = "JavaからGroovyへ"
    //=> Caught: groovy.lang.ReadOnlyPropertyException: Cannot set readonly property: title for class: Book
}

// toString()が自動生成されている
assert book.toString() == "Book(title:プログラミングGROOVY, author:..., 中野)"

// equals()が自動生成されている
assert book == new Book("プログラミングGROOVY", "..., 中野")
assert book != new Book("プログラミングGROOVY", "..., なかの")

将来のリリースでは、Javaのrecordと同等の記述ができるように、次のようなシンタックシュガーを提供する可能性があります。

// 注意: この構文はGroovy 4.0ではサポートされない
record Book(String title, String author){}

ひとまず当面は、@RecordTypeを提供してみてユーザからのフィードバックを求める、という方針のようです。

新機能: Groovy Contracts (incubating)

契約による設計(Design by Contract)のために、クラス不変条件/前提条件/事後条件の指定をサポートするアノテーションを提供します2。付与されたアノテーションに従って、AST変換によって必要に応じてコンストラクタ/メソッドにチェックロジックが挿入されて、以下の3点を確認します。

  • メソッドが実行される前に前提条件が満たされていること
  • メソッドが実行された後に事後条件が保持されていること
  • メソッドが呼び出される前後にクラス不変条件が真であること

たとえば、ロケットを表現するクラスで試してみるとこうなります。

import groovy.contracts.Ensures
import groovy.contracts.Invariant
import groovy.contracts.Requires
import groovy.test.GroovyAssert
import org.apache.groovy.contracts.ClassInvariantViolation
import org.apache.groovy.contracts.PostconditionViolation
import org.apache.groovy.contracts.PreconditionViolation

@Invariant({ speed() >= 0 })         // --[1]
class Rocket {
    boolean started = false
    int speed = 0

    @Requires({ isStarted() })       // --[2]
    @Ensures({ old.speed < speed })  // --[3]
    def accelerate(inc) { speed += inc }

    def isStarted() { started }

    def speed() { speed }
}

// 初期速度が負の場合、[1]のクラス不変条件違反が検出されて例外がスローされる
// (最後まで一通り実行できるように、期待する例外がスローされたかどうかをチェック
//  するGroovyのアサーション機能を使っている)
GroovyAssert.shouldFail(ClassInvariantViolation) {
    new Rocket(speed: -1)
    //=>
    // Caught: org.apache.groovy.contracts.ClassInvariantViolation: <groovy.contracts.Invariant> Rocket
    //
    // speed() >= 0
    // |       |
    // -1      false
}

def r = new Rocket()
assert !r.started
assert r.speed == 0

// start=falseの場合、[2]によって事前条件違反が検出されて例外がスローされる
GroovyAssert.shouldFail(PreconditionViolation) {
    r.accelerate(5)
    //=>
    // Caught: org.apache.groovy.contracts.PreconditionViolation: <groovy.contracts.Requires> Rocket.java.lang.Object accelerate(java.lang.Object)
    //
    // isStarted()
    // |
    // false
}

// 事前条件違反なので、内部状態自体は更新されていない
assert r.speed == 0

// start=trueの場合、[2]に違反しないため、速度を加速できる
r.started = true
r.accelerate(5)
assert r.speed == 5

// 減速(負の加速)は、[3]によって事後条件違反が検出されて例外がスローされる
GroovyAssert.shouldFail(PostconditionViolation) {
    r.accelerate(-1)
    //=>
    // Caught: org.apache.groovy.contracts.PostconditionViolation: <groovy.contracts.Ensures> Rocket.java.lang.Object accelerate(java.lang.Object)
    //
    // old.speed < speed
    // |   |     | |
    // |   5     | 4
    // |         false
    // ['speed':5]
}

// 事後条件違反なので、内部状態自体は更新済み
assert r.speed == 4

なかなか使い勝手が良さそうに見えますね。

新機能: GINQ (under investigation)

Parrotパーサーの立役者であるDaniel Sun氏が今まさに精力的に開発中の、.NETの統合言語クエリ(LINQ: Language-Integrated Query)という機能のGroovy版となるGINQです。

統合クエリ言語といってもSQLやDBとは直接は関係ありません。ざっくりいうと、様々なコレクション的な入力3に対する操作を専用のシンタックスで記述できるというものです。Groovyでは元々each, map, findAllなどのコレクション操作がかなり充実しているので「既存のそれと何が違うの?」というのは自然な疑問だと思います。かく言う自分もまだあまりよくわかっていないのですが、GINQユーザガイド(2020/11/28版)などをみると、既存のGroovyのコレクション操作がLINQ的に言う「メソッド構文」であるのだとしたら、今回のGINQはLINQ的に言う「クエリ構文」を実現したものなのかな、という感じがします。

ここでいう、クエリ構文というのはSQLに似たLINQ固有のDSLです。ちなみに、筆者はまだLINQよくわかってない勢なので、GINQのシンタックスが本家や他のLINQ実装のシンタックスと共通なのか、なにか違いがあるのか、などは不明です。

さて、GINQを利用したごく簡単なサンプルコードをいくつか紹介するとこんな感じです。

assert [2, 4, 6] == GQ {
    from n in [1, 2, 3]
    select n * 2
}.toList()

// Groovyの既存コレクション操作で書くと...
assert [2, 4, 6] == [1, 2, 3].collect { it * 2 }
assert [[1, 1], [2, 4], [3, 9]] == GQ {
    from v in (
        from n in [1, 2, 3]
        select n, Math.pow(n, 2) as powerOFN
    )
    select v.n, v.powerOFN
}.toList()

// Groovyの既存コレクション操作で書くと...
assert [[1, 1], [2, 4], [3, 9]] == [1, 2, 3].collect {
     [it, Math.pow(it, 2) as int]
}

どうなんでしょう。上の例だと、既存のコレクション操作の方が圧倒的に簡潔でわかりやすいですが、もっと複雑なことをしようとしたときに何かメリットがでてくるのでしょうか。

入力の多様性という観点からすれば、入力となるデータソースが、コレクション(java.lang.Iterable)、ストリーム(java.util.stream.Stream)、配列、JSON、RDBMS、先行するGINQの出力結果のいずれであっても、変わらずに同じシンタックスでクエリがかける、というのはデータサイエンティスト的にはうれしい場面がありそうな気もします。

というわけで、まだ全然わかっていないので、今後も注目していきたいと思います。

ちなみに、GINQは、現在公開されているGroovy 4.0のアルファ版(4.0.0-alpha-1)には含まれていないようで、上記のサンプルは実行できませんでした。GINQユーザガイドも高頻度で更新されつつある状況なので、手元で試せるのはもうちょっと先になりそうな雰囲気です。

なお、すでにPHPにもGinqというLINQクローンがあって大変紛らわしいのですが、GrailsによるORマッパーの「GORM」に対して、Go言語によるORマッパーの「gorm」が後発で登場するなど、昨今は名前カブりに対してあまり気にしない世の中なんでしょうかね。ググラビリティが低くなるので、ユーザとしては困り物ですが。

おわりに

さて、だいぶ長くなってしまいましたが、Groovyの最新動向はいかがでしたでしょうか。

明日は、j-yamaによるElastic Stackに関する記事です。引き続き、NTTテクノクロス Advent Calendar 2020をお楽しみください。

参考URL


  1. Codehausはすでにサービス終了しています。 

  2. かなり昔にあったGContractsの標準版という感じです。なお、GContractsはだいぶ前からメンテナンスが停止していて、アーカイブ化されています。 

  3. コレクション(java.lang.Iterable)、ストリーム(java.util.stream.Stream)、配列、JSON、RDBMS、先行するGINQの出力結果、などが入力のデータソースとして使用できます。 

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

Java 継承(inheritance)

1,継承(inheritance)とは

あるクラスが他のクラスから属性(attributes)メソッド(methods)を取得するプロセスです

2,ソースコード

Main.java
public class Main {

    public static void main(String[] args) {

        // inheritance = the process where one class acquires,
        //               the attrivutes and methods of another.

        Car car = new Car();
        car.go();

        Bicycle bike = new Bicycle();
        bike.stop();

        System.out.println(car.speed);
        System.out.println(bike.speed);

        System.out.println(car.doors);
        System.out.println(bike.pedals);

    }

}

継承元?

Vehicle.java
public class Vehicle {

    double speed;

    void go() {
        System.out.println("This vehicle is moming");
    }

    void stop() {
        System.out.println("This vehicle is stopped");
    }

}

クラスからクラスへ継承する場合、”子クラス extends 親クラス” と表記します?

Car.java
public class Car extends Vehicle {

    int wheels = 4;
    int doors = 4;

}
Bicycle.java
public class Bicycle extends Vehicle {

    int wheels = 2;
    int pedals = 2;

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

Java static修飾子

1,static修飾子とは

クラスのフィールド変数やメソッドをそのクラス内で共有します。
staticがついているものはそのクラスが所有することになります。

2,ソースコード

Main.java
public class Main {

    public static void main(String[] args) {

        // static = modifier. A single copy of a variabole/method is created and shared.
        // the class "owns" the static member

        Friend friend1 = new Friend("hiro");
        Friend friend2 = new Friend("tato");
        Friend friend3 = new Friend("yoshi");
        Friend friend4 = new Friend("taka");

        System.out.println(Friend.numberOfFriends);

        Friend.displayFriends();

    }

}

static がついているフィールド変数やメソッドはそのクラスで共有します。

Friend.java
public class Friend {

    String name;
    static int numberOfFriends;

    public Friend(String name) {
        this.name = name;
        numberOfFriends++;
    }

    static void displayFriends() {
        System.out.println("You have " + numberOfFriends + " friends");
    }

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

Transformation Advisor Localをインストールした時の手順メモ

1. はじめに

TomcatのJavaアプリ(VM環境)をTransformation Advisorを使ってモダナイズする 」という投稿で、利用する Transformation Advisor Localのインストールをこの投稿で記述します。

このツールの使用例については、上記ページも参照ください。

2. Transformation Advisor Localとは

ta-local.png

まず、Transformation Advisorはオンプレミス環境で動作しているJavaのミドルウェア・アプリケーションを分析し、生成されたマイグレーションバンドルを使って、 OpenShift の Libertyコンテナ上にJavaアプリをデプロイすることができるJavaアプリケーションのモダナイズするための移行支援ツールです。 (IBMの製品です。)

そして、Transformation AdvisorはOpenShift上にインストールしますが、
今回インストールするTransformation Advisor Localは、Windows・Linux・MacOSと、ローカルPCでもインストールして、Transformation Advisorを利用することができるツールです。

詳細はこちら:IBM Cloud Transformation Advisor

仕組みとしては、ローカルPCに、DockerとDocker Composeをインストールして、Transformation Advisorのコンテナイメージを起動して、Transformation Advisorを利用します。

ローカルPCにインストールすることができるで気軽に試すことができますが、ローカルPCの性能によっては、Dockerを動かす時点で、もっさりと遅いという事が起こるかもしれません。

3. Transformation Advisor Localのインストール

今回は、Transformation Advisor Localの90日評価版をLinux ( CentOS7 )の環境にインストールする手順を確認しました。

3.1 前提

  • 導入環境:
    • VMWare ESXi 6.7 の仮想マシン( スペックは適当に設定CPU4コア, メモリ8GB, HDD 32GB)
    • OS: CentOS Linux release 7.8.2003 (Core)の Minimam Install
  • インストール手順書: Installing IBM Cloud Transformation Advisor locally

3.2 手順

DockerとDocker Composeのインストール

  • 作業環境: CentOS7.8

Dockerの導入手順 https://docs.docker.com/get-docker 
に沿って下記の手順でDockerをインストールします。

$ sudo yum install  yum-utils

# docker のyum リポジトリ追加
$ sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo

# Dockerのインストール
$sudo yum install docker-ce docker-ce-cli containerd.io

#Dockerの起動
$sudo systemctl start docker

# Dockerの動作確認
$ sudo docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
0e03bdcc26d7: Pull complete
Digest: sha256:e7c70bb24b462baa86c102610182e3efcb12a04854e8c582838d92970a09f323
Status: Downloaded newer image for hello-world:latest

Docker Composeのインストール

  • 作業環境: CentOS7.8

Docker Composeの導入手順 https://docs.docker.com/compose/install/
に沿って下記の手順でDockerをインストールします。

#docker-composeコマンドのダウンロード&設置
$ sudo curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

#docker-composeコマンドに実行権限追加
$ sudo chmod +x /usr/local/bin/docker-compose
$ ls -l /usr/local/bin/docker-compose
-rwxr-xr-x. 1 root root 12218968 Nov 29 04:34 /usr/local/bin/docker-compose

Transformation Advisor Localのダウンロード

  • 作業環境: ローカルPC

以下のURLよりTransformation Advisor Localの90日評価版をダウンロードします。(IBMアカウントが必要になります。)
https://www.ibm.com/account/reg/signup?formid=urx-38642

image.png

ダウンロードページにて、「Transformation Advisor Localのインストールスクリプト」をダウンロードします。
image.png

Transformation Advisor Local の CentOS7.8へのインストール

  • 作業環境: CentOS7.8

事前にローカルPCから CentOS7.8の環境に、先ほどダウンロードしたTransformation Advisor Localのバイナリ「 transformationAdvisor.zip」をアップロードしてください。

そして、以下の手順でインストールします。

#必要なツールのインストール(unzipとnetstatを導入)
$ yum install -y unzip net-tools

# Transformation Advisor Localの解凍
$ unzip transformationAdvisor.zip 
$ ls
launchTransformationAdvisor.sh  LICENSE  LICENSES  scripts  transformationAdvisor.zip

インストールのために「 ./launchTransformationAdvisor.sh 」を実行します。

$ sudo ./launchTransformationAdvisor.sh
[sudo] password for dai:

Docker Compose not installed. Please install docker-compose and re-run the script.
https://docs.docker.com/compose/install/

しかし、docker-composeがインストールされていないとメッセージが表示されます。これは、sudoのsecure_pathに docker-composeのインストールされているパスが登録されていないために、発生する事象です。

下記の様にvisudoを実行して、secure_pathに「 /usr/local/bin 」を追加してください。

$ sudo visudo

(変更前)88 Defaults    secure_path = /sbin:/bin:/usr/sbin:/usr/bin
(変更後)88 Defaults    secure_path = /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin

再度、「launchTransformationAdvisor.sh」を実行して、インストールプログラムを起動します。 ライセンス情報が表示され、最後に、同意するか同意しないかの選択を 同意する 1 の選択をします。

sudo ./launchTransformationAdvisor.sh
LICENSE INFORMATION

The Programs listed below are licensed under the following License Information terms and conditions in addition to the Program license
terms previously agreed to by Client and IBM. If Client does not have previously agreed to license terms in effect for the Program, the
 International License Agreement for Evaluation of Programs (Z125-5543-05) applies.

Program Name (Program Number):
IBM Cloud Transformation Advisor local 2.3.0 (Evaluation)

The following standard terms apply to Licensee's use of the Program.

' ----- 省略 ----- '

1) I have read and agreed to the license agreements
2) Don't accept the license agreements

その後、下記のようなオペレーションメニューが表示されるので、「1) Install Transformation Advisor」の 1 を入力してEnterを押してインストールを開始します。

Select the operation.......

1) Install Transformation Advisor
2) Uninstall Transformation Advisor (keep database data)
3) Uninstall Transformation Advisor (remove database data)
4) Stop Transformation Advisor
5) Start Transformation Advisor
6) Check for latest Transformation Advisor
7) Working in an Air Gapped Environment
8) Quit

その後、正常にインストールが環境すると、下記の様にTransformation AdvisorのURLが表示されます。

Installing Transformation Advisor..............
Pulling db     ... done
Pulling server ... done
Pulling ui     ... done
Creating network "scripts_default" with the default driver
Creating scripts_db_1 ... done
Creating scripts_server_1 ... done
Creating scripts_ui_1     ... done
Configuring Transformation Advisor...............................................
Status
------------------------------------------------------------------------------------------------------
Transformation Advisor 2.3.0 is available for us at the following URL> http://192.168.26.253:3000

Transformation Advisor への接続

  • 作業環境: ローカルPC

ローカルPCのブラウザにて、前手順のインストール完了時に出力されるURLに接続し、Transformation Advisorの画面が表示されることを確認します。

image.png

4. 最後に

これで Transformation Advisor を利用することができるようになりました。
今回は、 Linux環境の手順を説明しましたが、Windows・MacOSの環境でも Docker・Docker Composeが導入済みの環境ならばインストールすることができます。

Transformation Advisor の具体的な使用例は、「TomcatのJavaアプリ(VM環境)をTransformation Advisorを使ってモダナイズする 」の投稿をぜひ、ご覧ください。

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

GCP: Google Cloud Functions から Google Cloud Storage のファイルを読み込む

参考

公式ドキュメントだとStorageにファイル作成する例しかなかったので、こちらのQiita記事を参考にさせていただきました。
ありがとうございます!
Cloud Storage Client Libraryを使ってGCS上のファイルをReadする - Qiita

概要

Google Cloud Functions (Java11) から Google Cloud Storage 上のファイルを読み込む。

  • ファイルの内容
    • テキストファイル
    • 1行1データ
    • 改行コードはLF
    • 日本語含む
    • UTF-8

やったこと

Storageにバケットを作って、ファイルを置く

バケットはStorageの管理単位のイメージです。

GCPコンソール > Storage メニューから「+バケットを作成」します。
以下を設定していきます。

  • バケット名
    • バケット名は一般公開されます
    • そのため機密情報や既に使われている値は利用できません
    • 命名ガイドライン
  • データの保存場所
  • データのデフォルトのストレージ クラス
  • オブジェクトへのアクセスを制御する方法
    • 「きめ細かい管理」か「均一」のいずれかを選択します image.png

作成したバケットの詳細を開き、「ファイルをアップロード」からファイルをアップします。
公開アクセスは「非公開」になっています。
image.png

Functionsを作る

GCPコンソール > Cloud Functions メニューから「+関数を作成」します。
関数名、リージョンは適宜設定します。

検証しやすいので、トリガーは「HTTP」にしました。
認証も「未認証の呼び出しを許可」にしておきます。

構成を決めたらコードを用意します。
ランタイムは「Java11」を選択して、サンプルソースを下記のように書き換えます。

Example.java
package com.example;

import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.cloud.storage.*;
import java.io.UnsupportedEncodingException;
import java.io.BufferedWriter;

public class Example implements HttpFunction {
  @Override
  public void service(HttpRequest request, HttpResponse response) throws Exception {
    response.setContentType("Content-Type: text/plain; charset=UTF-8");
    BufferedWriter writer = response.getWriter();

    String name = "";
    if (!request.getFirstQueryParameter("name").isEmpty()) {
      name = request.getFirstQueryParameter("name").get();
    }
    if(name.isEmpty()){
      writer.write("Query parameter 'name' is empty");
      return;
    }

    String[] datas = getBlobData("XXXXXX_projectId", "XXXXXX_bucketName", name);
    for(String data : datas){
      writer.write(String.format("'%s'%n", data));
    }
  }

  public String[] getBlobData(String projectId, String bucketName, String blobName) {
    Storage storage = StorageOptions.newBuilder().setProjectId(projectId).build().getService();
    BlobId blobId = BlobId.of(bucketName, blobName);
    Blob blob = storage.get(blobId);
    byte[] content = blob.getContent();
    try {
      return new String(content, "UTF-8").split("\n", 0);
    } catch (UnsupportedEncodingException e) {
      e.printStackTrace();
    }
    return new String[0];
  }
}

pom.xmlは下記を追加します。

pom.xml
<dependency>
    <groupId>com.google.cloud</groupId>
    <artifactId>google-cloud-storage</artifactId>
    <version>1.113.4</version>
</dependency>

最後に「デプロイ」してエラーがなければOK!

挙動確認

FunctionsのトリガーURLを呼び出します。
Chromeであればそのままbodyの内容が表示されるので、Storageに置いたファイルの内容が表示されれば成功です。

name=test.txtにしたとき
image.png

nameを空にしたとき
image.png

感想

同じGCPプロジェクトの Functions から Storage なら、認証なしで参照できて便利でした。

ただ Functions で、しかもJavaでやる処理じゃない気がしています(とても遅い)。
そのうちGAEに乗せ換えるか、Javaから他の言語にする方がいいかなと感じてはいます。

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

【Java】SimpleDateFormat (日時の文字列操作)

【タイトル】SimpleDateFormat

日時フォーマットの変換をする。

【説明】

SimpleDateFormatクラスは、日付と時刻のフォーマット(書式)を扱うクラスです。

【使用場面】

  • 年や月といったデータを処理中で使用したい場合
  • ライブラリを使用する際、所定の形式へ変換する必要がある場合
  • 日付操作の表示を任意で決めたいとき

CalendarDateTime の日付や数値に代入された、「 日付・時刻形式データを、任意の日付・時刻フォーマットに変換する 」クラスとして重宝します。

【書き方】

まずはこちらをJavaファイルの上部のパッケージ宣言の後に記述してインポートします。

import java.text.SimpleDateFormat;

宣言後、下記の形式でフォーマットを指定します。(書式とパターンは後述)

SimpleDateFormat オブジェクト名 = new SimpleDateFormat(”フォーマットパターン”)

こちらは SimpleDateFormat で使用する 書式パターン をまとめたものになります。
こちらのアルファベットを用いてフォーマットを作成していきます。

書式 概要 パターン 表示
G 紀元 G AD
y year, 年 yyyy
yyy
2020
20
M Month, 月 MM
MMM
MMMM
03
Mar
March
d day, 日 d
dd
1
01
H 時(0-23) H
HH
0
00
K 午前/午後の時(0-11) K
KK
0
00
h 午前/午後の時(1-12) h
hh
1
01
m m
mm
0
00
s s
ss
0
00
S ミリ秒 S
SSS
1
001
z タイムゾーン z Pacific Standard Time, PDT, GMT-08:00
Z タイムゾーン Z -0800

java.text.SimpleDateFormatクラスのメソッド

SimpleDateFormat を使用する時に扱うメソッドの紹介です。

【説明】

SimpleDateFormat のメソッドの一例です。

  • setTimeZone (タイムゾーンを設定)
  • format (指定されたDateを日付文字列にフォーマットする)
  • parse (文字列から解析してDateオブジェクトを生成)

その中でも特に重要な formatメソッドについて少し触れます。

formatメソッド

指定されたDateを日付文字列にフォーマットするメソッド

【説明】

SimpleDateFormat を扱う上で最もメインとなる、「日時を設定したフォーマットの文字列へ変換するメソッドです。

【書き方】

SimpleDateFormatのオブジェクト名.format(日時を格納した変数名)

出力するときなどはこのような使い方です、

System.out.println(sdf.format(date));

【サンプルコード】

// ① ここで SimpleDateFormat と Dateをインポート
import java.text.SimpleDateFormat;
import java.util.Date;

public class MainSimpleDateFormat {

    public static void main(String[] args) {

        // ② 現在時刻を取得してくる値を 変数 date に格納
        Date date = new Date();
        // SimpleDateFormat をオブジェクト化し、任意のフォーマットを設定
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年 MM月 dd日");
        // フォーマット指定なし
        System.out.println(date);
        // フォーマット指定あり
        System.out.println(sdf.format(date));
    }
}

【実行結果】

Sun Nov 29 20:11:31 JST 2020
2020年 11月 29日

【サンプルコード】 現在日付をyyyyMMdd形式の文字列で取得する

現在日付をyyyyMMdd形式で取得するサンプルです。

// ① ここでSimpleDateFormat と Calendarをインポート
import java.text.SimpleDateFormat;
import java.util.Calendar;

public class MainSimpleDateFormat {

    public static void main(String[] args) {

        // ② Calendarをオブジェクト化
        Calendar cl = Calendar.getInstance();

        // ③ SimpleDateFormat のパターンを任意で設定する。
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年 MM月 dd日");
        // ④ 文字列 str に SimpleDateFormat で整形した 日時 を格納する。
        String str = sdf.format(cl.getTime());
        // ⑤strを出力する
        System.out.println(str);
    }
}

【実行結果】

2020年 11月 29日

【まとめ】

参考文献・記事

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

【Java】String型に変換するためのvalueOfメソッドとtoStringメソッドについて

プログラミング勉強日記

2020年11月29日
前にtoString()メソッドの使い方について扱ったが、今回valueO()メソッドでもString型に変換することができることを知った。それを踏まえてJavaでString型に変換するために使われる2種類のメソッドの違いとそれぞれの使い方についてまとめる。

valueOfメソッドとは

 引数に指定した様々な型の形をString型の文字列として返すことができる。

基本的な書き方
String.valueOf(変換する値);

valueOfメソッドのサンプルコード

 short型、int型、long型、float型、double型をそれぞれvalueOfメソッドを使用してString型に変換する。

public class Main {
    public static void main(String[] args) throws Exception {

       // short型からString型に変換する
        short sh = 100;
        String strsh = String.valueOf(sh);
        System.out.println("short型からString型 : " + strsh);

        // int型からString型に変換する
        int num = 100;
        String strnum = String.valueOf(num);
        System.out.println("int型からString型 : " + strnum);

        // long型からString型に変換する
        long lon = 100;
        String strlon = String.valueOf(lon);
        System.out.println("long型からString型 : " + strlon);

        // float型からString型に変換する
        float fl = 100;
        String strfl = String.valueOf(fl);
        System.out.println("float型からString型 : " + strfl);

        // double型からString型に変換する
        double db = 100;
        String strdb = String.valueOf(db);
        System.out.println("double型からString型 : " + strdb);
    }
}
実行結果
short型からString型 : 100
int型からString型 : 100
long型からString型 : 100
float型からString型 : 100.0
double型からString型 : 100.0

toStringメソッドとは

 数値型などをString型に変換するために使う。引数にString型の文字列に変換したい引数を指定し、戻り値はString型の文字列を返す。

toStringメソッドのサンプルコード

public class Main {
  pubic static void main(String[] args) {
    int num1 = 1234;
    int num2 = 5678;
    System.out.println(num1 + num2);

    // 数値を文字列に変換する
    String str1 = Integer.toString(num1);
    String str2 = Integer.toString(num2);
    System.out.println(str1 + str2);
  }
}
実行結果
6912
12345678

valueOfメソッドとtoStringメソッドの違い

 基本的にはメソッドの目的は同じであるが、変換する対象の値がnullの場合の動作が異なる
 Integer型の値にnullを設定してString型に変換する場合、valueOfメソッドの場合はString型のnullを返すが、toStringメソッドの場合はnull参照時の例外が発生する。なので、toStringメソッドを使用する場合は変換する値がnullでどうか確かめる必要がある。

参考文献

【Java入門】数値を文字列へ変換する方法(valueOf/toString)
【Java】toString()メソッドの使い方

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

【Java】String型に変換するvalueOfメソッドとtoStringメソッドについて

プログラミング勉強日記

2020年11月29日
前にtoString()メソッドの使い方について扱ったが、今回valueO()メソッドでもString型に変換することができることを知った。それを踏まえてJavaでString型に変換するために使われる2種類のメソッドの違いとそれぞれの使い方についてまとめる。

valueOfメソッドとは

 引数に指定した様々な型の形をString型の文字列として返すことができる。

基本的な書き方
String.valueOf(変換する値);

valueOfメソッドのサンプルコード

 short型、int型、long型、float型、double型をそれぞれvalueOfメソッドを使用してString型に変換する。

public class Main {
    public static void main(String[] args) throws Exception {

       // short型からString型に変換する
        short sh = 100;
        String strsh = String.valueOf(sh);
        System.out.println("short型からString型 : " + strsh);

        // int型からString型に変換する
        int num = 100;
        String strnum = String.valueOf(num);
        System.out.println("int型からString型 : " + strnum);

        // long型からString型に変換する
        long lon = 100;
        String strlon = String.valueOf(lon);
        System.out.println("long型からString型 : " + strlon);

        // float型からString型に変換する
        float fl = 100;
        String strfl = String.valueOf(fl);
        System.out.println("float型からString型 : " + strfl);

        // double型からString型に変換する
        double db = 100;
        String strdb = String.valueOf(db);
        System.out.println("double型からString型 : " + strdb);
    }
}
実行結果
short型からString型 : 100
int型からString型 : 100
long型からString型 : 100
float型からString型 : 100.0
double型からString型 : 100.0

toStringメソッドとは

 数値型などをString型に変換するために使う。引数にString型の文字列に変換したい引数を指定し、戻り値はString型の文字列を返す。

toStringメソッドのサンプルコード

public class Main {
  pubic static void main(String[] args) {
    int num1 = 1234;
    int num2 = 5678;
    System.out.println(num1 + num2);

    // 数値を文字列に変換する
    String str1 = Integer.toString(num1);
    String str2 = Integer.toString(num2);
    System.out.println(str1 + str2);
  }
}
実行結果
6912
12345678

valueOfメソッドとtoStringメソッドの違い

 基本的にはメソッドの目的は同じであるが、変換する対象の値がnullの場合の動作が異なる
 Integer型の値にnullを設定してString型に変換する場合、valueOfメソッドの場合はString型のnullを返すが、toStringメソッドの場合はnull参照時の例外が発生する。なので、toStringメソッドを使用する場合は変換する値がnullでどうか確かめる必要がある。

参考文献

【Java入門】数値を文字列へ変換する方法(valueOf/toString)
【Java】toString()メソッドの使い方

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

Spring Cloudを使ったマイクロサービスシステム構築(多重構成編)

概要

前回投稿したSpring Cloudを使ったマイクロサービスシステム構築(シングル構成編)の構成もとに、多重構成化する。

 マイクロサービスの多重化

シングル構成だと、一つのマイクロサービスが停止した場合、そのサービスが利用できなくなる。またある特定のサービスへのアクセスが多い場合、多重化することでサービスを負荷分散できるようになる。

スクリーンショット 2020-11-28 15.11.18.png

マイクロサービスのポートが同じなので、異なるサーバにマイクロサービスを構築する。ms01ms02のサーバを用意し、それにマイクロサービスを配置する。各サーバで動作するマイクロサービスと受信ポートのイメージは以下のようなもの。

Service1-#3はポートを変えるので後で設定する。

スクリーンショット 2020-11-28 15.13.41.png

Eurekaの多重化

Spring Cloudの本家の記事を参考にEurekaをPeer Awareness構成にする。
やっていることは、profilesを2つ用意して、それぞれがお互いのEurekaをサーバとして認識することっぽい。

application.yml
server:
  port: 8761
eureka:
  client:
    registerWithEureka: false
    fetchRegistry: false

spring:
  cloud:
    inetutils:
      preferredNetworks:
        - 10.10.10
---
spring:
  profiles: peer1
eureka:
  instance:
    hostname: ms01
  client:
    serviceUrl:
      defaultZone: http://ms02:8761/eureka/

---
spring:
  profiles: peer2
eureka:
  instance:
    hostname: ms02
  client:
    serviceUrl:
      defaultZone: http://ms01:8761/eureka/

Javaのソースコードは前回から変更なしのため必要なら昔の記事を参照。これでms01ms02サーバにそれぞれ配置する。起動の際はどのような条件で起動するか、peerを指定して動作条件を決める。

ms01サーバで実行
[root@ms01 ~]# java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1

ms02サーバで実行
[root@ms02 ~]# java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2

正しく動作しているか確認。ms01のEureka管理コンソールを見ると、レプリカとしてms02のEurekaが登録されていて正常に登録されているようだ。

スクリーンショット 2020-11-28 15.24.34.png

Service-1/Service-2のマイクロサービス多重化

Eurekaとほぼ同じ構成になる。サーバ毎に条件を分けるpeer指定がないので記載してないが必要ならpeer指定してサーバ毎に動きを変えることも可能。
サンプルとしてService-1のapplication.ymlの設定を載せるがService-2も読み替えて設定。

application.yml
spring:
  application:
    name: app1-service

server:
  port: 8081

eureka:
  client:
    serviceUrl:
      defaultZone: http://ms01:8761/eureka/, http://ms02:8761/eureka/

Gatewayの多重化

Gatewayも一緒。

application.yml
server:
  port: 8080

eureka:
  client:
    serviceUrl:
      defaultZone: http://ms01:8761/eureka/, http://ms02:8761/eureka/

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      routes:
      - id: app1Module
#        uri: http://ms01:8081/ #単体で動かす場合はこれでもOK
        uri: lb://APP1-SERVICE # application:name を指定する
        predicates:
        - Path=/hello/**
      - id: app2Module
#        uri: http://ms01:8082/
        uri: lb://APP2-SERVICE 
        predicates:
        - Path=/goodbye/**

サービスの登録と起動

Service-1/Service-2とGatewayをそれぞれms01,ms02両方のサーバで起動してみる。Eurekaに二重登録されているっぽい。

スクリーンショット 2020-11-29 16.51.20.png

シングル構成の時と同様、マイクロサービスのURLにアクセスすると正常応答が返ってくる。
今回は二重起動しているので、下記それぞれで応答が返ってくる。

http://ms01:8080/hello
http://ms02:8080/hello
http://ms01:8080/goodbye
http://ms02:8080/goodbye

2台のサーバで3多重サービスをする

サーバがms01,ms02の2台でサービスを3多重にしたい場合、ポートを分けて起動する。
例えばService-1をms01で2つ起動する場合、既に8081ポートが利用済みなので、8083ポートで起動できるようにする。

application.xml
spring:
  application:
    name: app1-service
server:
  port: 8081  # デフォルトだと8081ポートで起動する

eureka:
  client:
    serviceUrl:
      defaultZone: http://ms01:8761/eureka/, http://ms02:8761/eureka/

---
spring:
  profiles: peer1  # peer1のプロファイルを指定して起動すると、8083ポートで起動する
server:
  port: 8083

あとは、これを起動するとよい。

[root@ms01 ~]# java -jar app1-service-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1

Eurekaの管理コンソールで見てみると、APP1-SERVICEのサービスがms01サーバの8081と8083ポートで動いているのがわかる。他にもms02サーバの8081ポートで動いているので、合計3多重で動いているのがわかる。

スクリーンショット 2020-11-29 17.57.42.png

確認のため、ms01,ms02サーバの8081で動いているAPP1-SERVICEを停止し、ms01サーバの8083ポートでサービスが状態にしても、従来通りのアクセス方法で(つまりポートを意識することなく)アクセス可能。

スクリーンショット 2020-11-29 18.03.59.png

この状態でも従来同様、http://ms01:8080/hello にアクセスすることでAPP1-SERVICEを呼び出せる。

他にも、ms01のサーバ自体を停止させてもシングル構成で動く。

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

[springで例外処理] 空文字入力時・同名のTODOが存在する時の例外処理

こんにちは。

今回は残るバリデーション2つの実装を行っていこうと思います。

TODOアプリ作成リンク集

1: [超基礎の理解] MVCの簡単な説明
2: [雛形を用意する] Spring Initializrで雛形を作ってHello worldしたい
3: [MySQLとの接続・設定・データの表示] MySQLに仮のデータを保存 -> 全取得 -> topに表示する
4: [POST機能] 投稿機能の実装
5: [PATCH機能] TODOの表示を切り替える
6: [JpaRepositoryの簡単な使い方] 検索機能の実装
7: [Thymeleaf テンプレートフラグメントで共通化] Headerの作成
8: [PUT機能] 編集機能の実装
9: [微調整]TODOの表示を作成日時が新しい順にソートする + 期日のデフォルトを今日の日付にする
10: [springで例外処理] 例外処理についての簡単なまとめ
11: [springで例外処理] 存在しないIDのTODOにアクセスした時の例外処理
12: [springで例外処理] 使われていないHttpMethodでリクエストが来た時の処理・サーバー内でエラーが起きた時の処理
13: [springで例外処理] TODOフォームのバリデーション1: 文字数制限・@Validatedを使うためのGradleの更新
14: [springで例外処理] 空文字入力時・同名のTODOが存在する時の例外処理(今ここ)

TODO登録時のバリデーションチェックを追加する。

今回追加する例外処理は

・TODO登録時にから文字が入力された時
・入力されたTODOタイトルがすでにデータベースに存在する時

の2点です。

から文字とは?

から文字とはスペース1つで登録しようとするケースの事です。

前回のバリデーションの処理では、TODO登録文字数が1~30字以内かどうかチェックするものを追加しましたが

仮にスペース一つが入力されるとこれは一文字とみなされてしまうので1~30字以内のチェックに引っかかりません!

スペース一つのタイトルは認めたくないので空文字入力時の処理を追加します。

コントローラへの追記

java/com/example/todo/TodoController.java
    @PostMapping("/register")
    public String register(@Validated @ModelAttribute TodoForm formData, BindingResult error, RedirectAttributes attributes) {
        if(error.hasErrors()) {
            attributes.addFlashAttribute("errorMessages", error);
            return "redirect:/top";
        }
        //今回追加するもの//
        if(formData.getTitle().isBlank()) {
            error.addError(new ObjectError("noSpace", "TODOを入力してください。"));
            attributes.addFlashAttribute("errorMessages", error);
            return "redirect:/top";
        }
        //ここまで//
        todoService.setTodo(formData);
        return "redirect:/top";
    }

こんな感じで追記します。

入力されたタイトルに対してisBlank()というメソッドを使ってから文字かどうかを判定しています。
似たようなメソッドでisEmpty()もありますが、こちらはスペースに対してはfalseが返ってくるので今回はisBlank()を使用しています。

空文字があった場合は、新しくObjectErrorを作りこれをフロントに送っています。

同名タイトルが存在する場合のバリデーション処理

今回のTODOでは同じ名前のTODOを追加できない仕様を想定しています。

やり方としては、入力されたタイトルを使ってデータベースに同名のタイトルのTODOがあるかをチェックして判定します。

Repositoryに関数追加

まずはDBとアプリケーションをつなぐ役目を持つTodoRepositoryに検索機能を追加します。

java/com/example/todo/dao/TodoRepository.java
@Repository
public interface TodoRepository extends JpaRepository<TodoEntity, Long> {
    //この1行を追加
    List<TodoEntity> findByTitle(String searchWord);

    List<TodoEntity> findByTitleContaining(String searchWord);

    List<TodoEntity> findAllByOrderByCreateTimeDesc();
}

この1行を追加する事で渡ってきたStringをもとにタイトル検索することができるようになりました。

findByTitleContainingとの違いですが、Containingは含むという意味ですので、部分一致・完全一致の両方に対応しています。

かたやfindByTitleは入力値が完全に一致する場合のみ対応しています。

ServiceにRepositoryに追加した関数を呼び出す処理をかく

続いてServiceクラスに先ほどの関数を呼び出す関数を追加します。

java/com/example/todo/TodoService.java
    public boolean ifExistByTitle(String title) {
        return !todoRepository.findByTitle(title).isEmpty();
    }

ここのreturnについて解説します。

findByTitle()
タイトルが完全一致するTODOがあればそのTODOをリストとして返します。

その結果に対して
.isEmpty()するので
中身がある(つまり同名TODOがある) -> false
中身がない(つまり同名TODOがない) -> true
を返すわけです。

そしてifExistByTitleは結果が
true -> 同名タイトルが存在する
false -> 同名タイトルが存在しない
ということを想定しています。

ですのでisEmpty()の結果を!によってひっくり返す事でifExistByTitleに相応しい結果を得る事できます。

ちなみにKotlinにはisNotEmpty()という関数があり、こんなややこしいことをせずに済みますw

もしこの書き方が好み出ない場合は

java/com/example/todo/TodoService.java
    public boolean a(String title) {
        if(todoRepository.findByTitle(title).isEmpty()) {
            return false;
        }
        return true;
    }

でも良いと思います。(InteliJだとシンプルにできるぜ!と先に書いた方を推奨してくることもありますが・・・)

コントローラに追記。

java/com/example/todo/TodoController.java
    @PostMapping("/register")
    public String register(@Validated @ModelAttribute TodoForm formData, BindingResult error, RedirectAttributes attributes) {
        if(error.hasErrors()) {
            attributes.addFlashAttribute("errorMessages", error);
            return "redirect:/top";
        }

        if(formData.getTitle().isBlank()) {
            error.addError(new ObjectError("noSpace", "TODOを入力してください。"));
            attributes.addFlashAttribute("errorMessages", error);
            return "redirect:/top";
        }
        //今回ついかする//
        if(todoService.ifExistByTitle(formData.getTitle())) {
            error.addError(new ObjectError("sameTitle", "同名のTODOが既に存在します。"));
            attributes.addFlashAttribute("errorMessages", error);
            return "redirect:/top";
        }
        //ここまで//
        todoService.setTodo(formData);
        return "redirect:/top";
    }

このように先ほどついかしたService・Repositoryの処理を追加してやりましょう。

これにより2つのバリデーションチェックを追加する事ができました!

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

【Java】Time (日付操作)

Timeクラス

【説明】

DateクラスとCalendarクラスの問題を解決するために新しく出来たクラスです。
TimeDateクラスとCalendarクラスの両方の特徴があり、

  • 日付データ保持
  • 日付操作

これら両方とも Timeクラス1つで賄うことが出来てしまいます。

Timeクラスには、

  • LocalDate
  • Localtimeのインスタンスを持つLocalDateTime
  • LocalDateTimeに加え
  • オフセット(標準時との時差)を含むOffsetDateTime
  • OffsetDateTimeに加え
  • タイムゾーンを含むZonedDateTimeがあります。
クラス名 機能と役割
Instant 世界における、ある「瞬間」の時刻を、ナノ秒単位で厳密に指し示し、保持する
ZoneDateTime 世界における、ある「瞬間」の時刻を、ナノ秒単位で厳密に指し示し、保持する 2020-11-30T23:30:59.999+09:00[Asia/Tokyo]
LocalDateTime 内部にLocalDateとLocalTimeのインスタンスを持っている。
日常的に使われる「曖昧な日時」を保持する
タイムゾーンのない日時
2020-11-30T23:30:59.999
Duration 2つの異なる時刻や日付の期間を保持する
Period 2つの異なる時刻や日付の期間を保持する
OffsetDateTime オフセット付きの日時。 2020-11-30T23:30:59.999+09:00

また、月の値の対応もCalendarクラスと異なっており、月の値が1場合 1月 として出力されます。

【使用場面】

時間 」を扱う場面全般で使用できます。

※ 現場の中には、「伝統的なDateCalendarを使用するので、Time Apiは積極活用しない」という場所もあるので、実務では現場のルールに従うこと。

【サンプルコード】

// LocalDateTime のインポート
import java.time.LocalDateTime;
// OffsetDateTime のインポート
import java.time.OffsetDateTime;
// ZonedDateTime のインポート
import java.time.ZonedDateTime;

public class AboutTimeMain {

    public static void main(String[] args) {
        // LocalDateTime で現在時刻を取得
        LocalDateTime ldtNow = LocalDateTime.now();
        // OffsetDateTime で現在時刻を取得
        OffsetDateTime odtNow = OffsetDateTime.now();
        // ZonedDateTime で現在時刻を取得
        ZonedDateTime zdtNow = ZonedDateTime.now();

        // それぞれのTimeクラスを出力する。
        System.out.println("LocalDateTime は" + ldtNow.toString());
        System.out.println("OffsetDateTime は" + odtNow.toString());
        System.out.println("ZonedDateTime は" + zdtNow.toString());
    }
}

【実行結果】

LocalDateTime は2020-11-29T16:48:21.834565
OffsetDateTime は2020-11-29T16:48:21.835357+09:00
ZonedDateTime は2020-11-29T16:48:21.836312+09:00[Asia/Tokyo]

【サンプルコード】

TimeクラスはDateクラスやCalendarクラスと異なり、「直接値を変更することができない」ため、「 日時などの値を変更する場合はインスタンスを生成しなおす」 必要があります。

// LocalDateTime のインポート
import java.time.LocalDateTime;

public class AboutTimeMain {

    public static void main(String[] args) {
        // LocalDateTime で現在時刻を取得
        LocalDateTime ldtNow = LocalDateTime.now();
        System.out.println(ldtNow.getMonthValue() + "月です。");

        // インスタンス の再生成
        ldtNow = ldtNow.withMonth(5);
        System.out.println(ldtNow.getMonthValue() + "月に変更しました。");
    }
}

【実行結果】

11月です。
5月に変更しました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Java】Time ( 日付操作 )

Timeクラス

【説明】

DateクラスとCalendarクラスの問題を解決するために新しく出来たクラスです。
TimeDateクラスとCalendarクラスの両方の特徴があり、

  • 日付データ保持
  • 日付操作

これら両方とも Timeクラス1つで賄うことが出来てしまいます。

Timeクラスには、

  • LocalDate
  • Localtimeのインスタンスを持つLocalDateTime
  • LocalDateTimeに加え
  • オフセット(標準時との時差)を含むOffsetDateTime
  • OffsetDateTimeに加え
  • タイムゾーンを含むZonedDateTimeがあります。
クラス名 機能と役割
Instant 世界における、ある「瞬間」の時刻を、ナノ秒単位で厳密に指し示し、保持する
ZoneDateTime 世界における、ある「瞬間」の時刻を、ナノ秒単位で厳密に指し示し、保持する 2020-11-30T23:30:59.999+09:00[Asia/Tokyo]
LocalDateTime 内部にLocalDateとLocalTimeのインスタンスを持っている。
日常的に使われる「曖昧な日時」を保持する
タイムゾーンのない日時
2020-11-30T23:30:59.999
Duration 2つの異なる時刻や日付の期間を保持する
Period 2つの異なる時刻や日付の期間を保持する
OffsetDateTime オフセット付きの日時。 2020-11-30T23:30:59.999+09:00

また、月の値の対応もCalendarクラスと異なっており、月の値が1場合 1月 として出力されます。

【使用場面】

時間 」を扱う場面全般で使用できます。

※ 現場の中には、「伝統的なDateCalendarを使用するので、Time Apiは積極活用しない」という場所もあるので、実務では現場のルールに従うこと。

【サンプルコード】

// LocalDateTime のインポート
import java.time.LocalDateTime;
// OffsetDateTime のインポート
import java.time.OffsetDateTime;
// ZonedDateTime のインポート
import java.time.ZonedDateTime;

public class AboutTimeMain {

    public static void main(String[] args) {
        // LocalDateTime で現在時刻を取得
        LocalDateTime ldtNow = LocalDateTime.now();
        // OffsetDateTime で現在時刻を取得
        OffsetDateTime odtNow = OffsetDateTime.now();
        // ZonedDateTime で現在時刻を取得
        ZonedDateTime zdtNow = ZonedDateTime.now();

        // それぞれのTimeクラスを出力する。
        System.out.println("LocalDateTime は" + ldtNow.toString());
        System.out.println("OffsetDateTime は" + odtNow.toString());
        System.out.println("ZonedDateTime は" + zdtNow.toString());
    }
}

【実行結果】

LocalDateTime は2020-11-29T16:48:21.834565
OffsetDateTime は2020-11-29T16:48:21.835357+09:00
ZonedDateTime は2020-11-29T16:48:21.836312+09:00[Asia/Tokyo]

【サンプルコード】

TimeクラスはDateクラスやCalendarクラスと異なり、「直接値を変更することができない」ため、「 日時などの値を変更する場合はインスタンスを生成しなおす」 必要があります。

// LocalDateTime のインポート
import java.time.LocalDateTime;

public class AboutTimeMain {

    public static void main(String[] args) {
        // LocalDateTime で現在時刻を取得
        LocalDateTime ldtNow = LocalDateTime.now();
        System.out.println(ldtNow.getMonthValue() + "月です。");

        // インスタンス の再生成
        ldtNow = ldtNow.withMonth(5);
        System.out.println(ldtNow.getMonthValue() + "月に変更しました。");
    }
}

【実行結果】

11月です。
5月に変更しました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Spring BootでCSV出力

Java+SpringBootで検索結果をCSVダウンロードできる機能を実装できたので備忘録として残しておきます。

まず、こんな感じで検索結果があったとします。
スクリーンショット 2020-11-29 16.06.35.png

で、このCSVボタンを押すと、検索結果全てがCSVファイルとしてダウンロードされるようにします

1 - まずこんな感じで検索ボタンが押された後のレコード情報をhiddenの<form>に入れて保持しておきます。

        <form id="csvform" method="post" th:action="@{/asset/csv}" th:object="${csvForm}">
          <div th:each="asset:${assets}">
            <input type="hidden" name="id" th:value="${asset.id}" />
            <input type="hidden" name="categoryId" th:value="${asset.categoryId}" />
            <input type="hidden" name="adminName" th:value="${asset.adminName}" />
            <input type="hidden" name="assetName" th:value="${asset.assetName}" />
            <input type="hidden" name="remarks" th:value="${asset.remarks}" />
          </div>
          <button type="submit">CSV</button>
        </form>

2 - で、CSボタンが押されたらこの情報全てをポスト送信し、コントローラー側で受け取ってCSV形式に整形し、ダウンロードしてくれます。この際に、CSV recordsではなく、List<CSV> recordsとすると、エラーが起きてしまうのはなぜだか分からずじまい... とりあえずList<Object>を受け取るにはList<Object>をフィールドにもっとクラスを用意しておかないといけないみたい。

  @PostMapping(value = "/csv", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE
      + "; charset=UTF-8; Content-Disposition: attachment")
  @ResponseBody
  public Object csvDownload(@ModelAttribute("csvForm") CSV records) throws JsonProcessingException {
    List<CsvColumn> csvList = new ArrayList<>();
    for (int i = 0; i < records.getId().size(); i++) { // レコードの数ぶんだけループ回して
      csvList.add(new CsvColumn(records.getId().get(i), records.getCategoryId().get(i), records.getAdminName().get(i), records.getAssetName().get(i), records.getRemarks().get(i)));
    }
    CsvMapper mapper = new CsvMapper();
    CsvSchema schema = mapper.schemaFor(CsvColumn.class).withHeader();
    return mapper.writer(schema).writeValueAsString(csvList);
  }
@Data
public class CSV {

  List<Integer> id;

  List<Integer> categoryId;

  List<String> adminName;

  List<String> assetName;

  List<String> remarks;

}
@JsonPropertyOrder({"資産ID", "資産種別", "管理者名", "資産名", "備考"})
@Data
public class CsvColumn {

  @JsonProperty("資産ID")
  private Integer id;

  @JsonProperty("資産種別")
  private Integer categoryId;

  @JsonProperty("管理者名")
  private String adminName;

  @JsonProperty("資産名")
  private String assetName;

  @JsonProperty("備考")
  private String remarks;

  public CsvColumn () {}

  public CsvColumn (Integer id, Integer categoryId, String adminName, String assetName, String remarks) {
    this.id = id;
    this.categoryId = categoryId;
    this.adminName = adminName;
    this.assetName = assetName;
    this.remarks = remarks;
  }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Clojure + JavaFX + カスタムJREでGUIアプリを作成し、リリースを自動化する

まえがき

  • ClojureでJavaFXを利用したGUIアプリケーションを実装します
  • ユーザ環境にJavaランタイムをインストールしなくて良いように、カスタムJREを使ってアプリケーションを起動できるようにする方法を書きます
  • リリース作業をGitHubActionsで自動化します

開発環境

Key Value
OS Ubuntu 18.04
Clojure 1.10.1
Leiningen 2.9.4
Java OpenJDK 14

JavaFXアプリケーションの配布について

以下の記事でより詳細に書かれているのですが、JavaFXアプリを最新のJavaで起動する形で配布するには準備が必要です。
OpenJFX時代のJDK選び - もしくはOpenJFX時代のアプリケーション配布

Clojureで作成した実行可能JARも同じ問題を抱えています。
ユーザにJavaのランタイム、JavaFX SDKを入れてもらう必要なく、JavaFXアプリケーションを配布するには、カスタムJREを作成して実行可能JARと一緒に配布する必要があります。

各プラットフォーム向けに配布する流れ

カスタムJREを作成してアプリを起動できるようにするのにはいくつか準備が必要で、手順もやや複雑です。なので、段階を追って以下の流れで説明します。

  • Clojure+JavaFXのアプリケーションを実装する
  • jlinkでカスタムJREを作成し、カスタムJREでアプリケーションを起動できるようにする
  • 各プラットフォーム向けに配布するために、CIと連携して自動リリースできるようにする

0. ソースコード

CIでの自動リリースまですべて構築したソースコードは以下です。
今回の記事はこのリポジトリのソースコードをベースに説明します。

https://github.com/jiro4989/iconisor

1. Clojure+JavaFXのアプリケーションを実装する

まずClojureでJavaFXアプリケーションを起動できるところまで実装します。
この時点ではカスタムJREなどは作らず、lein経由でアプリが起動できればOKです。

プロジェクトの管理にはleiningenを使います。
ClojureでJavaFXを扱うためのライブラリとしてcljfxを使います。
dependenciesに[cljfx "1.7.10"]を追加します。

pluginsにフォーマッタや静的解析を入れていますが、
こちらは今回の記事とは関係ないので入れなくても動作すると思います。

コードとしては以下です。

project.clj
(defproject iconisor "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://github.com/jiro4989/iconisor"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.10.1"]
                 [cljfx "1.7.10"]]
  :main iconisor.core
  ;:main ^:skip-aot iconisor.core
  :target-path "target/%s"
  :plugins [[lein-cloverage "1.2.0"]
            [lein-cljfmt "0.7.0"]
            [jonase/eastwood "0.3.10"]
            [lein-kibit "0.1.8"]]
  :profiles {:uberjar {:aot :all
                       :jvm-opts ["-Dclojure.compiler.direct-linking=true"]
                       :prep-tasks ["compile"]
                       :uberjar-name "iconisor.jar"
                       :injections [(javafx.application.Platform/exit)]}})

最初ここで躓いたのが :injections の設定です。

アプリケーションの配布用に実行可能JARを作成する必要があるのでlein uberjarを実行するとuberjarが永遠に終了しないという事象に遭遇しました。
以下のissuesを見つけて解決したのですが:injections [(javafx.application.Platform/exit)]という記述が必須です。
https://github.com/cljfx/cljfx/issues/17

次にmain関数を実装します。
とりあえずGUIが出れば良いので、cljfxのサンプルコードだけでOKです。

src/iconisor/core.clj
(ns iconisor.core
  (:require [cljfx.api :as fx])
  (:gen-class))

(defn -main [& args]
  (fx/on-fx-thread
   (fx/create-component
    {:fx/type :stage
     :showing true
     :always-on-top true
     :style :transparent
     :scene {:fx/type :scene
             :fill :transparent
             :stylesheets #{"styles.css"}
             :root {:fx/type :v-box
                    :children [{:fx/type :label
                                :effect {:fx/type :drop-shadow
                                         :radius 1
                                         :offset-y 2}
                                :tooltip {:fx/type :tooltip
                                          :text "I am a tooltip!"}
                                :text "Hi! What's your name?"}
                               {:fx/type :text-field}]}}})))

これでlein runを実行すると以下のようなテキストフィールドだけのGUIが起動するようになります。

demo.PNG

2. jlinkでカスタムJREを作成し、カスタムJREでアプリケーションを起動できるようにする

カスタムJREを作成するためにjlinkを実行します。
jlinkを実行するには依存ライブラリのモジュールが必要になります。
そのためOpenJFXのサイトからjmodsをダウンロードする必要があります。
僕は以下のシェルを作成し、コマンドラインで取得するようにしています。

install_jmods.sh
#!/bin/bash

set -eux

os_name=linux
version=14.0.1

rm -rf jmods || true
mkdir -p jmods
curl -o jmods/jmods.zip -sSL https://download2.gluonhq.com/openjfx/${version}/openjfx-${version}_${os_name}-x64_bin-jmods.zip
cd jmods
unzip jmods.zip

このシェルを実行するとカレントディレクトリにjmodsディレクトリが作成されます。

次に、以下のシェルスクリプトを作成します。

jlink.sh
#!/bin/bash

set -eux

outdir=$1

jlink --module-path ./jmods/javafx-jmods-14.0.1/ \
  --add-modules javafx.base,javafx.controls,javafx.swing,javafx.graphics,javafx.fxml \
  --compress=2 \
  --output "$outdir"

このシェルを使ってカスタムJREを作成します。
以下のように呼び出します。

./jlink.sh jre

これでjreディレクトリにカスタムJREが作成されます。

このカスタムJREを利用して実行可能JARが起動できることを確認します。
以下のように実行します。

# 実行可能JARの生成
lein uberjar

cd jre
./bin/java -jar ../target/uberjar/iconisor.jar

これでGUIが表示されれば、カスタムJREで起動できるようになっています。

3. CIと連携して自動リリースできるようにする

通常の開発ではjmodsを落としてくる必要はありませんし、カスタムJREの作成も不要です。

リリース作業のためだけにローカル環境を整えるのも手間なのと、リリース作業をコード化して再利用できるようにするために、CI環境(GitHubActions)だけでリリースを完了できるようにします。

具体的には、以下のコマンドを実行して、タグをpushするだけでリリースを終えられるようにするのを目指します。

git tag v0.1.0
git push origin --tags

追加の準備として、起動スクリプトを作成します。

前述のとおり、カスタムJREを使ってアプリケーションを起動するのであれば、コマンドラインからカスタムJREのJavaを指定する必要があります。
一般配布を目指すのでしたら、コマンドライン入力を求めるようにはしたくありません。
なので、前述のコマンドをラップしたスクリプトを作成し、配布時にカスタムJREと一緒に配布する形とします。
以下のスクリプトを作成します。

Windows用。文字コードはSJIS、改行コードはCRLFにしましょう。

github/dist/iconisor.bat
@echo off

.\jre\bin\java.exe -jar .\iconisor.jar

Mac, Linux用。文字コードはUTF-8、改行コードはLFにしましょう。

#!/bin/bash

# .github/dist/iconisor

./jre/bin/java -jar iconisor.jar

これらのスクリプトと一緒に、以下のディレクトリ構成となるようにリリース物を収集して圧縮するようなワークフローにしていきます。

iconisor_windows.zip/
  iconisor_windows/
    jre/          # カスタムJRE
    LICENSE
    README.txt
    iconisor.bat  # 起動スクリプト
    iconisor      # 起動スクリプト
    iconisor.jar  # 実行可能JAR

ということで、CI環境のみでWindows、Linux、Mac向けにリリース物を生成してアップロードするワークフローを書きました。
以下が全体です。

name: test

on:
  push:
    paths-ignore:
      - 'LICENSE'
      - 'README.*'
      - 'docs/*'
  pull_request:
    paths-ignore:
      - 'LICENSE'
      - 'README.*'
      - 'docs/*'

env:
  app-name: iconisor # TODO: アプリ名なのでアプリ名に合わせて変更が必要
  javafx-version: '14.0.1'

defaults:
  run:
    shell: bash

jobs:
  # リリース自体には不要。テストを書いているなら使う
  test:
    runs-on: ubuntu-latest
    continue-on-error: true
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-java@v1
        with:
          java-version: '14'
          java-package: jdk
          architecture: x64
      - name: Install xvfb
        run: sudo apt install -y xvfb
      - run: xvfb-run lein test
      - run: xvfb-run lein cloverage --codecov
      - uses: codecov/codecov-action@v1

  # リリース自体には不要。lintにかけるなら必要。project.cljへのplugin記述も必要
  linter:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        cmd:
          - 'cljfmt check'
          - 'kibit'
    steps:
      - uses: actions/checkout@v2
      - run: lein ${{ matrix.cmd }}

  # リリースのメイン処理
  build:
    runs-on: ${{ matrix.runs-on }}
    strategy:
      matrix:
        include:
          - runs-on: windows-latest
            os: windows
          - runs-on: macOS-latest
            os: osx
          - runs-on: ubuntu-latest
            os: linux
    steps:
      - uses: actions/checkout@v2

      - uses: actions/setup-java@v1
        with:
          java-version: '14'
          java-package: jdk
          architecture: x64
      - name: Install lein (unix)
        uses: DeLaGuardo/setup-clojure@master
        with:
          cli: '1.10.1.469'
          lein: 2.9.4
          boot: latest
        if: matrix.os != 'windows'

      - name: Install lein (windows)
        run: |
          from urllib import request
          import os
          lists = [
            ("https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/%s", "lein.bat"),
          ]
          for elem in lists:
            url = elem[0] % elem[1]
            file = elem[1]
            request.urlretrieve(url, file)
            os.system("lein.bat self-install")
        shell: python
        if: matrix.os == 'windows'

      - name: Install openjfx jmods
        run: |
          mkdir -p jmods
          curl -o jmods/jmods.zip -sSL https://download2.gluonhq.com/openjfx/${{ env.javafx-version }}/openjfx-${{ env.javafx-version }}_${{ matrix.os }}-x64_bin-jmods.zip
          (
            cd jmods
            unzip jmods.zip
          )

      - name: Initialize custom JRE
        run: ./jlink.sh jre

      - name: Build (unix)
        run: lein uberjar
        if: matrix.os != 'windows'

      - name: Build (windows)
        run: lein.bat uberjar
        if: matrix.os == 'windows'
        shell: cmd

      - name: Create artifact
        run: |
          mkdir -p ${{ env.app-name }}_${{ matrix.os }}/
          mv jre .github/dist/* LICENSE target/uberjar/${{ env.app-name }}.jar ${{ env.app-name }}_${{ matrix.os }}/

      - name: Compress artifact (unix)
        run: |
          tar czf ${{ env.app-name }}_${{ matrix.os }}.tar.gz ${{ env.app-name }}_${{ matrix.os }}
        if: matrix.os != 'windows'

      - name: Compress artifact (windows)
        run: |
          7z a ${{ env.app-name }}_${{ matrix.os }}.zip ${{ env.app-name }}_${{ matrix.os }}
        if: matrix.os == 'windows'

      - uses: actions/upload-artifact@v2
        with:
          name: artifact-${{ matrix.os }}
          path: |
            ${{ env.app-name }}_${{ matrix.os }}.tar.gz
            ${{ env.app-name }}_${{ matrix.os }}.zip
        if: startsWith(github.ref, 'refs/tags/')

  create-release:
    if: startsWith(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest
    needs:
      - build
    steps:
      - uses: actions/checkout@v1
      - name: Create Release
        id: create-release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: ${{ github.ref }}
          body: ${{ github.ref }}
          draft: false
          prerelease: false

      - name: Write upload_url to file
        run: echo '${{ steps.create-release.outputs.upload_url }}' > upload_url.txt

      - uses: actions/upload-artifact@v2
        with:
          name: create-release
          path: upload_url.txt

  upload-release:
    runs-on: ubuntu-latest
    needs: create-release
    strategy:
      matrix:
        include:
          - os: windows
            asset_name_suffix: windows.zip
            asset_content_type: application/zip
          - os: osx
            asset_name_suffix: osx.tar.gz
            asset_content_type: application/gzip
          - os: linux
            asset_name_suffix: linux.tar.gz
            asset_content_type: application/gzip
    steps:
      - uses: actions/download-artifact@v2
        with:
          name: artifact-${{ matrix.os }}

      - uses: actions/download-artifact@v2
        with:
          name: create-release

      - id: vars
        run: |
          echo "::set-output name=upload_url::$(cat upload_url.txt)"
      - name: Upload Release Asset
        id: upload-release-asset 
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.vars.outputs.upload_url }}
          asset_path: ${{ env.app-name }}_${{ matrix.asset_name_suffix }}
          asset_name: ${{ env.app-name }}_${{ matrix.asset_name_suffix }}
          asset_content_type: ${{ matrix.asset_content_type }}

解説すると

  • WindowsVM, MacVM, LinuxVMを並列に起動
  • それぞれのVMで実行可能JARを生成
  • jmodsのダウンロード
  • カスタムJREを作成
  • カスタムJRE、起動スクリプト類(.github/dist/*)、実行可能JARを収集して圧縮
  • artifactとしてアップロード
  • GitHub Releaseの作成
  • artifactをダウンロードし、Releasesにartifactをアップロード

という感じです。

結果として、以下のようにダウンロード可能になります。

https://github.com/jiro4989/iconisor/releases

まとめ

以下の話をしました。

  • Clojure+JavaFXアプリの実装
  • カスタムJREの作成方法
  • リリースワークフローの実装

ClojureでのJavaFXアプリ開発の一助となれば幸いです。

以上

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

AWSのJava SDK V2を使用して、Route53にAレコードを登録する

Main.java
package com.example;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.route53.Route53Client;
import software.amazon.awssdk.services.route53.model.Change;
import software.amazon.awssdk.services.route53.model.ChangeAction;
import software.amazon.awssdk.services.route53.model.ChangeBatch;
import software.amazon.awssdk.services.route53.model.ChangeResourceRecordSetsRequest;
import software.amazon.awssdk.services.route53.model.ChangeResourceRecordSetsResponse;
import software.amazon.awssdk.services.route53.model.RRType;
import software.amazon.awssdk.services.route53.model.ResourceRecord;
import software.amazon.awssdk.services.route53.model.ResourceRecordSet;

public class Main {

    public static void main(String[] args) throws IOException {

        Region region = Region.AWS_GLOBAL;
        Route53Client route53Client = Route53Client.builder()
                .region(region)
                .build();   

        upsertArecord("XXXXXXXXXXXXXXXXXXXX", "18.177.121.151", "test.example.com", route53Client);
    }

    public static void upsertArecord(String hostedZoneId, String ipAddress, String host, Route53Client route53Client) {

        ResourceRecord resourceRecord = ResourceRecord.builder().value(ipAddress).build();

        List<ResourceRecord> resourceRecords = new ArrayList<ResourceRecord>();
        resourceRecords.add(resourceRecord);

        // https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/route53/model/ResourceRecordSet.html
        // weightを入れるとSimpleタイプにならないので、weightは入れない。
        // ホスト名は、testではなく、test.example.com のような文字列にする必要がある。
        ResourceRecordSet resourceRecordSet = ResourceRecordSet.builder()
                .name(host)
                .type(RRType.A)
                .ttl(new Long(300))
                .resourceRecords(resourceRecords)
                .build();

        // https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/route53/model/Change.html
        // UPSERTにすることで、新規も更新もできる。
        Change change = Change.builder()
                .action(ChangeAction.UPSERT)
                .resourceRecordSet(resourceRecordSet).build();

        List<Change> changes = new ArrayList<Change>();
        changes.add(change);

        ChangeBatch changeBatch = ChangeBatch.builder()
                .changes(changes)
                .build();

        // https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/route53/model/ChangeResourceRecordSetsRequest.html
        ChangeResourceRecordSetsRequest request = ChangeResourceRecordSetsRequest.builder()
              .hostedZoneId(hostedZoneId)
              .changeBatch(changeBatch)
              .build();

        // 例外がthrowされなければ、要求の送信は完了(戻り値の状態値はPENDINGとなるが、それでOK)
        ChangeResourceRecordSetsResponse response = route53Client.changeResourceRecordSets(request);

        // Print the result
        // response.toString();     
    }

}
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>aws-sdk-java-route53</artifactId>
    <version>1.0</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <!-- https://github.com/aws/aws-sdk-java-v2/#using-the-sdk -->
    <!-- https://search.maven.org/search?q=g:software.amazon.awssdk%20AND%20a:bom -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>software.amazon.awssdk</groupId>
                <artifactId>bom</artifactId>
                <version>2.15.32</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>

        <!-- https://docs.aws.amazon.com/code-samples/latest/catalog/javav2-route53-pom.xml.html -->
        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>route53</artifactId>
        </dependency>

        <!-- slf4j -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.12</version>
        </dependency>

        <!-- http://www.slf4j.org/manual.html -->
        <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-simple -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.30</version>
        </dependency>

        <!-- logback -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.1.3</version>
        </dependency>

    </dependencies>

    <build>

        <finalName>my-route53</finalName>

        <plugins>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.3</version>
                <configuration>
                    <createDependencyReducedPom>false</createDependencyReducedPom>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.example.Main</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>

        </plugins>
    </build>

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

【Java】Calendar ( 日時の計算をするクラス )

Calendarクラス

【説明】

日付や 指定した日時の計算 」 などを行う場合に使用します。

【使用場面】

  • 入会してから90日後
  • 今日から1ヶ月と2週間後

といった、少々ややこしい計算が必要な時に便利です。

【基本構文】

java.util.Calendar の インポート

まずは import java.util.Calendar; で通常のようにインポートを行います。

// Calendarクラスのインポート
import java.util.Calendar;

getInstanceメソッド で オブジェクト の生成(重要)

Calendarクラスでは演算子newではなく、getInstanceメソッドを呼びオブジェクトを生成」します。

getInstanceメソッド

getInstanceメソッド は呼び出すごとに
現在の日付と時間に初期化された状態でオブジェクトを返す」メソッドです。

以上を踏まえて、Calendarクラスのインスタンス化( オブジェクト化 )のしかたはこちらです。

//Calendarクラスの オブジェクト の生成
Calendar calendar = Calendar.getInstance();

calendarクラスの「主要フィールド」のチートシート

フィールド 用途
YEAR
MONTH 月(0 ~ 11)
DATE,DAY_OF_MONTH 現在の 月 の 何日目 か
WEEK_OF_MONTH 現在の 月 の 何周目 か
DAY_OF_WEEK 現在の 週 の 何日目 か
AM_PM 午前 か 午後 か
HOUR 午前 / 午後 の 何時 か
HOUR_OF_DAY 24時間制 で 何時 か
MINUTE
SECOND

Calendarクラスに使うメソッド

Calendarクラスで特に使えそうなメソッドである、getTimeメソッド, setメソッド, addメソッド, formatメソッド のまとめです。

【使用場面】

  • getTimeメソッド (時刻を出力)
  • setメソッド (日時を設定する)
  • add (日付の加減算を行う)
  • format (文字列で日付のフォーマットを指定)

getTime()メソッド

【説明】

getTimeメソッドを利用することで、カレンダーから「 現在の時刻をjava.utilパッケージ の Dateオブジェクト 」 として取得できます。

【使用場面】

  • 現在時刻を表示したい時
  • setされた時刻を表示したい時

【基本構文】

基本的には カレンダーのオブジェクト名にドットでgetTime()繋げて呼び出します。

// getTime()
Calendarクラスのオブジェクト名.getTime()

【サンプルコード】 getTime()メソッドで現在時刻を出力

現在時刻を出力する場合以下のように記述します。

System.out.println(Calendarクラスのオブジェクト名.getTime());

getTime() メソッドを用いた一連の流れがこちらです。

package about_java_dateClass;

//Calendarクラスのインポート
import java.util.Calendar;

public class DateMain {

    public static void main(String[] args) {

        // Calendarクラスの オブジェクト の生成
        Calendar calendar = Calendar.getInstance();

        // 現在時刻を出力
        System.out.println(calendar.getTime());
    }
}

【実行結果】

Sun Nov 29 12:58:32 JST 2020

setメソッド (日時を設定する)

【説明】

setメソッドは日時を設定するために使用します。

【使用場面】

2020年を2030年に設定」といったような、日付を処理するようなプログラムによく使われます。

【基本構文】

calendar.set(年, 月, 日, 時, 分, 秒);

年月日時分をまとめて指定するとき

Calendar calendar = Calendar.getInstance();
calendar.set(2021, 1, 30, 22, 01, 55);
// 現在時刻を出力
System.out.println("現在時刻は" + date);
// setした時刻を出力
System.out.println("setした日時は" + calendar.getTime());

出力結果

現在時刻はSun Nov 29 12:38:43 JST 2020
setした日時はTue Mar 02 22:01:55 JST 2021

特定の日付/時刻要素(たとえば年だけ)を指定するとき

Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, 2040);

// 現在時刻を出力
System.out.println("現在時刻は" + date);
// setした時刻を出力
System.out.println("setした日時は" + calendar.getTime());

出力結果

現在時刻はSun Nov 29 12:40:19 JST 2020
setした日時はThu Nov 29 12:40:19 JST 2040

addメソッド (日付の加減算を行う)

【説明】

Calendarクラスで
現在時刻を基準に日付の加減算を行うとき 」には addメソッドを使用する
。addメソッドでは、指定された日付/時刻要素(getメソッドの表を参照)に対して、

  • 引数の値だけ加算
  • 引数が負数の場合には減算

を行います。

【使用場面】

現在時刻から何年後、何年前、何日後、何日前といった日にちを計算して表示するなど。

【基本構文】

Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.演算する対象, 値);

【サンプルコード】 現在時刻から3ヶ月前の日付を表示するプログラム

現在 日本時間で 2020年 11月 29日ですが、そこから addメソッドを使って日付操作を行ってみます。

//Calendarクラスのインポート
import java.util.Calendar;

public class DateMain {

    public static void main(String[] args) {

        Calendar calendar = Calendar.getInstance();

        // addメソッドで 5年前にする
        calendar.add(Calendar.YEAR, -5);
        // addメソッドで 1ヶ月後にする
        calendar.add(Calendar.MONTH, 1);
        // addメソッドで 10日前にする
        calendar.add(Calendar.DATE, -10);
        // 年を表示
        System.out.println(calendar.get(Calendar.YEAR) + "年");
        // 月を表示
        System.out.println(calendar.get(Calendar.MONTH) + "月");
        // 日を表示
        System.out.println(calendar.get(Calendar.DATE) + "日");

    }

}

【実行結果】

以上のプログラムで現在時刻 日本時間で 2020年 11月 29日を操作し実行結果が以下になります。

2015年
11月
19日

formatメソッド

【説明】

文字列で日付のフォーマットを指定することができるメソッドです。

【使用場面】

"〇〇〇〇年〇〇月〇〇日"といったような、馴染みのあるフォーマットに変えたい時に使用する。

日時の書式について

こちらが日時のフォーマットの書式をまとめたものになります。
こちらのアルファベットを用いてフォーマットを作成していきます。

書式 概要 パターン 表示
G 紀元 G AD
y year, 年 yyyy
yyy
2020
20
M Month, 月 MM
MMM
MMMM
03
Mar
March
d day, 日 d
dd
1
01
H 時(0-23) H
HH
0
00
K 午前/午後の時(0-11) K
KK
0
00
h 午前/午後の時(1-12) h
hh
1
01
m m
mm
0
00
s s
ss
0
00
S ミリ秒 S
SSS
1
001

【基本構文】

まずはパッケージ指定後、Calendarクラスといっしょにjava.text.SimpleDateFormat;をインポートします。

// java.text.SimpleDateFormat をインポート。
import java.text.SimpleDateFormat;

import java.util.Calendar;

インポート後インスタンス化を行います。SimpleDateFormatの略で「sdf」とする場合が多いかもしれません。
そして、時刻表示する場合のフォーマットを作成します。
以下の例場合の実行結果も添えます。

// Calendarクラスの オブジェクト の生成
        Calendar calendar = Calendar.getInstance();
        //日付のフォーマットを設定
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年 MM月 dd日 E曜日 HH時(hh時) mm分 ss秒 SSSミリ秒");
// フォーマットを適用し現在時刻を表示
System.out.println(sdf.format(calendar.getTime())); 

実行結果

2020年 11月 29日 日曜日 15時(03時) 04分 36秒 845ミリ秒

あとがき

最近だとjava.utilでの日付操作はモダンではないようですが、今の所現場で現役だそうなので、習得しておきたい。

参考資料

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

Spring Cloudを使ったマイクロサービスシステム構築(シングル構成)

概要

Gateway、EurekaといったSpring Cloudを使ってマイクロサービスを構築する。ここではシングル構成での構成で実現する。

前提

以下のURLから雛形を作成してマイクロサービスを作成する。

https://start.spring.io/

シングル構築でのマイクロサービス構築

まずはシングル構成で動くことを確認する。

スクリーンショット 2020-11-28 9.44.18.png

サーバは1台でその上に全てのマイクロサービスを実装する。ホスト名はlocalhostにした方が汎用的だが、将来クラスタ構成とするためホスト名であるms01を明示的に指定して構築する。

作成手順

最初にEurekaのマイクロサービスを作成し起動する。他のマイクロサービスはEurekaのクライアントとして動作するので、Eurekaサーバが動作している必要がある。Eurekaのクライアントが起動する際、クライアントの情報がEurekaサーバに登録される。

Eureka

Eurekaのコードはこんな感じ。

EurekaServerApplication.java
package comm.example.eurekaserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; //追加

@EnableEurekaServer  //追加
@SpringBootApplication
public class EurekaServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
application.yml
spring:
  cloud:
    inetutils:
      preferredNetworks:
        - 10.10.10

server:
  port: 8761
eureka:
  instance:
    hostname: ms01
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://ms01:8761/eureka/

Service-1/Service-2の作成

続きてユーザアプリケーションとしてのマイクロサービス、Service-1/Service-2を作成する。

Service-1のソース

App1ServiceApplication.java
package com.example.app1service;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;  //追加

@SpringBootApplication
@EnableEurekaClient   //追加
public class App1ServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(App1ServiceApplication.class, args);
    }

}
App1Controller.java
package com.example.app1service.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class App1Controller {

    @RequestMapping("/hello")
    public String hello() {
        return "Hello from Server-A";
    }

}
application.yml
spring:
  application:
    name: app1-service  # Eurekaの管理画面で表示されるサービス名

server:
  port: 8081

eureka:
  client:
    serviceUrl:
      defaultZone: http://ms01:8761/eureka/

Service-2もほぼ同様の作り。

App2ServiceApplication.java
package com.example.app2service;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;  //追加

@SpringBootApplication
@EnableEurekaClient     //追加
public class App2ServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(App2ServiceApplication.class, args);
    }

}
App2Controller.java
package com.example.app2service.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class App2Controller {

    @RequestMapping("/goodbye")
    public String hello() {
        return "GoodBye from EurekaClient App2";
    }
}
application.yml
spring:
  application:
    name: app2-service

server:
  port: 8082

eureka:
  client:
    serviceUrl:
      defaultZone: http://ms01:8761/eureka/

Gatewayの作成

Gatewayの作り方として、ルーティングをJavaで実装するパターンもあるようだが、ここではapplication.ymlファイルにルーティング情報を書くやり方で実現する。

GatewayApplication.java
package com.example.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class GatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }

}
application.yml
server:
  port: 8080

eureka:
  client:
    serviceUrl:
      defaultZone: http://ms01:8761/eureka/
spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      routes:
      - id: app1Module
        # uri: http://ms01:8081/ #単体で動かす場合はURL直指定でもOK。ただしクラスタに対応できない。
        uri: lb://APP1-SERVICE # application:name を指定する
        predicates:
        - Path=/hello/**       # app1で利用できるURLを指定
      - id: app2Module          # app2についてもapp1と同様の設定を行う
        uri: lb://APP2-SERVICE 
        predicates:
        - Path=/goodbye/**

デプロイ・起動

起動方法は全てのマイクロサービスで同じ手順なので代表例としてEurekaの手順を示すが、他のマイクロサービスも同様に起動できる。
Eclipse等でソースコードを作成後Mavenでeureka-server-0.0.1-SNAPSHOT.jarというjarファイルを作成する。で作成したjarファイルをサーバに転送して起動する。

なお起動順序は最初に言っているよう、Service-1/2およびGatewayはEurekaのクライアントとなるので、サーバであるEurekaを最初に起動し、その後EurekaクライアントのService-1/2およびGatewayを起動すること。
この例ではベタで起動しているが、systemctlで起動できるようサービス化してもいい。

[root@ms01 ~]# java -jar eureka-server-0.0.1-SNAPSHOT.jar

全てのマイクロサービスが起動したら、Eurekaの管理コンソール(っていうの?)には各マイクロサービスがEurekaのクライアントとして登録されている。

スクリーンショット 2020-11-28 13.57.50.png

単体で動くかチェックする

Service-1およびService-2については単独でも動作する。
ブラウザから下記URLを叩いて、正常に応答が帰ってくるようならSerivce-1/2は正しく動いている。

http://ms01:8081/hello
http://ms01:8082/goodbye

Gatewayを経由して動くかチェックする

Gatewayは8080ポートで待ち受けていて、'/hello'がService-1の/helloに、goodbyeがService-2の/goodbyeに振り分けられる。
http://ms01:8080/hello
http://ms01:8080/goodbye

ここまで動作すると、シングル構成でGatewayを経由して各マイクロサービスにルーティングできるようになる。
ユーザとしては、それぞれのマイクロサービスが異なるサーバで動作していても、サーバが複数あることを気にすることなくGatewayのURLとマイクロサービスのパスを知っているとサービスにアクセス可能となる。
ただし、この構成はシングル構成のため、例えばService-1が落ちた場合、サービスの提供ができなくなる。

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

【Java・SpringBoot】Spring JDBC でユーザー詳細画面(SpringBootアプリケーション実践編12)

ホーム画面からユーザー一覧画面に遷移し、ユーザーの詳細を表示するアプリケーションを作成して、Spring JDBCの使い方について学びます⭐️
前回はユーザー一覧画面を作ったので、今回はユーザー詳細画面を作ります^^
構成は前回の記事を参考にしてください

⭐️前回の記事
【Java・SpringBoot】Spring JDBC でユーザー一覧画面(SpringBootアプリケーション実践編11)

リポジトリークラスに1件検索するメソッドを実装

queryForMapメソッドで1件取得

  • 戻り値はMap型
  • 第1引数:SQL文、第2引数以降にPreparedStatementを指定
    • Map<String, Object> map = jdbc.queryForMap("SELECT * FROM m_user" + " WHERE user_id = ?", userId);
    • 戻り値のMapのgetメソッドにカラム名を指定することで、値を取得する
UserDaoJdbcImpl.java
package com.example.demo.login.domain.repository.jdbc;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.repository.UserDao;

@Repository("UserDaoJdbcImpl")
public class UserDaoJdbcImpl implements UserDao {

    @Autowired
    JdbcTemplate jdbc;

    // Userテーブルの件数を取得.
    @Override
    public int count() throws DataAccessException {

        //全件取得してカウント
        int count = jdbc.queryForObject("SELECT COUNT(*) FROM m_user", Integer.class);

        return count;
    }

    // Userテーブルにデータを1件insert.
    @Override
    public int insertOne(User user) throws DataAccessException {

        //1件登録
        int rowNumber = jdbc.update("INSERT INTO m_user(user_id,"
                + " password,"
                + " user_name,"
                + " birthday,"
                + " age,"
                + " marriage,"
                + " role)"
                + " VALUES(?, ?, ?, ?, ?, ?, ?)",
                user.getUserId(),
                user.getPassword(),
                user.getUserName(),
                user.getBirthday(),
                user.getAge(),
                user.isMarriage(),
                user.getRole());

        return rowNumber;
    }

    // Userテーブルのデータを1件取得
    @Override
    public User selectOne(String userId) throws DataAccessException {

        // 1件取得
        Map<String, Object> map = jdbc.queryForMap("SELECT * FROM m_user"
                + " WHERE user_id = ?", userId);

        // 結果返却用の変数
        User user = new User();

        // 取得したデータを結果返却用の変数にセットしていく
        user.setUserId((String) map.get("user_id")); //ユーザーID
        user.setPassword((String) map.get("password")); //パスワード
        user.setUserName((String) map.get("user_name")); //ユーザー名
        user.setBirthday((Date) map.get("birthday")); //誕生日
        user.setAge((Integer) map.get("age")); //年齢
        user.setMarriage((Boolean) map.get("marriage")); //結婚ステータス
        user.setRole((String) map.get("role")); //ロール

        return user;
    }

    // Userテーブルの全データを取得.
    @Override
    public List<User> selectMany() throws DataAccessException {

        // M_USERテーブルのデータを全件取得
        List<Map<String, Object>> getList = jdbc.queryForList("SELECT * FROM m_user");

        // 結果返却用の変数
        List<User> userList = new ArrayList<>();

        // 取得したデータを結果返却用のListに格納していく
        for (Map<String, Object> map : getList) {

            //Userインスタンスの生成
            User user = new User();

            // Userインスタンスに取得したデータをセットする
            user.setUserId((String) map.get("user_id")); //ユーザーID
            user.setPassword((String) map.get("password")); //パスワード
            user.setUserName((String) map.get("user_name")); //ユーザー名
            user.setBirthday((Date) map.get("birthday")); //誕生日
            user.setAge((Integer) map.get("age")); //年齢
            user.setMarriage((Boolean) map.get("marriage")); //結婚ステータス
            user.setRole((String) map.get("role")); //ロール

            //結果返却用のListに追加
            userList.add(user);
        }

        return userList;
    }

    // Userテーブルを1件更新.
    @Override
    public int updateOne(User user) throws DataAccessException {
        return 0;
    }

    // Userテーブルを1件削除.
    @Override
    public int deleteOne(String userId) throws DataAccessException {
        return 0;
    }

    //SQL取得結果をサーバーにCSVで保存する
    @Override
    public void userCsvOut() throws DataAccessException {

    }
}

サービスクラスに1件取得用のメソッド追加

UserService.java
package com.example.demo.login.domain.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.repository.UserDao;

@Service
public class UserService {
    @Autowired
    UserDao dao;

    public boolean insert(User user) {
        // insert実行
        int rowNumber = dao.insertOne(user);
        // 判定用変数
        boolean result = false;

        if (rowNumber > 0) {
            // insert成功
            result = true;
        }
        return result;
    }

    public int count() {
        return dao.count();
    }

    public List<User> selectMany() {
        // 全件取得
        return dao.selectMany();
    }

    /**
     * 1件取得用メソッド.
     */
    public User selectOne(String userId) {
        // selectOne実行
        return dao.selectOne(userId);
    }
}

ホーム画面用のコントローラークラスを修正

動的なURLに対応したメソッドを作る

  • @GetMapping@PostMappingの値に/{<変数名>}を付ける
    • ユーザーIDを受け取る場合は、@GetMapping(/userDetail/{id})
    • 通常は/userDetail/{id}とすればいいが。。。
    • ユーザーIDがメールアドレス形式の場合、sample@xxx.co.jpというユーザーIDが渡されてくると、sample@xxx.coしか受け取れない
      • 正規表現を使用して@GetMapping("/userDetail/{id:.+}")とする

@PathVariable

  • @PathVariableアノテーションを付けると、渡されてきたパス(URL)の値を引数の変数に入れることができる
    • 例:@PathVariable("id") String userId) {...}
    • http://localhost:8080/userDetail/sample@xxx.co.jpというURLでリクエストが来た場合、
      sample@xxx.co.jpという値が引数のuserIdという変数に入れられる
HomeController.java
package com.example.demo.login.controller;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import com.example.demo.login.domain.model.SignupForm;
import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.service.UserService;

@Controller
public class HomeController {

    @Autowired
    UserService userService;
    //結婚ステータスのラジオボタン用変数
    private Map<String, String> radioMarriage;

    /**
     * ラジオボタンの初期化メソッド
     */
    private Map<String, String> initRadioMarrige() {
        Map<String, String> radio = new LinkedHashMap<>();
        // 既婚、未婚をMapに格納
        radio.put("既婚", "true");
        radio.put("未婚", "false");
        return radio;
    }

    @GetMapping("/home")
    public String getHome(Model model) {
        //コンテンツ部分にユーザー詳細を表示するための文字列を登録
        model.addAttribute("contents", "login/home :: home_contents");
        return "login/homeLayout";
    }

    @GetMapping("/userList")
    public String getUserList(Model model) {

        //コンテンツ部分にユーザー一覧を表示するための文字列を登録
        model.addAttribute("contents", "login/userList :: userList_contents");
        //ユーザー一覧の生成
        List<User> userList = userService.selectMany();
        //Modelにユーザーリストを登録
        model.addAttribute("userList", userList);
        //データ件数を取得
        int count = userService.count();
        model.addAttribute("userListCount", count);
        return "login/homeLayout";
    }

    /**
     * ユーザー詳細画面のGETメソッド用処理.
     */
    @GetMapping("/userDetail/{id:.+}")
    public String getUserDetail(@ModelAttribute SignupForm form,
            Model model,
            @PathVariable("id") String userId) {
        // ユーザーID確認
        System.out.println("userId = " + userId);

        // コンテンツ部分にユーザー詳細を表示するための文字列を登録
        model.addAttribute("contents", "login/userDetail :: userDetail_contents");
        // 結婚ステータス用ラジオボタンの初期化
        radioMarriage = initRadioMarrige();
        // ラジオボタン用のMapをModelに登録
        model.addAttribute("radioMarriage", radioMarriage);

        // ユーザーIDのチェック
        if (userId != null && userId.length() > 0) {
            // ユーザー情報を取得
            User user = userService.selectOne(userId);
            // Userクラスをフォームクラスに変換
            form.setUserId(user.getUserId()); //ユーザーID
            form.setUserName(user.getUserName()); //ユーザー名
            form.setBirthday(user.getBirthday()); //誕生日
            form.setAge(user.getAge()); //年齢
            form.setMarriage(user.isMarriage()); //結婚ステータス
            // Modelに登録
            model.addAttribute("signupForm", form);
        }
        return "login/homeLayout";
    }

    @PostMapping("/logout")
    public String postLogout() {
        return "redirect:/login";
    }

    @GetMapping("/userList/csv")
    public String getUserListCsv(Model model) {
        return getUserList(model);
    }
}

ユーザー詳細画面作成

userDetail.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <meta charset="UTF-8"></meta>
</head>
<body>
    <div th:fragment="userDetail_contents">
        <div class="row">
            <div class="col-sm-5">
                <div class="page-header">
                    <h1>ユーザー詳細</h1>
                </div>
                <form method="post" th:action="@{/userDetail}" th:object="${signupForm}">
                    <table class="table table-bordered table-hover">
                        <tr>
                            <!-- ユーザーID(入力不可) -->
                            <th class="active col-sm-2">ユーザID</th>
                            <td class="col-sm-3">
                                <div class="form-group">
                                    <input type="text" class="form-control"
                                        th:field="*{userId}"
                                        readonly="readonly" />
                                </div>
                            </td>
                        </tr>
                        <tr>
                            <!-- パスワード -->
                            <th class="active">パスワード</th>
                            <td>
                                <div class="form-group"
                                    th:classappend="${#fields.hasErrors('password')} ? 'has-error'">
                                    <input type="text" class="form-control"
                                        th:field="*{password}" />
                                    <span class="text-danger"
                                        th:if="${#fields.hasErrors('password')}"
                                        th:errors="*{password}">
                                        password error
                                    </span>
                                </div>
                            </td>
                        </tr>
                        <tr>
                            <!-- ユーザー名 -->
                            <th class="active">ユーザ名</th>
                            <td>
                                <div class="form-group"
                                    th:classappend="${#fields.hasErrors('userName')} ? 'has-error'">
                                    <input type="text" class="form-control"
                                        th:field="*{userName}" />
                                    <span class="text-danger"
                                        th:if="${#fields.hasErrors('userName')}"
                                        th:errors="*{userName}">
                                        userName error
                                    </span>
                                </div>
                            </td>
                        </tr>
                        <tr>
                            <!-- 誕生日 -->
                            <th class="active">誕生日</th>
                            <td>
                                <div class="form-group"
                                    th:classappend="${#fields.hasErrors('birthday')} ? 'has-error'">
                                    <input type="text" class="form-control"
                                        placeholder="yyyy/MM/dd"
                                        th:field="*{birthday}" />
                                    <span class="text-danger"
                                        th:if="${#fields.hasErrors('birthday')}"
                                        th:errors="*{birthday}">
                                        birthday error
                                    </span>
                                </div>
                            </td>
                        </tr>
                        <tr>
                            <!-- 年齢 -->
                            <th class="active">年齢</th>
                            <td>
                                <div class="form-group"
                                    th:classappend="${#fields.hasErrors('age')} ? 'has-error'">
                                    <input type="text" class="form-control"
                                        th:field="*{age}" />
                                    <span class="text-danger"
                                        th:if="${#fields.hasErrors('age')}"
                                        th:errors="*{age}">
                                        age error
                                    </span>
                                </div>
                            </td>
                        </tr>
                        <tr>
                            <!-- 結婚 -->
                            <th class="active">結婚</th>
                            <td>
                                <div class="form-group">
                                    <div th:each="item : ${radioMarriage}">
                                        <input type="radio" name="radioMarrige"
                                            th:value="${item.value}"
                                            th:text="${item.key}"
                                            th:field="*{marriage}">
                                        </input>
                                    </div>
                                    <span class="text-danger"
                                        th:if="${#fields.hasErrors('marriage')}"
                                        th:errors="*{marriage}">
                                        marriage error
                                    </span>
                                </div>
                            </td>
                        </tr>
                    </table>
                    <!-- 更新ボタン -->
                    <button class="btn btn-primary btn-lg pull-right"
                        type="submit"
                        name="update">
                        更新
                    </button>
                    <!-- 削除ボタン -->
                    <button class="btn btn-danger btn-lg" type="submit" name="delete">
                        削除
                    </button>
                </form>
            </div>
        </div>
    </div>
</body>
</html>

SpringBootを起動してユーザ一覧画面確認!

  • http://localhost:8080/userList
  • ユーザ一覧から、ユーザ詳細画面に遷移します
  • パスワード以外の情報が反映されました〜^^

詳細.png

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

Java 二次元配列

はじめに

学習用のメモになります。

二次元配列とは?

二次元配列とは、2つのインデックスで要素を指定する配列のことです。

詳しくはこちら!

サンプルコード

Main.java
public class Main {
    public static void main(String[] args) {
        String[][] teams = {{"勇者", "戦士", "魔法使い"}, {"盗賊", "忍者", "商人"}, {"スライム", "ドラゴン", "魔王"}};

        System.out.println(teams[0][0]);
         //実行結果 :勇者

        teams[0][0] = "剣士";
        // 勇者 → 剣士 に更新

        System.out.println(teams[0][0]);
        //実行結果:剣士


        System.out.println(teams.length);
        //実行結果:2
        //配列の長さ

        System.out.println(teams[0].length);
        //実行結果:3
        //teams[0]の配列の長さ

    }
}

二次元配列の宣言

Main.java
 String[][] teams = {{"勇者", "戦士", "魔法使い"}, {"盗賊", "忍者", "商人"}, {"スライム", "ドラゴン", "魔王"}};

二次元配列変数の宣言は、配列の型と次元がふたつあることを宣言する。変数の型の後に[]をふたつ書けば二次元配列になる。

二次元配列をループ処理する

Main.java
// 2次元配列をループで処理する
public class Main {
    public static void main(String[] args) {
        String[][] teams = {{"勇者", "戦士", "魔法使い"}, {"盗賊", "忍者", "商人"}, {"スライム", "ドラゴン", "魔王"}};

        for (int i = 0; i < teams.length; i++) {
            for(int j = 0;  j < teams[i].length; j++) {
                System.out.print(teams[i][j] + " ");
            }
            System.out.println("");

        }
        //出力結果
        //勇者 戦士 魔法使い 
        //盗賊 忍者 商人 
        //スライム ドラゴン 魔王 


        //拡張forでのループ処理
        for (String[] team : teams) {
            for (String player : team) {
                System.out.print(player + " ");
            }
        }
    }
}

二次元配列をnewで作成する

Main.java
// 2次元配列をnewで作成する

public class Main {
    public static void main(String[] args) {
        int[][] numberA = new int[3][4];
        for(int i =0;i<numberA.length; i++){
            for(int j =0;j<numberA[i].length;j++){
                numberA[i][j]=i*10+j;      //初期値を指定している
                System.out.print(numberA[i][j]+" ");  

            }
            System.out.println("");
            System.out.println("---");
        }
    }
}

出力結果

0 1 2 3 
---
10 11 12 13 
---
20 21 22 23 
---
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Java】Date ( 日付操作 )

Date

Dateクラスは、「日付を管理しているクラス」です。

【使用場面】

上記のように Dateクラスは

  • 簡単に日付と時刻を取得できる
  • 日付の計算には不向き

以上のような特性のため、「単純に時刻を表示したい時」につかうといいとおもいます。

【書き方】

まず、Javaで日付を操作するときには以下を インポート する必要がありますので、こちらをパッケージの下に記述してください。

// Dateのインポート
import java.util.Date;

インポートしましたら、通常通りインスタンス化を行います。

// Dateのインスタンス化
Date date = new Date();

【サンプルコード】 現在時刻を表示

こちらは単純に現在時刻を表示するプログラムです。

System.out.println(インスタンス名);で現在時刻を表示します。

package about_java_dateClass;

//Dateのインポート
import java.util.Date;

public class DateMain {

    public static void main(String[] args) {
        // Dateのインスタンス化
        Date date = new Date();

        // 現在時刻を出力
        System.out.println(date);

    }

}

【実行結果】

Sun Nov 29 11:02:14 JST 2020

【補足】

タイムゾーン

上記の実行結果の 「JST」 の部分です。
日本標準時刻 が世界標準+9時間 であることを示しています。

つまり上記の実行結果は「 日本時間で○○ですよ 」といった意味合いなると思います。

GMT(グリニッジ標準時)

世界標準時刻 は、 GMT(グリニッジ標準時) と呼ばれ、
その GMT を基準として、

  • 日本: JST
  • アメリカ: PST

のように表記し、「 GMT ± n時間の差 」があることを明示的に示します。

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

Javaのマルチスレッドについての学習

Java8でマルチスレッドとマルチプロセスの勉強をする。

1.はじめに

1-1.イメージ

マルチプロセスとマルチスレッドの簡単なイメージは以下になる。

image.png

マルチプロセスの場合は、メモリ空間が別になるので、データのやり取りにはプロセス間通信が必要になる。

逆にマルチスレッドでは、メモリ空間が同じなため、複数スレッドが同時に同じデータにアクセスして、「レースコンディション」のバグがでないように、排他制御を行い、「スレッドセーフ」にする必要がある。

1-2.非同期処理と並列処理について

マルチスレッドのメリットを2つ記載する。
・「フリーズ」状態にならず、すぐに応答する
 →非同期処理(並行)
・パフォーマンスが良くなる
 →並列処理

1-2.準備

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

【Java・SpringBoot】Spring JDBC でユーザー一覧画面(SpringBootアプリケーション実践編11)

ホーム画面からユーザー一覧画面に遷移し、ユーザーの詳細を表示するアプリケーションを作成して
Spring JDBCの使い方について学びます⭐️
今回はユーザーを登録(insert)した結果を確認できるように、ユーザー一覧画面を実装します!
構成は前回の記事を参考にしてください

⭐️前回の記事
【Java・SpringBoot】Spring JDBC(SpringBootアプリケーション実践編9)
【Java・SpringBoot】Spring JDBC でユーザー登録(SpringBootアプリケーション実践編10)

リポジトリークラス実装

queryForObjectメソッドでObjectの取得

  • 第1引数にSQL文、第2引数に戻り値のオブジェクトのclassを指定
    • int count = jdbc.queryForObject("SELECT COUNT(*) FROM m_user", Integer.class);

queryForListメソッドで複数件のselect

  • 戻り値の型にはList<Map<String,Object>>を指定
    • Listが行、Mapが列
      • List<Map<String, Object>> getList = jdbc.queryForList("SELECT * FROM m_user");
    • Mapのgetメソッドでテーブルのカラム名を指定することで値を取得可能
    • 引数を追加すれば、PreparedStatementを使える
    • 以下では拡張for文を使って、List<Map<String,Object>>List<User>に変換している
      • for (Map<String, Object> map : getList) {...}
UserDaoJdbcImpl.java
package com.example.demo.login.domain.repository.jdbc;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.repository.UserDao;

@Repository("UserDaoJdbcImpl")
public class UserDaoJdbcImpl implements UserDao {

    @Autowired
    JdbcTemplate jdbc;

    // Userテーブルの件数を取得.
    @Override
    public int count() throws DataAccessException {

        //全件取得してカウント
        int count = jdbc.queryForObject("SELECT COUNT(*) FROM m_user", Integer.class);
        return count;
    }

    // Userテーブルにデータを1件insert.
    @Override
    public int insertOne(User user) throws DataAccessException {

        //1件登録
        int rowNumber = jdbc.update("INSERT INTO m_user(user_id,"
                + " password,"
                + " user_name,"
                + " birthday,"
                + " age,"
                + " marriage,"
                + " role)"
                + " VALUES(?, ?, ?, ?, ?, ?, ?)",
                user.getUserId(),
                user.getPassword(),
                user.getUserName(),
                user.getBirthday(),
                user.getAge(),
                user.isMarriage(),
                user.getRole());

        return rowNumber;
    }

    // Userテーブルのデータを1件取得
    @Override
    public User selectOne(String userId) throws DataAccessException {
        return null;
    }

    // Userテーブルの全データを取得.
    @Override
    public List<User> selectMany() throws DataAccessException {

        // M_USERテーブルのデータを全件取得
        List<Map<String, Object>> getList = jdbc.queryForList("SELECT * FROM m_user");
        // 結果返却用の変数
        List<User> userList = new ArrayList<>();
        // 取得したデータを結果返却用のListに格納していく
        for (Map<String, Object> map : getList) {

            //Userインスタンスの生成
            User user = new User();
            // Userインスタンスに取得したデータをセットする
            user.setUserId((String) map.get("user_id")); //ユーザーID
            user.setPassword((String) map.get("password")); //パスワード
            user.setUserName((String) map.get("user_name")); //ユーザー名
            user.setBirthday((Date) map.get("birthday")); //誕生日
            user.setAge((Integer) map.get("age")); //年齢
            user.setMarriage((Boolean) map.get("marriage")); //結婚ステータス
            user.setRole((String) map.get("role")); //ロール
            //結果返却用のListに追加
            userList.add(user);
        }
        return userList;
    }

    // Userテーブルを1件更新.
    @Override
    public int updateOne(User user) throws DataAccessException {
        return 0;
    }

    // Userテーブルを1件削除.
    @Override
    public int deleteOne(String userId) throws DataAccessException {
        return 0;
    }

    //SQL取得結果をサーバーにCSVで保存する
    @Override
    public void userCsvOut() throws DataAccessException {
    }
}

サービスクラス

  • カウントと複数検索用のメソッドを用意
UserService.java
package com.example.demo.login.domain.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.repository.UserDao;

@Service
public class UserService {
    @Autowired
    UserDao dao;

    public boolean insert(User user) {
        // insert実行
        int rowNumber = dao.insertOne(user);
        // 判定用変数
        boolean result = false;

        if (rowNumber > 0) {
            // insert成功
            result = true;
        }
        return result;
    }

    //カウント用メソッド.
    public int count() {
        return dao.count();
    }

    //全件取得用メソッド.
    public List<User> selectMany() {
        // 全件取得
        return dao.selectMany();
    }
}

ホーム画面用のコントローラークラスを修正

  • ホーム画面のユーザー管理というリンクをクリックすると、データベースから全ユーザーの情報を取ってくる
  • コントローラークラスでは、カウント結果と複数検索結果をModelクラスに登録(addAttribute)し、ユーザー一覧画面で、Modelから値を取得して表示する
HomeController.java
package com.example.demo.login.controller;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.service.UserService;

@Controller
public class HomeController {

    @Autowired
    UserService userService;

    @GetMapping("/home")
    public String getHome(Model model) {

        //コンテンツ部分にユーザー詳細を表示するための文字列を登録
        model.addAttribute("contents", "login/home :: home_contents");
        return "login/homeLayout";
    }

    @GetMapping("/userList")
    public String getUserList(Model model) {

        //コンテンツ部分にユーザー一覧を表示するための文字列を登録
        model.addAttribute("contents", "login/userList :: userList_contents");
        //ユーザー一覧の生成
        List<User> userList = userService.selectMany();
        //Modelにユーザーリストを登録
        model.addAttribute("userList", userList);
        //データ件数を取得
        int count = userService.count();
        model.addAttribute("userListCount", count);
        return "login/homeLayout";
    }

    @PostMapping("/logout")
    public String postLogout() {
        //ログイン画面にリダイレクト
        return "redirect:/login";
    }

    @GetMapping("/userList/csv")
    public String getUserListCsv(Model model) {
        return getUserList(model);
    }
}

ユーザー一覧画面を作成

日付型のフォーマット指定

  • #dates.format()メソッド
    • 第1引数:フォーマットする値
    • 第2引数:フォーマットを指定
      • <td th:text="${#dates.format(user.birthday, 'YYYY/MM/dd')}"></td>

動的URL

  • ユーザーを1件取得して表示するには、検索するためのユーザーIDをコントローラークラスに渡す必要がある
  • th:hrefの値の中にユーザーIDを入れる
    • <a class="btn btn-primary" th:href="@{'/userDetail/' + ${user.userId}}">
userList.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <meta charset="UTF-8"></meta>
</head>
<body>
    <!-- コンテンツ部分 -->
    <div th:fragment="userList_contents">
        <div class="page-header">
            <h1>ユーザー一覧</h1>
        </div>
        <table class="table table-bordered table-hover table-striped">
            <tr>
                <th class="info col-sm-2">ユーザID</th>
                <th class="info col-sm-2">ユーザ名</th>
                <th class="info col-sm-2">誕生日</th>
                <th class="info col-sm-2">年齢</th>
                <th class="info col-sm-2">結婚</th>
                <th class="info col-sm-2"></th>
            </tr>
            <tr th:each="user : ${userList}">
                <td th:text="${user.userId}"></td>
                <td th:text="${user.userName}"></td>
                <td th:text="${#dates.format(user.birthday, 'YYYY/MM/dd')}"></td>
                <td th:text="${user.age}"></td>
                <td th:text="${user.marriage} ? '既婚' : '未婚'"></td>
                <td>
                    <!-- ユーザー詳細画面へのリンク(動的URL) -->
                    <a class="btn btn-primary" th:href="@{'/userDetail/' + ${user.userId}}">
                        詳細
                    </a>
                </td>
            </tr>
        </table>
        <!-- ユーザー一覧の件数 -->
        <label th:text=" '合計:' + ${userListCount} + '件' "></label><br/>
        <!-- 更新・削除処理の結果表示用 -->
        <label class="text-info" th:text="${result}">結果表示</label><br/>
        <!-- CSV出力用のリンク -->
        <a class="btn btn-primary" th:href="@{'/userList/csv'}">CSV出力</a>
    </div>
</body>
</html>

SpringBootを起動して画面を確認!

  • ログイン後のホーム画面から、ユーザー管理のリンクをクリックすると、ユーザー一覧画面が表示されます
  • ユーザー登録画面で登録をすると、この画面で見れるようになりました〜^^

ユーザ管理画面.png

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

Java 標準入力

はじめに

学習用のメモになります。

標準入力で1行データを読み込む

//標準入力

import java.util.*;
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String data = sc.nextLine();
    }
}

1行に複数要素がある場合

splitメソッドで,区切りで要素を配列(array)に代入する

import java.util.*;
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String data = sc.nextLine();

        String[] array = data.split("、");
        System.out.println(array[0]);
    }
}

複数行の要素を出力する場合

import java.util.*;
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        while (sc.hasNextLine()) {
            String data = sc.nextLine();
            System.out.println(data);
        }
    }
}

hasNextLineは入力する行がまだあるかみているメソッド

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

Java オブジェクト(Object Oriented Programming)とコンストラクタ、スコープ(local,global)

1,オブジェクトとは

Object is an instance of a class that may contain attributes and methods.
車、人間、サイコロ、ピザのインスタンスを作りながら様々なルールを覚えていきましょう!

2,ソースコード

ルール1,mainメソッドとは別のファイルに記述する

mainクラスを記述しているクラスをアプリケーションクラス、それ以外を、一般クラスと言う。

Main.java
public class Main {

    public static void main(String[] args) {

        // Object Oriented Programming
        // object = an instance of a class that may contain attributes and methods.

        Car myCar1 = new Car();
        Car myCar2 = new Car();

        System.out.println(myCar1.make);
        System.out.println(myCar1.model);
        System.out.println();
        System.out.println(myCar2.make);
        System.out.println(myCar2.model);

        myCar1.drive();
        myCar1.brake();

    }

}

※一般クラスには、mainメソッドは記述しない?

Car.java
public class Car {

    String make = "Chevrolet";
    String model = "Corvette";
    int year = 2020;
    String color = "blue";
    double price = 50000.00;

    void drive() {
        System.out.println("You drive the car.");
    }

    void brake() {
        System.out.println("You step on the brakes.");
    }

}

ルール2,コンストラクターを作る時は、クラス名と同じメソッド名にし先頭は大文字

Main.java
public class Main {

    public static void main(String[] args) {

        // constructor = special method that is called when an object is instantiated(created).

        Human human1 = new Human("hiro", 25, 70);
        Human human2 = new Human("taro", 20, 65);

        System.out.println(human1.name);
        human1.eat();
        human1.drink();
        System.out.println(human2.name);
        human2.eat();
        human2.drink();

    }

}

※public Huan()がコンストラクタになります?

Human.java
public class Human {

    String name;
    int age;
    double weight;

    public Human(String name, int age, double weight) {
        this.name = name;
        this.age = age;
        this.weight = weight;
    }

    public void eat() {
        System.out.println(this.name + "is eating.");
    }

    public void drink() {
        System.out.println(this.name + "is drinking");
    }

}

ルール3,スコープにも注意

Main.java
public class Main {

    public static void main(String[] args) {

        DiceRoller diceRoller = new DiceRoller();

    }

}

※フィールドに書くと、global変数になる?

DiceRoller.java
import java.util.Random;

public class DiceRoller {

    Random random;
    int number = 0;

    public DiceRoller() {
        random = new Random();
        int number = 0;
        roll();
    }

    void roll() {
        number = random.nextInt(6) + 1;
        System.out.println(number);

    }

}

ルール4,コンストラクタはデータ型、順番、パラメーターの数が重要

Main.java
public class Main {

    public static void main(String[] args) {

        Pizza pizza = new Pizza("thicc crust", "tomato", "mozzerella", "pepperoni");

        System.out.println("Here are the ingredients of your pizza");
        System.out.println(pizza.bread);
        System.out.println(pizza.sauce);
        System.out.println(pizza.cheese);
        System.out.println(pizza.topping);

    }

}

※コンストラクタのデータ型、順番、パラメーターの数に注意?

DiceRoller.java
public class Pizza {

    String bread;
    String sauce;
    String cheese;
    String topping;

    public Pizza() {
    }

    public Pizza(String bread) {
        this.bread = bread;

    }

    public Pizza(String bread, String sauce) {
        this.bread = bread;
        this.sauce = sauce;
    }

    public Pizza(String bread, String sauce, String cheese) {
        this.bread = bread;
        this.sauce = sauce;
        this.cheese = cheese;
    }

    public Pizza(String bread, String sauce, String cheese, String topping) {
        this.bread = bread;
        this.sauce = sauce;
        this.cheese = cheese;
        this.topping = topping;
    }

Java チュートリアル:clipboard:

:coffee: Java テキストファイル(.txt .csv)への出力 java.io.FileWriter : :white_check_mark:here
:coffee: Java テキストファイル(.txt .csv)からの入力 java.io.FileReader : :white_check_mark:here
:coffee: Java モニターからの入力 java.util.Scanner : :white_check_mark:here
:coffee: Java GUIからの入力 javax.swing.JOptionPane : :white_check_mark:here
:coffee: Java Mathクラス Math.sqrt : :white_check_mark:here
:coffee: Java 乱数の生成 java.util.Random : :white_check_mark:here
:coffee: Java Stringクラス : :white_check_mark:here
:coffee: Java Wrapperクラス : :white_check_mark:here
:coffee: Java ArrayListクラス java.util.ArrayList : :white_check_mark:here
:coffee: Java メソッド(method)の作り方とオーバーロード : :white_check_mark:here
:coffee: Java printf()メソッド : :white_check_mark:here
:coffee: Java オブジェクト(Object Oriented Programming)とコンストラクタ、スコープ(local,global) : :white_check_mark:here

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

Java printf()メソッド

1,printf() メソッドは、フォーマットに用いるメソッドです。

2,ソースコード

Main.java
public class Main {

    public static void main(String[] args) {

        System.out.printf("%d This is a format String ", 123);

        boolean myBoolean = true;
        char myChar = '@';
        String myString = "hiro";
        int myInt = 50;
        double myDouble = 1000;

        System.out.printf("%b", myBoolean);
        System.out.printf("%c", myChar);
        System.out.printf("%s", myString);
        System.out.printf("%d", myInt);
        System.out.printf("%f", myDouble);

        System.out.printf("Hello %10s", myString); //
        System.out.printf("Hello %-10s", myString); //

        System.out.printf("You have this much money %.2f", myDouble);
        System.out.printf("You have this much money %.1f", myDouble);
        System.out.printf("You have this much money %f", myDouble);

        System.out.printf("You have this much money %+f", myDouble);
        System.out.printf("You have this much money %020f", myDouble);
        System.out.printf("You have this much money %,f", myDouble);

    }

}

Java チュートリアル:clipboard:

:coffee: Java テキストファイル(.txt .csv)への出力 java.io.FileWriter : :white_check_mark:here
:coffee: Java テキストファイル(.txt .csv)からの入力 java.io.FileReader : :white_check_mark:here
:coffee: Java モニターからの入力 java.util.Scanner : :white_check_mark:here
:coffee: Java GUIからの入力 javax.swing.JOptionPane : :white_check_mark:here
:coffee: Java Mathクラス Math.sqrt : :white_check_mark:here
:coffee: Java 乱数の生成 java.util.Random : :white_check_mark:here
:coffee: Java Stringクラス : :white_check_mark:here
:coffee: Java Wrapperクラス : :white_check_mark:here
:coffee: Java ArrayListクラス java.util.ArrayList : :white_check_mark:here
:coffee: Java メソッド(method)の作り方とオーバーロード : :white_check_mark:here
:coffee: Java printf()メソッド : :white_check_mark:here
:coffee: Java オブジェクト(Object Oriented Programming)とコンストラクタ、スコープ(local,global) : :white_check_mark:here

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

Java メソッド(method)の作り方とオーバーロード

1,Javaではクラスメソッドをオリジナルで作ることができます。

※一つ一つルールをしっかり覚える必要があります。

2,ソースコード

ルール1,static を必ずつける

Main.java
public class Main {

    public static void main(String[] args) {

        String name = "hiro";
        int age = 21;

        hello(name, age);

        int x = 3;
        int y = 4;
        int ans = add(x, y);
        System.out.println(ans);

    }

    public static void hello(String name, int age) {
        System.out.println("Hello " + name);
        System.out.println("You are " + age + " years old.");
    }

    public static int add(int x, int y) {
        int ans = x + y;
        return ans;
    }

}

ルール2,オーバーロードをする場合は同じメソッド名にし、異なるパラメーターを書く

Main.java
public class Main {

    public static void main(String[] args) {

        int a = add(1, 2);
        System.out.println(a);
        int b = add(1, 2, 3);
        System.out.println(b);

    }

    public static int add(int a, int b) {
        System.out.println("This is overloaded method #1 ");
        return a + b;
    }

    public static int add(int a, int b, int c) {
        System.out.println("This is overloaded method #2 ");
        return a + b + c;
    }

}

Java チュートリアル:clipboard:

:coffee: Java テキストファイル(.txt .csv)への出力 java.io.FileWriter : :white_check_mark:here
:coffee: Java テキストファイル(.txt .csv)からの入力 java.io.FileReader : :white_check_mark:here
:coffee: Java モニターからの入力 java.util.Scanner : :white_check_mark:here
:coffee: Java GUIからの入力 javax.swing.JOptionPane : :white_check_mark:here
:coffee: Java Mathクラス Math.sqrt : :white_check_mark:here
:coffee: Java 乱数の生成 java.util.Random : :white_check_mark:here
:coffee: Java Stringクラス : :white_check_mark:here
:coffee: Java Wrapperクラス : :white_check_mark:here
:coffee: Java ArrayListクラス java.util.ArrayList : :white_check_mark:here
:coffee: Java メソッド(method)の作り方とオーバーロード : :white_check_mark:here
:coffee: Java printf()メソッド : :white_check_mark:here
:coffee: Java オブジェクト(Object Oriented Programming)とコンストラクタ、スコープ(local,global) : :white_check_mark:here

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

Java ArrayListクラス java.util.ArrayList

1,ArrayListの作成方法

➊ArrayList food = new ArrayList();

2,ソースコード

1次元配列

Main.java
import java.util.ArrayList;

public class Main {

    public static void main(String[] args) {

        ArrayList<String> food = new ArrayList<String>();

        food.add("pizza");
        food.add("hambueger");
        food.add("hotdog");

        food.set(0, "sushi");
        food.remove(2);
        food.clear();

        for (int i = 0; i < food.size(); i++) {
            System.out.println(food.get(i));
        }
    }

}

2次元配列

Main.java
import java.util.ArrayList;

public class Main {

    public static void main(String[] args) {

        ArrayList<ArrayList<String>> groceryList = new ArrayList<>();

        ArrayList<String> bakeryList = new ArrayList<>();

        bakeryList.add("pasta");
        bakeryList.add("garlic bread");
        bakeryList.add("donuts");

        ArrayList<String> produceList = new ArrayList<>();

        produceList.add("tomatoes");
        produceList.add("zucchini");
        produceList.add("peppers");

        ArrayList<String> drinksList = new ArrayList<>();

        drinksList.add("soda");
        drinksList.add("coffee");

        groceryList.add(bakeryList);
        groceryList.add(produceList);
        groceryList.add(drinksList);

        for (int i = 0; i < groceryList.size(); i++) {
            System.out.println(groceryList.get(i));

        }

    }

}

2次元配列とfor-each roop

Main.java
import java.util.ArrayList;

public class Main {

    public static void main(String[] args) {

        String[] animals = { "cat", "dog", "rat", "bird" };

        for (String animal : animals) {
            System.out.println(animal);
        }

        ArrayList<String> newAnimals = new ArrayList<>();

        newAnimals.add("cat");
        newAnimals.add("dog");
        newAnimals.add("rat");
        newAnimals.add("bird");

        for (String animal : newAnimals) {
            System.out.println(animal);
        }

    }

}

Java チュートリアル:clipboard:

:coffee: Java テキストファイル(.txt .csv)への出力 java.io.FileWriter : :white_check_mark:here
:coffee: Java テキストファイル(.txt .csv)からの入力 java.io.FileReader : :white_check_mark:here
:coffee: Java モニターからの入力 java.util.Scanner : :white_check_mark:here
:coffee: Java GUIからの入力 javax.swing.JOptionPane : :white_check_mark:here
:coffee: Java Mathクラス Math.sqrt : :white_check_mark:here
:coffee: Java 乱数の生成 java.util.Random : :white_check_mark:here
:coffee: Java Stringクラス : :white_check_mark:here
:coffee: Java Wrapperクラス : :white_check_mark:here
:coffee: Java ArrayListクラス java.util.ArrayList : :white_check_mark:here
:coffee: Java メソッド(method)の作り方とオーバーロード : :white_check_mark:here
:coffee: Java printf()メソッド : :white_check_mark:here
:coffee: Java オブジェクト(Object Oriented Programming)とコンストラクタ、スコープ(local,global) : :white_check_mark:here

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

Java Wrapperクラス

1,Wrapperクラスの紹介です。

Wrapperクラスは、プリミティブ型のデータを参照型と同じような扱いにできるクラスです。
これによって、参照型に含まれているメソッドが使えるようになります。

2,ソースコード

Main.java
public class Main {

    public static void main(String[] args) {


        // primitive    // Wrapper
        // ---------    // -------
        // boolean      // Boolean
        // char         // Character
        // int          // Integer
        // double       // Double

        Boolean a = true;
        Character b = '@';
        Integer c = 123;
        Double d = 3.14;
        String e = "hiro";

        if (a == true) {
            System.out.println("This is true");
        }

    }

}

Java チュートリアル:clipboard:

:coffee: Java テキストファイル(.txt .csv)への出力 java.io.FileWriter : :white_check_mark:here
:coffee: Java テキストファイル(.txt .csv)からの入力 java.io.FileReader : :white_check_mark:here
:coffee: Java モニターからの入力 java.util.Scanner : :white_check_mark:here
:coffee: Java GUIからの入力 javax.swing.JOptionPane : :white_check_mark:here
:coffee: Java Mathクラス Math.sqrt : :white_check_mark:here
:coffee: Java 乱数の生成 java.util.Random : :white_check_mark:here
:coffee: Java Stringクラス : :white_check_mark:here
:coffee: Java Wrapperクラス : :white_check_mark:here
:coffee: Java ArrayListクラス java.util.ArrayList : :white_check_mark:here
:coffee: Java メソッド(method)の作り方とオーバーロード : :white_check_mark:here
:coffee: Java printf()メソッド : :white_check_mark:here
:coffee: Java オブジェクト(Object Oriented Programming)とコンストラクタ、スコープ(local,global) : :white_check_mark:here

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