20200706のJavaに関する記事は8件です。

VSCodeでSpringBootアプリケーションを開発しているときにハマったこと

始めに

最近は、Javaでの開発もVSCodeに一本化しようと思い色々と試しています。

既に同じようなことでハマってる方がいましたが、自分へのメモとして。

環境

一応、私の環境セットを載せてありますが、フレームワークは何でもよいです。(VSCode, Gradle環境で再現します。)

  • IDE:VSCode
  • 言語:Java
  • フレームワーク:Spring Boot
  • ビルドツール:Gradle

事象

build.gradleのdependenciesに依存ライブラリを追加で定義する。例えば、Spring BootアプリケーションにDoma2への依存を追加することにする。

build.gradle
// ... 省略
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'org.postgresql:postgresql'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    // Doma2への依存を追加
    annotationProcessor "org.seasar.doma.boot:doma-spring-boot-starter:1.4.0"
}
// ... 省略

依存ライブラリダウンロード後、dependencies treeを表示する。(Doma2の依存関係が追加されていることを確認するため)

// ... 省略
|    \--- org.seasar.doma.boot:doma-spring-boot-autoconfigure:1.4.0
|         +--- org.seasar.doma.boot:doma-spring-boot-core:1.4.0
|         |    +--- org.seasar.doma:doma-core:2.35.0
|         |    +--- org.springframework:spring-context:4.3.25.RELEASE -> 5.2.7.RELEASE (*)
|         |    +--- org.springframework:spring-jdbc:4.3.25.RELEASE -> 5.2.7.RELEASE (*)
|         |    \--- org.springframework.data:spring-data-commons:1.13.23.RELEASE -> 2.3.1.RELEASE
|         |         +--- org.springframework:spring-core:5.2.7.RELEASE (*)
|         |         +--- org.springframework:spring-beans:5.2.7.RELEASE (*)
|         |         \--- org.slf4j:slf4j-api:1.7.26 -> 1.7.30
|         +--- org.springframework:spring-jdbc:4.3.25.RELEASE -> 5.2.7.RELEASE (*)
|         \--- org.springframework.boot:spring-boot-autoconfigure:1.5.22.RELEASE -> 2.3.1.RELEASE (*)
// ... 省略

試しに、Entityクラスを定義してみると、@Entity@Tableといったアノテーションの自動補完が効かない事象が発生。

EmployeeEntity.java
// ... 省略

@Getter
@Setter
// @Entity <- 自動補完が効かない!
// @Table(name = "employees") <- 自動補完が効かない!
public class EmployeeEntity {

  private Integer id;

  private String name;

}

解決方法

VSCodeのJavaのimport補完は、Eclipseと同様に.classpathを読み込むため、依存ライブラリを追加した段階で.classpathを再生成すればよい。

build.gradle
plugins {
    id 'org.springframework.boot' version '2.3.1.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
    id 'eclipse' // 追加
}

// ... 省略
$ ./gradlew eclipse

BUILD SUCCESSFUL in 1s
3 actionable tasks: 3 executed

image-01.png
OK!

終わりに

やっぱり軽量なのが個人的にいいなと思うポイントです。

とはいえ、ユーザライブラリを読み込むような開発ケースではEclipseでしか対応できないのでまだ完全移行とはいかなそうです。

参考

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

[Java] Mockitoでコンストラクタに引数を渡す/メソッドのデフォルト呼び出しをcallRealMethodにする

Mockitoのちょっとしたメモ

コンストラクタに引数を渡す

package jp.jig.product.live.server.common.service;

import org.junit.Test;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class ApiServiceTest {

    public static class ApiService {

        private final String endpoint;
        private final int timeout;

        public ApiService(String endpoint, int timeout) {
            this.endpoint = endpoint;
            this.timeout = timeout;
        }

        public String request() {
            if (endpoint == null) {
                throw new IllegalStateException("endpoint is null");
            }

            return endpoint;
        }

        public boolean validTimeout() {
            return timeout > 0;
        }
    }

    @Test
    public void test() {

        ApiService apiService = mock(ApiService.class);
        when(apiService.request()).thenCallRealMethod();
        apiService.request(); // -> throw IllegalStateException
    }

}

例えば上記のような ApiService のmockを作って、 request() メソッドを callRealMethod する場合などで
コンストラクタで値を設定したいといった場面があると思います。

このとき mock() の第2引数に MockSetting#useConstructor を使うことでmockオブジェクトを作りつつ、コンストラクタに引数を渡すことができます

    @Test
    public void test() {
        ApiService apiService = mock(ApiService.class, withSettings().useConstructor("http://localhost", 100));
        when(apiService.request()).thenCallRealMethod();
        apiService.request(); // -> http://localhost
    }

コンストラクタにロギングを仕込んでみると、ログも出力されるので、コンストラクタ内の処理も実行されていることがわかります。

デフォルト呼び出しをcallRealMethodにする

テストの際に基本的には本来のメソッドを呼び出したいが、一部のメソッドだけモックしたい。。。といったことがあると思います。

ApiService apiService = mock(ApiService.class,
 withSettings().defaultAnswer(CALLS_REAL_METHODS));

こちらは MockSetting#defaultAnswerCALLS_REAL_METHODS を渡すことで、デフォルトでcallRealMethodになります。

ただし注意点が一つあり、callRealMethodになっているメソッドで when() を使うときは注意が必要になります

    @Test
    public void test() {
        ApiService apiService = mock(ApiService.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
        when(apiService.request()).thenReturn("hogehoge"); // -> throw IllegalStateException
    }

上記コードは一見普通に動きそうですが、 when(apiService.request()) の時点でメソッドが呼び出されてしまいます。
これを回避するためには doReturn を使うのがいいらしいです。

    @Test
    public void test() {
        ApiService apiService = mock(ApiService.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
        doReturn("hogehoge").when(apiService).request();

        apiService.request(); // -> hogehoge
    }

doReturnを使うことで callRealMethod を呼び出さずにmockを設定することができます。

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

【Java】祝日判定サンプル

概要

平日のみbatchを稼働させたくて、土日はcronで簡単に除外できるけど祝日はサクッと除外できないのでJava側で祝日判定した。
ちょっと検索した感じJavaのサンプルがあんまり見当たらなかったので公開します。

注意

  • 振替休日はちょっと複雑なので考慮しません
  • 過去の年は考慮しません
  • 今年(2020年)は特殊なので考慮しません
  • 春分の日と秋分の日の計算式は1980~2099年のみ有効

環境

  • Java 1.8
  • SpringBoot 2.2.1.RELEASE

1. 祝日の定義classを作成

  • 固定日付、ハッピーマンデー、春分の日、秋分の日の4パターン
PublicHoliday.java
package com.tamorieeeen.sample.consts;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 祝日
 *
 * @author tamorieeeen
 *
 */
@Getter
@AllArgsConstructor
public enum PublicHoliday {

    NEWYEAR("元日", "fixed", 1, 1, 0),
    ADULT("成人の日", "monday", 1, 0, 2),
    FOUNDATION("建国記念の日", "fixed", 2, 11, 0),
    BIRTHDAY("天皇誕生日", "fixed", 2, 23, 0),
    SPRING("春分の日", "spring", 3, 0, 0),
    SHOWA("昭和の日", "fixed", 4, 29, 0),
    CONSTITUTION("憲法記念日", "fixed", 5, 3, 0),
    GREEN("みどりの日", "fixed", 5, 4, 0),
    CHILDREN("こどもの日", "fixed", 5, 5, 0),
    SEA("海の日", "monday", 7, 0, 3),
    MOUNTAIN("山の日", "fixed", 8, 11, 0),
    AGED("敬老の日", "monday", 9, 0, 3),
    AUTUMN("秋分の日", "autumn", 9, 0, 0),
    SPORTS("スポーツの日", "monday", 10, 0, 2),
    CULTURE("文化の日", "fixed", 11, 3, 0),
    THANKSGIVING("勤労感謝の日", "fixed", 11, 23, 0);

    private String name;
    // fixed: 日付固定
    // monday: 第何月曜日
    // spring: 春分の日
    // autumn: 秋分の日
    private String type;
    // 月
    private int month;
    // 日付型: 日
    private int dayOfMonth;
    // 第何月曜日型: 第何か
    private int weekOfMonth;

    /**
     * 固定祝日を取得
     */
    public static List<PublicHoliday> getFixedHoliday() {

        return Stream.of(values())
                .filter(v -> v.type.equals("fixed"))
                .collect(Collectors.toList());
    }

    /**
     * ハッピーマンデーを取得
     */
    public static List<PublicHoliday> getHappyMonday() {

        return Stream.of(values())
                .filter(v -> v.type.equals("monday"))
                .collect(Collectors.toList());
    }

    /**
     * 春分の日を取得
     */
    public static PublicHoliday getSpringDay() {

        return Stream.of(values())
                .filter(v -> v.type.equals("spring"))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException());
    }

    /**
     * 秋分の日を取得
     */
    public static PublicHoliday getAutumnDay() {

        return Stream.of(values())
                .filter(v -> v.type.equals("autumn"))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException());
    }
}

2. 祝日判定classを作成

  • 第何月曜日はCalendar.DAY_OF_WEEK_IN_MONTHを使う
  • 春分の日: int(20.8431+0.242194*(年-1980)-int((年-1980)/4))
  • 秋分の日: int(23.2488+0.242194*(年-1980)-int((年-1980)/4))
HolidayService.java
package com.tamorieeeen.sample.service;

import java.time.LocalDate;
import java.util.Calendar;
import org.springframework.stereotype.Service;

import com.tamorieeeen.sample.consts.PublicHoliday;

/**
 *
 * @author tamorieeeen
 *
 */
@Service
public class HolidayService {

    // 1年は365.242194日なので、1年あたり0.242194日進む
    private static final double DIFF_OF_YEAR = 0.242194;
    // うるう年であった1980年から計算
    private static final int LEAP_YEAR = 1980;
    // 1980年の春分の時刻「20.8431日」
    private static final double SPRING_DAY_BASE = 20.8431;
    // 1980年の秋分の時刻「23.2488日」
    private static final double AUTMN_DAY_BASE = 23.2488;

    /**
     * 祝日かどうか
     */
    public boolean isHoliday() {

        LocalDate today = LocalDate.now();

        if (this.isFixedHoliday(today) || this.isHappyMonday(today) ||
                this.isSpringDay(today) || this.isAutmnDay(today)) {

            return true;
        }

        return false;
    }

    /**
     * 固定祝日かどうか
     */
    private boolean isFixedHoliday(LocalDate today) {

        return PublicHoliday.getFixedHoliday()
                .stream()
                .map(h -> LocalDate.of(
                        today.getYear(), h.getMonth(), h.getDayOfMonth()))
                .anyMatch(h -> h.isEqual(today));
    }

    /**
     * ハッピーマンデーかどうか
     */
    private boolean isHappyMonday(LocalDate today) {

        return PublicHoliday.getHappyMonday()
                .stream()
                .map(h -> LocalDate.of(
                        today.getYear(), h.getMonth(),
                        this.getDayOfMonth(h.getMonth(), h.getWeekOfMonth())))
                .anyMatch(h -> h.isEqual(today));
    }

    /**
     * ハッピーマンデーの日付を取得
     */
    private int getDayOfMonth(int month, int weekOfMonth) {

        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.MONTH, month - 1);
        cal.set(Calendar.DAY_OF_WEEK, 2);
        cal.set(Calendar.DAY_OF_WEEK_IN_MONTH, weekOfMonth);

        return cal.get(Calendar.DAY_OF_MONTH);
    }

    /**
     * 春分の日かどうか
     */
    private boolean isSpringDay(LocalDate today) {

        int dayOfMonth =
                this.getMovedDays(today.getYear(), SPRING_DAY_BASE)
                - this.getLeapYearDays(today.getYear());
        LocalDate springDay = LocalDate.of(
                today.getYear(),
                PublicHoliday.getSpringDay().getMonth(),
                dayOfMonth);

        return today.isEqual(springDay);
    }

    /**
     * 秋分の日かどうか
     */
    private boolean isAutmnDay(LocalDate today) {

        int dayOfMonth =
                this.getMovedDays(today.getYear(), AUTMN_DAY_BASE)
                - this.getLeapYearDays(today.getYear());
        LocalDate autmnDay = LocalDate.of(
                today.getYear(),
                PublicHoliday.getAutumnDay().getMonth(),
                dayOfMonth);

        return today.isEqual(autmnDay);
    }

    /**
     * 進んでる分を取得
     */
    private int getMovedDays(int year, double baseDay) {

        return (int) Math.floor(baseDay + this.getDiffDays(year));
    }

    /**
     * 1980年からの差分を取得
     */
    private double getDiffDays(int year) {

        return DIFF_OF_YEAR * (year - LEAP_YEAR);
    }

    /**
     * うるう年の分を取得
     */
    private int getLeapYearDays(int year) {

        return (int) Math.floor((year - LEAP_YEAR) / 4.0);
    }
}

3. 呼び出しclassを作成

  • holidayService.isHoliday()がtrueなら祝日、falseなら平日
SampleController.java
package com.tamorieeeen.sample.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import com.tamorieeeen.sample.service.HolidayService;

import lombok.extern.slf4j.Slf4j;

/**
 *
 * @author tamorieeeen
 *
 */
@Slf4j
@Controller
public class SampleController {

    @Autowired
    private HolidayService holidayService;

    @GetMapping("/sample")
    public String top() {

        if (holidayService.isHoliday()) {
            log.info("today is holiday.");
        } else {
            log.info("today is weekday.");
        }

        return "sample";
    }
}

感想

春分の日と秋分の日の計算がめんどくさい!

参考

内閣府「国民の祝日」について
「今月の第3月曜日」の日付を求める
PHPで春分の日、秋分の日を計算

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

JAVAでimmutableなクラスをつくる

imutableとは

immutableとは不変のこと。
つまり、immutableなクラスとはインスタンスの中身が変わらない(=書き換えられない)クラスのことです。
immudatableなクラスを書き換えるということは、インスタンスを再生産するということになります(繰り返し処理の中で何度もインスタンスが作られると古いインスタンスが溜まっていき、処理速度が遅くなるため、使い方は注意しないといけません)。immutableなクラスを利用する時には、中身を変更してないか、参照型の値を利用してないか等、確認する必要があります。

immutableのメリット

immutableなオブジェクトは、作成時から状態が変わらないため、処理の途中で意図しない形に値が変化する心配はありません。
immutableなクラスで代表的なものに、Stringクラスがあります。

作成時の条件

immutableなクラスを作るには、以下の4つの条件を満たす必要があります。

1.クラスをfinalとして宣言

2.すべてのフィールドをfinalかつprivateにする
クラスをfinalとして宣言、またはコンストラクタをプライベートにして、ファクトリメソッドでインスタンス生成

3.setterを定義しない
クラスをfinalとして宣言、またはコンストラクタをプライベートにして、ファクトリメソッドでインスタンス生成

4.フィールドにmmutable(可変)なオブジェクトへの参照を含んでいない

Fruits.java
public final class Fruits {
    private final String color;
    private final int amount;

    public Fruits(String name, String, int age) {
        this.name = color;
        this.age = amount;
    }

    public String getColor() {
        return color;
    }

    public int getAmount() {
        return amount;
    }
}

変更しようとするとエラーになります。

FruitsTest.java
@Test
public void fruitsTest() {
    final Person Fruits = new Fruits("Red", 28);
    // error
    // fruits.setColor("yellow");
    // fruits.amount(30);
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaの比較 compareTo()メソッドを使って

filename.rb
public static void main(String[] args) {
        System.out.println("-----a.compareTo(b)を使ってみよう!-----");
        //aが大きい場合
        Integer a = 2;
        Integer b = 1;
        System.out.println("a の方が大きい場合" + " " +a.compareTo(b)); // 1 が返る
       //aが小さい場合
        a = 1;
        b = 2;
        System.out.println("a の方が小さい場合" + " " +a.compareTo(b)); // -1 が返る
      //a  b が同じ数字の場合
        a = 1;
        b = 1;
        System.out.println("a と b が同じ場合" + " " +a.compareTo(b)); // 0 が返る


    }

compareTo()・・・・まだ使い道がよくわかりません(笑)

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

VSCode Remote Containers を利用して最強のローカル開発環境を作りたい

はじめに

VSCodeの神拡張機能であるRemote Containersの自分なりの設定の紹介です
公式サンプルは公開されていますが、そのままだと流石に使いずいので自分なりに使いやすいように編集した設定を紹介します
なお、本記事で紹介する設定ファイルは全て以下のリポジトリで公開しています(紹介していない環境のものも入っています)
https://github.com/sabure500/remote-container-sample

また、Remote Containersを使ってみて良いなと思ったので色々使いやすいように設定を弄っていますが、本記事は最強のローカル環境を「作りたい」なので、ここをこうした方が良いといった案があったら是非教えてくれると嬉しいです

VSCode Remote Containers とは

VSCodeの拡張機能であり、使用することでコンテナの中でVSCodeを開いて作業を行うことができるようになる
コンテナの中で直接VSCodeを開いて作業ができるようになるため、開発環境をサンドボックス化してローカルマシン上には全く影響しないところで開発を行うことができる
類似の拡張機能シリーズでこれ以外にも「Remote SSH」と「Remote WSL」が存在し、これはそれぞれSSH接続先またはWSLの中でVSCodeを開いて作業を行うことができるようになる
それぞれの詳細は 公式サイト を参照

インストール

VSCode Remote Containers で開発環境を作る場合は以下の2つのインストールが必要です
逆にいうと、以下の2つがあればローカルマシンには他に何も入れずにNode,python,Go,Java等の環境が作れます
* Visual Studia Code
* Docker Desktop for Windows or Mac

Docker Desktop for Windows or Mac

以下の公式ページからインストーラをダウンロードする
https://www.docker.com/products/docker-desktop

Visual Studio Code

VSCode本体のインストール

以下の公式ページからダウンロードする
https://code.visualstudio.com/docs

Remote Containersの導入

Remote Containersは通常の拡張機能なので、VSCodeインストール後に起動して左のタブから拡張機能を選択し「Remote Container」と検索することで一覧に出てくるのでそこからインストールできる
もしくは以下のマーケットプレースのページからインストールしても良い
https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers

Remote Containersの起動

後ほど紹介するRemoteContainersの設定ファイルがある場所をWorkspaceとして起動後に、VSCode左下の緑の「><」マークをクリックし、「Remote-Containers : Reopen in Container」を選択する

環境構築設定の紹介

Remote Containersにおける環境構築は.devcontainerディレクトリ上にdevcontainer.jsonというRemote Containers用の設定ファイルとDockerfile(もしくは、docker-compose.yaml等)を配置することで行う
環境毎の設定を紹介していく

GoogleCloudSDK

ローカル環境でGoogleCloudSDKのコマンドを利用するときもRemote Containersを利用している
設定ファイルは以下で公開しており、基本的にはこれをそのまま使うことで誰でもすぐに同じGoogleCloudSDKの環境を利用できる
その環境構築用の設定を記述していく
全体のディレクトリ構成は以下のようになっている

.
├ .devcontainer
    ├ devcontainer.json
    ├ Dockerfile
    ├ .config/fish/config.fish
    └ .local/share/fish/fish_history

Dockerfile

実際に開発環境として使うコンテナを作成する用のファイル
最初に全体像を示し、その後各行を解説する

Dockerfile
FROM google/cloud-sdk:297.0.1-alpine

# ===== common area =====
RUN apk add --no-cache fish git openssh curl
COPY .config/fish/config.fish /root/.config/fish/config.fish
# =======================

# ===== kubernetes resource install =====
ENV KUBECTL_VERSION 1.18.4
ENV KUSTOMIZE_VERSION 3.1.0
ENV ARGOCD_VERSION 1.5.2
RUN curl -sfL -o /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl \
    && curl -sfL -o /usr/local/bin/kustomize https://github.com/kubernetes-sigs/kustomize/releases/download/v${KUSTOMIZE_VERSION}/kustomize_${KUSTOMIZE_VERSION}_linux_amd64 \
    && curl -sfL -o /usr/local/bin/argocd https://github.com/argoproj/argo-cd/releases/download/v${ARGOCD_VERSION}/argocd-linux-amd64 \
    && chmod +x /usr/local/bin/kubectl /usr/local/bin/kustomize /usr/local/bin/argocd
# =======================
  • ベースイメージ

    FROM google/cloud-sdk:297.0.1-alpine
    

    ベースイメージは google/cloud-sdk:297.0.1-alpine を利用する

  • 環境で利用する汎用的なパッケージのインストール

    RUN apk add --no-cache fish git openssh curl
    

    ベースイメージがalpineなので、apkを利用して開発環境上で利用したいパッケージをインストールする
    ここではfish,git,ssh,curlを入れているが、bashを使いたい場合はfishではなくbashを導入する等各自カスタマイズする

  • fishシェル用の設定

    COPY .config/fish/config.fish /root/.config/fish/config.fish
    
    config/fish/config.fish
    set normal (set_color normal)
    set magenta (set_color magenta)
    set yellow (set_color yellow)
    set green (set_color green)
    set red (set_color red)
    set gray (set_color -o black)
    
    # Fish git prompt
    set __fish_git_prompt_showdirtystate 'yes'
    set __fish_git_prompt_showstashstate 'yes'
    set __fish_git_prompt_showuntrackedfiles 'yes'
    set __fish_git_prompt_showupstream 'yes'
    set __fish_git_prompt_color_branch yellow
    set __fish_git_prompt_color_upstream_ahead green
    set __fish_git_prompt_color_upstream_behind red
    
    # Status Chars
    set __fish_git_prompt_char_dirtystate '⚡'
    set __fish_git_prompt_char_stagedstate '→'
    set __fish_git_prompt_char_untrackedfiles '☡'
    set __fish_git_prompt_char_stashstate '↩'
    set __fish_git_prompt_char_upstream_ahead '+'
    set __fish_git_prompt_char_upstream_behind '-'
    
    function fish_prompt
      set last_status $status
    
      set_color $fish_color_cwd
      printf '%s' (prompt_pwd)
      set_color normal
    
      printf '%s ' (__fish_git_prompt)
    
      set_color normal
    end
    

    作業用のシェルとしてはfishシェルを利用する
    初期設定のままでは使いづらいので、Gitのブランチを表示する等のプロンプトを変更する設定ファイルをコンテナ上にコピーして配置する
    fishの設定ファイルは以下のブログの記事を参考にさせてもらっています
    https://www.martinklepsch.org/posts/git-prompt-for-fish-shell.html

  • GoogleCloudSDkと一緒に使うコマンド類のインストール

    # ===== kubernetes resource install =====
    ENV KUBECTL_VERSION 1.18.4
    ENV KUSTOMIZE_VERSION 3.1.0
    ENV ARGOCD_VERSION 1.5.2
    RUN curl -sfL -o /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl \
        && curl -sfL -o /usr/local/bin/kustomize https://github.com/kubernetes-sigs/kustomize/releases/download/v${KUSTOMIZE_VERSION}/kustomize_${KUSTOMIZE_VERSION}_linux_amd64 \
        && curl -sfL -o /usr/local/bin/argocd https://github.com/argoproj/argo-cd/releases/download/v${ARGOCD_VERSION}/argocd-linux-amd64 \
        && chmod +x /usr/local/bin/kubectl /usr/local/bin/kustomize /usr/local/bin/argocd
    # =======================
    

    GCPのリソースとして専らGKEを使うことが多いので、Kubernetes関連のリソースをコンテナ内にインストールしています

devcontainer.json

VSCodeからコンテナを開く際の設定ファイル
利用するDockerfileやコンテナ上でVSCodeを利用する際の拡張機能、またローカル環境からのVolume等を記述する
他に何ができるかの詳細は公式のリファレンスを参照
最初に全体像を示し、その後各行を解説する

devcontainer.json
{
    "name": "Google Cloud SDK Remote-Container",
    "build" : {
        "dockerfile": "Dockerfile"
    },
    "settings": {
        "terminal.integrated.shell.linux": "/usr/bin/fish",
    },
    "extensions": [
        "alefragnani.bookmarks",
        "mhutchie.git-graph",
        "redhat.vscode-yaml",
        "zainchen.json"
    ],
    "mounts": [
        "source=${localEnv:HOME}/.ssh/,target=/root/.ssh/,type=bind,consistency=cached",
        "source=${localEnv:HOME}/.gitconfig,target=/root/.gitconfig,type=bind,consistency=cached",
        "source=${localWorkspaceFolder}/.devcontainer/.local/share/fish/fish_history,target=/root/.local/share/fish/fish_history,type=bind,consistency=cached",
        "source=${localEnv:HOME}/.config/gcloud/,target=/root/.config/gcloud/,type=bind,consistency=cached",
        "source=${localEnv:HOME}/.kube/,target=/root/.kube/,type=bind,consistency=cached",
    ],
}
  • 利用するコンテナイメージ

        "build" : {
            "dockerfile": "Dockerfile"
        },
    

    Dockerfileの置いてある場所を指定する
    最初にディレクトリ構造で示した通り、同じディレクトリ上にあるのでそのまま"Dockerfile"と書いている

  • コンテナ固有のVSCodeの設定

        "settings": {
            "terminal.integrated.shell.linux": "/usr/bin/fish",
        },
    

    コンテナ上独自で設定したいVSCodeの設定を記載する
    例えば、ローカル上では導入しないがコンテナ上では導入するExtension用の設定等
    settingsの記述に関してはローカル上で書かれていることは改めてdevcontainer.json上で書かなくてもコンテナ上で引き継がれる
    ここではコンテナ上ではターミナルのシェルはfishを利用することだけ記述している

  • コンテナ環境上のVSCodeで利用する拡張機能の設定

        "extensions": [
            "alefragnani.bookmarks",
            "mhutchie.git-graph",
            "redhat.vscode-yaml",
            "zainchen.json"
        ],
    

    コンテナ環境上で利用したい拡張機能を記述する
    拡張機能に関してはsettingsの設定と違い、ローカル上で導入されていてもdevcontainer.json上で書かれていないものはコンテナ上で導入されないので注意

  • ローカル環境からのマウント

        "mounts": [ 
            "source=${localEnv:HOME}/.ssh/,target=/root/.ssh/,type=bind,consistency=cached",
            "source=${localEnv:HOME}/.gitconfig,target=/root/.gitconfig,type=bind,consistency=cached",
            "source=${localWorkspaceFolder}/.devcontainer/.local/share/fish/fish_history,target=/root/.local/share/fish/fish_history,type=bind,consistency=cached",
            "source=${localEnv:HOME}/.config/gcloud/,target=/root/.config/gcloud/,type=bind,consistency=cached",
            "source=${localEnv:HOME}/.kube/,target=/root/.kube/,type=bind,consistency=cached",
        ],
    

    ローカル環境上のファイルを使いたい、または、コンテナを再起動しても消去されて欲しくないファイルをマウントする
    "source="でローカルのパス、"target="でコンテナ上のパスを指定している
    また、${localEnv:XXXX}と書くことで、ローカル環境上で環境変数「XXXX」を利用できる
    ここでは5つのファイル・ディレクトリをマウントしている

    1. sshの設定 sshの設定は複数の環境から利用されるため、ローカル環境からマウントして利用する
    2. gitの設定 gitの設定も複数の環境から利用されるため、ローカル環境からマウントして利用する
    3. fishの操作履歴 ここが結構ポイントで、コンテナ上で作業しているとコンテナを停止すると操作履歴が全て削除されてしまう 自分は作業をするときにカーソル↑等を利用して過去のhistoryのコマンド履歴を利用することが多く消されると不便だったので、消えないようにWorkspaces上でマウントしておく
    4. gcpの設定 GCPログイン情報等も毎回消えると面倒なのでローカル上からマウントする
    5. kubectlの設定 同じくGKEのクラスタ登録等も毎回消えると面倒なのでローカル上からマウントする

Java

Java用の環境として、OpenJDK+Wildfly+maven+Gradleが入った環境を使用している
このJava用の環境も上で紹介したGoogleCloudSDKの環境と相違点に絞って紹介する
全体のディレクトリ構成は以下のようになっている

.
├ .devcontainer
    ├ devcontainer.json
    ├ docker-compose.yaml
    ├ Dockerfile
    ├ .m2/
    ├ .gradle/
    ├ .config/fish/config.fish
    └ .local/share/fish/fish_history

docker-compose

Javaの環境は別にコンテナで立てるDB環境と接続するためにDockerのNetworkを利用するためにDockerfileではなく、docker-composeを利用する

最初に全体像を示し、その後各行を解説する

docker-compose.yaml
version: "3"
services:
  jdk-wildfly-maven:
    build: .
    ports:
      - "8080:8080"
      - "9990:9990"
    command: /bin/sh -c "while sleep 1000; do :; done"
    volumes: 
      - $HOME/.ssh:/root/.ssh
      - $HOME/.gitconfig:/root/.gitconfig
      - .local/share/fish/fish_history:/root/.local/share/fish/fish_history
      - ..:/workspace
      - ./jboss_home/configuration/standalone.xml:/opt/wildfly/standalone/configuration/standalone.xml
      - .m2:/root/.m2
      - .gradle:/root/.gradle
    networks:
      - remote-container_common-network
networks: 
  remote-container_common-network:
    external: true
  • ローカル環境からコンテナ環境へのポートフォワード

    ports:
      - "8080:8080"
      - "9990:9990"
    

    Wildflyのデフォルトポートである8080と9990に対して、ローカル環境で対象のポートにアクセスした際にコンテナ上にアクセスするようにする

  • コンテナのデフォルトコマンドの上書き

    command: /bin/sh -c "while sleep 1000; do :; done"
    

    コンテナ起動時のデフォルトコマンドが失敗したり終了したりした場合にコンテナが停止しないように、デフォルトコマンドを上書きする
    ここで記述しているコマンドはdocker-composeを利用しない場合のRemote Containersのデフォルト設定
    docker-composeを利用する場合は明示的に書いてあげる必要がある

  • ローカル環境からのマウント

    volumes: 
      - $HOME/.ssh:/root/.ssh
      - $HOME/.gitconfig:/root/.gitconfig
      - .local/share/fish/fish_history:/root/.local/share/fish/fish_history
      - ..:/workspace
      - ./jboss_home/configuration/standalone.xml:/opt/wildfly/standalone/configuration/standalone.xml
      - .m2:/root/.m2
      - .gradle:/root/.gradle
    

    ローカル環境からのマウントはdevcontainer.jsonではなくdocker-compose.yamlで書く必要がある
    Dockerfileの場合と違うところは、"..:/workspace"と書いているようにworkspace自体を明示的に指定してマウントしている
    java環境独自の設定として".m2:/root/.m2"や".gradle:/root/.gradle"でGradleやMavenの設定やリポジトリをマウントしている。これはこの環境でしか利用しないものなので、ユーザーホーム($HOME)ではなくWorkSpace上に直接マウントしている
    またWildflyの設定ファイルであるstandalone.xmlについては、データソースの設定等が開発中に常に変更される可能性があり、コンテナ再起動のたびに削除されても困るので"./jboss_home/configuration/standalone.xml:/opt/wildfly/standalone/configuration/standalone.xml"といった形でマウントしている

  • docker networkの利用

        networks:
          - remote-container_common-network
    networks: 
      remote-container_common-network:
        external: true
    

    Javaはアプリケーションの実行環境として利用するのでDBと接続をしたい
    DBに関してもローカル環境ではコンテナとして起動するので他のコンテナと接続するためにDocker Networkを作成してそれを利用する
    そのためこの環境を利用するためには以下のコマンドで事前にネットワークを作成する必要がある

    docker network create --driver bridge remote-container_common-network
    

Dockerfile

docker-compose.yamlで指定しているOpenJDK+Wildfly+maven+Gradle環境を作成するDockerfile
ベースイメージとしてはJBoss公式イメージ"jboss/wildfly"はサイズが大きく使いずらかったりalpineベースのイメージを使いたかったこともあり、Adoptopenjdkを元にして作成している

Dockerfile
FROM adoptopenjdk/openjdk11:alpine-slim

# ===== common area =====
ENV WORKSPACE_DIR "/workspace"
RUN apk add --no-cache fish git openssh curl wget tar unzip\
    && mkdir -p $WORKSPACE_DIR
COPY .config/fish/config.fish /root/.config/fish/config.fish
# =======================

# ==== wildfly install =====
ENV JBOSS_HOME "/opt/wildfly"
ENV WILDFLY_VERSION "20.0.0.Final"

RUN wget -P /opt http://download.jboss.org/wildfly/${WILDFLY_VERSION}/wildfly-${WILDFLY_VERSION}.tar.gz \
    && tar -zxvf /opt/wildfly-${WILDFLY_VERSION}.tar.gz -C /opt \
    && rm /opt/wildfly-${WILDFLY_VERSION}.tar.gz \
    && mv /opt/wildfly-${WILDFLY_VERSION} ${JBOSS_HOME} \
    && $JBOSS_HOME/bin/add-user.sh admin admin --silent
# =======================

# ==== maven install =====
ENV MAVEN_HOME "/opt/maven"
ENV MAVEN_VERSION 3.6.3
ENV PATH "$PATH:$MAVEN_HOME/bin"
ENV MAVEN_CONFIG "$HOME/.m2"
RUN curl -fsSL -o /opt/apache-maven-${MAVEN_VERSION}-bin.tar.gz http://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz \
    && tar -zxvf /opt/apache-maven-${MAVEN_VERSION}-bin.tar.gz -C /opt \
    && rm /opt/apache-maven-${MAVEN_VERSION}-bin.tar.gz \
    && mv /opt/apache-maven-${MAVEN_VERSION} /opt/maven
# =======================

# ==== gradle install ====
ENV GRADLE_HOME "/opt/gradle"
ENV GRADLE_VERSION 6.5
ENV PATH "$PATH:$GRADLE_HOME/bin"
RUN curl -fsSL -o /opt/gradle-${GRADLE_VERSION}-bin.zip https://downloads.gradle-dn.com/distributions/gradle-${GRADLE_VERSION}-bin.zip \
    && unzip -d /opt /opt/gradle-${GRADLE_VERSION}-bin.zip \
    && rm /opt/gradle-${GRADLE_VERSION}-bin.zip \
    && mv /opt/gradle-${GRADLE_VERSION} /opt/gradle
# =======================

devcontainer.json

Dockerfileを利用する場合とdocker-composeを利用する場合で、devcontainer.jsonのデフォルトの設定や利用できる設定が変わっている(例えば、マウントはdevcontainer.jsonには記述しても効かなくなり、docker-composeにて記述しなければならなくなる)
詳細は公式リファレンス参照

devcontainer.json
{
    "name": "JDK&Wildfly&Maven&Gradle Remote-Container",
    "dockerComposeFile": "docker-compose.yaml",
    "service" : "jdk-wildfly-maven",
    "workspaceFolder": "/workspace",
    "settings": {
        "terminal.integrated.shell.linux": "/usr/bin/fish",
        "java.home": "/opt/java/openjdk",
        "maven.executable.preferMavenWrapper": false,
        "maven.executable.path": "/opt/maven/bin",
        "maven.terminal.useJavaHome": true,
        "java.jdt.ls.vmargs": "-noverify -Xmx1G -XX:+UseG1GC -XX:+UseStringDeduplication -javaagent:\"/root/.vscode/extensions/gabrielbb.vscode-lombok-1.0.1/server/lombok.jar\"",
    },
    "extensions": [
        "alefragnani.bookmarks",
        "mhutchie.git-graph",
        "vscjava.vscode-java-pack",
        "shengchen.vscode-checkstyle",
        "gabrielbb.vscode-lombok",
        "naco-siren.gradle-langua"
    ],
    "shutdownAction": "stopCompose"
}
  • 利用するイメージとサービス

        "dockerComposeFile": "docker-compose.yaml",
        "service" : "jdk-wildfly-maven",
    

    docker-composeを利用する場合は"dockerComposeFile"を利用してdocker-compose.yamlの場所を指定する
    また、docker-composeの場合は複数のコンテナが起動している可能性があるので、serviceでどのコンテナでVSCodeを開くのかも指定する

  • workspaceフォルダの指定

        "workspaceFolder": "/workspace",
    

    Dockerfileの場合と違い、docker-composeの場合は明示的にマウントするworkspaceの場所を指定する必要がある
    この場合に存在しないディレクトリは指定できないため、Dockerfile上で先に/workspaceといったディレクトリを作っておく

  • java特有の設定

        "settings": {
            "terminal.integrated.shell.linux": "/usr/bin/fish",
            "java.home": "/opt/java/openjdk",
            "maven.executable.preferMavenWrapper": false,
            "maven.executable.path": "/opt/maven/bin",
            "maven.terminal.useJavaHome": true,
            "java.jdt.ls.vmargs": "-noverify -Xmx1G -XX:+UseG1GC -XX:+UseStringDeduplication -javaagent:\"/root/.vscode/extensions/gabrielbb.vscode-lombok-1.0.1/server/lombok.jar\"",
        },
        "extensions": [
            "alefragnani.bookmarks",
            "mhutchie.git-graph",
            "vscjava.vscode-java-pack",
            "shengchen.vscode-checkstyle",
            "gabrielbb.vscode-lombok",
            "naco-siren.gradle-langua"
        ],
    

    ローカル環境のVSCode上では入れていないが、Java環境では利用したい拡張機能を指定している
    また、その拡張機能に対応したコンテナ上でのみ適用したいsettingsの内容を記述している

MySQL

前の章で作成したJavaから利用するDBもコンテナで作成する
この環境はあまりRemote Containersで作成する意味はないが、統一するために一応Remote Containersで作っている
単純にdocker-composeで起動しても特に問題はない
他のコンテナ(Java環境)と接続するためにこの環境を作る前に以下のコマンドでDocker networkを作っておくこと
(すでにある場合は問題なし)

docker network create --driver bridge remote-container_common-network

全体のディレクトリ構成や設定ファイルは以下のようになっている

.
├ .devcontainer
│  ├ devcontainer.json
│  ├ docker-compose.yaml
│  └ my.cnf
└ db
docker-compose.yaml
version: "3"
services:
  mysql:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: pass
      TZ: "Asia/Tokyo"
    ports:
      - "3306:3306"
    volumes: 
      - ../db/data:/var/lib/mysql
      - ./my.cnf:/etc/mysql/conf.d/my.cnf
    networks:
      - remote-container_common-network
  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    environment:
      PMA_ARBITRARY: 1
    ports:
      - 3307:80
    depends_on:
      - mysql
    networks:
      - remote-container_common-network
networks: 
  remote-container_common-network:
    external: true
devcontainer.json
{
    "name": "MySQL Remote-Container",
    "dockerComposeFile": "docker-compose.yaml",
    "service" : "mysql",
    "workspaceFolder": "/root",
    "settings": {
        "terminal.integrated.shell.linux": "/bin/bash",
    },
    "extensions": [],
    "shutdownAction": "stopCompose"
}

補足

今回はあくまで自分用のローカル環境の構築なので、複数のアプリケーションを1つの環境で扱うことを想定してRemote Containersの設定を作っている
つまり以下のようなこと

.
├ .devcontainer
├ application1
│  ├ .git
│  └ source code
├ application2
│  ├ .git
│  └ source code

上記のような構成ではなく、1つのアプリケーションに対して1つのRemote Containersの設定を作る方がgit上でそのアプリの開発環境も管理でき開発者全員が同じ環境を使える等のメリットがあるため良いと思われる
つまり以下のようなこと

.
├ application1
│  ├ .devcontainer
│  ├ .git
│  └ source code
├ application2
│  ├ .devcontainer
│  ├ .git
│  └ source code

ただし、このような管理の仕方の場合は今回紹介したようなfishの独自設定をぶち込んだりすると戦争が起りかねないのでチームでよく相談してどうするかを決定した方が良いと思われます

VSCode Remote Containers のメリット・デメリット

最後に使っていて思ったメリット・デメリットをまとめて終わりにする

メリット

  • 自分のローカル環境が汚れない
    VSCodeとDockerさえあれば余計なものを何も入れなくて良い点がすごく良い
  • 開発環境の設定もアプリ開発者の間で共有できる
    同じ開発環境を共有していることになるので人によっては動く動かないといった事象の発生をなくせる
    また、そもそもDockerfileが必須になるため本番環境もDockerfileで作れば開発と本番での差異を限りなく少なくできる
    ただし、あくまで開発環境用のDockerfileと本番環境用のDockerfileは別で作った方が良い
    (本記事では紹介していないがRemote Containersのextendの機能を利用して本番環境のDockerfileを元に開発にい必要なパッケージの導入部分のみ上書いて開発環境で利用することもできる)
  • 開発環境で作ったイメージがCI/CDで流用できる
    開発環境上でツールを使ってチェック等をしている時は、CI/CDの時にも同様のチェックをすることは多い
    この時に今のCICDツールはJOBのコンテナ上での実行をサポートしていることが多いので、開発環境作成時に作ったコンテナをそのままであったり一部だけ改良して利用できたりする

デメリット

  • VSCodeでしか使えない
    VSCodeの拡張機能なので、それ以外のIDEでは使えない
    かなり有用な機能なので自分が知らないだけで他のIDEでも同じようなことができるのかもしれないですが...
  • コンテナ上では利用できない拡張機能もある
    Remote Containersで起動したコンテナ上のVSCodeでは利用できない拡張機能が一部存在する
  • 利用するツールについてある程度理解していないと環境構築できない
    Dockerfileを作成することが必須であり、このツールの設定ファイルはどこに展開されるかといったことやマウントした方が良い部分とそうでない部分といったことを理解して意識する必要がある
    ただし、設定を自作するのではなく他人が作成した設定ファイルをそのまま利用する場合は今までよりさらに何も知らなくても使えます
  • 一つのコンテナ環境にどこまで詰め込んで良いか分かりづらい
    今回紹介したものだとJavaとGoogleCloudSDKで分けているが、やろうと思えばこれはさらに細分化できるし、逆に1つにまとめることもできる
    アプリケーションの単位でremote-containerの設定を作るなら分かりやすいが、GoogleCloudSDKのように自分用のローカル環境として作る場合は境目が難しく基準となる答えはまだ持っていない

参考

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

Java Gold対策:フォーマット

はじめに

本稿は、Java Goldの学習のまとめとしてフォーマットについてまとめる。

フォーマットとは

Javaで扱うデータを表示箇所に応じて、
書式を変更したい場合に書式化するためのもの。

数値や、日付などでそれぞれフォーマットする
方法が異なるため、以下にそのフォーマットの
方法をまとめる。

数値のフォーマットで使用する主なクラス一覧

java.text.NumberFormat
java.text.DecimalFormat

■ java.text.NumberFormat

NumberFormatはnewすることが出来ないため、
staticメソッドによってオブジェクトを取得する。

NumberFormatの主なメソッドは以下

メソッド 内容
static final NumberFormat getInstance() デフォルトロケールに対応した数値formatオブジェクトを返却
static final NumberFormat getInstance(Locale locale) 引数に指定したロケールに対応した数値formatオブジェクトを返却
static final getCurrencyInstance() デフォルトロケールに対応した通貨formatオブジェクトを返却
static final getCurrencyInstance() 引数に指定したロケールに対応した通貨formatオブジェクトを返却
Number parse(String string) throws ParseException 引数で指定された文字列を数値として返却

▼ 使用例

java.text.NumberFormat

// 出力結果:1,000
NumberFormat format = NumberFormat.getInstance();
System.out.println(format.format(1000));

// 出力結果:¥1,000
NumberFormat format = NumberFormat.getCurrencyInstance();
System.out.println(format.format(1000));

// 出力結果:1000
try {
    NumberFormat format = NumberFormat.getInstance(Locale.JAPAN);
    String numberStr    = "1000";
    Number value1       = format.parse(numberStr);
    System.out.println(value1);
} catch (ParseException e) {
    // 指定したロケールと異なる値(numberStr = "$1000")を指定した場合
}

■ java.text.DecimalFormat

DecimalFormatは、NumberFormatと異なりnewでオブジェクトを取得する。
また、以下パターンを使用することで自由にフォーマットをすることが出来る。

記号 内容
0 数字(その桁に数値が無い場合は"0"を表示)
数字(その桁に数値が無い場合はブランク)
. 数字桁区切り文字
- マイナス記号
, カンマ区切り
% 100売してパーセントを表す
¥u00a5 通貨記号

▼ 使用例

// 出力:1,11111
DecimalFormat formatter = new DecimalFormat("#,#####");
System.out.println(formatter.format(111111));

// 出力:01,1111
DecimalFormat formatter = new DecimalFormat("00,0000");
System.out.println(formatter.format(11111));

// 出力:¥11,11,11
DecimalFormat formatter = new DecimalFormat("\u00a5##,##");
System.out.println(formatter.format(111111));

// 備考(0埋め)
// DecimalFormat型
// 出力:0100.00
DecimalFormat formatter = new DecimalFormat("####.##");
formatter.setMinimumFractionDigits(2);
formatter.setMaximumFractionDigits(4);
formatter.setMinimumIntegerDigits(4);
formatter.setMaximumIntegerDigits(6);
System.out.println(formatter.format(100));

日付のフォーマットで使用する主なクラス一覧

java.time.format.DateTimeFormatter

■ DateTimeFormatter

主に使用するメソッド一覧

メソッド 内容
static DateTimeFormatter ofLocalizedDate(FormatStyle dateStyle) 日付のフォーマット
static DateTimeFormatter ofLocalizedTime(FormatStyle timeStyle) 時間のフォーマット
static DateTimeFormatter ofLocalizedDate(FormatStyle dateStyle) 日付と時間のフォーマット
static DateTimeFormatter ofPattern(String pattern) 引数に任意のパターン文字列を指定してフォーマッターを作成(パターンは以下に記載)

パターン文字列一覧

パターン 内容
G 西暦
y
M
d 月の日
E 曜日
a 午前/午後
h 時間(12時表記)
H 時間(24時表記)
m
s
SS ミリ秒
x タイムゾーン
LocalDate date = LocalDate.of(2020, Month.JULY, 6);
LocalTime time = LocalTime.of(10, 20, 30);
LocalDateTime dateTime = LocalDateTime.of(date, time);

// 以下出力結果
// 2020/07/06
// 2020/07/06
DateTimeFormatter formatter1 = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
System.out.println(formatter1.format(date));
System.out.println(formatter1.format(dateTime));

// 10:20:30
// dateをformatするとUnsupportedTemporalTypeException
DateTimeFormatter formatter2 = DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM);
System.out.println(formatter2.format(time));

// 10:20
DateTimeFormatter formatter3 = DateTimeFormatter.ofPattern("hh:mm");
System.out.println(formatter3.format(time));

※ FormatStyleについてはこちらを参照

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

【JavaServlet】千里の道も一歩から 五歩目

進行状況

  • jsp⇔サーブレット間のパラメータの受け渡し
  • クッキー/セッション管理

今回やること

  • EL式
  • JSTLタグ

EL式使いましょう

EL式(Expression Language)とは、ざっくり言うとjsp側でリクエストパラメータの受け渡しを簡潔に記述できる方法です。
${パラメータ名}と書くだけです。(オブジェクトの場合は${パラメータ名.プロパティ名}

jsp側でnameという名前のパラメータを受け取って表示させる場合、

mypage.jsp(今まで)
<%
  String username=request.getAttribute("name").toString();
%>
<%=username%>さん、ようこそ

と二段階のプロセスを踏んでいましたが、EL式を使うと

mypage.jsp
${name}さん、ようこそ

と書くだけで良くなります。
記述量が減って見やすくなるのもそうですが、javaのロジックを書く必要がなくなることが最大のメリットです。
EL「式」と付いているように、${age > 20}のように簡単な式を記述することも可能です。

JSTLタグ使いましょう

前項でEL式が使えるようになりましたが、ループや条件分岐といった処理はどうしてもjavaのコードを埋め込まないといけません。
そこで登場するのがJSTLと呼ばれるタグライブラリです。タグライブラリとは、<forEach>のように、HTMLのようなタグを使ってjavaのロジックを表現することができる機能を集めたファイル群です。

導入方法

JSTLはApacheが提供している無料で使えるオープンソースのライブラリです。

Apache Taglibs Downloads

1.上記リンクを開き、ページ下部のjarファイルのうちimpl、spec、jstlをダウンロード。
2.プロジェクトフォルダのWEB-INF/lib内に上記3つのjarファイルを格納。
3.jspファイルに以下を記述。

mypage.jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>

4.web.xmlのappバージョンが3.0以上の場合、2.4に書き換えましょう。(このコーナーでは3.1で作っていましたが、2.4以降ではJSTL内でEL式が使えないらしいです。不便!)

web.xml
<!-- 3.1はコメントアウト -->
<!--
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID"
version="3.1">
-->
<web-app version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

ちなみに、appバージョン別のweb-appタグの記述方法は以下のサイトに詳しく載っています。
KATSUMI KOKUZAWA'S BLOG

使い方

  • if文
mypage.jsp
  <c:choose>
    <c:when test="${name == 'hato'}">管理者さん、こんにちは</c:when>
    <c:when test="${name == ''}">名無しさん、こんにちは</c:when>
    <c:otherwise>一般ユーザーさん、こんにちは</c:when>
  </c:choose>
タグ 要素 内容
choose - このタグで囲った中身が一連のif文ブロック。
when - if文のif elseに相当。
when item このitemがtrueの場合、タグで囲まれた部分が実行。
otherwise - if文のelseに相当。whenのどこにもヒットしなかった場合に実行。
  • for文
LoginServlet.java
List<String> list=Arrays.asList("one","two","three","four");
req.setAttribute("list",list);
mypage.jsp
<c:forEach var="v" items="${list}" varStatus="list">
    ${v} ${list.count}
    <!--
    one 1
    two 2
    three 3
    four 4
    が出力
    -->
</c:forEach>

<c:forEach begin="1" end="7" step="2" varStatus="c">
    hello ${c.count}
    <!--
    hello 1
    hello 2
    hello 3
    が出力
    -->
</c:forEach>
タグ 要素 内容
forEach var itemsから取り出した要素にアクセスする変数
items コレクションを指定(大体はEL式を書く)
varStatus 繰り返しの状態にアクセスする変数(ループ回数など)
begin ループの開始位置
end ループの終了位置
step 増加量(基本は1)

上のforEachは拡張for文

exFor.java
for(String item:list){
  System.out.println(item);
}

下のforEachは普通のfor文

For.java
for(int i=1;i<=7;i=i+2){
  System.out.println("hello"+i);
}

と挙動が似ているので覚えやすいです。

その他のタグについて

coreライブラリには他にも<c:param><c:set>などありますが、全てを説明するには長いので省略します。また、ライブラリもformatxmlなど豊富にそろっています。

まとめ

  • EL式で<% request.getAttribute("hogehoge"); %>から卒業
  • JSTLで<% if(~) %><% for(~) %>から卒業 実用的に作る場合はjavascriptはどうしても必要になってきますが、EL式とJSTLを駆使することでjavaのロジックを書く必要がなくなり、jspファイルを簡潔にすることができます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む