20201123のJavaに関する記事は29件です。

Javaで順列(階乗通り・n!通りの列挙・全探索)

n!通りを列挙したいときに、
Javaではc++でいうnext_permutationのようなものがないので
自作する必要があります。

実装

import java.util.Arrays;

public class Main {
    private static void permutation(int[] seed) {
        int[] perm = new int[seed.length];
        boolean[] used = new boolean[seed.length];
        buildPerm(seed, perm, used, 0);
    }

    private static void buildPerm(int[] seed, int[] perm, boolean[] used, int index) {
        if (index == seed.length) {
            procPerm(perm);
            return;
        }

        for (int i = 0; i < seed.length; i++) {
            if (used[i])
                continue;
            perm[index] = seed[i];
            used[i] = true;
            buildPerm(seed, perm, used, index + 1);
            used[i] = false;
        }
    }

    private static void procPerm(int[] perm) {
        System.out.println(Arrays.toString(perm));
    }

    public static void main(String[] args) throws Exception {
        permutation(new int[] { 1, 2, 3 });
    }
}

実行結果

[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 1, 2]
[3, 2, 1]

例題

ABC183C

実装例

import java.util.Scanner;
import java.util.stream.IntStream;

public class Main {

    private static long t[][];
    private static int n;
    private static int k;
    private static int ans = 0;

    private static void permutation(int[] seed) {
        int[] perm = new int[seed.length];
        boolean[] used = new boolean[seed.length];
        buildPerm(seed, perm, used, 0);
    }

    private static void buildPerm(int[] seed, int[] perm, boolean[] used, int index) {
        if (index == seed.length) {
            procPerm(perm);
            return;
        }

        for (int i = 0; i < seed.length; i++) {
            if (used[i]) {
                continue;
            }
            perm[index] = seed[i];
            used[i] = true;
            buildPerm(seed, perm, used, index + 1);
            used[i] = false;
        }
    }

    private static void procPerm(int[] perm) {
        // System.out.println(Arrays.toString(perm));

        // 都市1から出発
        int start = 0;

        // 経路の移動時間の合計
        long sum = 0;

        // 都市1から出発してすべての都市を経由する経路の合計の計算
        for (int i = 0; i < perm.length; i++) {
            sum += t[start][perm[i]];
            start = perm[i];
        }

        // 最後の都市から都市1に戻る経路を合計
        sum += t[start][0];

        // 移動時間=kの場合ans++
        if (sum == k) {
            ans++;
        }
    }

    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);

        n = sc.nextInt();
        k = sc.nextInt();
        t = new long[n][n];

        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                t[i][j] = sc.nextLong();
            }
        }

        // すべての都市をちょうど1度ずつ訪問する順列を生成
        // ex) 4都市の場合(開始終了は都市1固定のため除く)
        // [2, 3, 4], [2, 4, 3]
        // [3, 2, 4], [3, 4, 2]
        // [4, 2, 3], [4, 3, 2]
        permutation(IntStream.range(1, n).toArray());

        System.out.println(ans);

        sc.close();
    }
}

参考

https://maku77.github.io/java/numstr/permutation.html

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

Spring Boot を使ったアプリケーション開発

Spring.PNG

1, 使用言語

Spring Bootを利用したアプリケーション開発には

1,Groovyによるアプリケーション開発

2,javaによるアプリケーション開発

の大きく2つに分けて考えることができます。

2, Spring Boot CLI の用意

Groovyを利用したアプリケーション開発には、「Spring Boot CLI」というソフトウェアを使います。
環境変数Pathの設定も必要です。

3, Groovy スクリプトを作成

code.png

4, app.groovyを実行する

app.groovyがある場所にカレントディレクトリを移動して、以下のように実行

    spring run app.groovy

helloworld.PNG

5, 実行完了

ブラウザに表示されたら成功です:v:

本格的な開発にはJavaを使うので次回は、Javaを使った開発を進めていきます:airplane:

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

Java スロットゲーム

処女作です。
何かアウトプットしてみたいなと思っているときに、パチスロの広告を見て思いつきました。

意識したことは下記2点。
・何も見ずに書く
・if文、for文を必ず使う

テキスト1冊読んだだけの初心者ですので、お手柔らかにお願いします。

処理の内容

①箱3つの配列を用意
②それぞれにランダムな数字を格納
③3つの数字が等しいかを比較
④等しかった場合、それらが7であるかを判定
⑤結果を出力

slot.java
public class Slot {
    public static void main(String[] args){
        int slotArray[] = new int[3];
        for(int i=0; i<3; i++){
            slotArray[i] = (int)(Math.random() * 10);
            System.out.print(slotArray[i]);
        }

        if(slotArray[0] == slotArray[1] && slotArray[0] == slotArray[2]){
            if(slotArray[0] == 7){
                System.out.println();
                System.out.println("大当たりーーー!!!!");
            } else {
                System.out.println();
                System.out.println("当たり!");
            }
        } else {
            System.out.println();
            System.out.println("もう1回!");
        }
    }
}

終わりに

配列、型変換なども使えてよかったです(小並感)
こうしたらもっとよくなる、などありましたら教えていただけると嬉しいです!

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

windows環境でjavaのprocessbuilderからshell scriptを実行させるための設定

  1. Install Git for windows. (If you have installed sourcetree, this is done.)
  2. Add ;.SH to the end of PATHTEXT from Control Panel > System > Advanced > Environment Variables.
  3. Add C:\Program Files\Git\usr\bin;C:\Program Files\Git\mingw64\bin to the end of PATH from Control Panel > System > Advanced > Environment Variables.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

個人的に駆け出しエンジニアにお勧めしたいUdemyのカリキュラム

はじめに

この記事に広告宣伝目的はありません。
サービス元(Udemy)やカリキュラム提供者等の関係性は無く、筆者が実際にコースを受講した中でオススメしたいと、思ったコースを記載しております。
※個人の見解ですのでそこはご了承ください

筆者について

2020年6月から都内のWeb系受託開発企業(SESもやってる)企業にエンジニアとして勤務している、バリバリの駆け出しエンジニアです。
プログラミング歴は2019年11月から1年間程で、それ以前はパソコンもまともに触った事がない位にITに疎い人間でした。
自己学習でUdemyを利用する機会が多く、その中で「これは良い!」と思ったカリキュラムをいくつかピックアップしようと思いこの記事を書きました。
初投稿の記事なので、その辺りは暖かく見守っていただければと思います。
※順不同です。

1. ウェブ開発入門完全攻略コース - プログラミング をはじめて学び創れる人へ!未経験から現場で使える開発スキルを習得!

https://www.udemy.com/share/101WGMCEUacVdTQHg=/

プログラミングを学習し始めたばかりの方は、まずこのコースを受講してもらいたいです。
具体的にはProgateのカリキュラムが一通り終わって、次の学習方法に迷っているという段階の方にオススメできるコースです。
コース内容としては、HTML/CSSから始まりBootStrap4/JavaScript/Ruby/RoR/AWS(Cloud9)/MySQL/Git/GitHubなどWebエンジニアとして最低限必要になるであろう部分を広範囲で提供してくれています。
カリキュラムの総数は24時間を超えるボリュームなので、初心者勢には気が重いかもしれませんがやって損はないコースだと思います。
唯一気になるところがあるとすれば、RoR等の環境構築がAWS(Cloud9)で行われている事でしょうか。

2. PHP+MySQL(MariaDB) Webサーバーサイドプログラミング入門

https://www.udemy.com/share/101XkACEUacVdTQHg=/

YouTubeでも学習コンテンツ等をアップしている「ともすた」の、たにぐちまことさんが講師を務めるカリキュラムです。
こちらは12時間のカリキュラムでPHPとMySQLを中心にWebシステム構築の流れを学んで行く事が出来ます。
最大のメリットは、フレームワークを使わない純粋なPHPでCRUDを構築するカリキュラムだと言う事です。
正直フレームワークが身についていれば、純粋な処理の流れは理解していなくてもシステムの構築が出来てしまうのが実情ではあります。
Session/Cookieの使い方や、SQLでのDBアクセスの流れなどフレームワークで見えなくなりやすい部分が可視化されているカリキュラムになっているため、非常に有意義なカリキュラムだと思います。
ただし、前述の通りフレームワークを使用していないので、処理の記述が若干複雑かもしれません(Progate齧ったレベルだと)。初心者でも理解は十分できるとは思いますが、受講のタイミングとしては、Progateや前述1のカリキュラムを終えて、1つか2つフレームワーク(RoRやLaravelなど)を使った制作を行ったのちに、受講するとフレームワークの恩恵もより理解できるんじゃないかなぁと思います。
実際に筆者がそうだったので体験談です。

AWS:ゼロから実践するAmazon Web Services。手を動かしながらインフラの基礎を習得

https://www.udemy.com/share/101YbyCEUacVdTQHg=/

今やエンジニアにとって必須と言っても過言ではないAWSの学習コンテンツです。
内容的には、AWSにおける最もベースとなるであろう部分を丁寧に噛み砕いて解説してくれて、一緒に手を動かしながら作業を進めていく事ができ、実際にWordPressの環境構築まで行えるので、初心者向けのコンテンツの中ではかなり実践的な内容になっています。
スライドの資料や解説・講師の喋り方など、全てが丁寧すぎるくらい丁寧でとても分かりやすいコンテンツです。
筆者は今年の6月にエンジニアになった際に、オンプレからAWSへの乗せ替え案件にアサインし実際に実務でAWSを触らせてもらう事ができました。STからのアサインだったので構築等は行いませんでしたが、EC2/RDSなどの構成などを設計図で確認したときは、このカリキュラムの内容が頭に入っていたので全くわからないと言う事はなかったので、実際にこのコースが実務で応用が効く事は実証済みです。

インフラ希望じゃなければ優先度は低くなるかもですが、1度受講して欲しいコースの一つです。

Spring & Hibernate for Beginners (includes Spring Boot)

https://www.udemy.com/share/101Wc4CEUacVdTQHg=/

筆者が今回、最もオススメしたいコースがこちらになります。
タイトルの雰囲気から薄々感じる人もいるかもしれませんが、海外の講師によるカリキュラムになります。
事前にお伝えしておきますが、日本語字幕等ののサポートはありません。全て英語になります。
ただ受講した人間としてお伝えします、そんなの関係ないです。英語がわからなくても十分理解できます。

ちなみに筆者は、幼少期に英語の教育ビデオを延々と見て、小学生の頃に公文式で英語のカリキュラムを完凸したのちに、偏差値40の高校の最初のテストで100点満点中12点と言う記録を出した英語音痴です。

内容はJava言語のカリキュラムになり、人気フレームワークSpringとHibernateと言うORMを使用したWebアプリケーションの構築を学習していく内容になります。
昨今の駆け出し勢がよく使用する動的スクリプト言語、特にRoRでActiveRecordによって隠されている部分などがどう動くのかや、Javaでシステム開発をする上で必要な基礎知識が身につきます。
41時間のボリュームMAXのコンテンツですが、Javaエンジニア目指している人や駆け出しでJava案件に入っている人(受託やSESは多いんじゃない?)は是非チャレンジしてみてください!

最後に

現状は4つピックアップさせていただいていますが、今後もUdemyは利用していくので、良いカリキュラムがあれば更新していこうと思います。
最後に言うのもあれですが、Udemyはあくまで学習コンテンツであり大枠で括ってしまえば模写の領域です。
Udemyをやっていれば、実務で通用すると言うのはまずあり得ませんので、一定のラインまできたと感じたら、Udemyで作ったコードを教科書にしながら、自分で1からWebアプリを作ってみるのが、個人学習においてUdemyを最も有効に活用できる手段だと思っています。

読んでいただきありがとうございました。
もし、皆さんがUdemyで購入したコンテンツでオススメの物があれば、コメントで教えていただけると嬉しいです(Java/Kotlinとかが筆者は気になってるみたいですよ)。

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

StreamAPIの話

StreamAPIの話

ストリームAPIとは並列化、直列化

解りやすい記事
https://www.casleyconsulting.co.jp/blog/engineer/191/

mapの説明が解りやすい

public interface Stream<T> ...
  <R> Stream<R> map(Function<T, R> mapper);

map 戻り値ありのコール関数(戻り値と引数あり)を渡して結果を
ストリーム型に変換するもの
http://enterprisegeeks.hatenablog.com/entry/2015/11/30/081118

java.util.stream 順次/並列ストリームについて理解する(直列と並列が解りやすい)

https://qiita.com/nmby/items/52d1b0e2dad5df475737

おまけ
javaをVSコードで実行する
※VSコードの説明、コンフィグはユーザー単位、フォルダ単位がある
※プロジェクトを作成してから作成した方が良い

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

【Java・SpringBoot・Thymeleaf】データバインド(SpringBootアプリケーション実践編2)

ログインをして、ユーザー一覧を表示するアプリケーションを作成し、
Springでの開発について勉強していきます?
前回の記事で作った画面に引き続き、データバインドを実装します

前回の記事?
【Java・SpringBoot・Thymeleaf】ログイン・新規登録画面作成(SpringBootアプリケーション実践編1)

データバインドとは?

  • 画面の入力項目とオブジェクトのフィールドのマッピング(割り当て)を行うこと
  • 画面から渡された値を、フィールドのデータ型に合わせて変換してくれる

Springでのデータバインド

  • Springでは、データバインドをある程度自動で行ってくれるが
    • ex: 画面からテキストで入力した数値を、オブジェクトのint型に変換するなど
  • データの型変換が難しい場合は、アノテーションを使うことでバインドできます

データバインド実践!

構成は以下のようになっています

Project Root
└─src
    └─ main
        └─ java  
            └─ com.example.demo
                └─ login
                    └─ controller                ...コントローラクラス用パッケージ
                        └─ LoginController.java
                        └─ SignupController.java
                └─ domain                        ...ビジネスロジック用パッケージ
                    └─ model                     ...Modelクラス用パッケージ
                        └─ SignupForm.java
        └─ resouces
            └─ static                            ...css,js用フォルダ
            └─ templates
                └─ login
                    └─ login.html
                    └─ signup.html

ユーザー登録処理の内容

  • 1.ログイン画面からコントローラー(SignupController)にGETリクエストを送信
  • 2.ユーザー登録画面に遷移、ユーザー登録用フォームクラスのインスタンス(SignupForm)をユーザー登録画面に渡す
  • 3.ユーザー登録ボタンをクリックすると、フォームクラスのインスタンスをコントローラークラスに渡す
  • 4.フォームクラスのインスタンスを受け取ったら/loginにリダイレクト(ユーザーの登録はしない)
    =ユーザー登録画面から、ユーザー登録用フォームクラスへデータバインドしているということです

ユーザー登録画面用のフォームクラスを作成

  • データバインド用のアノテーション
    • @DateTimeFormat:指定されたフォーマットの文字列を日付型に変換
      • 以下の例では画面から渡されてきた文字列を日付型に変換
      • pattern属性にどのようなフォーマットでデータが渡されてくるかを指定している
      • @DateTimeFormat(pattern = "yyyy/MM/dd")
    • @NumberFormat:指定されたフォーマットの文字列を数値型に変換
SignupForm.java
package com.example.demo.login.domain.model;

import java.util.Date;
import org.springframework.format.annotation.DateTimeFormat;
import lombok.Data;

@Data
public class SignupForm {
    private String userId;
    private String userName;
    @DateTimeFormat(pattern = "yyyy/MM/dd")
    private Date birthday;

    private int age;
    private boolean marriage;

}

コントローラークラスを編集し、フォームクラスを受け取る

@ModelAttribute

  • 引数のフォームクラスに@ModelAttributeアノテーションを付けると、自動でModelクラスに登録(addAttribute)してくれる!
    • public String getSignUp(@ModelAttribute SignupForm form, Model model) {...}
  • つまり、以下のコードイメージ
//イメージはこう
@GetMapping("/signup")
public String getSignUp(SignupForm form, Model model){
    //フォームクラスをModelに登録 
    model.addAttribute("SignupForm",form);
    //login.htmlに画面遷移
    return"login/signup";
}

データバインド結果を受け取る

  • メソッドの引数にBindingResultクラスを追加
    • hasErros()メソッドで、データバインドに失敗しているかどうかが分かる
    • バリデーションエラーが発生した場合も、失敗しているかどうかが分かる
        // 入力チェックに引っかかった場合、ユーザー登録画面に戻る
        if (bindingResult.hasErrors()) {
            // GETリクエスト用のメソッドを呼び出して、ユーザー登録画面に戻る
            return getSignUp(form, model);
        }
  • データバインドに失敗した場合、hasErrors()メソッドでfalseが返る
    • 上のコードではデータバインドに失敗した場合、ユーザー登録画面に戻り、getSignUpメソッドを呼び出す
    • →ラジオボタン用の変数を初期化してくれる
SignupController.java
package com.example.demo.login.controller;

import java.util.LinkedHashMap;
import java.util.Map;
import com.example.demo.login.domain.model.SignupForm;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class SignupController {

    //ラジオボタン用変数
    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;
    }

    //GETメソッド
    @GetMapping("/signup")
    public String getSignUp(@ModelAttribute SignupForm form, Model model) {
        // ラジオボタンの初期化メソッド呼び出し
        radioMarriage = initRadioMarrige();
        // ラジオボタン用のMapをModelに登録
        model.addAttribute("radioMarriage", radioMarriage);
        // signup.htmlに画面遷移
        return "login/signup";
    }

    //POSTメソッド
    //データバインド結果の受けとり
    @PostMapping("/signup")
    public String postSignUp(@ModelAttribute SignupForm form,
                             BindingResult bindingResult,
                             Model model) {
        // 入力チェックに引っかかった場合、ユーザー登録画面に戻る
        if (bindingResult.hasErrors()) {
            // GETリクエスト用のメソッドを呼び出して、ユーザー登録画面に戻る
            return getSignUp(form, model);
        }
        // formの中身をコンソールに出して確認
        System.out.println(form);
        // login.htmlにリダイレクト
        return "redirct:/login";
    }
}

ユーザ登録用の画面

th:object属性

  • Modelに登録されているオブジェクトを受け取る
    • th:object="${<ModelAttributeのキー名>}"
    • 以下の例ではSignupFormクラスを受け取っている
      • <form method="post" action="@{/signup}" th:object="${signupForm}">
  • th:objectを付けたタグの中では、th:fieldでそのオブジェクト名を省略可能

th:fieldの使い方

  • th:fieldを使用すると、オブジェクトの中のフィールドを取得し、コントローラークラスに値を渡せる
  • ①th:object属性を書かなかった場合は
    • th:field="${<ModelAttributeのキー名.フィールド名>}"
  • フィールドを1つしか使わない場合など、th:objectを書かなくても値の取得・送信ができる
<!-- コード修正例 -->
<inputtype="text"th:field="${signupForm.userId}"/>
  • ②th:objectが付いたタグ内であれば、オブジェクト名を省略可能
    • th:field="∗{<フィールド名>}"
    • 画面から送るフィールドが多いときに有効

エラーメッセージをまとめて一覧表示

  • th:each属性:拡張for文のようにModelに登録されている値が繰り返し呼ばれる
    • th:each="<変数名>:${<ModelAttributeのキー名>}"
<li th:each="error : ${#fields.detailedErrors()}">
     <span th:text="${error.message}">Error message</span>
</li>
  • これでデータバインド実装完成!
signup.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"></meta>

    <!-- Bootstrapの設定 -->
    <link th:href="@{/webjars/bootstrap/3.3.7-1/css/bootstrap.min.css}" rel="stylesheet"></link>
    <script th:src="@{/webjars/jquery/1.11.1/jquery.min.js}"></script>
    <script th:src="@{/webjars/bootstrap/3.3.7-1/js/bootstrap.min.js}"></script>

    <title>SignUp</title>
</head>
<body>
    <div class="col-sm-5">
        <div class="page-header">
            <h1>ユーザー登録画面</h1>
        </div>
        <form method="post" th:action="@{/signup}" th:object="${signupForm}">
            <table class="table table-bordered table-hover">
                <!-- ユーザーID -->
                <tr>
                    <th class="active col-sm-3">ユーザID</th>
                    <td>
                        <div class="form-group">
                            <input type="text" class="form-control" th:field="*{userId}" />
                        </div>
                    </td>
                </tr>
                <!-- パスワード -->
                <tr>
                    <th class="active">パスワード</th>
                    <td>
                        <div class="form-group">
                            <input type="text" class="form-control" th:field="*{password}" />
                        </div>
                    </td>
                </tr>
                <!-- ユーザー名 -->
                <tr>
                    <th class="active">ユーザー名</th>
                    <td>
                        <div class="form-group">
                            <input type="text" class="form-control"  th:field="*{userName}" />
                        </div>
                    </td>
                </tr>
                <!-- 誕生日 -->
                <tr>
                    <th class="active">誕生日</th>
                    <td>
                        <div class="form-group">
                            <input type="text" class="form-control" placeholder="yyyy/MM/dd" th:field="*{birthday}"/>
                        </div>
                    </td>
                </tr>
                <!-- 年齢 -->
                <tr>
                    <th class="active">年齢</th>
                    <td>
                        <div class="form-group">
                            <input type="text" class="form-control" th:field="*{age}" />
                        </div>
                    </td>
                </tr>
                <!-- 結婚 -->
                <tr>
                    <th class="active">結婚</th>
                    <td>
                        <div class="form-group">
                            <div th:each="item : ${radioMarriage}">
                                <input type="radio" name="radioMarrige"
                                       th:text="${item.key}"
                                       th:value="${item.value}"
                                       th:field="*{marriage}">
                                </input>
                            </div>
                        </div>
                    </td>
                </tr>
            </table>
            <!-- エラーメッセージの一覧表示 -->
            <ul>
                <li th:each="error : ${#fields.detailedErrors()}">
                    <span th:text="${error.message}">Error message</span>
                </li>
            </ul>

            <button class="btn btn-primary" type="submit">ユーザー登録</button>
        </form>
    </div>
</body>
</html>

SpringBootを起動して、ログイン画面を確認!

  • http://localhost:8080/login
  • ユーザー登録ボタンをクリックするとログイン画面に遷移し、コンソールにSignupFormクラスの中身が表示される!
  • 画面からフォームクラスに値を渡し、フォームクラスをコントローラークラスに渡すことができました?

登録ok.png

//コンソール
SignupForm(userId=NEKO, password=password, userName=neko, birthday=Tue Jan 23 00:00:00 JST 2018, age=2, marriage=false)

バインド失敗時

  • 2つのフィールドでバインドに失敗しているため、エラーメッセージが2つ表示されます
  • が分かりにくい。。。
  • 次はこのエラーメッセージを編集します^^

error.png

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

SpringBootでJacksonを使おうとしてちょっとハマった

Jakcson使おうとしてちょっとハマった。

SpringBootでトークンによる認証つきのRestfulAPIを開発中。
認証エラーが起きた時のエラーレスポンスをjacksonで行おうと思い、bulid.gradleにjacksonの設定を記述しコンパイル。しかし以下のエラーが吐かれてしまった。

Caused by: java.lang.NoClassDefFoundError: com/fasterxml/jackson/databind/ser/std/ToStringSerializerBase

Caused by: java.lang.ClassNotFoundException: com.fasterxml.jackson.databind.ser.std.ToStringSerializerBase

原因究明

1. 依存ファイルの記述が足らない?

この時点での自分のbuild.gradleは以下のようになっていた。

    implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.4'

しかし、このページを見てみるとどうやらjackson-databindだけではなくjackson-core、jackson-annotationも必要らしい。ということで

    implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.4'
    implementation 'com.fasterxml.jackson.core:jackson-core:2.9.4'
    implementation 'com.fasterxml.jackson.core:jackson-annotations:2.9.4'

追加。

しかし、コンパイルはうまくいかず最初のものと同じエラーが出てしまった。
困った。

2. バージョンが古い?

頭をかきつついろいろ調べていると、今度はこのページに行き着いた。

アプリケーションの pom.xml で定義していた Jackson の version と Dropzwizard が必要とする version が異なっていたために発生していた。アプリケーションでは、2.7.4 を定義していたが、Dropwizard の最新版 1.3.4 が 依存する Version は、2.9.6 で以下の通り、pom の dependency を書き換えた。

バージョンが古いとうまくいかないことがあるらしい。
早速、「jackson version」でググってみると公式ページがヒットし、最新バージョンが2.11.2であることが判明。そこで、さらに以下のように書き換え。

    implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.2'
    implementation 'com.fasterxml.jackson.core:jackson-core:2.11.2'
    implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.2'

すると、うまくいった!

まとめ

このページで筆者の方もおっしゃられている通り、

ライブラリでの、NoClassDefFoundError はほとんどは 依存ライブラリの version が古いか、新しいかのどちらかで発生するように思う。

ということだろう。しっかり最新バージョンを調べて使おう!(当たり前)

参考文献

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

【Java】getter・setterのメモ

getter・setter

getter・setterとは

結論から言うと、private になっているフィールド変数

  • 変更するのが setter
  • 取得するのが getter

です!
なぜ、どうして、どのように使うのかというのは、カプセル化したものを扱うためです。

getter

getterフィールドの値を取り出すだけのメソッド です。

前述の通り、private に設定されたフィールドは外部からアクセスが出来ないので、メソッドを介してアクセスする必要があり、それが getter というわけです。

getter の書き方

こちらがgetter メソッドの書き方になります。

//フィールド変数を定義
private データ型 フィールド名

// getter
public データ型 getフィールド名() {
    return this.フィールド名;
}

こちらは、number というフィールド変数に対して作った getter のサンプルです。

// フィールド変数
private int number;

// Getter
public int getNumber() {
    return this.number;
}

getter の命名規則

getter の命名規則は setter と同じです。

  • フィールド名の頭に「 get 」をつけたキャメルケースで書くこと。( 例: getName )
  • getter, setter は 外部で扱えなくてはいけないため、すべて 「public」で記述すること!

setter

setter は単純に「フィールドに値を代入するためのメソッド」 です。

setter の書き方

こちらがsetter メソッドの書き方になります。

// フィールド変数
private int number;
// setter
public void setNumber(int number) {
    this.number = number;
}

こちらは、number というフィールド変数に対して作った setter のサンプルです。

// フィールド変数
private int number;
// setter
public void setNumber(int number) {
    this.number = number;
}

setter の命名規則

先程述べたように、setter の命名規則は getter と同じです。

  • フィールド名の頭に「 get 」をつけたキャメルケースで書くこと。( 例: getName )
  • getter, setter は 外部で扱えなくてはいけないため、すべて 「public」で記述すること!

setter をチェックを掛けて使う

setterメソッドを使って書き込むことで、書き込むデータのチェックを行うことが出来ます。

例えば、フィールド に100以下の値しか入れたくない場合、フィールドに直接値を書き込む場合は書き込む側がチェックをする必要があります。
しかし、setterメソッドであれば、メソッドの中でチェックしておけば書き込む側の手間がなくなり安全性も高くなります。

// フィールド変数
private int フィールド名;

// setter
public void setフィールド名(int 引数名) {
    if ( 条件式 ){
    this.フィールド名 = 引数名;
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Java8 Optionalの逆引きレシピ

はじめに

Java Advent Calendar 2020の20日目の記事です。(Zennとダブルポストです。)

2014年にJava8がリリースされ、そこからOptionalの機能が導入されました。
Javaのリリースサイクルも変わり、2020年12月時点では、最新のLTS版はJava11(2018年リリース)、 Java15(2020年9月リリース)が最新です。

Java8がリリースされてから、11がでるまでの間に始まったプロジェクトでは、まずJava8が使われているでしょう。AWSではJava8のサポートをかなりのばしているので、まだまだ使う機会がありそうです。

Java8のOptionalは決して使いやすいとはいえないですが、それでもマッチしたケースではそれなり有効です。ケース別にどういった使い方をすればいいかをまとめてみました。

英語ではOptionalを使ってはいけないケースをまとめた記事も結構あります。そのうちのいくつかをピックアップしてみました。

原則、Optional#get()は使わない

大前提として、Optional#get() を原則使わないようにしましょう。
Optional#get()は値が存在しないときは例外スローされます。なので、これをいきなり使うのはnull checkしないで参照するのと同じことになります。(NullPointerExceptionのかわりに、NoSuchElementExceptionがスローされます。)

なんのチェックもしないでOptional#get()を使った場合、最近のIDEやSonarLintなどの静的解析ツールでは、警告がでてきます。Optionalからの値の取り出し方として良くないとして、すでに知られているといえます。Optionalを使い始めのときにやりがちなので、注意が必要です。

Optionalを使うためには、Optionalで包んだ値の取り出し方(使い方)を知っていることが重要です。関数型のパラダイムを最初からとりいれている言語であれば、取り出し方自体がJava8に比べて豊富で、IDEでだいたい然るべきメソッドにたどり着きやすいと思います。Java8では選択肢が少ないとの、高階関数のAPIがあまり使い勝手が良くないので、取り出し方を先に意識しないと、「Optional使ってみたけど、なんか読みづらいし、書きにくい」、となりがちです。

Optionalからの取り出し方

各メソッドの詳細はjavadocを参照してください。

以下のメソッドをつかって取り出します。

メソッド 使用例 サンプルコード
ifPresent 存在するときだけ処理する userOpt.ifPresent(x -> x.save())
orElse 存在しないときは初期値を返す userOpt.orElse(new User())
orElseGet 存在しないときは、関数を使って初期値を返す userOpt.orElseGet(User::new)
orElseThrow 存在しないときは例外スローする userOpt.orElseThrow(IllegalStateException::new)

なんらかの変換や絞り込みが必要な場合は、以下のメソッドを先に使ってから、上記にあげたメソッドで値を取り出します。

メソッド 使用例 サンプルコード
filter ある条件を満たすものに絞り込む user.filter(User::isActive)
map プロパティをとりだす、別の型にするなどの変換をする user.map(User::getName)

Java8ではあまり使うことはなさそうですが、単純にやると、Optional<Optional<T>> のように、Optionalが2重になるような変換がある場合は、flatMapを使います。今回はこれはとりあげません。

上記を組み合わせて、必要な値の取り出しや処理します。

包んだ値をそのまま使う

Spring Data JPA では、findByIdなど、検索結果が最大1件しかない場合はOptionalで受け取ることができます。こういった場合、entityのままで扱いたいケースが多いです。

ifPresent

// 更新処理
Optional<User> user = userRepository.findById(id);
user.ifPresent(x -> {
   user.setStatus(2);
   userRepository.save(user);
});

orElse

// ユーザ詳細画面の表示
@GetMapping(/{id})
public ModelAndView showUser(@PathVariable("id") Long id) {
  ModelAndView view = new ModelAndView("userDetail");
  User user = userRepostitory.findById(id).orElse(new User());
  view.set("user", user);
  return view;
}

orElseThrow

// ユーザ詳細情報のAPI
@GetMapping(/{id})
public ResponseEntity<User> findById(@PathVariable("id") Long id) {
  return userRepository.findById(id).orElseThrow(() -> new NotFoundException(id + " does not exist"));
}

包んだ値が持っているプロパティを使う

entityがもっている特定のプロパティだけが必要なケースがあります。
そういう場合は、Optional#mapを使います。

Optional<User> user = ...;
String userName = user.map(User::getName).orElse("");
Long userId = user.map(User::getId).orElseGet(() -> 1L);
UserStatus userStatus = user.map(User::getStatus).orElseThrow(IllegalStateException::new);
Integer userStatusValue = user.map(User::getStatus).map(UserStatus::getValue).orElse(0);

包んだ値をもとになんらかの変換して使う

entitをそのままでなく、画面表示用にいろんな加工が必要な場合などは、別のオブジェクトに変換することがあります。変換先のオブジェクトにstatic factory methodを用意しておくと、Optional#mapをうまく使えます。

public class UserViewDTO

   public static UserViewDTO of(User user) {...}
Optional<User> user = userRepository.findById(id);
UserViewDTO viewDTO = user.map(UserViewDTO::of).orElse(UserViewDTO::new);

Optionalの作り方

Optionalクラスにstatic factory methodが用意されています。

ofNullable

nullのときは空のOptionalを、そうでないときはその値のOptionalを返します。
実際のコーディングではこれを使うことが多いです。

Optionalで取り扱いたいが、ライブラリではそうなっていないときなどに、使います。
例えば、Spring Data JPAと違って、ebeanというORMでは、主キーでの検索結果はOptional型になっていません。同じ様にOptionalで扱いたければ、下記のようにします。

Optional<User> user = Optional.ofNullable(User.find.byId(1L));

empty

空のOptionalを返すメソッドです。よくあるのは、例外がおきたときなど、明示的に空のOptionalを返せるときに使います。

try {
  // 処理に成功したらOptionalを返す
  return Optional.ofNullable(...);
} catch (Exception e) {
  // 例外が発生したら空のOptionalを返す 
  return Optional.empty(); 
}

of

non-nullな値をOptionalにするときに使います。nullでないことがわかっていて、かつOptionalを使った方がいいという、実用的なケースがあまりなさそうです。返り値がOptionalのメソッドのテストコードなどでは、使いみちがあるかもしれません。

return Optional.of(new User());

ケース別使用例

存在しないときは初期値を使える

orElseかorElseGetを使います。初期値の生成のコストが大きいときは、orElseGetを使います。

Optional<User> user = ...;
Long userId =  user.map(User::getId).orElse(1L);

値がなにも入っていないインスタンスを初期値として渡す場合はorElseGetが使えます。
nullを返すよりも空のインスタンスの方が、なにかと取り扱いがいいです。
upsert処理用のインスタンスを作るときなどに使えます。

User modifiedUser = user.orElseGet(User::new);

存在しないときは例外スローできる

orElseThrowを使います。存在しないときの処理を、例外をキャッチする別の箇所でやれる場合などに使えます。Rest APIで、id指定のエンドポイントの実装などに使えます。

@GetMapping("/users/{id}")
public ResponseEntity<User> showById(@PathVariable("id") long id) {
   Optional<User> userOpt = userRepository.findById(id);
   User user = user.orElseThrow(() -> new NotFoundException("not found"));
   return ResponseEntity.ok(user);
}

存在しないときはなにも処理しない

ifPresentを使います。存在するときにだけになにか処理するときに使えます。
更新系の処理で、正しいidが指定されたときだけ実行するというときなどに使えます。

Optional<User> user = userRepository.findById(id);
user.ifPresent(x -> {
  x.setName(form.getName());
  userRepository.save(x);
});

存在しないときは別の処理をする

Java8では、isPresentとif文を使うことになります。
この場合は、isPresentのチェック後に、Optional#get()で値を取り出します。
存在しない場合はログ出力するときなどです。

Optional<User> userOpt = ...;
if (userOpt.isPresent()) {
  User user =  userOpt.get();
  // 存在したときの処理
} else {
  // 存在しないときの処理
}

Java8でこのケースになった場合は、Optionalを使い続けるメリットがあまりないです。
orElseなどで、値を取り出し、そこで値があるかないかを別の形でチェックするのを検討してみましょう。

Optional<User> userOpt = ...;
User user = userOpt.orElseGet(User::new);
if (user.getId() == null) {
  // 存在しないケース
} else {
  // 存在するケース
}

Java9からifPresentOrElseが追加されていて、存在しないケースも関数でかけるようになりました。

Optional<User> user = ...;
user.ifPresentOrElse(
   x -> {
     // 存在したケース1
     x.setName(name);
     userRepository.save(x);
   }, 
  () -> {
   // 存在しないケース 
   logger.info("not found")
  }):

存在したとき、なんらかの条件で絞り込みたい

filterを使います。DBからの取得する段階では絞り込みができないときなどに使います。

Optional<User> user = userRepository.findById(id);
User activeUser = user.filter(x -> x.getStatus().isActive())
                      .orElseThrow(IllegalStateException::new);

Optionalは使わない方がいいケース

Stringや、intなどのプリミティブ型をOptionalで包みたい

Stringの場合は、nullを極力使わず、値がないことは空文字で表現すればだいたいのことはできます。

String name = ...;
if (StringUtils.isEmpty(name)) {
  ...
} else {
  ...
}

int,longなどは、OptionalInt、OptionalLongなどの専用のOptional型は用意されています。
これは、Listなど、コレクション型からStream APIを使った際に使うことが想定されているようです。

non-nullが担保できる場合は、プリミティブ型(int,longなど)を使い、nullableな場合は、ラッパー型(Integer,Longなど)を使い、必要に応じてnull checkすればいいです。

リストなどのコレクション型をOptionalで包みたい

基本的には不要です。nullを使わず、空のコレクションかどうかで、値の有無を判定しましょう。
例えば、返り値がコレクション型のメソッドを作る場合、nullではなく空のコレクションを返却すればいいです。そうすれば、Optionalでラップする機会はないです。大概のライブラリはそのような使用になっています。

List<Integer> list = ...;
if (list.isEmpty()) {
  ...
} else {
  ...
}

メソッドの引数にOptionalを使いたい

nullを引数に渡すことは一般的にはよくないことと知られています。これもそれの延長ということです。
Optionalを渡すと、なかで存在するかどうかのチェックをすることになります。
メソッド内でやるよりも、呼び出し側でOptionalをはずした方が、メソッドも使いやすく、読みやすくなるようです。

public void execute(String name, Optional<User> userOpt) {
   // 初期値にできるものがケースによって違うと、orElseでOptionalをはずしづらくなる
   User user = userOpt.orElse(...) 
}

public class Something {

  public void execute(String name, User user) {...}

}


Something something = new Something();
Optional<User> user = ...;

user.ifPresent(x -> something.execute("test", x));
something.execute("test", user.orElseGet(User::new));

成功(正常)か失敗(異常)かどうかを返り値で判定したい

成功のときは値があるOptional、そうでないときは空のOptionalを返す、として、呼び出し側で、成功か失敗に応じて処理を分けたい、というような使い方です。

try {
  ... 
  return Optional.of(...);
} catch (Exception e) {
  return Optional.empty();
}

Optional<Result> result = ...;
if (result.isPresent()) {
 ...
} else {
 ...
}

これはisPresentを使うケースにつながるので、単純なケースでない限りはやらない方が良さそうです。
Either型がある言語では、それを使えばいいのですが、ない場合はそれに近いものを用意せざるを得ないです。
結果用のオブジェクトをつくって、その中に成功かどうかのフラグや、返却したいものをつめるといいです。

public class SomethingResult {

  private final boolean isSuccess;

  private final Something result;

  private final Exception error;

  public static SomethingResult success(Something something) {...};

  public static SomethingResult error(Exception exception) {...};

}
try {
  ...
  return SomethingResult.success(...);
} catch (Exception e) {
  return SomethingResult.error(e);
}


SomethingResult result = ...;
if (result.isSuccess()) {
  ...
} else {
  ...
}

まとめ

参考リンク

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

【Java】戻り値についてのメモ

戻り値 ( 返り値 )

メソッドには、メソッドの呼び出し元にメソッドからの特定の情報を返すという仕組みがあり、その返される特定の情報のことを 「戻り値( 返り値 )」return 値 の部分 といいます。

戻り値とは?

個人的に一番わかり易い例えだったのが「 ググる 」という行為に例えた場合です。

  1. 引数」:入力された検索ワード
  2. メソッド」を実行:検索ボタンを押す
  3. 戻り値」:条件に合う記事情報が 配列として返却

戻り値の型 = return文に指定する処理結果の型

戻り値の返し方

「戻り値」がある場合は、「処理の結果を返す必要」があります。

返し方は以下です。

1. return文 を記述
2. 処理結果を呼び出し元へ返す

その時、 戻り値の型 と return文に指定する処理結果の型 が合ってなければなりません。

void

メソッドの処理結果を呼び出し元へ返す必要がない場合は、
戻り値の型を「 void 」 と記述して、戻り値なしのメソッドにします。

voidとは

「このメソッドの戻り値は返しません(ありません)」 という意味のキーワードになります。

【voidを記述しない】 戻り値があるメソッド例

こちらは、引数に渡された x と y の値を加算し、加算結果を呼び出し元へ返すメソッドです。
「計算結果をほかの計算に使う」ため、voi

//計算結果は、別の計算に用いる予定があるため、void を記述しない

public int plus(int x, int y) {
    return x + y;
}

【void を使用する】 戻り値のないメソッド例

こちらは、 フィールド「message」の内容を画面に表示するメソッド
です。「表示をするだけで、結果を他に使用しない」ので、戻り値をなしにします。

public void print() {
    System.out.println(message);
}

使用例

「こちらは、商品の税抜き価格に消費税を足して、税込価格と税抜き価格の両方を表示する」プログラムです。

public class Main {

    public static void main(String[] args) {

        double price = 980; // 商品の値段
        final double taxRate = 1.1; // 消費税率 10%

        double inTaxPrice = calcInTax(price, taxRate);

        printAllPrice(price, inTaxPrice);

    }

    public static double calcInTax(double price, double taxRate) {
        // TODO 自動生成されたメソッド・スタブ
        return price * taxRate;
    }

    public static void printAllPrice(double price, double inTaxPrice) {
        System.out.println("税抜き価格は " + price + "円です。");
        System.out.println("税込み価格は " + inTaxPrice + "円です。");
    }

}

実行結果

税抜き価格は 980.0円です。
税込み価格は 1078.0円です。

コード解説

  1. 税込価格計算に必要な「税抜き価格」と「消費税率」を格納。消費税率は final を記述して定数に。
  2. 1で用意した変数、定数を calcInTaxメソッド で計算してもらい、返り値を inTaxPrice(税込価格)に格納する
  3. price と intaxPrice をprintAllPriceメソッドに渡して、メッセージを表示する。

メソッドには、戻り値があって引数がないパターンや、
戻り値がなくて引数があるパターンなど、自由自在に作成することが可能です。
プログラミングの用途に合わせて、自分自身でどのパターンのメソッドにするか決めて作成します。

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

【Java】引数についてメモ

引数

引数とは、「このデータを使って処理をして!」とメソッドへ受け渡す値を 引数 といいます。

引数は一つだけでなく、 複数指定することができ 、複数指定する際は ,(カンマ) で区切ります。

サンプルコード

ポケモンで敵トレーナーに勝負を挑まれたときのメッセージを出力するメソッドをつくってみました。
複数の引数を与えて処理を行います。

【Main.java】

//【Main.java】
public class Main {
    public static void main(String[] args) {

            //引数に使う値をここで作ってます
EnCount enCountTrainer = new EnCount();
        String trainerJob = "ジムリーダー";
        String trainerName = "ミカン";
        String enemyPokemon = "ハガネール";
        String myPokemon = "ハッサム";

//メソッドを呼び出しています
            enCountTrainer.enCountMsg(trainerJob,trainerName,enemyPokemonName,myPokemonName);
    }
}

【EnCountTrainer.java】

//【EnCountTrainer.java】
public class EnCountTrainer {
    public void enCountMsg(String trainerJob, String trainerName, String enemyPokemonName, String myPokemonName) {
        System.out.println(trainerJob + "の " + trainerName + "が");
        System.out.println("しょうぶを しかけてきた!");
        System.out.println(trainerJob + "の " + trainerName + "は");
        System.out.println(enemyPokemonName + "をくりだした!");
        System.out.println("ゆけっ" + myPokemonName + "!");
    }

}

【実行結果】

ジムリーダーの ミカンが
しょうぶを しかけてきた!
ジムリーダーの ミカンは
ハガネールをくりだした!
ゆけっハッサム!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SpringBootことはじめ 1.SpringBootでHello World REST API

はじめに

前回、SpringCLIでHelloWorldのREST APIを作成したので
今回はEclipseのプロジェクトからSpringプロジェクトを作成して、Hello World REST APIアプリを作成してみる。

環境

ソフトウェア バージョン
OS Windows10 Pro
Java JDK12
Eclipse 2020-12 M1
SpringBoot 2.4.0
Spring Tool Suite (STS) 4.8.1

実施

1. Eclipseを入手

まずはEclipseの入手から。
PleiadesのページからEclipse最新版(2020-12)を入手。
このEclipseには初めからSpring Tool Suite (STS) 4.8.1プラグインが入っている。

2. Spring用のプロジェクト作成

Eclipseを起動し、パッケージエクスプローラで右クリック→[新規]→[その他]を選択。
以下のウインドウが表示されるので[Spring Boot]→[Spring スターター・プロジェクト]を選択。

1.png

次にプロジェクト設定。
名前を「HelloSpring」にして、型にGradleを指定。
他、以下の通り。

2.png

依存ライブラリの指定。
今回はHelloWorldを返すだけのREST APIなので
特に必要なライブラリは無し。
[完了]ボタンを押す。

3.png

すると、以下の構成のプロジェクトリソースが生成される。

4.png

HelloSpringApplication.javabuild.gradleの中身はこんな感じ。

HelloSpringApplication.java
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HelloSpringApplication {

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

}
build.gradle
plugins {
    id 'org.springframework.boot' version '2.4.0'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '12'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

build.gradleにはSpringBootのプラグインやら依存関係が追加されている。

3. REST Controllerクラス作成

HTTPリクエストを受け付けて「Hello World!」と「Hello SpringBoot!」を返すユーザエンドポイントとなるControllerクラスを作る。
前回で作ったREST Controllerクラスを流用。

HelloController.java
package com.example.demo;

@RestController
public class HelloController {

    @RequestMapping("/")
    public String home() {
        return "Hello World!";
    }

    @RequestMapping("/sb")
    public String helloSp() {
        return "Hello SpringBoot!";
    }

}

すると、@RestController@RequestMappingが参照できないエラーが出る。
Spring Boot単体では最低限のSpringアプリケーション用クラスしか入ってないということのようだ。
@RestController@RequestMappingはSpringMVCのアノテーションとのこと。

REST APIを作成したいのであれば、ライブラリspring-boot-starter-webを依存関係に追加すれば良さそう。
上記のSpringMVCクラス群に加えてTomcatやJacksonのようなライブラリも付いてくるとのこと。
いかにもREST API作成のために必要なものが揃えられそうな感じのものなので早速Gradleの設定ファイルに依存関係を追加。

gradle.build
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    //↓これを追加
    implementation 'org.springframework.boot:spring-boot-starter-web'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

先ほどのHelloControllerクラスにimport文を追加して完成。

HelloController.java
package com.example.demo;

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

@RestController
public class HelloController {

    @RequestMapping("/")
    public String home() {
        return "Hello World!";
    }

    @RequestMapping("/sb")
    public String helloSp() {
        return "Hello SpringBoot!";
    }

}

4. 実行

早速Springアプリケーションを動かしてみる。
gradleのbootRunタスクを実行。
Eclipse上から下記をダブルクリックすればいい。

5.png

すると、コンソールに以下のように表示される。

> Task :compileJava UP-TO-DATE
> Task :processResources UP-TO-DATE
> Task :classes UP-TO-DATE
> Task :bootRunMainClassName UP-TO-DATE

> Task :bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.4.0)

2020-11-23 13:46:19.374  INFO 7004 --- [           main] com.example.demo.HelloSpringApplication  : Starting HelloSpringApplication using Java 12.0.2 on xxxxxxxx with PID 7004 (M:\develop\tools\eclipse\pleiades-2020-12\workspace\HelloSpring\build\classes\java\main started by xxxxx in M:\develop\tools\eclipse\pleiades-2020-12\workspace\HelloSpring)
2020-11-23 13:46:19.376  INFO 7004 --- [           main] com.example.demo.HelloSpringApplication  : No active profile set, falling back to default profiles: default
2020-11-23 13:46:20.040  INFO 7004 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-11-23 13:46:20.045  INFO 7004 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-11-23 13:46:20.046  INFO 7004 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.39]
2020-11-23 13:46:20.088  INFO 7004 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-11-23 13:46:20.088  INFO 7004 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 689 ms
2020-11-23 13:46:20.177  INFO 7004 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-11-23 13:46:20.273  INFO 7004 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-11-23 13:46:20.279  INFO 7004 --- [           main] com.example.demo.HelloSpringApplication  : Started HelloSpringApplication in 1.115 seconds (JVM running for 1.335)

この状態で以下にアクセスしてみる。

  • http://localhost:8080/
    6.png

  • http://localhost:8080/sb
    7.png

お~できた!やったぜ:metal:

まとめ

今回はSpringBootとSpringMVCを使ってREST APIをさくっと作ったけど
どの部分がBootの部分でどういう役割を持っているかは
はっきりと理解した方がいいような気がした。

そこら辺はまた今度。

参考

Springブートスターター入門

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

mainメソッド・メソッドの呼び出し

mainメソッド

mainメソッドは 特別なメソッドで,ほかのメソッドと比較して以下のような特徴を持っています。

  • Javaのプログラムで一番最初に処理が行われる
  • 一つのクラスに一つしか記述することが出来ない
  • Stringの配列、 argsという名前の 引数 を持っている。

mainメソッドの記述

記述の仕方は以下になります。

public static void main(String[] args) {
...
...
}

また、mainメソッドは
明確な使い所はありますが、今は頭の片隅にでも入れておきましょう。

メソッドの呼び出し(メソッドの実行)

メソッドの呼び出しとは

作成したクラスのインスタンスを生成してから 以下のような形式でメソッドの呼び出しを行うこと です。

インスタンス名.メソッド名();

例えば下記に示すものは、インスタンス名 「 pokemon 」 が 「** attack **」 という名前のメソッドを呼び出す例です。

pokemon.attack();

こちらも、クラスメソッドによっては、引数の値を与えてメソッドの呼び出しを行わなければなりません。

インスタンス名.メソッド名(引数の値);

クラス型

「PocketMonster」という クラスの変数であれば

PocketMonster pokemon = new PocketMonster();

上記のように記述して、

  1. new演算子により PocketMonster クラスを呼び出す
  2. pokemon という名前で使用可能な状態にする。

このように、「そのクラスを呼び出して使用できるようにすること」を、
クラスインスタンス化(実体化)」と呼びます。

また、生成されたもの(実体)を インスタンス と呼びます。

インスタンス の生成は、 他のクラス内や mainメソッド内など、さまざまな場所で行うことができます。

生成されたインスタンスをもとにして、

  • そのクラスに宣言してあるフィールドを参照する
  • 定義してあるメソッドを呼び出す

ような事ができるようになります。

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

【Java】カプセル化

カプセル化

カプセル化とは

オブジェクト指向の重要な機能の1つで、

  • 大切なデータが他の人が操作した時変わってしまわないように
  • クレジットカードの情報が外にもれないように
  • ネットで勝手に買物をされないように

そのためには、外部からそれら「見られたくない」、「変更されたくない」情報に対しては外部からのアクセスを制限する必要があります。
つまり、

  • 「オブジェクトの情報が外部に公開しないようにすること」
  • 「使い手に必要ないものを隠してしまうこと」
  • 作成したクラスに対する アクセス方法を限定すること

それがカプセル化です!

カプセル化の必要性

上記のような、作成したクラスのデータ(フィールド変数)や処理(メソッド)を他のクラスから隠蔽するには、情報を アクセス修飾子 の private でクラスの外部から変更できないように設定する必要があります。

また、「カプセル化」することによって、作成したクラスが何をしてくれるクラスなのかが 明確 になり、他のクラスから使用しやすくなります。

カプセル化の仕方

結論、カプセル化のやり方は、、

そのようにすれば大概はカプセル化になっています。

よほどの理由がない限りはフィールド変数 はすべて private に設定しましょう。

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

【Java】環境構築 VSCode

VSCode上でJavaの開発環境を構築する方法について:writing_hand:
Go や JavaScript をVSCodeで動かして勉強しているので、「ついでにJavaの開発環境も作っとこ~」と軽い気持ちで始めたのですが、意外と面倒でした。。。

1.VSCodeのダウンロード

Goの学習を始めるときにダウンロードしていました。
【Go】開発環境構築 Windows

2. プラグインのインストール

Javaで検索し、以下のプラグインをインストールしました。

  • Java Extension Pack
  • Java Test Runner
  • Debugger for Java

2. JDKのインストール

JDKは既にインストールしていたのですが、VSCode上では「JDKのバージョンが古い」とエラーが出ました。
プラグインの関係上、JDKのバージョンが11以上でないと動作しないようです。

解決策として、

  • 設定でpathを設定
  • 最新のJDKをインストール

が提示されました。
最初インストールが面倒でpathを設定する方法を試しましたが、うまくいきませんでした。。。
諦めてOracleのサイトからJDKをインストールしました。

インストール後、設定から新しいjdkのpathを指定しました。

settings.json
{
    "java.home": "C:\\Program Files\\Java\\jdk-15.0.1"
}

適当なファイルを作成し、以下のプログラムを動かしました。

Test.java
public class Test {    
    public static void main(String[] args) {
        System.out.println("あいうえお");
    }
}

動きはしましたが、2点問題がありました。

  • PowerShellとVScodeの文字コードが違うため、コンソール上で文字化けする。
  • classファイルがデフォルトのフォルダ(ユーザーフォルダの奥深く)に作成される。

3. 出力設定

以下のファイルを対象のJavaワークスペースの「.vscode」フォルダに配置しました。

配置すると、

  • VSCodeで実行したときの文字化けが解決。
  • classファイルが実行ファイルと同階層に保存される。

となります。

tasks.json
{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format

    // Copy this file into ".vscode" folder which is a subfolder of the workspace
    // Type "Cntr + Shift + B" to compile and run the code

    "version": "2.0.0",
    "tasks": [
        {
            "label": "javac",
            "type": "shell",
            "command": "javac",
            "args": [   
                "-encoding",
                "UTF8",     
                "${file}"
            ],

        },
        {
            "label": "java",
            "type": "shell",
            "command": "java",
            "args": [
                "-cp",
                "'.;${fileDirname}'",
                "${fileBasenameNoExtension}"
            ],
            "group":{
                "kind": "build",
                "isDefault": true
            },
            "dependsOn": [
                "javac"
            ],
        }
    ]
}

4.実行方法

▼文字化け問題を回避する実行方法

VSCode上で実行する場合

  • 3で書いたtasks.jsonを配置しているワークスペース内のファイルを選択した状態でctrl+Shift+B
  • プログラム内に表示される「Run Debug」をクリック

コマンドプロンプト上で実行する場合
以下コマンドを実行

javac -encoding Shift_JIS (ファイル名).java
java (ファイル名)

参考サイト

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

【Java】new演算子

new演算子

new演算子は、Javaのクラスを インスタンス化 するために利用されます。
new演算子に続けて、クラスのコンストラクタを呼び出して実行するコードを記述すると、クラスのインスタンスが生成されます。

new演算子の使い方は以下のとおりです。

コンストラクタはクラスと同じ名前を付ける決まりになっています。
なので、実際には以下のように書いてください。

//クラス名とコンストラクタ名は同じにしましょう!

クラス名 変数名 = new コンストラクタ名();

システム開発の現場では、インスタンス化 することを「newする」とか言ったりするみたいです。

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

【Java】フィールド変数

【フィールド】フィールド変数

フィールドまたは フィールド変数 とは、クラス内の 変数 のことです。
先程のコードのここの部分です。

//クラス
public class HelloWorld {
    // フィールド ここの部分
    private String printHello;
    // コンストラクタ
    public HelloWorld() {

フィールドの書き方

  • フィールドを宣言するとき、「 public 」「 private 」などの アクセス修飾子 を記述します。
  • アクセス修飾子を省略することが可能
  • 初期化を省略することが可能。(その場合は、デフォルト値が入ります。)

フィールドとして宣言した変数は、コンストラクタやメソッド内で変数を宣言することなく、そのクラス内であればどこからでも値を参照したり更新したりすることができます。

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

【Java】インスタンス化

クラスのインスタンス化

クラス という 「型」 から 「具体的な実体」 に生成されたものを インスタンス と呼びます。
そして、それらの一連の流れを インスタンス化 とよびます。

システム開発の現場では、インスタンスを生成することを「newする」とか言ったりするみたいです。

インスタンス化の書き方

//コンストラクタ名とクラス名は同じにすること!

  1            2               3        4
クラス名 インスタンス名(変数名) = new コンストラクタ名()
  1. クラス名
  2. インスタンス名(変数名)
  3. 「=」で結んで、new演算子
  4. コンストラクタ名

の順で記述します。

Calulator calc = new Calulator;

上記のインスタンスを生成した例ですが、最初のクラス名「Calulator」と
最後のコンストラクタ名「Calulator()」が 同じ です。
コンストラクタの説明にもあるように、
コンストラクタ名=クラス名 であるためです。

インスタンスを生成するクラスのコンストラクタの作り方によって左右されますが、インスタンスを生成するクラスのコンストラクタによっては引数の値を与えてインスタンスを生成しなければなりません。

クラス名 インスタンス名(変数名) = new コンストラクタ名(引数の値)

サンプルコード2

まずこちらが、「データ」や「処理」を書いた クラス側のコード です。
最初に言った「クラスのプログラミング」に当たる部分です。

//クラス
public class HelloWorld {
    // フィールド
    private String printHello;

    // コンストラクタ
    public HelloWorld() {
        printHello = "Hello World";
    }

    // メソッド
    public void print() {
        System.out.println(printHello);
    }
}

下記がクラスの呼び出し側の「プログラムを実行するためのクラス」のコードです。
このままではまだ型の状態なので、こちらでプログラムを実行できる状態、つまり インスタンス化 します。

//プログラムを実行するためのクラス(mainメソッドを持つクラス)
public class Main {
    // mainメソッド
    public static void main(String[] args) {
        // HelloJavaクラスのインスタンスを生成して、HelloJavaクラス型の変数printHelloWorldに保持する
        HelloWorld printHelloWorld = new HelloWorld();
        // 生成したインスタンスより、HelloWorldクラスのprintメソッドを呼び出す
        printHelloWorld.print();
    }
}

実行結果
image.png

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

【Java】アクセス修飾子

アクセス修飾子

修飾子とは

変数名、関数名の前に付与する単語 のこと。

アクセス修飾子とは

*「クラスや変数がどこからアクセス可能であるか」 *の公開範囲 を決定するための修飾子です。

アクセス修飾子の種類

こちらがアクセス修飾子種類と内容です。
以下の表では下に行くほどアクセスの条件が厳しくなっていきます。

アクセス修飾子 内容
public すべてのクラスからアクセスできる
protected 現在のクラスとサブクラスからアクセスできる
なし 現在のクラスと同じパッケージのクラスからアクセスできる
private 現在のクラスからだけアクセスできる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

S3にファイルをアプロードして メタデータに content-disposition を設定する

ゴール

  • Webブラウザを使ってアップロードしたファイルをCloudFront経由でダウンロードする。
  • ダウンロードしたファイルをブラウザで保存する時のダイアログには、S3のKeyとは別の任意のファイル名を指定できる。

キャプチャ2.PNG

上の「iOS の画像 (1).jpg」を content-disposition で指定する。

バージョン情報

  • JDK: amazon-corretto-11.0.3.7.1-windows-x64
  • Spring boot : 2.2.4.RELEASE
  • AWS SDK for Java: 2.14.28

はまりポイント

PutObjectRequest.Builder#contentDisposition(string)) の引数はURLエンコードを行う必要がある。

PutObjectRequest.builder().contentDisposition( "attachment; filename=\"iOS の画像(1).jpg\"")と書くと、下のエラーになる。

software.amazon.awssdk.services.s3.model.S3Exception: The request signature we calculated does not match the signature you provided. Check your key and signing method. (Service: S3, Status Code: 403, Request ID: XXXXXXXXXXXXXXXX, Extended Request ID: YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
)

今回は、下の通りにした。 +%20 に置換する理由はファイル名の半角スペースがあるとURLエンコードで %20 に変換されるので半角スペースに戻すため。

PutObjectRequest.builder().contentDisposition( 
    "attachment; filename=\""
    + URLEncoder.encode(fileName, "UTF-8").replace("+", "%20") 
    + "\"")

設定した content-disposition の値は AWSコンソールのメタデータで確認できる。
s3-metadata.png

コントローラーの実装

コントローラー
@RestController
public class SummernoteApiController {

  private WyswygService wyswygService;

  public SummernoteApiController(@Autowired WyswygService wyswygService) {
    this.wyswygService = wyswygService;
  }

  @PostMapping("/api/attachfile")
  public String attachfile(@RequestParam("upload_file") MultipartFile uploadFile) {
    if (uploadFile.isEmpty()) {
      throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "添付するファイルを指定してください");
    }

    try {
      String publishedUrl = wyswygService.uploadToS3(uploadFile);
      return publishedUrl;
    } catch (IOException | S3Exception e) {
      e.printStackTrace();
      throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
    }
  }
}

サービスクラス(WyswygService.java)
@Component
public class WyswygService {

  /** AWSのリージョン */
  @Value("${aws.region}")
  private String regsion;

  /** 画像などをアップロードするS3バケット */
  @Value("${aws.s3.assetsBucket}")
  private String assetsBucket;

  /** S3バケットに保存する時のパスのPrefix */
  @Value("${aws.s3.assetsPrefix}")
  private String assetsPrefix;

  /** CloudFrontのホスト名 */
  @Value("${aws.s3.cloudFrontHost}")
  private String cloudFrontHost;

  /** https://github.com/huxi/sulky/tree/master/sulky-ulid */
  private ULID ulid = new ULID();

  /**
   * ファイルをAmazonS3にアップロードする
   *
   * @param uploadFile アップロードするファイル
   * @return CloudForntからアクセスできるパス
   * @throws IOException
   */
  public String uploadToS3(MultipartFile uploadFile) throws IOException {
    String contentType =
        uploadFile.getContentType() != null
            ? uploadFile.getContentType()
            : "application/octet-stream ";
    String fileName =
        uploadFile.getOriginalFilename() != null
            ? uploadFile.getOriginalFilename()
            : "attached_file.dat";
    String s3key= this.genrateS3KeyPrefix() + fileName.substring(fileName.lastIndexOf("."));
    String cloudFrontUrl = String.format("https://%s%s", this.cloudFrontHost, key);

    PutObjectRequest putObject =
        PutObjectRequest.builder()
            .bucket(this.assetsBucket)
            .key(s3key.startsWith("/") ? s3key.substring(1) : s3key) // 先頭に「/」があると重複するので削除する
            .contentType(contentType)
            .contentDisposition(
                "attachment; filename=\""
                    + URLEncoder.encode(fileName, "UTF-8").replace("+", "%20")
                    + "\"")
            .build();

    s3Client.putObject(putObject, RequestBody.fromInputStream(uploadFile.getInputStream(), uploadFile.getSize()));

    return cloudFrontUrl ;
  }

  /**
   * S3のキー(ファイルパス)のプレフィックスを生成する
   * ulidを使って時間でソートできる一意な文字列で保存する。
   */
  private String genrateS3KeyPrefix() {
    String month = DateTimeFormatter.ofPattern("yyyy-MM").format(LocalDate.now());
    return String.format(
        "%s/%s/%s", this.assetsPrefix, month, ulid.nextValue().increment().toString());
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

build.gradle変更後Gradle プロジェクトのリフレッシュが必要だった件

Spring Bootで@Sizeアノテーション(javax.validation)が使用できなかったので。
色々と調べた結果、build.gradleに依存性を注入すれば良いと書いてあり、試したが、インポートができなかった。そこで、プロジェクトを右クリックし、GraldeメニューからGradleプロジェクトのリフフレッシュを実行したら@Sizeアノテーションがインポートできました。

参考
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.3-Release-Notes#validation-starter-no-longer-included-in-web-starters

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

【Java・SpringBoot・Thymeleaf】ログイン・新規登録画面作成(SpringBootアプリケーション実践編1)

ログインをして、ユーザー一覧を表示するアプリケーションを作成し、
Springでの開発について勉強していきます?

まずはコントローラーと画面を作成

  • ログインボタンを押すと、何も処理をせずにログイン画面に戻る
  • 新規登録はこちらをクリックすると、ユーザー登録画面に遷移
  • ユーザー登録ボタンを押すと、何も処理をせずにログイン画面に戻る

ような機能を先に作ります

ログインコントローラー

  • GETメソッド、POSTメソッドでHTTPリクエストが来たら、login.htmlに遷移する
  • returnに指定するhtmlファイルは、src/main/resources/templatesからの相対パス
    • src/main/resources/templates/login/login.htmlを指定
LoginController.java
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class LoginController {

    //GETメソッド
    @GetMapping("/login")
    public String getLogin(Model model) {
        //login.htmlに画面遷移
        return "login/login";
    }

    //POSTメソッド
    @PostMapping("/login")
    public String postLogin(Model model) {
        return "login/login";
    }
}

ログイン画面

headでBootstrap、JQueryを読み込む

  • 先にJQueryを読み込んでからBootstrapのjsを読み込む

Thymeleafでcss/jsファイルを読み込む時の属性

  • th:href="@{<resources配下からの相対パス>}"
  • th:src="@{<resources配下からの相対パス>}"

Thymelwafでアンカータグを使う

  • th:href=@GetMappingの文字列を指定
  • HTTPリクエストはGETメソッドで送信
    • <a th:href="@{'/signup'}">新規登録はこちら</a>
login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"></meta>
    <link th:href="@{/webjars/bootstrap/3.3.7-1/css/bootstrap.min.css}" rel="stylesheet"></link>
    <script th:src="@{/webjars/jquery/1.11.1/jquery.min.js}"></script>
    <script th:src="@{/webjars/bootstrap/3.3.7-1/js/bootstrap.min.js}"></script>

    <title>Login</title>
</head>
<body class="text-center">
    <h1>Login</h1>
    <form method="post" action="/login">
        <label>ユーザーID</label>
        <input type="text" /><br/>
        <br />
        <label>パスワード</label>
        <input type="password" /><br/>
        <br />
        <button class="btn btn-primary" type="submit">ログイン</button>
    </form>
    <br />
    <a th:href="@{'/signup'}">新規登録はこちら</a>
</body>
</html>

新規登録コントローラー

Thymeleafでラジオボタンの値を動的に変更

  • Mapを用意しそのMapに入ったキーと値を画面に表示する
    • initRadioMarrige()というメソッドでMapに値を入れる
    • ユーザー登録画面にGETリクエストが来たら、ModelクラスにMapを登録
    • 画面からMapの値を取得できるようになる!
// ラジオボタンの初期化メソッド呼び出し
radioMarriage = initRadioMarrige(); 
// ラジオボタン用のMapをModelに登録
model.addAttribute("radioMarriage", radioMarriage);

リダイレクト

  • メソッドの返却値にredirect:<遷移先パス>指定
    • return "redirect:/login";
  • リダイレクトすると、遷移先のControllerクラスのメソッドが呼ばれGETメソッドでHTTPリクエストが送られる
    • LoginControllerのgetLoginメソッドが呼び出される
      • /signupにGETメソッドでHTTPリクエスト→signup.htmlに遷移
      • /signupにPOSTメソッドでHTTPリクエスト→ログイン画面にリダイレクト
SignupController.java
import java.util.LinkedHashMap;
import java.util.Map;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class SignupController {

    //ラジオボタン用変数
    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("/signup")
    public String getSignUp(Model model) {

        // ラジオボタンの初期化メソッド呼び出し
        radioMarriage = initRadioMarrige();

        // ラジオボタン用のMapをModelに登録
        model.addAttribute("radioMarriage", radioMarriage);

        // signup.htmlに画面遷移
        return "login/signup";
    }

    @PostMapping("/signup")
    public String postSignUp(Model model) {

        // login.htmlにリダイレクト
        return "redirect:/login";
    }
}

新規登録画面

ラジオボタンの実装

  • th:each属性:拡張for文のようにModelに登録されている値が繰り返し呼ばれる
    • th:each="<変数名>:${<ModelAttributeのキー名>}"
      • <div th:each="item : ${radioMarriage}">
    • th:eachタグ内では、Modelに登録されている値を変数名で取得できる
      • th:each属性のdivタグの中では、itemという変数を使うことができる
      • itemの中身はSignupControllerクラスで取得したMapが入っている
<div th:each="item : ${radioMarriage}">
    <input type="radio" name="radioMarrige"
         th:text="${item.key}"
         th:value="${item.value}">
    </input>
</div>
  • th:text:画面に表示される文字列を指定
    • th:text="${item.key}
      • 上の例ではMapクラスのkeyの値(既婚/未婚)を画面に表示
  • th:value:画面からControllerクラスに送る値を指定
    • th:value="${item.value}
      • 上の例ではMapクラスのvalue(true/false)を送る
signup.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"></meta>

    <!-- Bootstrapの設定 -->
    <link th:href="@{/webjars/bootstrap/3.3.7-1/css/bootstrap.min.css}" rel="stylesheet"></link>
    <script th:src="@{/webjars/jquery/1.11.1/jquery.min.js}"></script>
    <script th:src="@{/webjars/bootstrap/3.3.7-1/js/bootstrap.min.js}"></script>

    <title>SignUp</title>
</head>
<body>
    <div class="col-sm-5">
        <div class="page-header">
            <h1>ユーザー登録画面</h1>
        </div>
        <form method="post" th:action="@{/signup}">
            <table class="table table-bordered table-hover">
                <!-- ユーザーID -->
                <tr>
                    <th class="active col-sm-3">ユーザID</th>
                    <td>
                        <div class="form-group">
                            <input type="text" class="form-control" />
                        </div>
                    </td>
                </tr>
                <!-- パスワード -->
                <tr>
                    <th class="active">パスワード</th>
                    <td>
                        <div class="form-group">
                            <input type="text" class="form-control" />
                        </div>
                    </td>
                </tr>
                <!-- ユーザー名 -->
                <tr>
                    <th class="active">ユーザー名</th>
                    <td>
                        <div class="form-group">
                            <input type="text" class="form-control" />
                        </div>
                    </td>
                </tr>
                <!-- 誕生日 -->
                <tr>
                    <th class="active">誕生日</th>
                    <td>
                        <div class="form-group">
                            <input type="text" class="form-control" placeholder="yyyy/MM/dd" />
                        </div>
                    </td>
                </tr>
                <!-- 年齢 -->
                <tr>
                    <th class="active">年齢</th>
                    <td>
                        <div class="form-group">
                            <input type="text" class="form-control" />
                        </div>
                    </td>
                </tr>
                <!-- 結婚 -->
                <tr>
                    <th class="active">結婚</th>
                    <td>
                        <div class="form-group">
                            <div th:each="item : ${radioMarriage}">
                                <input type="radio" name="radioMarrige"
                                    th:text="${item.key}"
                                    th:value="${item.value}">
                                </input>
                            </div>
                        </div>
                    </td>
                </tr>
            </table>

            <button class="btn btn-primary" type="submit">ユーザー登録</button>
        </form>
    </div>
</body>
</html>

SpringBootを起動して、ログイン画面を確認!

  • http://localhost:8080/login
  • ログインボタンを押しても、何も処理をせずにログイン画面に戻る
  • 新規登録はこちらをクリックすると、ユーザー登録画面に遷移
  • ユーザー登録ボタンを押すと、何も処理をせずにログイン画面に戻る ことが確認できましたo(^_^)o
  • 次はデータバインドを実装します

login.png

signup.png

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

【Play Flamework】Javaプロジェクトの作成

はじめに

皆さんおはこんばにちは。
エンジニア歴1年目、現場未経験の弱小自称 Web エンジニアです。

研修で Spring Bootを使った開発をしたことはあるのですが
今回、チーム開発で初めて Play Flamework を扱うことになったので
その記録を残そうと思い、本記事の作成にいたった次第でございます。

環境

・MacOS Catalina バージョン10.15.7
・sbt 1.3.13

sbtのインストール

sbtとはビルドツールの一つで、Gradleと同じ立場のツールと言えます。
基本的にPlayFrameworkでは、sbtを使ってビルドや実行を行う事になります。

Gradle とか Maven とかあったな〜。
Spring Boot の研修のとき訳もわからず使ってたンゴ(未だによくわかってない)

ターミナル
$ brew install sbt

MacOS が Mojave の人はこれだけで簡単にインストールできるみたい。
拙者は Catalina だったからか、brew install gccxcode-select --installが必要だった。

プロジェクトの作成(Java)

Play Flamework では、Java の他に Scala を使った開発もできるらしい。
今回は Java プロジェクトを作りたいので、以下のコマンドを実行。

ターミナル
$ sbt new playframework/play-java-seed.g8

プロジェクト名はどうするか、パッケージ名はどうするか聞かれます。
プルジェクト名は適当にqiita-play-apiとし、
パッケージ名は空欄のまま Enter を押しました。(デフォルト値として「com.example」が設定される)

ターミナル
This template generates a Play Java project

name [play-java-seed]: qiita-play-api
organization [com.example]:

実行

プロジェクトが完成したら、そのディレクトリに移動してプロジェクトを実行してみる。

ターミナル
$ cd qiita-play-api
$ sbt run

おそらく初回作成時は、
拙者のPCに何をしよるんじゃああああああああああああああああああああああああ!!!!!!!!!!!
ってくらい長い長いログが出力されるかと思います。

--- (Running the application, auto-reloading is enabled) ---

[info] p.c.s.AkkaHttpServer - Listening for HTTP on /[0:0:0:0:0:0:0:0]:9000

(Server started, use Enter to stop and go back to the console...)

上記のようなログが出力されたら、http://localhost:9000 にアクセスして

PlayFlamework初期画面

こんな感じの陽気な画面が表示されたら一件落着。

参考記事

【爆速環境構築】sbtを使ったPlayFramework2.7のプロジェクト作成方法【Java】
【Play超入門】Play FrameworkでWeb APIを作る ~導入編~【Scala】

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

【Play Flamework】 Javaプロジェクトの作成

はじめに

皆さんおはこんばにちは。
エンジニア歴1年目、現場未経験の弱小自称 Web エンジニアです。

研修で Spring Boot を使った開発をしたことはあるのですが
今回、チーム開発で初めて Play Flamework を扱うことになったので
その記録を残そうと思い、本記事の作成にいたった次第でございます。

環境

・MacOS Catalina バージョン10.15.7
・sbt 1.3.13

sbtのインストール

sbtとはビルドツールの一つで、Gradleと同じ立場のツールと言えます。
基本的にPlayFrameworkでは、sbtを使ってビルドや実行を行う事になります。

Gradle とか Maven とかあったな〜。
Spring Boot の研修のとき訳もわからず使ってたンゴ(未だによくわかってない)

ターミナル
$ brew install sbt

MacOS が Mojave の人はこれだけで簡単にインストールできるみたい。
拙者は Catalina だったからか、brew install gccxcode-select --installが必要だった。

プロジェクトの作成(Java)

Play Flamework では、Java の他に Scala を使った開発もできるらしい。
今回は Java プロジェクトを作りたいので、以下のコマンドを実行。

ターミナル
$ sbt new playframework/play-java-seed.g8

プロジェクト名はどうするか、パッケージ名はどうするか聞かれます。
プロジェクト名は適当にqiita-play-apiとし、
パッケージ名は空欄のまま Enter を押しました。(デフォルト値として「com.example」が設定される)

ターミナル
This template generates a Play Java project

name [play-java-seed]: qiita-play-api
organization [com.example]:

実行

プロジェクトが完成したら、そのディレクトリに移動してプロジェクトを実行してみる。

ターミナル
$ cd qiita-play-api
$ sbt run

おそらく初回作成時は、
拙者のPCに何をしよるんじゃああああああああああああああああああああああああ!!!!!!!!!!!
ってくらい長い長いログが出力されるかと思います。

--- (Running the application, auto-reloading is enabled) ---

[info] p.c.s.AkkaHttpServer - Listening for HTTP on /[0:0:0:0:0:0:0:0]:9000

(Server started, use Enter to stop and go back to the console...)

上記のようなログが出力されたら、http://localhost:9000 にアクセスして

PlayFlamework初期画面

↑こんな感じの陽気な画面が表示されたら一件落着。

参考記事

【爆速環境構築】sbtを使ったPlayFramework2.7のプロジェクト作成方法【Java】
【Play超入門】Play FrameworkでWeb APIを作る ~導入編~【Scala】

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

【Java】初心者でも分かる!Javaで世界のナベアツを実装してみた

1.はじめに

皆さんはFizzBuzzという問題を聞いたことがあるでしょうか。
聞いたことがない方はQiitaにも記事がたくさんあるので見てみるといいでしょう。

僕はその問題を聞いたときにふと思ったのです。
「応用すれば世界のナベアツが作れそうだ」と。
世界のナベアツ

作成する前にネットでも調べたところ、僕の思った以上にナベアツプログラムが溢れていたため、ここでは差別化するために初心者にも分かりやすく、かつ面白い出力にできるようにしています。
よろしければどうぞご覧になっていってください

2.仕様(書き方含む)

  • 【必須】「3で割り切れる」か「3の付く数字」の場合アホになる。
  • 【ナベアツ】出力は顔文字で行い、アホの時は1回ごとに表情を変えさせる
  • 【ナベアツ】デフォルト出力は数字、アホ出力は文字列の数字で行う。
  • 【初心者用】単純なif文、for文のみで作成(三項演算子、switch、拡張for文などは一切使用しない)
  • 【初心者用】importは行わない。
  • 【初心者用】インデントは2段まで(ネストは一切使用しない)
  • 【初心者用】メソッドチェーンを使用しない
  • 【できれば】少しくすりとするようなネタコードを入れる
  • 【妥協】基本的に数える数字は100までとする。

٩( ᐛ ) さんっ!実装!

やはり、だらだらと過程を書くのはつまらないのでさくっと実装を載せます

.world_nabeatsu
public class World {
    //アホな場合用セリフ配列
    static final String[] AHO_NUMBER = { "", "いち", "に", "さん", "よん", "ご", "ろく", "なな", "はち", "きゅう" };

    //今回使用するフォント(デフォルトとイタリック(斜体))
    static final String DEFAULT = "\u001B[0m";
    static final String ITALIC = "\u001B[3m";

    //前回のアホの情報。一回ごとに表情を変えるため。
    boolean isAhoContinuous = false;

    //引数で渡された数字分、なべあつが数字をコンソールに出力する。
    void aho(int maxNumber) {
        for (int number = 1; number <= maxNumber; number++) {
            //引数で渡した数字が「3の倍数」か「3の付く数字」かをチェックする
            //[アホ]=true //[アホじゃない]=false
            boolean isAho = ahoCheck(number);

            //処理が一瞬で終わってしまうの防ぐためスレッドスリープする。
            stopTime(400);

            //なべあつに喋ってもらう
            sayNumber(isAho, number);
        }
        //無事に終わったときの処理
        stopTime(600);
        System.out.println(" ᐠ( ᐛ )ᐟ  く オモロー!");
    }

    private boolean ahoCheck(int number) {
        //3桁の数字の場合、アホになのでなべあつは逃げる
        //でも100は頑張って言う
        if (number > 100) {
            throw new NullPointerException("もう無理……");
        }

        //3の倍数かチェック
        if (number % 3 == 0) {
            return true;
        }

        //3の付く数字かチェック
        //containsメソッドを使う為にString型に変換
        String strNum = String.valueOf(number);

        //数字に3がついていれば「true」を返す
        if (strNum.contains("3")) {
            return true;
        }

        //上記のどれでもない場合「false」を返す
        return false;
    }

    private void sayNumber(boolean isAho, int number) {
        if (isAho) {
            //アホの場合
            sayAhoNumber(number);
        } else {
            //真面目な場合
            System.out.println(DEFAULT + "(`・ω・´)く " + number);
        }
    }

    private void sayAhoNumber(int number) {
        //アホ用の文字列数字を取得
        String ahoNumber = getAhoNumber(number);

        //前回のアホの状況に応じて顔の向きを変化
        if (isAhoContinuous) {
            System.out.print(DEFAULT + " ( ᐖ )۶  く ");
            isAhoContinuous = false;
        } else {
            System.out.print(DEFAULT +"  ٩( ᐛ )   く ");
            isAhoContinuous = true;
        }
        System.out.println(ITALIC + ahoNumber + "っ!");
    }

    private String getAhoNumber(int number) {
        //桁数を取得のためString型に変換
        String strNum = String.valueOf(number);

        //2桁の場合
        if (strNum.length() == 2) {
            //十の位の数字を取得
            String ahoNum = getTenthPlaceNumber(strNum);

            //一の位の数字を取得
            char tmp = strNum.charAt(1);
            int no = Character.getNumericValue(tmp);
            ahoNum += AHO_NUMBER[no];

            return ahoNum;
        }
        //1桁の場合
        return AHO_NUMBER[number];
    }

    private String getTenthPlaceNumber(String strNum) {
        //十の位の数字をint型で取得
        char tmp = strNum.charAt(0);
        int no = Character.getNumericValue(tmp);

        String str = "";

        //十の位が1の場合「じゅう」だけを返し、
        //それ以外は「数字 + じゅう」と返す
        if (no == 1) {
            str = "じゅう";
        } else {
            str = AHO_NUMBER[no] + "じゅう";
        }
        return str;
    }

    private void stopTime(int time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {}
    }
}

上記にはエントリポインタがないのでメインクラスも作成

Aho
public class Aho {
    public static void main(String[] args) {

        World nabeatsu  = new World();

        nabeatsu.aho(100);
    }
}

というわけでなべあつの完成である。
実は初心者用ということで一クラスにまとめたかったのだが(メインは2行だけでアホにすらなれないし)、
けど、どうしてもこの一文が書きたかったのだ。もう仕方ない。

World nabeatsu  = new World();

出オチ感はいなめない

4.実行結果

というわけで、早速上記をそのまま実行してみる。

result

(`・ω・´)く 1
(`・ω・´)く 2
٩( ᐛ )く さんっ!
(`・ω・´)く 4
(`・ω・´)く 5
( ᐖ )۶く ろくっ!

~~中略~~

(`・ω・´)く 26
٩( ᐛ )く にじゅうななっ!
(`・ω・´)く 28
(`・ω・´)く 29
( ᐖ )۶く さんじゅうっ!
٩( ᐛ )く さんじゅういちっ!
( ᐖ )۶く さんじゅうにっ!
٩( ᐛ )く さんじゅうさんっ!
( ᐖ )۶く さんじゅうよんっ!
٩( ᐛ )く さんじゅうごっ!
( ᐖ )۶く さんじゅうろくっ!
٩( ᐛ )く さんじゅうななっ!
( ᐖ )۶く さんじゅうはちっ!
٩( ᐛ )く さんじゅうきゅうっ!
(`・ω・´)く 40

~~中略~~

(`・ω・´)く 98
٩( ᐛ )く きゅうじゅうきゅうっ!
(`・ω・´)く 100
ᐠ( ᐛ )ᐟく オモロー!

なかなかいい感じになったと思う。実際に実行していただけると分かるが、Thread.sleep(処理を指定時間止めるメソッド)を使用したり、アホの場合はイタリック(斜体)で表示されたりとコンソール上でも動きがあって結構面白い動きができているんじゃないだろうか。

5.解説

全体

まずは全体の感想として、少しばかり冗長になりすぎたかなという反省点。ネストを使用しない分メソッドが多くなったり、単純な書き方を意識するあまり、全体的に長くなってしまった。もう少し分かりづらくてもぎゅっと引き締めた方がむしろ初心者にとっても分かりやすいかも?

        //containsメソッドを使う為にString型に変換
        String strNum = String.valueOf(number);

        //数字に3がついていれば「true」を返す
        if (strNum.contains("3")) {
            return true;
        }

ここなんかは

        if(number / 10 == 3 || number == 3) {
            return true;
        }

で、代用できたりする。わざわざString型に変換などしなくても後者の方が分かりやすかったんじゃないかと書き終わった後に気づいたが、前者は前者で仕様変更した場合三桁以上でも対応できたりするのでこのままにしておいた。

流れ

初心者で「ちょっとコード長すぎてどこ見ていいか分かんないよー」って方向けに

プログラムの流れが分かりやすいよう、メインフローとなる部分を抜き出します

    void aho(int maxNumber) {
        for (int number = 1; number <= maxNumber; number++) {
            //引数で渡した数字が「3の倍数」か「3の付く数字」かをチェックする
            //[アホ]=true //[アホじゃない]=false
            boolean isAho = ahoCheck(number);

            //なべあつに喋ってもらう
            sayNumber(isAho, number);
        }

ここでやっていることは
1. for文で1から数字をインクリメントしながら繰り返し処理をします
2. 現在の数字がアホになる数字(3の倍数か3が付く数字)かを判定します
٩( ᐛ )くさんっ! 2の判定に応じてコンソールに表示する文字を変化させます

この大きな流れさえ掴めれば、あとは細かいところを実装するだけです。

遊び心(という名の妥協)

//3桁の数字の場合、アホになのでなべあつは逃げる
//でも100は頑張って言う
        if (number > 100) {
            throw new NullPointerException("もう無理……");
        }

この部分。最初は何桁(int型の範囲)でも対応できるようにしようと思っていたのですが、これを実装しようとしたらこれ以上コードが長くなってしまって、もっと根本的な「初心者でも分かりやすく」が成り立たなくなってしまうことを危惧して諦めました。
もしこの記事を見て、自分で書いてみよう、と思われた方がいらっしゃいましたら、ぜひ何桁でも対応できるパターンも書いてみてください。

( ᐖ )۶ ろくっ! 最後に

ここまでご覧いただきありがとうございます。
ふと思い立ったので書いてみたのですが、やはりこういった遊び心が溢れるプログラムは書いていて楽しいですね。

もしよろしければこのコードこういう書き方すればもっと分かりやすいよ!などありましたらぜひコメントお願いいたします。
世界のナベアツ

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

【Java】アノテーションの使い方

プログラミング勉強日記

2020年11月23日
Javadocについて調べたときにアノテーションについて知ったので、まとめる。

アノテーションとは

 アノテーションは注釈、注記といった意味を表す英語で、プログラミング用語でも注意書きを追加する機能になり余白に文字を追加する機能。アノテーションを理解できればJavaのコードがより見やすくなる。
 アノテーションはデータの有無、データの数によって3種類に分かれる。

  • 名前だけでデータのないマーカー(@Override、@Deprecatedなど)
  • データ1つ持つ単位アノテーション(@SuppressWarningsなど)
  • 複数のデータを持つフルアノテーション

よく使われるアノテーション

@Override

 記述するメソッドが親クラスのメソッドをオーバーライドしていることを示す。親クラスにないメソッドをエラーにして、記述の間違いをなくすことができる。

@Deprecated

 記述するメソッドの仕様が非推奨であることを示す。Javaのバージョンが新しくなっても、前の記述を残してきたいときなどに使う。

@SuppressWarning

 引数にメッセージを指定することで、記述するメソッドによる警告を非表示にすることができる。

その他のアノテーション

 上で紹介した3つのアノテーションとは別に、独自のアノテーションを定義するために用意されているメタアノテーションと呼ばれているものがある。

@Target

独自に定義したアノテーションが何を対象としているか宣言するために使われる。クラスやメソッドに付けるのか、変数につけるのかを決める。

@Retention

 コンパイル時やプログラム実行時にアノテーションの情報を保持するかどうかを決めるために使われる。

@author

 作者の名前を記述する

@param

 引数と引数の説明を記述

@return

 戻り値の説明を記述

@exception

 メソッドが投げる例外クラスとその説明を記述

@version

 クラスやメソッドなどのバージョンを記述

@see

 他の関連するクラスやメソッドなどの参照先を記述

参考文献

いまさら聞けない「Javadoc」と「アノテーション」入門 (1/4)
【Java入門】アノテーションの使い方と作成する方法
Javaの「アノテーション」とは?使い方・作り方を解説

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

ArchUnit使い方メモ

ArchUnit とは

  • Java プログラムのアーキテクチャをユニットテストするためのライブラリ
  • パッケージやクラス、レイヤ間の依存関係をチェックしたり、循環参照をチェックしたりできる
  • 読みは「あーきゆにっと」

環境

Java

>java -version
openjdk version "11.0.8" 2020-07-14
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.8+10)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.8+10, mixed mode)

OS

Windows 10 Home 64bit

JUnit

build.gradle
dependencies {
    testImplementation "org.junit.jupiter:junit-jupiter:5.7.0"
    ...
}

Hello World

実装

フォルダ構成
|-build.gradle
`-src/
  |-main/java/
  | `-example/
  |   |-controller/
  |   | `-FooController.java
  |   `-service/
  |     `-FooService.java
  |
  `-test/java/
    `-example/
      `-ArchUnitTest.java
build.gradle
plugins {
    id "java"
}

sourceCompatibility = 11
targetCompatibility = 11

[compileJava, compileTestJava]*.options*.encoding = "UTF-8"

repositories {
    mavenCentral()
}

dependencies {
    testImplementation "org.junit.jupiter:junit-jupiter:5.7.0"
    testImplementation "com.tngtech.archunit:archunit-junit5:0.14.1"
}

test {
    useJUnitPlatform()
}
FooService.java
package example.service;

import example.controller.FooController;

public class FooService {
    // Service が Controller に依存した状態になっている
    FooController controller;
}
ArchUnitTest.java
package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class ArchUnitTest {

    @Test
    void test() {
        JavaClasses javaClasses = new ClassFileImporter().importPackages("example");

        ArchRule rule = noClasses().that().resideInAPackage("..service..")
                        .should().dependOnClassesThat().resideInAPackage("..controller..");

        rule.check(javaClasses);
    }
}

実行結果

実行結果
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package '..service..' should depend on classes that reside in a package '..controller..'' was violated (1 times):
Field <example.service.FooService.controller> has type <example.controller.FooController> in (FooService.java:0)

説明

依存関係

build.gradle
dependencies {
    ...
    testImplementation "com.tngtech.archunit:archunit-junit5:0.14.1"
}
  • JUnit5 で ArchUnit を使う場合は、 com.tngtech.archunit:archunit-junit5 を依存関係に追加する

クラスをインポートする

ArchUnitTest.java
package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
...
import org.junit.jupiter.api.Test;

public class ArchUnitTest {

    @Test
    void test() {
        JavaClasses javaClasses = new ClassFileImporter().importPackages("example");
        ...
    }
}
  • ArchUnit のテストは、まずクラスをインポートすることから始める
  • インポートには ClassFileImporter を使用する
  • 上記例の importPackages(String) は、指定したパッケージ以下のクラスを再帰的に読み込んで JavaClasses 型で返している
    • JavaClasses には、読み込まれたクラスが含まれている

アーキテクチャのルールを定義する

ArchUnitTest.java
package example;

...
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class ArchUnitTest {

    @Test
    void test() {
        ...
        ArchRule rule = noClasses().that().resideInAPackage("..service..")
                        .should().dependOnClassesThat().resideInAPackage("..controller..");
        ...
    }
}
  • アーキテクチャのルール(検証内容)は、 ArchRule という型で定義する
  • ArchRule を作るための API として、 ArchRuleDefinition に静的なファクトリメソッドが用意されている
    • ここでは noClasses()ArchRule の定義を開始している
  • ArchRule の定義は流れるようなインタフェースで構築できるようになっている
    • noClasses() は、このあとに続くルールに該当するクラスが存在しないことを定義している
    • resideInAPackage(String) は、パッケージの範囲を絞り込んでいる
      • 2つのドット(..)は、任意のサブパッケージを表している
      • したがって ..service.. は、すべての service という名前のパッケージ以下の、すべてのサブパッケージを対象としている
    • dependOnClassesThat() は、 should() より前で指定した対象が依存する先を定義している
      • .should().dependOnClassesThat().resideInAPackage("..controller..") は、 controller パッケージ以下のクラスに依存しなければならないことを宣言している
    • 以上の宣言が組み合わさることで、「service パッケージ以下には、 controller パッケージ以下のクラスに依存しているクラスが存在しない」ことをルールとして定義している
      • noClasses() なので、後ろのルールに該当するクラスが存在しないことを定義していることになる

ルールをチェックする

ArchUnitTest.java
...

public class ArchUnitTest {

    @Test
    void test() {
        ...        
        rule.check(javaClasses);
    }
}
  • 作成した ArchRulecheck(JavaClasses) メソッドに、インポートした JavaClasses を渡す
  • これにより、 JavaClasses に含まれるクラスが ArchRule で定義されたルールを守っているかどうかを検証できる

3つのAPI

ArchUnit には、次の3つの API が用意されている

  • Core API
  • Lang API
  • Library API

Core API

FooController
package example.controller;

import example.service.FooService;

public class FooController {
    private final FooService fooService;

    public FooController(FooService fooService) {
        this.fooService = fooService;
    }

    public String invoke() {
        return fooService.execute();
    }
}
CoreAPIでクラスの情報を参照している例
package example;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import example.controller.FooController;

public class Main {

    public static void main(String[] args) {
        JavaClasses javaClasses = new ClassFileImporter().importPackages("example");

        JavaClass fooControllerClass = javaClasses.get(FooController.class);

        fooControllerClass.getFields().forEach(field -> {
            System.out.println("name=" + field.getName() + ", type=" + field.getRawType());
        });

        fooControllerClass.getMethods().forEach(method -> {
            System.out.println("name=" + method.getName() + ", return type=" + method.getRawReturnType());
        });
    }
}
実行結果
name=fooService, type=JavaClass{name='example.service.FooService'}
name=invoke, return type=JavaClass{name='java.lang.String'}
  • ArchUnit の最も基本となる API
  • JavaClassJavaMethod のような、 Java プログラムの要素を表す型で構成されている
  • リフレクション API と同じように、クラスのメタ情報を参照できる
  • さらに、クラスの参照情報なども取得できるようになっている
クラスの参照情報を取得する例
package example;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import example.service.FooService;

public class Main {

    public static void main(String[] args) {
        JavaClasses javaClasses = new ClassFileImporter().importPackages("example");

        JavaClass fooServiceClass = javaClasses.get(FooService.class);
        fooServiceClass.getDirectDependenciesToSelf().forEach(System.out::println);
    }
}
実行結果(実際は3行で出力されるが、見やすいように少し整形している)
Dependency{
  originClass=JavaClass{name='example.controller.FooController'},
  targetClass=JavaClass{name='example.service.FooService'},
  lineNumber=13,
  description=Method <example.controller.FooController.invoke()> calls method <example.service.FooService.execute()> in (FooController.java:13)
}
Dependency{
  originClass=JavaClass{name='example.controller.FooController'},
  targetClass=JavaClass{name='example.service.FooService'},
  lineNumber=0,
  description=Field <example.controller.FooController.fooService> has type <example.service.FooService> in (FooController.java:0)
}
Dependency{
  originClass=JavaClass{name='example.controller.FooController'},
  targetClass=JavaClass{name='example.service.FooService'},
  lineNumber=0,
  description=Constructor <example.controller.FooController.<init>(example.service.FooService)> has parameter of type <example.service.FooService> in (FooController.java:0)
}
  • getDirectDependenciesToSelf() により、 FooService を直接参照している場所を取得している
  • このように、 Core API は解析対象となる Java プログラムの情報を参照するための、最も低レベルな API を提供している

Core API は抽象度が低い

  • 理屈上は、この Core API を使うことで Java プログラムの構造を解析し、意図したアーキテクチャ・ルールを守れているかをチェックできる
  • しかし、そのためにはプログラムの情報を抽出して検証する処理を自力で実装する必要があり、かなり大変な作業になる
  • Core API は最も基本的な API のため何でもできるが、「チェックしたいアーキテクチャ・ルール」を記述するには抽象度が低すぎる
  • そこで、 Core API をラップしてより抽象度の高い記述をできるようにした Lang API が用意されている

主要クラスの型階層

archunit.jpg

  • Core API に含まれる Java コードの構造を表す主要なクラスは、上図のような関係になっている
  • JavaMember はメソッド・コンストラクタ・フィールドをまとめたものを表し、
    JavaCodeUnit はメソッドとコンストラクタをまとめたものを表している
  • JavaClass は、 JavaMember とは独立した存在となっている
  • その他のクラスも含めた完全な図は ユーザーガイドの図 を参照

Lang API

package example;

import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class Main {

    public static void main(String[] args) {
        ArchRule rule = noClasses().that().resideInAPackage("..service..")
                            .should().dependOnClassesThat().resideInAPackage("..controller..");

        System.out.println(rule.getDescription());
    }
}
実行結果
no classes that reside in a package '..service..' should depend on classes that reside in a package '..controller..'
  • Lang API は、前述の Core API よりも抽象度の高い記述でアーキテクチャのルールを定義できる API となっている
  • ArchRuleDefinition のファクトリメソッドを起点として、流れるようなインタフェースでルールを記述できる

Library API

package example;

import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.library.Architectures.*;

public class Main {

    public static void main(String[] args) {
        ArchRule rule = layeredArchitecture()
                .layer("Controller").definedBy("..controller..")
                .layer("Service").definedBy("..service..")
                .layer("Persistence").definedBy("..persistence..")
                .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
                .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
                .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service");

        System.out.println(rule.getDescription());
    }
}
実行結果
Layered architecture consisting of
layer 'Controller' ('..controller..')
layer 'Service' ('..service..')
layer 'Persistence' ('..persistence..')
where layer 'Controller' may not be accessed by any layer
where layer 'Service' may only be accessed by layers ['Controller']
where layer 'Persistence' may only be accessed by layers ['Service']
  • Library API は、特定のアーキテクチャパターンに関するルールを、より簡潔に記述できるようにした API を提供している
  • ここでは、レイヤードアーキテクチャのレイヤーを定義し、レイヤー間の依存関係のルールを宣言している
  • 2020年11月現在、レイヤードアーキテクチャ以外にもオニオンアーキテクチャ用の API も用意されている

クラスの読み込み

プロジェクト構成
`-src/
  |-main/java/
  | `-example/archunit/
  |   |-Hoge.java
  |   |-foo/
  |   | `-Foo.java
  |   `-bar/
  |     `-Bar.java
  `-test/java/
    `-example/
      `-Main.java
package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;

public class Main {

    public static void main(String[] args) {
        JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit");
        javaClasses.forEach(System.out::println);
    }
}
実行結果
JavaClass{name='example.archunit.bar.Bar'}
JavaClass{name='example.archunit.foo.Foo'}
JavaClass{name='example.archunit.Hoge'}
  • ArchUnit を使うためには、まずは検証対象となるクラスを読み込む必要がある
  • クラスの読み込みには、 ClassFileImporter を使用する
  • importPackages(String...) メソッドを使用すると、引数で指定したパッケージ以下を再帰的に探索してクラスを読み込むことができる
  • 読み込み結果は JavaClasses という型で返される
  • クラスは、実行時のクラスパスから読み込まれる
  • したがって、テスト実行時などは src/test/java 以下のクラスも読み込み対象になるので注意が必要
package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;

public class Main {

    public static void main(String[] args) {
        JavaClasses javaClasses = new ClassFileImporter().importPackages("example"); // 対象を example パッケージに変更
        javaClasses.forEach(System.out::println);
    }
}
実行結果
JavaClass{name='example.archunit.foo.Foo'}
JavaClass{name='example.archunit.bar.Bar'}
JavaClass{name='example.Main'}
JavaClass{name='example.archunit.Hoge'}
  • src/test/java フォルダ以下の Main クラスも読み込まれている

パス指定で読み込む

プロジェクト構成
|-build.gradle
|-build/
| |-classes/java/main/
| : `-example/archunit/
|     :
`-src/
  • Gradle でコンパイルすると、 src/main/java 以下のソースのコンパイル結果は build/classes/java/main 以下に出力される
  • この場所を指定して読み込むこともできる
package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;

public class Main {

    public static void main(String[] args) {
        JavaClasses javaClasses = new ClassFileImporter().importPath("build/classes/java/main");
        javaClasses.forEach(System.out::println);
    }
}
実行結果
JavaClass{name='example.archunit.bar.Bar'}
JavaClass{name='example.archunit.foo.Foo'}
JavaClass{name='example.archunit.Hoge'}
  • importPath(String) メソッドを使うと、引数で指定したパスからクラスファイルを読み込む

その他の読み込み方

上記以外にも、特定のクラスだけを読み込んだり、 URL 指定で読み込む方法なども提供されている。
詳細は、 ClassFileImporter の Javadoc を参照。

読み込み対象の場所を絞る

package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;

public class Main {

    public static void main(String[] args) {
        JavaClasses javaClasses = new ClassFileImporter()
                .withImportOption(location -> location.contains("/foo/") || location.contains("/bar/"))
                .importPackages("example.archunit");
        javaClasses.forEach(System.out::println);
    }
}
実行結果
JavaClass{name='example.archunit.bar.Bar'}
JavaClass{name='example.archunit.foo.Foo'}
  • withImportOption(ImportOption) を使用すると、読み込み対象となる場所を任意に絞ることができる
    • withImportOption() は、 ImportOption を置き換えた新しい ClassFileImporter インスタンスを返す
    • 元の ClassFileImport インスタンスはそのままなので注意
  • ImportOptionincludes(Location) メソッドには、現在読み込もうとしている場所が Location オブジェクトで渡されるので、この場所を対象とするかどうかを boolean で返すように実装する

組み込みの ImportOption

package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;

public class Main {

    public static void main(String[] args) {
        JavaClasses javaClasses = new ClassFileImporter()
                .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
                .importPackages("example");
        javaClasses.forEach(System.out::println);
    }
}
  • ImportOption.Predefined に、よく使いそうな ImportOption の定数が用意されている
  • DO_NOT_INCLUDE_ARCHIVES
    • アーカイブを対象外にする
    • アーカイブと jar の違いは Location.isArchive() で説明されている
    • JAR ファイルと、Java 9 で追加された JRT ファイルとかいうのを含んだものをアーカイブと呼んでいる
  • DO_NOT_INCLUDE_JARS
    • JAR ファイルを対象外にする
  • DO_NOT_INCLUDE_TESTS
    • テストコードを対象外にする
    • 具体的には、 Location の場所が次のいずれかの正規表現にマッチすると対象外になる
      • .*/target/test-classes/.*
      • .*/build/classes/([^/]+/)?test/.*
      • .*/out/test/classes/.*

依存するクラスの自動的な読み込み

パッケージ構成
`-example/archunit/
  |-fizz/
  | |-Hoge.java
  | `-Fuga.java
  `-buzz/
    |-Foo.java
    `-Bar.java
Hoge.java
package example.archunit.fizz;

@Deprecated
public class Hoge {}
Fuga.java
package example.archunit.fizz;

public class Fuga {}
Foo.java
package example.archunit.buzz;

import example.archunit.fizz.Hoge;

public class Foo {
    Hoge hoge = new Hoge();
}
Bar.java
package example.archunit.buzz;

import example.archunit.fizz.Fuga;

public class Bar {
    Fuga fuga = new Fuga();
}
package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class ArchUnitTest {

    @Test
    void test() {
        JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit.buzz");
        javaClasses.forEach(System.out::println);

        final ArchRule rule = noClasses()
                .should().accessClassesThat().areAnnotatedWith(Deprecated.class);

        rule.check(javaClasses);
    }
}
  • example.archunit.buzz パッケージを読み込んで、 @Deprecated で注釈されたクラスにアクセスしているクラスが存在しないことをチェックしている
実行結果
JavaClass{name='example.archunit.buzz.Bar'}
JavaClass{name='example.archunit.buzz.Foo'}

java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'no classes should access classes that are annotated with @Deprecated' was violated (1 times):
Constructor <example.archunit.buzz.Foo.<init>()> calls constructor <example.archunit.fizz.Hoge.<init>()> in (Foo.java:6)
  • ClassFileImporter で読み込んだクラスは Foo, Bar の2つだけになっている(Hoge クラスは読み込まれていない)
  • しかし、 Hoge クラスが @Deprecated で注釈されており、制約に違反していることは判定できている
  • このように、読み込んだクラスが依存しているクラスは、たとえインポート対象に入っていなくても自動的に読み込まれるようになっている
  • この仕組みのおかげで、インポート対象の指定は検証対象だけを意識した設定で済むようになっている
  • しかし、もしこの自動的な読み込みがパフォーマンスに影響を与えるような場合は、次のようにして自動的な読み込みをオフにすることもできる
    • 自動的にインポートされるクラスが大量にあるが、それらが検証で利用されていないようなケース

自動的な読み込みをオフにする

archunit.properties
resolveMissingDependenciesFromClassPath=false
  • テスト実行時のクラスパス直下に archunit.properties というファイルを配置する
  • ここに、 resolveMissingDependenciesFromClassPath=false と設定する
  • すると、依存先の自動的な読み込み機能がオフになる
  • 上と同じテストを、このプロパティファイルを配置した状態で実行すると、結果は下のようになる
自動読み込みをオフにした場合の実行結果
JavaClass{name='example.archunit.buzz.Bar'}
JavaClass{name='example.archunit.buzz.Foo'}

Process finished with exit code 0
  • テストは正常終了した
  • 自動読み込みがオフになっている場合、インポート先に指定していないクラスは、自動的にスタブに置き換えられる
    • この場合、 Hoge クラスがスタブに置き換えられて解析が行われている
  • スタブは要はハリボテなので、オリジナルのクラスが持つ特徴(@Deprecated で注釈されていること)は反映されていない
  • したがって、テストは通ってしまっている
  • 自動読み込みの機能をオフにする場合は、依存先のクラスもインポート対象に含まれるように注意して指定する必要がある

Lang API の文法

Lang API は、基本的に以下の文法で記述できるようになっている。

<対象> that <対象を絞り込む条件> should <検証する制約>

例えば、 Hello World で書いた Lang API の記述は、次のように読み取ることができる。

// <対象>: 制約を満たす対象が存在しないことを定義
noClasses()
  .that()
  // <対象を絞り込む条件>: service 以下のパッケージに絞り込み
  .resideInAPackage("..service..")
  .should()
  // <検証する制約>: 「controller パッケージに依存している」という制約を定義
  .dependOnClassesThat().resideInAPackage("..controller..");

つまり、「<対象> that <対象を絞り込む条件>」で検証対象を特定し、「should <検証する制約>」で検証内容を記述するという形になっている。

条件の追加

package example;

import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class Main {

    public static void main(String[] args) {
        ArchRule rule =
            noClasses()
                .that()
                    .resideInAPackage("..service..")
                    .or().resideInAPackage("..persistence..")
                .should()
                    .accessClassesThat().resideInAPackage("..controller..")
                    .orShould().accessClassesThat().resideInAPackage("..ui..");

        System.out.println(rule.getDescription());
    }
}
実行結果
no classes that reside in a package '..service..' or reside in a package '..persistence..' should access classes that reside in a package '..controller..' or should access classes that reside in a package '..ui..'
  • <対象を絞り込む条件><検証する制約> の部分は、 orand で条件を追加できるようになっている

対象の選択

  • Lang API で検証対象を選択するには ArchRuleDefinition に定義された static なファクトリメソッドを使用する
  • まずは、分かりやすいものだけを表でまとめる
メソッド 説明
classes() 全型(クラス・インタフェース)を対象にする。
メンバークラス、ローカルクラス、匿名クラスも対象。
theClass(Class), theClass(String) 特定の型だけを対象にする。文字列の方は完全名を渡す。
constructors() 全コンストラクタを対象にする。
fields() 全フィールドを対象にする。
methods() 全メソッドを対象にする。
members() 全メンバー(コンストラクタ・フィールド・メソッド)を対象にする。
ここで言っているメンバーとは、主要クラスの型階層で出てきた JavaMember を指しているので、メンバークラスは関係ない。
codeUnits() 全コードユニット(コンストラクタ・メソッド)を対象にする。
  • さらに、これらを否定するメソッドも用意されている
    • noClasses()
    • noClass(Class), noClass(String)
    • noConstructors()
    • noFields()
    • noMethods()
    • noMembers()
    • noCodeUnits()
  • これらは、 should() 以後の条件に一致する対象が存在しないことを定義したいときに使用する

対象の絞り込み条件

  • that() の後ろで指定する、対象を絞り込むためのメソッドについて整理する
  • that() の戻り値の型は、最初に対象を選択したメソッド(classes() とか methods() とか)によって変化する
対象の選択 that()が返す型
classes().that() ClassesThat
members().that() MembersThat
codeUnits().that() CodeUnitsThat
constructors().that() CodeUnitsThat (※ConstrutcorsThat は無い)
methods().that() MethodsThat
fields().that() FieldsThat
  • MembersThatCodeUnitThat主要クラスの型階層 と同じ継承関係になっているので、親で定義された絞り込みのメソッドは子のクラスでも使用できる

対象を絞り込むメソッドの一覧

注意事項

  • areNotAnnotatedWith() のような否定形のメソッドは省略している(基本的にどのメソッドにも否定形のメソッドが用意されている)
  • areAnnotatedWith(String), areAnnotatedWith(Class) のようにオーバーロードされているメソッドは、1種類だけ(基本的に Class 型のものだけ)記載している

ClassesThat

メソッド 説明
areAnnotatedWith(Class) 指定されたアノテーションで注釈されている型に絞る
areAnonymousClasses() 匿名クラスに絞る
areAssignableFrom(Class) 指定された型のインスタンスを代入可能な型に絞る
(対象型の変数 = 引数の型のインスタンス;)
areAssignableTo(Class) 指定された型に代入可能なクラスに絞る
(引数の型の変数 = 対象型のインスタンス;)
areEnums() enumに絞る
areInnerClasses() 内部クラスに絞る
areInterfaces() インタフェースに絞る
areLocalClasses() ローカルクラスに絞る
areMemberClasses() メンバークラスに絞る
areMetaAnnotatedWith(Class) 指定されたメタアノテーションで注釈されたアノテーションで注釈されている型に絞る。
areNestedClasses() ネストクラスに絞る
arePackagePrivate() パッケージプライベートな型に絞る
arePrivate() private な型に絞る
areProtected() protected な型に絞る
arePublic() public な型に絞る
areTopLevelClasses() トップレベルクラスに絞る
belongToAnyOf(Class...) 指定された型と、その型に属する全てのネストクラスを対象にする
haveFullyQualifiedName(String) 完全修飾名が指定された文字列と一致する型に絞る
haveModifier(JavaModifier) 指定された修飾子を持つ型に絞る
haveNameMatching(String) 完全修飾名が指定された正規表現にマッチする型に絞る
haveSimpleName(String) 単純名が指定された文字列と一致する型に絞る
haveSimpleNameContaining(String) 単純名に指定された文字列を含む型に絞る
haveSimpleNameEndingWith(String) 単純名が指定された文字列で終わる型に絞る
haveSimpleNameStartingWith(String) 単純名が指定された文字列で始まる型に絞る
implement(Class) 指定されたインタフェースを実装したクラスに絞る
resideInAnyPackage(String...) 指定されたいずれかのパッケージに含まれる型に絞る
resideInAPackage(String) 指定されたパッケージに含まれる型に絞る
resideOutsideOfPackage(String) 指定されたパッケージに含まれない型に絞る
resideOutsideOfPackages(String...) 指定されたいずれのパッケージにも含まれない型に絞る

MembersThat

メソッド 説明
areAnnotatedWith(Class) 指定されたアノテーションで注釈されているメンバーに絞る
areDeclaredIn(Class) 指定された型内で宣言されているメンバーに絞る(親の型で宣言されているものは含まない)
areDeclaredInClassesThat() この後で定義する条件を満たすクラスで宣言されているメンバーに絞る
areMetaAnnotatedWith(Class) 指定されたメタアノテーションで注釈されたアノテーションで注釈されているメンバーに絞る
arePackagePrivate() パッケージプライベートなメンバーに絞る
arePrivate() private なメンバーに絞る
areProtected() protected なメンバーに絞る
arePublic() public なメンバーに絞る
haveFullName(String) 完全名が指定された文字列と一致するメンバーに絞る。
haveFullNameMatching(String) 完全名が指定された正規表現にマッチするメンバーに絞る
haveModifier(JavaModifier) 指定された修飾子を持つメンバーに絞る
haveName(String) 名前が指定された文字列と一致するメンバーに絞る
haveNameMatching(String) 名前が指定された正規表現にマッチするメンバーに絞る

CodeUnitsThat

メソッド 説明
declareThrowableOfType(Class) throws句に指定された型の例外を含むコードユニットに絞る
haveRawParameterTypes(Class...) 仮引数の型の並びが指定されたものと一致するコードユニットに絞る
haveRawReturnType(Class) 戻り値の型が指定されたものと一致するコードユニットに絞る

MethodsThat

メソッド 説明
areFinal() final なメソッドに絞る
areStatic() static なメソッドに絞る

FieldsThat

メソッド 説明
areFinal() final なフィールドに絞る
areStatic() static なフィールドに絞る
haveRawType(Class) 型が指定されたものと一致するフィールドに絞る

対象を絞り込むメソッドの詳細

実際の動きを見てみないと理解しづらいメソッドに絞って説明。

匿名クラス・内部クラス・ローカルクラス・メンバークラス・ネストクラス・トップレベルクラスの違い

TopLevelClass
package example.archunit;

public class TopLevelClass {
    class MemberClass {}
    static class StaticMemberClass {}
    interface MemberInterface {}

    void method() {
        class LocalClass {}
        Object anonymousClass = new Object() {};
    }
}
TopLevelInterface
package example.archunit;

public class TopLevelInterface {}
package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class ArchUnitTest {

    @Test
    void test() {
        JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit");

        report("areAnonymousClasses()", javaClasses, classes().that().areAnonymousClasses().should().beAnnotatedWith(Deprecated.class));
        report("areInnerClasses()", javaClasses, classes().that().areInnerClasses().should().beAnnotatedWith(Deprecated.class));
        report("areLocalClasses()", javaClasses, classes().that().areLocalClasses().should().beAnnotatedWith(Deprecated.class));
        report("areMemberClasses()", javaClasses, classes().that().areMemberClasses().should().beAnnotatedWith(Deprecated.class));
        report("areNestedClasses()", javaClasses, classes().that().areNestedClasses().should().beAnnotatedWith(Deprecated.class));
        report("areTopLevelClasses()", javaClasses, classes().that().areTopLevelClasses().should().beAnnotatedWith(Deprecated.class));
    }

    void report(String title, JavaClasses javaClasses, ArchRule rule) {
        System.out.println("[" + title + "]");
        System.out.println(rule.evaluate(javaClasses).getFailureReport() + "\n");
    }
}
実行結果
[areAnonymousClasses()]
Architecture Violation [Priority: MEDIUM] - Rule 'classes that are anonymous classes should be annotated with @Deprecated' was violated (1 times):
Class <example.archunit.TopLevelClass$1> is not annotated with @Deprecated in (TopLevelClass.java:0)

[areInnerClasses()]
Architecture Violation [Priority: MEDIUM] - Rule 'classes that are inner classes should be annotated with @Deprecated' was violated (3 times):
Class <example.archunit.TopLevelClass$1> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelClass$1LocalClass> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelClass$MemberClass> is not annotated with @Deprecated in (TopLevelClass.java:0)

[areLocalClasses()]
Architecture Violation [Priority: MEDIUM] - Rule 'classes that are local classes should be annotated with @Deprecated' was violated (1 times):
Class <example.archunit.TopLevelClass$1LocalClass> is not annotated with @Deprecated in (TopLevelClass.java:0)

[areMemberClasses()]
Architecture Violation [Priority: MEDIUM] - Rule 'classes that are member classes should be annotated with @Deprecated' was violated (3 times):
Class <example.archunit.TopLevelClass$MemberClass> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelClass$MemberInterface> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelClass$StaticMemberClass> is not annotated with @Deprecated in (TopLevelClass.java:0)

[areNestedClasses()]
Architecture Violation [Priority: MEDIUM] - Rule 'classes that are nested classes should be annotated with @Deprecated' was violated (5 times):
Class <example.archunit.TopLevelClass$1> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelClass$1LocalClass> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelClass$MemberClass> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelClass$MemberInterface> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelClass$StaticMemberClass> is not annotated with @Deprecated in (TopLevelClass.java:0)

[areTopLevelClasses()]
Architecture Violation [Priority: MEDIUM] - Rule 'classes that are top level classes should be annotated with @Deprecated' was violated (2 times):
Class <example.archunit.TopLevelClass> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelInterface> is not annotated with @Deprecated in (TopLevelInterface.java:0)

整理すると、以下のような関係になっている。

archunit.jpg

  • 以下2つがポイントだと思う
    • areInnerClasses()static なクラスは対象外
    • areNestedClasses() は、トップレベルクラス内で宣言された全てのクラスが対象になる

メタアノテーションとは

パッケージ構成
`-example/archunit/
  |-MetaAnnotation.java
  |-FooAnnotation.java
  |-BarAnnotation.java
  |-Foo.java
  `-Bar.java
MetaAnnotation.java
package example.archunit;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MetaAnnotation {}
FooAnnotation.java
package example.archunit;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@MetaAnnotation // ★@MetaAnnotation で注釈されている
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FooAnnotation {}
BarAnnotation.java
package example.archunit;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// ★@MetaAnnotation では注釈されていない
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface BarAnnotation {}
Foo.java
package example.archunit;

@FooAnnotation
public class Foo {}
Bar.java
package example.archunit;

@BarAnnotation
public class Bar {}
package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import example.archunit.MetaAnnotation;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class ArchUnitTest {

    @Test
    void test() {
        JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit");

        ArchRule rule = classes().that().areMetaAnnotatedWith(MetaAnnotation.class)
                .should().beAnnotatedWith(Deprecated.class);

        rule.check(javaClasses);
    }
}
実行結果
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that are meta-annotated with @MetaAnnotation should be annotated with @Deprecated' was violated (1 times):
Class <example.archunit.Foo> is not annotated with @Deprecated in (Foo.java:0)
  • メタアノテーションとは、「アノテーションを注釈するアノテーション」のこと
  • 上の例では @MetaAnnotation がメタアノテーションで、 @FooAnnotation を注釈している
  • areMetaAnnotatedWith(Class) は、引数で指定されたメタアノテーション(@MetaAnnotaion)で注釈されているアノテーション(@FooAnnotation)で注釈されている型(Foo)に絞っている
  • Bar を注釈している BarAnnotation はメタアノテーションで注釈されていないので、対象にはならない
  • Spring Framework の @Component とかがメタアノテーションになる

完全修飾名

Hoge.java
package example.archunit;

import java.util.List;

public class Hoge {
    int primitive;
    String object;
    int[] primitiveArray;
    String[] objectArray;
    int[][] nestedPrimitiveArray;
    String[][] nestedObjectArray;
    List<String> genericClass;
    MemberClass memberClass;
    StaticMemberClass staticMemberClass;

    class MemberClass {}
    static class StaticMemberClass {}
}
package example;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import example.archunit.Hoge;

public class Main {

    public static void main(String[] args) {
        JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit");

        final JavaClass hogeClass = javaClasses.get(Hoge.class);
        hogeClass.getFields().forEach(field -> {
            System.out.println(field.getRawType().getFullName());
        });
    }
}
実行結果
int
java.lang.String
[I
[[Ljava.lang.String;
example.archunit.Hoge$StaticMemberClass
[Ljava.lang.String;
[[I
example.archunit.Hoge$MemberClass
java.util.List

コードユニットの完全名

Hoge.java
package example.archunit;

import java.util.List;

public class Hoge {
    Hoge(int i) {}
    public void method1() {}
    protected String method2(int i, String s, int[] ints, List<String> list) {return null;}

    public static class MemberClass {
        void method() {}
    }
}
package example;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import example.archunit.Hoge;

public class Main {

    public static void main(String[] args) {
        JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit");

        final JavaClass hogeClass = javaClasses.get(Hoge.class);
        hogeClass.getCodeUnits().forEach(method -> System.out.println(method.getFullName()));

        final JavaClass memberClass = javaClasses.get(Hoge.MemberClass.class);
        memberClass.getCodeUnits().forEach(method -> System.out.println(method.getFullName()));
    }
}
実行結果
example.archunit.Hoge.method1()
example.archunit.Hoge.method2(int, java.lang.String, [I, java.util.List)
example.archunit.Hoge.<init>(int)
example.archunit.Hoge$MemberClass.method()
example.archunit.Hoge$MemberClass.<init>()
  • コードユニットの完全名は、 型の完全修飾名.メソッド名(仮引数の型の完全修飾名, ...) となる
    • コンストラクタの場合は、 メソッド名<init> となる

パッケージのパターン指定

  • resideInAPackage(String) などのパッケージをマッチングさせるメソッドでは、 AspectJ 風の特殊な記法でパッケージのパターンを指定できる
  • 具体的には、次の2つのパターンが用意されている
    • *:ドット(.)以外の、1文字以上の文字列
    • ..:0以上の任意の数のパッケージ
  • 下図は、パッケージ構成に対してパターンがどのようにマッチするかをまとめている
    • 丸印のところが、パターンに対してパッケージ名がマッチするケースになる

archunit.jpg

制約の定義

  • should() の後ろで指定する、制約(ルール)を定義するメソッドについて整理する
  • that() と同じで、選択した対象によって should() の戻り値の型は次のように変化する
対象の選択 should()が返す型
classes().should() ClassesShould
members().should() MembersShould
codeUnits().should() CodeUnitsShould
constructors().should() CodeUnitsShould (※ConstructorsShould は無い)
methods().should() MethodsShould
fields().should() FieldsShould

制約を定義するメソッドの一覧

注意事項

  • notBe(Class) のような否定形のメソッドは省略している
  • be(Class), be(String) のようにオーバーロードされているメソッドは、1種類だけ(基本的に Class 型のものだけ)記載している

ClassesShould

メソッド 説明
accessClassesThat() 対象の型に、この後で定義する条件を満たす型にアクセスしている箇所が1つ以上あることを制約にする。
否定形での利用が前提。
accessField(Class, String) 対象の型からアクセスしているフィールドのうち、最低1つは指定されたフィールドであることを制約にする。
否定形での利用が前提。
be(Class) 対象の型が、指定されたクラスと一致することを制約にする。
beAnnotatedWith(Class) 対象の型が、指定されたアノテーションで注釈されていることを制約にする。
beAnonymousClasses() 対象の型が匿名クラスであることを制約にする。
beAssignableFrom(Class) 対象の型の変数に、指定された型のインスタンスを代入できることを制約にする。(対象型の変数 = 指定された型のインスタンス;)
beAssignableTo(Class) 対象の型のインスタンスを、指定された型の変数に代入できることを制約にする。(指定された型の変数 = 対象型のインスタンス;)
beEnums() 対象の型が enum であることを制約にする。
beInnerClasses() 対象の型が内部クラスであることを制約にする。
beInterfaces() 対象の型がインタフェースであることを制約にする。
beLocalClasses() 対象の型がローカルクラスであることを制約にする。
beMemberClasses() 対象の型がメンバークラスであることを制約にする。
beMetaAnnotatedWith(Class) 対象の型が、指定されたメタアノテーションで注釈されたアノテーションで注釈されていることを制約にする。
beNestedClasses() 対象の型がネストされた型であることを制約にする。
bePackagePrivate() 対象の型がパッケージプライベートであることを制約にする。
bePrivate() 対象の型が private であることを制約にする。
beProtected() 対象の型が protected であることを制約にする。
bePublic() 対象の型が public であることを制約にする。
beTopLevelClasses() 対象の型がトップレベルであることを制約にする。
callConstructor(Class, Class...) 対象の型で実行しているコンストラクタのうち、最低1つは指定されたコンストラクタと一致することを制約にする。
否定形での利用が前提。
callMethod(Class, String, Class...) 対象の型で実行しているメソッドのうち、最低1つは指定されたメソッドと一致することを制約にする。
否定形での利用が前提。
dependOnClassesThat() 対象の型が、この後で定義する条件を満たす型に依存することを制約にする。
否定形での利用が前提。
getField(Class, String) 対象の型からアクセスしているフィールドのうち、最低1つは指定したフィールドの読み取りであることを制約にする。
否定形での利用が前提。
haveFullyQualifiedName(String) 対象の型の完全修飾名が、指定された文字列と一致することを制約にする。
haveModifier(JavaModifier) 対象の型が、指定された修飾子を持つことを制約にする。
haveNameMatching(String) 対象の型の完全修飾名が、指定された正規表現にマッチすることを制約にする。
haveOnlyFinalFields() 対象の型が持つフィールドが全て final であることを制約にする。
haveOnlyPrivateConstructors() 対象の型が持つコンストラクタが全て private であることを制約にする。
haveSimpleName(String) 対象の型の単純名が、指定された文字列と一致することを制約にする。
haveSimpleNameContaining(String) 対象の型の単純名が、指定された文字列を含むことを制約にする。
haveSimpleNameEndingWith(String) 対象の型の単純名が、指定された文字列で終わることを制約にする。
haveSimpleNameStartingWith(String) 対象の型の単純名が、指定された文字列で始まることを制約にする。
implement(Class) 対象のクラスが、指定されたインタフェースを実装していることを制約にする。
onlyAccessClassesThat() 対象の型が、この後で定義する条件を満たすクラスのインスタンスにだけアクセスしていることを制約にする。
onlyAccessFieldsThat(DescribedPredicate) 対象の型が、指定した条件を満たすフィールドにだけアクセスしていることを制約にする。
onlyAccessMembersThat(DescribedPredicate) 対象の型が、指定した条件を満たすメンバーにだけアクセスしていることを制約にする。
onlyBeAccessed() 対象の型が、このあとに定義する条件を満たすクラスからのみアクセスされていることを制約にする。
onlyCallCodeUnitsThat(DescribedPredicate) 対象の型が、指定した条件を満たすコードユニットだけを呼び出していることを制約にする。
onlyCallConstructorsThat(DescribedPredicate) 対象の型が、指定した条件を満たすコンストラクタだけを呼び出していることを制約にする。
onlyCallMethodsThat(DescribedPredicate) 対象の型が、指定した条件を満たすメソッドだけを呼び出していることを制約にする。
onlyDependOnClassesThat() 対象の型が、この後に定義する条件を満たすクラスにだけ依存していることを制約にする。
onlyHaveDependentClassesThat() 対処の型が、この後に定義する条件を満たすクラスからのみ依存されていることを制約にする。
resideInAnyPackage(String...) 対象の型が、指定したいずれかのパッケージに含まれることを制約にする。
resideInAPackage(String) 対象の型が、指定されたパッケージに含まれることを制約にする。
resideOutsideOfPackage(String) 対象の型が、指定されたパッケージに含まれないこと制約にする。
resideOutsideOfPackages(String) 対象の型が、指定されたいずれのパッケージにも含まれないことを制約にする。
setField(Class, String) 対象の型からアクセスしているフィールドのうち、最低1つは指定したフィールドの書き込みであることを制約にする。
否定形での利用が前提。

DescribedPredicate を引数に受け取るメソッドは、後述する 事前定義された述語API を使うと比較的簡単に DescribedPredicate を構築できる。

MembersShould

メソッド 説明
beAnnotatedWith(Class) 対象のメンバーが、指定されたアノテーションで注釈されていることを制約にする。
beDeclaredIn(Class) 対象のメンバーが、指定された型の中で宣言されていることを制約にする。
beDeclaredInClassesThat() 対象のメンバーが、この後に定義する条件を満たす型の中で宣言されていることを制約にする。
beMetaAnnotatedWith(Class) 対象のメンバーが、指定されたメタアノテーションで注釈されたアノテーションで注釈されていることを制約にする。
bePackagePrivate() 対象のメンバーが、パッケージプライベートであることを制約にする。
bePrivate() 対象のメンバーが、 private であることを制約にする。
beProtected() 対象のメンバーが、 protected であることを制約にする。
bePublic() 対象のメンバーが、 public であることを制約にする。
haveFullName(String) 対象のメンバーの完全修飾名が、指定された文字列と一致することを制約にする。
haveFullNameMatching(String) 対象のメンバーの完全修飾名が、指定された正規表現にマッチすることを制約にする。
haveModifier(JavaModifier) 対象のメンバーが、指定された修飾子を持つことを制約にする。
haveName(String) 対象のメンバーの名前が、指定された文字列と一致することを制約にする。
haveNameMatching(String) 対象のメンバーの名前が、指定された正規表現にマッチすることを制約にする。

CodeUnitsShould

メソッド 説明
declareThrowableOfType(Class) 対象のコードユニットの throws 句に、指定された例外の型が含まれていることを制約にする。
haveRawParameterTypes(Class...) 対象のコードユニットの仮引数が、指定された型の並びと一致することを制約にする。
haveRawReturnType(Class) 対象のコードユニットの戻り値の型が、指定された型と一致することを制約にする。

MethodsShould

メソッド 説明
beFinal() 対象のメソッドが final であることを制約にする。
beStatic() 対象のメソッドが static であることを制約にする。

FieldsShould

メソッド 説明
beFinal() 対象のフィールドが final であることを制約にする。
beStatic() 対象のフィールドが static であることを制約にする。
haveRawType(Class) 対象のフィールドの型が指定された型と一致することを制約にする。

制約を定義するメソッドの詳細

否定形での利用を前提としたメソッド

  • 以下のメソッドは、おそらく否定形で使用することが前提となっている
    • accessClassesThat()
    • accessField(Class, String)
    • getField(Class, String)
    • setField(Class, String)
    • callConstructor(Class, Class...)
    • callMethod(Class, String, Class...)
    • dependOnClassesThat()
  • これらは、いずれも「条件に一致する処理が最低1つあること」を制約にする
  • そのままだと、いまいち使い道が分からない
    • 制約の内容に対して、出力されるエラーメッセージが関係しておらず、一見すると意味がわからないエラーメッセージになる
  • 以下は、 accessClassesThat() をそのまま利用した場合の例
パッケージ構成
`-example/archunit/
  |-foo/
  | `-Foo.java
  |-bar/
  | `-Bar.java 
  `-target/
    |-NoAccess.java
    |-DeclareFooField.java
    `-CallFooConstructor.java
Foo.java
package example.archunit.foo;

public class Foo {}
Bar.java
package example.archunit.bar;

public class Bar {}
NoAccess.java
package example.archunit.target;

public class NoAccess {}
DeclareFooField.java
package example.archunit.target;

import example.archunit.foo.Foo;

public class DeclareFooField {
    Foo foo;
}
CallFooConstructor.java
package example.archunit.target;

import example.archunit.bar.Bar;
import example.archunit.foo.Foo;

public class CallFooConstructor {
    Foo foo = new Foo();
    Bar bar = new Bar();
}
package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class ArchUnitTest {

    @Test
    void test() {
        JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit.target");

        ArchRule rule = classes().should().accessClassesThat().resideInAPackage("example.archunit.foo");

        rule.check(javaClasses);
    }
}
実行結果
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes should access classes that reside in a package 'example.archunit.foo'' was violated (2 times):
Constructor <example.archunit.target.DeclareFooField.<init>()> calls constructor <java.lang.Object.<init>()> in (DeclareFooField.java:5)
Constructor <example.archunit.target.NoAccess.<init>()> calls constructor <java.lang.Object.<init>()> in (NoAccess.java:3)
  • accessClassesThat() は、対象の型が、この後ろで定義した条件を満たす型に1回以上アクセスしていることを制約として定義する
  • 上記例では、 example.archunit.target 配下のクラスは、1回以上 example.archunit.foo 配下のクラスにアクセスしていることが制約として定義されている
  • NoAccess は、全くアクセスしていないのでエラーになっている
  • DeclareFooField は、一見すると example.archunit.foo.Foo を使っているので問題ないように見える
    • しかし、 ArchUnit における「アクセス」は、フィールドを参照したりメソッドを実行していることを表している
    • したがって、フィールドを定義しているだけの DeclareFooField は「アクセスしていない」ことになり、エラーとなっている
  • CallFooConstructor は、コンストラクタを呼び出して「アクセスしている」ので、エラーにはなっていない
    • 関係ないクラス Bar にもアクセスしているが、これはこの制約には関係しない

否定形で使用する

  • 上記例を見ると、制約内容に対してエラーメッセージの内容が全然マッチしていなくて、何が原因でエラーになっているのかわからない
  • 実際のところ、この accessClassesThat() や上で挙げた getField() などのメソッドは、否定形で使用されることが想定されている(たぶん)
否定形で使用している例
package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class ArchUnitTest {

    @Test
    void test() {
        JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit.target");

        ArchRule rule = noClasses().should().accessClassesThat().resideInAPackage("example.archunit.foo");

        rule.check(javaClasses);
    }
}
  • noClasses() になって、 example.archunit.target 配下には example.archunit.foo 配下のクラスにアクセスしているクラスは存在しないことが制約となっている
実行結果
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'no classes should access classes that reside in a package 'example.archunit.foo'' was violated (1 times):
Constructor <example.archunit.target.CallFooConstructor.<init>()> calls constructor <example.archunit.foo.Foo.<init>()> in (CallFooConstructor.java:6)
  • Foo のコンストラクタを実行している CallFooConstructor がエラーになった
  • エラーメッセージも、制約内容と整合の取れた内容になっていて、問題の原因が理解できるようになっている

only 系のメソッド

  • 名前に only が付く以下のメソッドは、名前の通り「その対象にだけを利用している・されている」ことを制約にできる
    • onlyAccessClassesThat()
    • onlyAccessFieldsThat(DescribedPredicate)
    • onlyAccessMembersThat(DescribedPredicate)
    • onlyCallCodeUnitsThat(DescribedPredicate)
    • onlyCallConstructorsThat(DescribedPredicate)
    • onlyCallMethodsThat(DescribedPredicate)
    • onlyBeAccessed()
    • onlyDependOnClassesThat()
    • onlyHaveDependentClassesThat()
  • ただし、この only はちょっと癖があるので使い方に注意が必要
  • 以下は、 onlyAccessClassesThat() の利用例
パッケージ構成
`-example/archunit/
  |-foo/
  | `-Foo.java
  |-bar/
  | `-Bar.java
  `-target/
    |-NoAccess.java
    |-AccessFoo.java
    `-AccessBar.java
Foo.java
package example.archunit.foo;

public class Foo {
    public String value;
}
Bar.java
package example.archunit.bar;

public class Bar {}
NoAccess.java
package example.archunit.target;

public class NoAccess {}
AccessFoo.java
package example.archunit.target;

import example.archunit.foo.Foo;

public class AccessFoo {
    String value = new Foo().value;
}
AccessBar.java
package example.archunit.target;

import example.archunit.bar.Bar;

public class AccessBar {
    Bar bar = new Bar();
}
package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class ArchUnitTest {

    @Test
    void test() {
        JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit.target");

        ArchRule rule = classes().should().onlyAccessClassesThat().resideInAPackage("example.archunit.foo");

        rule.check(javaClasses);
    }
}
  • example.archunit.target 配下のクラスは、 example.archunit.foo パッケージ配下のクラスにだけアクセスしていることを制約にしているつもりのテストコード
  • AccessBarexample.archunit.bar.Bar にアクセスしているので、テストは落ちる気がする
  • 一方で、 NoAccess はどこにもアクセスしていないし AccessFooFoo にしかアクセスしていないので、この2つはテストが通りそうな気がする
  • しかし、実際の結果は次のようになる
実行結果
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes should only access classes that reside in a package 'example.archunit.foo'' was violated (6 times):
Constructor <example.archunit.target.AccessBar.<init>()> calls constructor <example.archunit.bar.Bar.<init>()> in (AccessBar.java:6)
Constructor <example.archunit.target.AccessBar.<init>()> calls constructor <java.lang.Object.<init>()> in (AccessBar.java:5)
Constructor <example.archunit.target.AccessBar.<init>()> sets field <example.archunit.target.AccessBar.bar> in (AccessBar.java:6)
Constructor <example.archunit.target.AccessFoo.<init>()> calls constructor <java.lang.Object.<init>()> in (AccessFoo.java:5)
Constructor <example.archunit.target.AccessFoo.<init>()> sets field <example.archunit.target.AccessFoo.value> in (AccessFoo.java:6)
Constructor <example.archunit.target.NoAccess.<init>()> calls constructor <java.lang.Object.<init>()> in (NoAccess.java:3)
  • AccessBar がエラーになるのは良いとして、 NoAccess, AccessFoo もエラーになってしまっている
  • NoAccess は、コンストラクタで Object クラスのコンストラクタを呼んでいるとしてエラーになっている
  • AccessFoo は、同じくコンストラクタのエラーに加えて、自分自身のフィールド value にアクセスしていることでエラーとなっている
  • このように、 only のついた制約はかなり厳しい条件となる
  • したがって、上記テストを意図通りに動かしたい場合は、アクセスを許可するパッケージを以下のように緩和する必要がある
許可パッケージを緩和した例
package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class ArchUnitTest {

    @Test
    void test() {
        JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit.target");

        ArchRule rule = classes().should().onlyAccessClassesThat()
                .resideInAnyPackage("example.archunit.foo", "example.archunit.target", "java..");

        rule.check(javaClasses);
    }
}
  • resideInAnyPackage(String...) を使い、複数のパッケージを指定できるようにしている
  • 対象のパッケージに、対象自身を含むパッケージと java パッケージ配下を追加することで、条件を緩和している
実行結果
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes should only access classes that reside in any package ['example.archunit.foo', 'example.archunit.target', 'java..']' was violated (1 times):
Constructor <example.archunit.target.AccessBar.<init>()> calls constructor <example.archunit.bar.Bar.<init>()> in (AccessBar.java:6)
  • これで、当初期待したとおり AccessBar だけがエラーになった

対象・条件・制約のカスタマイズ

対象の選択 の表を見ると、「全てのパッケージを対象にする」メソッドは用意されていないことが分かる。
パッケージ以外にも、分析したい視点によって対象は様々なケースが考えられる。

ArchRuleDefinition では、これら全てのケースに対応したメソッドを用意しておくことはできない。
その代わりに、任意の対象を抽出するための API が用意されている。

package example;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaPackage;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.AbstractClassesTransformer;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.ClassesTransformer;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import org.junit.jupiter.api.Test;

import java.util.stream.Collectors;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class ArchUnitTest {

    @Test
    void test() {
        JavaClasses javaClasses = new ClassFileImporter().importPath("build/classes/java/main");

        // 全パッケージを対象とする
        ClassesTransformer<JavaPackage> packages = new AbstractClassesTransformer<>("packages") {
            @Override
            public Iterable<JavaPackage> doTransform(JavaClasses classes) {
                return classes.stream().map(JavaClass::getPackage).collect(Collectors.toSet());
            }
        };

        // クラスを含むパッケージのみを条件にする
        DescribedPredicate<JavaPackage> haveClasses = new DescribedPredicate<>("contains classes") {
            @Override
            public boolean apply(JavaPackage javaPackage) {
                return !javaPackage.getClasses().isEmpty();
            }
        };

        // package-info.java を持つことを制約にする
        ArchCondition<JavaPackage> havePackageInfo = new ArchCondition<>("have package-info.java") {
            @Override
            public void check(JavaPackage javaPackage, ConditionEvents events) {
                if (!javaPackage.tryGetPackageInfo().isPresent()) {
                    events.add(SimpleConditionEvent.violated(javaPackage, javaPackage.getName() + " package does not have package-info.java."));
                }
            }
        };

        ArchRule rule = all(packages).that(haveClasses).should(havePackageInfo);

        rule.check(javaClasses);
    }
}
実行結果
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'packages that contains classes should have package-info.java' was violated (4 times):
example.controller package does not have package-info.java.
example.sandbox package does not have package-info.java.
example.service package does not have package-info.java.
example.target package does not have package-info.java.

対象のカスタマイズ

        // 全パッケージを対象とする
        ClassesTransformer<JavaPackage> packages = new AbstractClassesTransformer<>("packages") {
            @Override
            public Iterable<JavaPackage> doTransform(JavaClasses classes) {
                return classes.stream().map(JavaClass::getPackage).collect(Collectors.toSet());
            }
        };
  • all(ClassesTransformer) を使うことで、対象を絞り込む処理をカスタマイズできる
    • ClassesTransformer を実装したクラスを all() メソッドに渡す
    • AbstractClassesTransformer を継承して作れば、 doTransform(JavaClasses) だけを実装すればよくなる
    • doTransform() には読み込まれた JavaClasses が渡されるので、対象にしたいオブジェクトに変換して return する
  • all() メソッドを使用した場合、その後の that()should() も、同じようにカスタマイズしたクラスを渡す必要がある

条件のカスタマイズ

        // クラスを含むパッケージのみを条件にする
        DescribedPredicate<JavaPackage> haveClasses = new DescribedPredicate<>("contains classes") {
            @Override
            public boolean apply(JavaPackage javaPackage) {
                return !javaPackage.getClasses().isEmpty();
            }
        };
  • that(DescribedPredicate) には、対象を絞り込む条件を実装した DescribedPredicate インスタンスを渡す
    • コンストラクタ引数には、 ArchRulegetDescription() で返される説明に使用される文言(that の後ろ)を設定する
    • packages that contains classes should have package-info.javacontains classes の部分
  • apply(T) メソッドは、受け取った対象が条件に一致する場合に true を返すよう実装する

事前定義された述語 API

  • all() を使った場合は、常に DescribedPredicate を実装したクラスを用意しなければならないのかというと、そうでもない
  • 述語を記述するための事前定義された API が用意されており、それを使えば all() を使わない場合と同じような記述ができる
package example;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;

import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;

public class Main {

    public static void main(String[] args) {
        DescribedPredicate<JavaClass> resideInTheFooPackage = resideInAPackage("..foo..");
        ...
    }
}

条件の連結

package example;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;

import java.io.Serializable;

import static com.tngtech.archunit.core.domain.JavaClass.Predicates.type;
import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name;

public class Main {

    public static void main(String[] args) {
        DescribedPredicate<JavaClass> isHogeSerializable = type(Serializable.class).and(name("Hoge"));
        ...
    }
}
  • DescribedPredicate には、条件を連結させるために and()or() などの様々なメソッドが用意されている
  • これをつなげることで、より複雑な条件を組み立てられる

連結順序の制約

  • 上の例では、 type(Serializable.class).and(name("Hoge")) のように先に Serializable 型であることを定義して、その後で名前が Hoge である条件を追加している
  • これを、もし逆順で組み立てようとするとコンパイルエラーになる
逆順で組み立てようとした場合
DescribedPredicate<HasName> isHogeSerializable = name("Hoge").and(type(Serializable.class));
// java: 不適合な型: com.tngtech.archunit.base.DescribedPredicate<com.tngtech.archunit.core.domain.JavaClass>を
//   com.tngtech.archunit.base.DescribedPredicate<? super com.tngtech.archunit.core.domain.properties.HasName>に変換できません:
  • 順序を逆にしても意味は変わらないはずだが、 Java の言語仕様の制約上、このコンパイルエラーは回避できない
    • DescribedPredicate<T>and() メソッドの引数は、 DescribedPredicate<? super T> という下限付き境界ワイルドカード型で定義されている
    • したがって、
      • JavaClass.Predicates.type()and() は、引数の型が DescribedPredicate<? super JavaClass> となり、
        HasName.Predicates.name()and() は、引数の型が DescribedPredicate<? super HasName> となる
    • JavaClass implements HasName の関係があるので、
      • 引数 DescribedPredicate<? super JavaClass>DescribedPredicate<HasName> は渡せるが、
        引数 DescribedPredicate<? super HasName>DescribedPredicate<JavaClass> は渡せない
  • これを回避する方法は、以下の2つのいずれかになる
    • コンパイルが通る順序で書く
    • forSubType() を使って型の情報を変更する
  • forSubType() を使った場合は、次のような実装になる
forSubType()を使った例
package example;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;

import java.io.Serializable;

import static com.tngtech.archunit.core.domain.JavaClass.Predicates.type;
import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name;

public class Main {

    public static void main(String[] args) {
        // forSubType() を使って、無理やり型を DescribedPredicate<JavaClass> に変更する
        DescribedPredicate<JavaClass> nameIsHoge = name("Hoge").forSubType();
        DescribedPredicate<JavaClass> isHogeSerializable = nameIsHoge.and(type(Serializable.class));
        ...
    }
}

制約のカスタマイズ

        // package-info.java を持つことを制約にする
        ArchCondition<JavaPackage> havePackageInfo = new ArchCondition<>("have package-info.java") {
            @Override
            public void check(JavaPackage javaPackage, ConditionEvents events) {
                if (!javaPackage.tryGetPackageInfo().isPresent()) {
                    events.add(SimpleConditionEvent.violated(javaPackage, javaPackage.getName() + " package does not have package-info.java."));
                }
            }
        };
  • should(ArchCondition) には、検証する制約の処理を実装した ArchCondition インスタンスを渡す
    • コンストラクタ引数には、 ArchRulegetDescription() で返される説明に使用される文言(should の後ろ)を設定する
    • packages that contains classes should have package-info.javahave package-info.java の部分
  • check(T, ConditionEvents) メソッドで検証処理を実装する
    • 検証エラーの場合は、第二引数で受け取った ConditionEvents に違反の情報を追加する

事前定義された API

  • こちらも、条件の絞り込みのための述語 API と同じように、事前定義された API が用意されている
  • ただし、述語の API はいくつかの Predicates クラスに分かれて定義されていたのに対して、制約を定義するための API は ArchConditions クラスに全てまとめられている
ArchConditionsに定義されたメソッドでArchConditionを構築する
package example;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.lang.ArchCondition;

import static com.tngtech.archunit.lang.conditions.ArchConditions.accessClassesThatResideIn;

public class Main {

    public static void main(String[] args) {
        ArchCondition<JavaClass> resideInFooPackage = accessClassesThatResideIn("..foo..");
        ...
    }
}

参照元の取得

  • ArchUnit の Core API は、標準ライブラリのリフレクション API と似た機能を提供している
  • しかし、リフレクションにはない独自の重要な機能として、参照元を取得できるという特徴がある
パッケージ構成
`-example/archunit/
  |-foo/
  | |-Foo.java
  | `-Bar.java
  |-fizz/
  | |-Fizz.java
  | `-Buzz.java
  `-target/
    `-Hoge.java
Foo.java
package example.archunit.foo;

import example.archunit.target.Hoge;

public class Foo {
    void foo() {
        new Hoge().method();
    }
}
Bar.java
package example.archunit.foo;

import example.archunit.target.Hoge;

public class Bar {
    String value = new Hoge().field;
}
Fizz.java
package example.archunit.fizz;

public class Fizz {
    public void fizz() {}
}
Buzz.java
package example.archunit.fizz;

public class Buzz {
    public String buzz;
}
Hoge.java
package example.archunit.target;

import example.archunit.fizz.Buzz;
import example.archunit.fizz.Fizz;

public class Hoge {
    public String field;
    public void method() {
        new Fizz().fizz();
        String value = new Buzz().buzz;
    }
}
package example;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import example.archunit.target.Hoge;

public class Main {

    public static void main(String[] args) {
        final JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");

        final JavaClass hogeClass = classes.get(Hoge.class);

        hogeClass.getFieldAccessesToSelf().forEach(System.out::println);
        hogeClass.getFieldAccessesFromSelf().forEach(System.out::println);
        hogeClass.getMethodCallsToSelf().forEach(System.out::println);
        hogeClass.getMethodCallsFromSelf().forEach(System.out::println);
    }
}
実行結果
JavaFieldAccess{origin=JavaConstructor{example.archunit.foo.Bar.<init>()}, target=target{example.archunit.target.Hoge.field}, lineNumber=6, accessType=GET}
JavaFieldAccess{origin=JavaMethod{example.archunit.target.Hoge.method()}, target=target{example.archunit.fizz.Buzz.buzz}, lineNumber=10, accessType=GET}
JavaMethodCall{origin=JavaMethod{example.archunit.foo.Foo.foo()}, target=target{example.archunit.target.Hoge.method()}, lineNumber=7}
JavaMethodCall{origin=JavaMethod{example.archunit.target.Hoge.method()}, target=target{example.archunit.fizz.Fizz.fizz()}, lineNumber=9}
  • getFieldAccessesToSelf()getMethodCallsFromSelf() などのメソッド、その対象に(から)アクセスしているモノを取得できる
  • ~ToSelf は、その対象にアクセスしているモノを抽出し、
    ~FromSelf は、その対象からアクセスしているモノを抽出する
  • ここでは、クラスに(から)アクセスしているモノを全て抽出するメソッドを使用しているが、 hogeClass.getField("field").getAccessesToSelf()hogeClass.getMethod("method").getAccessesFromSelf() のように特定の要素に(から)アクセスしているモノを取得することもできる

参照を表すクラス

archunit.jpg

  • 参照を表すクラスは、上図のような構成になっている
  • 参照には向きがあり、 origin が参照元、 target が参照先を表している

参照先を表すクラス

archunit.jpg

  • 参照先を表すクラスは、次の3つが存在する
    • FieldAccessTarget
    • MethodCallTarget
    • ConstructorCallTarget
  • これらは、参照を表すクラス(JavaFieldAccess など)と1対1の関係になっている
  • 一方で、具体的な参照先を表すクラス(JavaField など)との関係は、 0* があったりするのが分かる
    • アクセスしているはずなのに具体的な参照先が 0 であったり、メソッドの呼び出し先が複数あるのは、普通に考えると不自然に思える

アクセス先が0になるケース

パッケージ構成
`-src/
  |-main/java/
  | `-example/archunit/
  |   |-foo/
  |   | `-Parent.java
  |   `-target/
  |     |-Child.java
  |     `-Hoge.java
  `-test/resources/
    `-archunit.properties
archunit.properties
resolveMissingDependenciesFromClassPath=false
Parent.java
package example.archunit.foo;

public class Parent {
    public String field;
    public void method() {}
}
Child.java
package example.archunit.target;

import example.archunit.foo.Parent;

public class Child extends Parent {}
Hoge.java
package example.archunit.target;

public class Hoge {
    void hoge() {
        final Child child = new Child();
        child.field = "test";
        child.method();
    }
}
package example;

import com.tngtech.archunit.core.domain.AccessTarget;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaField;
import com.tngtech.archunit.core.domain.JavaFieldAccess;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.domain.JavaMethodCall;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import example.archunit.target.Hoge;

import java.util.Set;

public class Main {

    public static void main(String[] args) {
        final JavaClasses classes = new ClassFileImporter().importPackages("example.archunit.target");

        final JavaClass hogeClass = classes.get(Hoge.class);

        for (JavaFieldAccess javaFieldAccess : hogeClass.getFieldAccessesFromSelf()) {
            final AccessTarget.FieldAccessTarget target = javaFieldAccess.getTarget();
            final JavaField javaField = target.resolveField().orNull();
            System.out.println("owner=" + target.getOwner() + ", javaField=" + javaField);
        }

        for (JavaMethodCall javaMethodCall : hogeClass.getMethodCallsFromSelf()) {
            final AccessTarget.MethodCallTarget target = javaMethodCall.getTarget();
            final Set<JavaMethod> javaMethods = target.resolve();
            System.out.println("owner=" + target.getOwner() + ", javaMethods=" + javaMethods);
        }
    }
}
  • example.archunit.target をインポートして、 Hoge からアクセスしている先を抽出している
実行結果
owner=JavaClass{name='example.archunit.target.Child'}, javaField=null
owner=JavaClass{name='example.archunit.target.Child'}, javaMethods=[]
  • 具体的な参照先の情報は空になっている
  • このように、参照先のフィールドやメソッドが親クラスに定義されていて、その親クラスがインポート対象に含まれていない場合に、具体的な参照先が空になる
  • ちなみに、自動的な読み込みをオンにして実行したら、下のように情報がちゃんと取れる
自動的な読み込みをオンにした場合の実行結果
owner=JavaClass{name='example.archunit.target.Child'}, javaField=JavaField{example.archunit.foo.Parent.field}
owner=JavaClass{name='example.archunit.target.Child'}, javaMethods=[JavaMethod{example.archunit.foo.Parent.method()}]

メソッドのアクセス先が複数になるケース

パッケージ構成
`-example/archunit/
  |-foo/
  | |-FooInterface.java
  | |-BarInterface.java
  | |-AbstractClass.java
  | `-ConcreteClass.java
  `-target/
    `-Hoge.java
FooInterface.java
package example.archunit.foo;

public interface FooInterface {
    void method();
}
BarInterface.java
package example.archunit.foo;

public interface BarInterface {
    void method();
}
  • 全く同じシグネチャのメソッドが、 FooInterfaceBarInterface の両方に定義されている
AbstractClass.java
package example.archunit.foo;

public abstract class AbstractClass implements FooInterface, BarInterface {}
  • FooInterfaceBarInterface の両方を実装した抽象クラスがあるが、インタフェースで定義されている抽象メソッドを実装していない
ConcreteClass.java
package example.archunit.foo;

public class ConcreteClass extends AbstractClass {
    @Override
    public void method() {}
}
  • 具象クラスでは、当然抽象メソッドを実装している
Hoge.java
package example.archunit.target;

import example.archunit.foo.AbstractClass;
import example.archunit.foo.ConcreteClass;

public class Hoge {
    void hoge() {
        AbstractClass cc = new ConcreteClass();
        cc.method();
    }
}
  • 抽象メソッド(method()) を、抽象クラス(AbstractClass)をレシーバにして実行している
package example;

import com.tngtech.archunit.core.domain.AccessTarget;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.domain.JavaMethodCall;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import example.archunit.target.Hoge;

import java.util.Set;

public class Main {

    public static void main(String[] args) {
        final JavaClasses classes = new ClassFileImporter().importPackages("example.archunit.target");

        final JavaClass hogeClass = classes.get(Hoge.class);

        for (JavaMethodCall javaMethodCall : hogeClass.getMethodCallsFromSelf()) {
            final AccessTarget.MethodCallTarget target = javaMethodCall.getTarget();
            final Set<JavaMethod> javaMethods = target.resolve();
            System.out.println("owner=" + target.getOwner() + ", javaMethods=" + javaMethods);
        }
    }
}
  • フィールドのときと同じように、呼び出し先のメソッドの情報を出力している
実行結果
owner=JavaClass{name='example.archunit.foo.AbstractClass'}, javaMethods=[JavaMethod{example.archunit.foo.BarInterface.method()}, JavaMethod{example.archunit.foo.FooInterface.method()}]
  • 解決されたメソッドが、インタフェースで定義された2つのメソッドになっている
  • 要するに、下図のような関係になっていると、 ArchUnit は呼び出し先の具体的なメソッドを一意に特定できなくなり、候補となるメソッドが全て取得されるようになっている

archunit.jpg

  • ちなみに、 AbstractClassmethod() を実装していた場合は以下のようになる
AbstractClassでmethod()を実装している場合の実行結果
owner=JavaClass{name='example.archunit.foo.AbstractClass'}, javaMethods=[JavaMethod{example.archunit.foo.AbstractClass.method()}]
  • ところで、 AbstractClass が抽象メソッドを実装していない状態(ConcreteClass が実装している最初の状態)で、メソッド呼び出しのレシーバを ConcreteClass にすると、次のような結果になる
レシーバをConcreteClassにした場合実装
package example.archunit.target;

import example.archunit.foo.ConcreteClass;

public class Hoge {
    void hoge() {
        ConcreteClass cc = new ConcreteClass();
        cc.method();
    }
}
レシーバがConcreteClassのときの実行結果
owner=JavaClass{name='example.archunit.foo.ConcreteClass'}, javaMethods=[JavaMethod{example.archunit.foo.ConcreteClass.method()}, JavaMethod{example.archunit.foo.BarInterface.method()}, JavaMethod{example.archunit.foo.FooInterface.method()}]
  • ConcreteClass.method() だけになるのかなと思ったら、インタフェースも抽出された
  • この動きの理由はよくわからない

エラーの説明を変更する

パッケージ構成
`-example/archunit/
  |-foo/
  | `-Foo.java
  `-bar/
    `-Bar.java
Foo.java
package example.archunit.foo;

import example.archunit.bar.Bar;

public class Foo {
    Bar bar;
}
Bar.java
package example.archunit.bar;

public class Bar {}
package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class ArchUnitTest {

    @Test
    void test() {
        final JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");

        // foo パッケージは bar パッケージに依存してはいけない
        ArchRule rule = noClasses().that().resideInAPackage("..foo..")
                .should().dependOnClassesThat().resideInAPackage("..bar..");

        rule.check(classes);
    }
}
実行結果(エラーの説明のみ)
Rule 'no classes that reside in a package '..foo..' should depend on classes that reside in a package '..bar..'' was violated
  • デフォルトでは、 ArchRule の内容に従って自動的に組み立てられた説明が出力される
  • この説明はかなり分かりやすい形になっているので、たいていの場合はデフォルトの出力で問題はない
  • しかし、次のようなケースではデフォルトの説明だけでは十分ではないことがありえる
    • ルールの意図を説明したい場合(何故そのようなルールを設けているのか、設計の意図を伝えたい)
    • orand で条件を組み合わせた結果、デフォルトで出力される説明が分かりにくくなった場合
  • エラー時の説明は、以下の方法で調整できる

ルールの意図を追加する

package example;

...

public class ArchUnitTest {

    @Test
    void test() {
        final JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");

        ArchRule rule = noClasses().that().resideInAPackage("..foo..")
                .should().dependOnClassesThat().resideInAPackage("..bar..")
                .because("俺が気に入らないから");

        rule.check(classes);
    }
}
実行結果
Rule 'no classes that reside in a package '..foo..' should depend on classes that reside in a package '..bar..', because 俺が気に入らないから' was violated
  • ArchRule の定義の中で because(String) メソッドを使うことで、説明の後ろに意図を説明する because 文が追加される

説明を完全に置き換える

package example;

...

public class ArchUnitTest {

    @Test
    void test() {
        final JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");

        ArchRule rule = noClasses().that().resideInAPackage("..foo..")
                .should().dependOnClassesThat().resideInAPackage("..bar..")
                .as("foo が bar に依存するとかマジないわー");

        rule.check(classes);
    }
}
実行結果
Rule 'foo が bar に依存するとかマジないわー' was violated
  • as(String) メソッドを使用すると、エラー時の説明を完全に置き換えることが可能
  • because(String) との併用も可能

違反を無視する

  • 既存プロジェクトに ArchUnit を適用していて段階的にチェックを入れていきたいような場合、一部のコードは制約違反があってもテストが失敗しないようにしたくなる
  • 一部のコードだけテストの対象外にしたい場合、例えば that() の後で対象となるコードを絞り込むといった方法が考えられる
  • また、これ以外にも ArchUnit には特定の制約違反があっても無視するようにできる手段が用意されている
パッケージ構成
`-src/
  |-main/java/
  | `-example/archunit/
  |   |-foo/
  |   | `-Foo.java
  |   `-target/
  |     |-Hoge.java
  |     `-Fuga.java
  `-test/resources/
    `-archunit_ignore_patterns.txt
  • クラスパスのルートに archunit_ignore_patterns.txt というテキストファイルが来るようにしている
archunit_ignore_patterns.txt
# Hoge は特別扱い
.*Hoge.*
Foo.java
package example.archunit.foo;

public class Foo {}
Hoge.java
package example.archunit.target;

import example.archunit.foo.Foo;

public class Hoge {
    Foo foo;
}
Fuga.java
package example.archunit.target;

import example.archunit.foo.Foo;

public class Fuga {
    Foo foo;
}
  • いずれも、 Foo クラスに依存している
package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class ArchUnitTest {

    @Test
    void test() {
        JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");

        ArchRule rule = noClasses().that().resideInAPackage("..target..")
                .should().dependOnClassesThat().resideInAPackage("..foo..");

        rule.check(classes);
    }
}
  • target パッケージ以下のクラスは foo パッケージ以下のクラスに依存しないことをテスト
  • 通常なら、 Hoge, Fuga 両方のクラスがエラーになる
実行結果
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package '..target..' should depend on classes that reside in a package '..foo..'' was violated (1 times):
Field <example.archunit.target.Fuga.foo> has type <example.archunit.foo.Foo> in (Fuga.java:0)
  • 実際にエラーになったのは、 Fuga クラスのみになった
  • このように、クラスパスルートに archunit_ignore_patterns.txt というテキストファイルを配置することで、特定の制約違反を無視できるようになる
  • archunit_ignore_patterns.txt には、無視したい制約違反のメッセージとマッチさせる正規表現を記述する
    • クラス名とかではなく、違反のメッセージにマッチさせる点に注意
    • 上記エラーメッセージでいうと、 Field <example.archunit.target.Fuga.foo> has type <example.archunit.foo.Foo> in (Fuga.java:0) の部分にマッチさせる正規表現となる
  • # で始まる行はコメント扱いになる

Library API

レイヤードアーキテクチャのテスト

archunit.jpg

仮に上図のように定義された依存関係をテストする場合の例。

パケージ構成
`-example/archunit/
  |-domain/
  | |-RepositoryInterface.java
  | `-DomainObject.java
  |-infrastructure/
  | `-RepositoryImpleClass.java
  |-service/
  | `-ServiceClass.java
  `-presentation/
    `-PresentationClass.java
DomainObject.java
package example.archunit.domain;

public class DomainClass {
    public String method() {
        return "domain";
    }
}
RepositoryInterface.java
package example.archunit.domain;

public interface RepositoryInterface {
    DomainClass find();
}
RepositoryImplClass.java
package example.archunit.infrastructure;

import example.archunit.domain.DomainClass;
import example.archunit.domain.RepositoryInterface;

public class RepositoryImplClass implements RepositoryInterface {
    @Override
    public DomainClass find() {
        return new DomainClass();
    }
}
ServiceClass.java
package example.archunit.service;

import example.archunit.domain.DomainClass;
import example.archunit.domain.RepositoryInterface;
import example.archunit.presentation.PresentationClass;

public class ServiceClass {
    // ★Service が Presentation に依存してしまっている
    private final PresentationClass presentationClass;
    private final RepositoryInterface repositry;

    public ServiceClass(PresentationClass presentationClass, RepositoryInterface repositry) {
        this.presentationClass = presentationClass;
        this.repositry = repositry;
    }

    public String method() {
        DomainClass domainObject = repositry.find();
        return domainObject.method();
    }
}
PresentationClass.java
package example.archunit.presentation;

import example.archunit.service.ServiceClass;

public class PresentationClass {
    private final ServiceClass service;

    public PresentationClass(ServiceClass service) {
        this.service = service;
    }

    public String method() {
        return service.method();
    }
}
package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.library.Architectures.*;

public class ArchUnitTest {

    @Test
    void test() {
        JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");

        ArchRule rule = layeredArchitecture()
                .layer("Domain").definedBy("example.archunit.domain..")
                .layer("Infrastructure").definedBy("example.archunit.infrastructure..")
                .layer("Service").definedBy("example.archunit.service..")
                .layer("Presentation").definedBy("example.archunit.presentation..")
                .whereLayer("Presentation").mayNotBeAccessedByAnyLayer()
                .whereLayer("Service").mayOnlyBeAccessedByLayers("Presentation")
                .whereLayer("Infrastructure").mayOnlyBeAccessedByLayers("Service", "Presentation")
                .whereLayer("Domain").mayOnlyBeAccessedByLayers("Presentation", "Service", "Infrastructure");

        rule.check(classes);
    }
}
  • Architectures.layeredArchitecture() を使って、レイヤー間の依存関係を定義している
  • layer(String).definedBy(String) で、レイヤーの名前と含まれるパッケージを定義している
  • whereLayer(String)mayNotBeAccessedByAnyLayer()mayOnlyBeAccessedByLaysers(String...) で、レイヤー間の依存関係を定義している
    • mayNotBeAccessedByAnyLayer() は、そこからも依存されないことを定義している
    • mayOnlyBeAccessedByLayers(String...) は、 whereLayer(String) で指定したレイヤーにアクセスできるレイヤー(複数可)を定義している
実行結果
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'Layered architecture consisting of
layer 'Domain' ('example.archunit.domain..')
layer 'Infrastructure' ('example.archunit.infrastructure..')
layer 'Service' ('example.archunit.service..')
layer 'Presentation' ('example.archunit.presentation..')
where layer 'Presentation' may not be accessed by any layer
where layer 'Service' may only be accessed by layers ['Presentation']
where layer 'Infrastructure' may only be accessed by layers ['Service', 'Presentation']
where layer 'Domain' may only be accessed by layers ['Presentation', 'Service', 'Infrastructure']' was violated (2 times):
Constructor <example.archunit.service.ServiceClass.<init>(example.archunit.presentation.PresentationClass, example.archunit.domain.RepositoryInterface)> has parameter of type <example.archunit.presentation.PresentationClass> in (ServiceClass.java:0)
Field <example.archunit.service.ServiceClass.presentationClass> has type <example.archunit.presentation.PresentationClass> in (ServiceClass.java:0)
  • Service レイヤーが Presentation レイヤーに依存してしまっているので、テストはエラーとなった

循環参照のチェック

パッケージ構成
`-example/archunit/
  |-one/
  | |-One.java
  | `-Foo.java
  |-two/
  | `-Two.java
  |-three/
  | `-Three.java
  |
  `-sub/
    |-hoge/
    | `-Hoge.java
    |-fuga/
    | `-Fuga.java
    `-piyo/
      `-Piyo.java
One.java
package example.archunit.one;

import example.archunit.two.Two;

public class One {
    private Two two;

    public One(Two two) {
        this.two = two;
    }
}
Two.java
package example.archunit.two;

import example.archunit.three.Three;

public class Two {
    private Three three;

    public Two(Three three) {
        this.three = three;
    }
}
Three.java
package example.archunit.three;

import example.archunit.one.Foo;

public class Three {
    private Foo foo;

    public Three(Foo foo) {
        this.foo = foo;
    }
}
  • one.One -> two.Two -> three.Three -> one.Foo という形で、パッケージの循環参照が発生している
  • 実装は省略するが、 sub 以下のクラスは sub.hoge.Hoge -> sub.fuga.Fuga -> sub.piyo.Piyo -> sub.hoge.Hoge -> ... のような循環参照になっている
package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.*;

public class ArchUnitTest {

    @Test
    void test1() {
        JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");

        ArchRule rule = slices().matching("example.archunit.(*)").should().beFreeOfCycles();

        rule.check(classes);
    }

    @Test
    void test2() {
        JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");

        ArchRule rule = slices().matching("example.archunit.(**)").should().beFreeOfCycles();

        rule.check(classes);
    }
}
  • test1() は、 matching() の引数が "example.archunit.(*)"
    test2() は、 matching() の引数が "example.archunit.(**)" になっている
test1の実行結果
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'slices matching 'example.archunit.(*)' should be free of cycles' was violated (1 times):
Cycle detected: Slice one -> Slice two -> Slice three -> Slice one
...
test2の実行結果
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'slices matching 'example.archunit.(**)' should be free of cycles' was violated (2 times):
Cycle detected: Slice one -> Slice two -> Slice three -> Slice one
...
Cycle detected: Slice sub.fuga -> Slice sub.piyo -> Slice sub.hoge -> Slice sub.fuga
...
  • パッケージの循環参照をチェックするには、まず SlicesRuleDefinition.slices()matching(String) で対象のパッケージを絞り込む
    • matching(String) の引数には、対象のパッケージを特定するためのパターンを指定する
    • 基本は resideInAPackage(String) で指定していたパターン と同じ
    • ただし、対象のパッケージを限定するための丸括弧 () が必要
      • 丸括弧で囲われた部分が異なるパッケージ間での依存の有無がチェックされる
      • a.(*).. のような指定の場合
        • a.ba.c の間で循環参照があればエラーになる
        • a.b.da.b.e の間で循環参照があってもエラーにはならない
    • (*) は、ドットを含まない任意の文字列にマッチし、 (**) はドットを含んだ任意の文字列とマッチする
    • したがって、 a.(*)a 直下のパッケージにマッチし、 a.(**) は孫も含む a の下の全てのパッケージにマッチする
    • 詳しくは、 PackageMatcher の Javadoc を参照
  • beFreeOfCycles() で、循環参照が無いことを制約として定義している
    • be free of chcles は、「サイクル(循環参照)が無い」と翻訳するらしい1(サイクルが自由=サイクルして良い、かと思って最初混乱した)

PlantUML で描いた図を使ってチェックする

パッケージ構成
`-src/
  |-main/java/
  | `-example/archunit/
  |   |-util/
  |   | `-UtilClass.java
  |   |-order/
  |   | `-OrderClass.java
  |   |-shipment/
  |   | `-ShipmentClass.java
  |   `-sales/
  |     `-SalesClass.java
  `-test/resources/
    `-component-diagram.pu
  • component-diagram.pu という PlantUML のファイルが追加されている
component-diagram.pu
@startuml ArchUnit

[受注] <<..order..>> as order
[出荷] <<..shipment..>> as shipment
[売上] <<..sales..>> as sales

order <-- shipment
shipment <-- sales

@enduml

archunit.jpg

  • PlantUML の コンポーネント図 を使って、パッケージ間の依存関係を記述している
  • ステレオタイプで、マッチさせるパッケージのパターンを設定しておく
OrderClass.java
package example.archunit.order;

import example.archunit.util.UtilClass;

public class OrderClass {
    UtilClass utilClass;
}
ShipmentClass.java
package example.archunit.shipment;

import example.archunit.order.OrderClass;

public class ShipmentClass {
    OrderClass orderClass;
}
SalesClass.java
package example.archunit.sales;

import example.archunit.order.OrderClass;
import example.archunit.shipment.ShipmentClass;

public class SalesClass {
    ShipmentClass shipmentClass;
    OrderClass orderClass; // order パッケージに依存している
}
package example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.library.plantuml.PlantUmlArchCondition;
import org.junit.jupiter.api.Test;

import java.net.URL;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
import static com.tngtech.archunit.library.plantuml.PlantUmlArchCondition.Configurations.*;
import static com.tngtech.archunit.library.plantuml.PlantUmlArchCondition.*;

public class ArchUnitTest {

    @Test
    void test1() {
        JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");

        URL diagram = getClass().getResource("/component-diagram.pu");
        PlantUmlArchCondition condition = adhereToPlantUmlDiagram(diagram, consideringOnlyDependenciesInDiagram());

        ArchRule rule = classes().should(condition);

        rule.check(classes);
    }
}
実行結果
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes should adhere to PlantUML diagram <component-diagram.pu> while ignoring dependencies not contained in the diagram' was violated (1 times):
Field <example.archunit.sales.SalesClass.orderClass> has type <example.archunit.order.OrderClass> in (SalesClass.java:0)
  • PlantUmlArchCondition.adhereToPlantUmlDiagram(...) メソッドを使用して PlantUML のテキストを読み込むことで、 PlantUML に記述した内容と実装を比較検証する PlantUmlArchCondition インスタンスを生成できる
    • adhereToPlantUmlDiagram() の第一引数には、読み込む PlantUML のファイルを渡す(File, URL, Path など)
    • 第二引数には、検証の範囲を絞り込む設定(PlantUmlArchCondition.Configuration)を渡す
    • PlantUmlArchCondition.Configuration に、以下3つのファクトリメソッドが用意されているので、そのいずれかを使用する
      • consideringAllDependencies()
        • 全てのクラス(java.lang.Object も含む)への依存を対象にして検証する
        • めっちゃシビアだけど、コンポーネントの描き忘れで検証を見落とすリスクは減る
      • consideringOnlyDependenciesInDiagram()
        • PlantUML の中に登場するコンポーネントへの依存を対象にして検証する
        • 前述の例では、 OrderClassUtilClass に依存していたが、 PlantUML の図中に含まれていないので検証では特にエラーにもなっていなかった
        • コンポーネント図への記述を忘れると検証されない(見逃す恐れがある)、ということなので注意
      • consideringOnlyDependenciesInAnyPackage(String, String...)
        • PlantUML の中に登場し、かつ引数で指定したパターンにマッチするパッケージを対象にして検証する

JUnit5 サポート

  • ClassFileImporter によるクラスの読み込みは、プロジェクトの規模によっては時間がかかるようになり、サイズも大きくなる恐れがある
  • ArchUnit には、一度読み込んだクラスの情報をキャッシュし、各テスト間で簡単に使いまわしできるようにする JUnit の拡張機能が用意されている
  • JUnit4 もサポートされているが、ここでは JUnit5 で検証する

Hello World

パッケージ構成
`-example/archunit/
  |-foo/
  | `-Foo.java
  `-bar/
    `-Bar.java
Foo.java
package example.archunit.foo;

import example.archunit.bar.Bar;

public class Foo {
    Bar bar;
}
Bar.java
package example.archunit.bar;

public class Bar {}
package example;

import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

@AnalyzeClasses(packages = "example.archunit")
public class ArchUnitTest {
    @ArchTest
    static ArchRule fooDoesNotDependOnBar = noClasses().that().resideInAPackage("..foo..")
            .should().dependOnClassesThat().resideInAPackage("..bar..");
}
実行結果
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package '..foo..' should depend on classes that reside in a package '..bar..'' was violated (1 times):
Field <example.archunit.foo.Foo.bar> has type <example.archunit.bar.Bar> in (Foo.java:0)

説明

...
import com.tngtech.archunit.junit.AnalyzeClasses;
...
@AnalyzeClasses(packages = "example.archunit")
public class ArchUnitTest {
...
  • まずは、テストクラスを @AnalyzeClasses で注釈する
    • このアノテーションで、読み込む範囲を定義する
    • このアノテーションは、 ClassFileImporter の機能と対応している
    • ここでは packages で読み込むパッケージを指定している
    @ArchTest
    static ArchRule fooDoesNotDependOnBar = noClasses().that().resideInAPackage("..foo..")
            .should().dependOnClassesThat().resideInAPackage("..bar..");
  • 次に、通常と同じ用に ArchRuleDefinition を使って検証したい制約(ArchRule)を定義する
  • 通常と異なるのは、作成した ArchRule を static フィールドに保存して、@ArchTest で注釈している点
    • これにより、 @AnalyzeClasses で読み込まれたクラスに対して ArchRulecheck() が自動的に実行されるようになる
    • static でなくても動くけど、テストケースごとにインスタンスを生成させる意味もないので static の方がスマートだと思う
    • 可視性は public でも private でも動いたので、何もつけなくて良さげ

テストクラスを読み込まないようにする

  • ClassFileImporter のときは、 withImportOption()ImportOption.Predefined.DO_NOT_INCLUDE_TESTS を渡すことでテストクラスを読み込み対象から外すことができた
  • @AnalyzeClasses にも、同じように ImportOption を指定する方法が用意されていて、以下のように設定する
package example;

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
...

@AnalyzeClasses(packages = "example.archunit", importOptions = ImportOption.DoNotIncludeTests.class)
public class ArchUnitTest {
    ...
}
  • importOptions に、 ImportOption を実装したクラスの Class オブジェクトを指定する
  • ImportOption には、 Predefined の定数と同じように ImporOption を実装したクラスがあらかじめメンバークラスとして定義されているので、それらを指定できる

その他のクラスの読み込み方

  • packages のパッケージ指定以外にも、以下のような指定方法が用意されている
  • packagesOf による、 Class 指定の読み込み
    • @AnalyzeClasses(packagesOf = {Foo.class, Bar.class})
    • この場合、指定された Class が存在するパケージが読み込みの対象となる
    • この方法には、リファクタリングでパッケージ構成などが変わった場合に記述を修正しなくて済むというメリットがある(IDE でのリファクタリングが前提)
  • LocationProvider による実装
    • @AnalyzeClasses(locations = MyLocationProvider.class)
    • locations に、 LocationProvider を実装したクラスの Class オブジェクトを渡す
    • LocationProviderget(Class) メソッドでは、読み込み対象となる場所を LocationSet で返すように実装する

キャッシュの制御

  • @AnalyzeClasses によって読み込まれたクラスはキャッシュされ、同じテストクラス内の各テストで再利用される
  • また、 @AnalyzeClasses によって読み込まれたクラスは、デフォルトではそのテストクラスが終了したあともキャッシュされる
    • そして、別のテストクラスで読み込み場所の指定が同じ @AnalyzeClasses があったときは、キャッシュされた情報が再利用される
    • このキャッシュは、ヒープが足りなくなってきたときに破棄されるようになっている
  • 読み込む対象が同じで使い回す方が効率がいい場合は、このデフォルトの動きで問題ない
  • しかし、あるテストクラスでしか指定しない特別な読み込み場所だったりした場合は、他のテストクラスで再利用されることは無いので、キャッシュはヒープを圧迫するだけでむしろデメリットとなり得る
  • そこで、次のように @AnalyzeClasses を設定することでキャッシュの生存期間を絞ることができるようになっている
package example;

import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.CacheMode;
...

@AnalyzeClasses(packages = "example.archunit", cacheMode = CacheMode.PER_CLASS)
public class ArchUnitTest {
    ...
}
  • cacheModeCacheMode.PER_CLASS を指定することで、キャシュの生存期間をそのテストクラスの実行中だけに絞ることができる

ルールのグループ化

FooRules
package example;

import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class FooRules {
    @ArchTest
    static ArchRule fooDoesNotDependOnBar = noClasses().that().resideInAPackage("..foo..")
            .should().dependOnClassesThat().resideInAPackage("..bar..");
}
BarRules
package example;

import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class BarRules {
    @ArchTest
    static ArchRule classesInBarAreNotDeprecated = classes().that().resideInAnyPackage("..bar..")
            .should().notBeAnnotatedWith(Deprecated.class);
}
  • クラスを @AnalyzeClasses で注釈せず、 @ArchTest で注釈したルールだけを宣言した FooRulesBarRules を用意する
package example;

import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchRules;
import com.tngtech.archunit.junit.ArchTest;

@AnalyzeClasses(packages = "example.archunit")
public class ArchUnitTest {
    @ArchTest
    static ArchRules fooRules = ArchRules.in(FooRules.class);

    @ArchTest
    static ArchRules barRules = ArchRules.in(BarRules.class);
}
  • ArchRules.in(Class) を使ってルールだけを宣言したクラス(FooRules, BarRules)を読み込み、 ArchRules 型のフィールドに格納し、 @ArchTest で注釈する
  • これを実行すれば、 FooRules, BarRules 内の全ての @ArchTest で注釈されたルールがテストされる
  • このように、ルールをグループ化してまとめることができる
    • 例えば、 ServiceLayerRules, DomainLayerRules みたいな感じでまとめることができる
  • 汎用的なルールであれば、様々なプロジェクトで使い回せるようにまとめることもできるかもしれない

参考

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

【Java】メソッド(関数)

概要

この記事は私のJavaの学習なメモです。
Javaの メソッド、引数、戻り値に関して学んだ内容をアウトプットします。

メソッド(関数)

メソッド(関数)とは 

「幾つかの 処理をまとめて入れておくもの」 です。
自分の場合、「関数」 という呼び方のほうが馴染み深い気がしますが、** オブジェクト指向プログラミングで使う呼び方は「メソッド」らしいです。 **


たとえばゲームの開発をしているとして、「攻撃をする」という処理が記述されている メソッドがあったとします。
それを100ケ所で使う場合は、毎回同じコードを100回記述するのではなく、 メソッド を呼び出すだけで済みます。

「剣で攻撃をする」という処理を「魔法で攻撃をする」と修正する必要があったとしても
メソッド の処理を変えれば、100箇所に反映されます。
「一つの仕事は一つにまとめる」というのがプログラミングで大事な考え方で「DRY ( Don't Repeat Yourself )の原則」という言葉があります。
同じことを繰り返さないことによって、メンテナンスがしやすくなったり、開発そのものが簡単になります。

メソッドの書き方

単純なメソッドの書き方はこちらです。

アクセス修飾子 戻り値の型 メソッド名(引数の型 引数) {
}
  1. アクセス修飾子 (省略可能)
  2. 戻り値の型
  3. メソッド名
  4. 引数の型, 引数

の順に記述していきます。
これらをもとに、引数に ユーザーの名前 をつかって 文字を出力する という 「printName」というメソッドをつくったとしたらこういう感じになります。

    public void printName(String name) {
        System.out.println("私の名前は" + name);
    }

1. アクセス修飾子 (省略可能)

こちらはアクセス修飾子の種類などについて以前書いた記事で触れているのでこちらを参照していただけると嬉しいです。

public, protected, 記述なし, private の順番で規制が厳しくなります。

2. 戻り値の型

「このメソッドを実行したら、なんの 型 の値を返すのか」というのをここで指定します。

3. メソッド名

ここに書くのは処理のタイトルのようなもので、これがメソッドです。
次回以降は「printName()」と記述するだけで記述した処理を行ってくれます。

4. 引数の型, 引数

この()←丸括弧の中にこのメソッドを実行する際に一緒に渡される値のことです。渡される値のことを 「引数」 といいます。

メソッドの型と順番があっていれば 変数名が違っていても問題なし

EnCountTrainer.javaの enCountTrainer メソッドでは、変数名が、enemyPokemonName , myPokemonName となっていますが、
Main.javaでは、enCountTrainer.enCountMsg(..., ..., enemyPokemon, myPokemon) となってます。
渡す変数と受け取る変数の名前が違っていますが、これは問題ありません。

printTriangleArea メソッドは int型 を 2つ 受け取れるメソッドなので、その順番の通りに、変数の中身(今回の場合 int型 )が受け渡されるため正常にメソッドは動きます。
とはいえ、呼び出し元と先であまりにも違いすぎるメソッド名は混乱しますので避けましょう。

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