ふるてつのぶろぐ

福岡在住のエンジニアです。

写真提供:福岡市

新人向けのプログラミング教育・ SPA /フロントエンド学習に Udemy で Angular の動画講座を作りました(新人研修、独習、SPA 未経験のエンジニアさん、SPA になじめないデザイナーさん、そして初学者のかた向けです)

Udemy 講座のご紹介

こんにちは、ふるてつです
今月 Udemy で初心者向けの Angular 講座を開設しました
その紹介をさせてください!!

✨講座の紹介動画

こちらは講座の紹介動画です
youtu.be

✨講座のリンク

こちらは講座へのリンクです
www.udemy.com

✨講座の概要

講座の概要を説明します
こちらは「Angular」を初めて学ぶ方向けのコースです
通販サイトの商品管理システムを制作しながら、Angularでの開発を学びます
ある程度の業務経験やプログラミングの経験があると良いですが、 細かい部分まで解説しますのでプログラミング初心者の方でも学べるようになっています

👨対象者さん

  • Angularで実際に業務をしたい方
  • 書籍などで学習したが覚えられない方
  • jQueryなどからAngularに移行したい方
  • 実務的なUnitテスト・E2Eテストの勘所を抑えたい方

👩具体的にどんな方

具体的にはこちらのような方を想定しています。

  • IT業界の新人さん
  • SPA未経験のエンジニアさん
  • SPAになかなかなじめないデザイナーさん(オブジェクト指向色を極力減らしたので学びやすいと思います)
  • プログラミング初学者の方

✨特徴

このコースでは実際の開発業務に沿って、開発からテスト工程までを実施します。

  1. 実際の業務にそった手順で html、css をテンプレートに取り込むところからはじめて、TypeScriptやRxJS、Angular Materialを使い実際に画面を製造します
  2. サインイン画面を含む6画面を作ります。画面数を多くこなすことで学習が定着しやすくなります
  3. Webサービスは別途準備したSpring bootを使いRxJSを使って接続します
  4. その他には、カスタムバリデーション、カスタムパイプ、カスタムディレクティブなどのテクニックを使います
  5. 各画面の製造と同時にJasmine+Karmaを使ったUnitテストも追加します
  6. 終盤のレクチャーではメディアクエリを使ったPCとスマホ両方に適したResponsive対応も行います
  7. 最後にCypress+cypress-image-snapshotを使ってE2Eテストを行います

👍メリット

  • Udemy講座なので動画をつかって独力で学べます
  • 専門学校のコースでまなぶようなボリュームを低価格で学べます
  • Angular Cli や Unitテスト、E2E テストなど SPA のフルスタックな機能をフル活用します

🐛デメリット

  • すでに他のSPAを学習している方にとっては逆に負担になるかもしれません
  • 講座が長いので早く学びたい方には向かないかもしれません(早く学びたい方は学習する画面を減らしたり、Unitテストをスキップしてください)
  • html、css 自体の学習にはむきません(講師自体にデザイナー経験はないので)

😊講座を作ってみての感想

今回はじめて Udemy の講座を作ったのですが思ったよりも長い期間かかりました。
どちらかというとプログラムよりも動画を録画し始めてからが思考錯誤で長くかかりました。
例えば最初は座って声を録音していたのですが、声に元気がないらしく元気感を出すためにわざと立って録音するように途中から変えました。
あとは声が響かないようにマイクのまわりを段ボールで囲んだりもしました。
録音はわたしの部屋でしましたがノイズが入らないように録音中は夏でもエアコンや扇風機はつけないようにして、そして汗だくになるので裸同然の格好で録音していました。しかも立って。
わたし自身おはなしは上手ではないのですが、こういった感じで頑張っていい動画を作りましたので、ぜひご覧いただければと思います。

まとめ


今日はわたしが作った Udemy 講座の紹介をさせていただきました
もしこの講座をご覧になって、内容が良さそうだと思われたらぜひとも応援ください!!

よろしくお願いいたします。

いまだにJava 8 備忘録 - SpotBugsの使い方

今日すること

こんにちはふるてつです。
きょうはSpotBugsのおはなしをします。
f:id:tetsufuru:20210531211456p:plain:w300
SpotBugsJavaの静的解析ツールでCheckstyleとおなじくよく見かけるツールです。
SpotBugsプラグインには開発画面上で動かすSpotBugs Eclipse pluginGradleなどのタスクで動かすSpotBugs Gradle pluginの2種類があります。
まずは開発画面上で動かすSpotBugs Eclipse pluginを説明します。

1. SpotBugs Eclipse plugin の設定

1 - 1. SpotBugs Eclipse plugin のインストール

インストールはEclipse marketplaceを使います。
Eclipse のメニューから「ヘルプ」-「Eclipseマーケットプレイス」を選びます。
そして「Eclipseマーケットプレイス」の画面で「SpotBugs Eclipse plugin」などのキーワードで検索してインストールします。
f:id:tetsufuru:20210601001050p:plain

※ここで注意点ですが、Pleiades のオールインワンなどはすでにビルトインでSpotBugs Eclipse pluginが入っていると思います。
お使いの環境にSpotBugs Eclipse pluginが入っているかは設定画面から確認できます。
Eclipse のメニューから「ウィンドウ」-「設定」を選び、設定画面の左側メニュー欄で「Java」をクリックします。
Java」のサブメニューの中に「SpotBugs」が表示されていればインストール済です。
f:id:tetsufuru:20210601003800p:plain
インストールされているSpotBugsのバージョンを確認しておきます。
「SpotBugs」の設定画面の3番目のタブ(プラグインおよびその他の設定)画面で確認できます。
わたしの環境では4.2.0になっています、あとで Gradle 側の設定を書くときに参考にするのでおぼえておきます。
f:id:tetsufuru:20210601003925p:plain

1 - 2. 「分析力」などの設定

次にSpotBugs Eclipse pluginの設定を行います
設定できる項目はざっとみて4項目あります。

  • 「分析力」
  • 「報告する最小ランク」
  • 「レポートする最低の信頼度」
  • 「報告(可視)バグ・カテゴリー」

a「分析力」

分析力ですがSpotBugsのドキュメントを確認します
下記になります、こちらの「分析力」によってチェックする内容が変わります。
f:id:tetsufuru:20210610224257p:plain 参考:分析力 — spotbugs 4.2.3 ドキュメント

表の右端の列 max が一番多くの項目をチェックするのでそちらを選択したいと思います
(ちなみにデフォルトの「分析力」は右端から2番目の more の列になります)
Eclipse では「最大」、「最小」、「デフォルト」を選択できますが、 maxに相当する「最大」を選んでおきます。

b「報告する最小ランク」

「報告する最小ランク」は高くするほど軽微なものまで報告されます
ずべて確認したいのでスライダーで 20 にまで上げておきます。

c「レポートする最低の信頼度」

わかりずらい項目ですが、まずここでいう「信頼度」はバグの可能性と思うといいです
「信頼度」が高いとバグである可能性が高くて、低いとバグではないかもしれないという感じです
そして「レポートする最低の信頼度」ですが high を設定するとバグの確立が高いものしかレポートされません
バグでなさそうなものまでレポートしてほしい場合は、low を設定します、わたしは low にします。

d「報告(可視)バグ・カテゴリー」

報告するカテゴリーが選択できます、とりあえず全部チェックONにしました。

f:id:tetsufuru:20210601153929p:plain
まとめるとわたしの設定は上記のようになります

1 - 3. 除外設定

わたしはとくに除外設定は行っていませんが「SpotBugs」の設定画面の2番目のタブ(フィルター・ファイル)で設定できると思います
除外用のxmlファイルを選択できるようです f:id:tetsufuru:20210601155019p:plain

xmlファイルの記入例はこちらにあります、ご参考までに。
f:id:tetsufuru:20210601155335p:plain
参考:フィルタファイル — spotbugs 4.2.3 ドキュメント

2. SpotBugs Gradle plugin の設定

2 - 1. SpotBugs Gradle plugin のインストール

ではGradleのタスクで動かす方のSpotBugsについて説明します
これまでにセクション1で説明したのは Eclipse でリアルタイムに動かすプラグインで、これから説明するのはそれとは別物と思ってください。
こちらはリアルタイムではなくGradlebuildコマンドを実行したときにタスク実行されてソースをチェックします。
ビルドをせずチェックのみをおこないたい場合はcheckコマンドで単独で実行できます。

Gradleの設定は Gradle のサイトを参考にします、下記です。
f:id:tetsufuru:20210603001644p:plain 参考:Gradle - Plugin: com.github.spotbugs

最新の書き方とレガシーな書き方が書いてあります、わたしの場合はGradleのマルチプロジェクトにしていて、 親プロジェクトの中で両方の書き方を使っています。

わたしの build.gradle の内容
関係ない項目をだいぶ省略してますがわたしはこのような感じで書いています。

buildscript {
    ext {
        spotbugsVersion = '4.7.1'
        spotbugsToolVersion = '4.2.0'
    }
}

plugins {
    id "com.github.spotbugs" version "${spotbugsVersion}" // ←これは SpotBugs Gradle plugin のバージョン
}

subprojects {
    apply plugin: 'com.github.spotbugs'

    spotbugs {
        toolVersion = "${spotbugsToolVersion}" // ←これはSpotBugs自体のバージョン
        includeFilter = rootProject.file('config/spotbugs/spotbugs-include-filter.xml')
        spotbugsMain {
            reports {
                html {
                    enabled = true
                    stylesheet = 'fancy-hist.xsl'
                }
            }
        }
    }
}
project(':prj-sub') {}

project(':prj-main') {
    dependencies {
        implementation project(':prj-sub')
    }
}
  • plugins{} について
    まずは plugins の中で SpotBugs を使うよう宣言しておきます
    このバージョンは Gradle plugin のバージョンを設定します。
  • subprojects{} について
    そして子プロジェクトの中の設定は subprojects{} の中で行います
    apply plugin と書いて、その下の spotbugs{} の中で細かい設定をおこないます
  • spotbugs{} について
    toolVersion はSpotBugs自体のバージョンを選択します
    わたしは画面側の Eclipse plugin と同じバージョンにしたいので最初のセクションでしらべておいた 4.2.0 にします
    画面側でバグ・カテゴリーをすべてチェックONにしていたので Gradle 側でも同じ設定にします。 その設定は xml ファイルに書いて includeFilter で指定します
    (includeFilter の内容は別途下で説明します)
    最後の spotbugsMain{} はレポートの設定です、 このあたりの書き方は SpotBugs の git の readme が参考になるとおもいます、下記のような内容が書いてあります。
    f:id:tetsufuru:20210603011408p:plain f:id:tetsufuru:20210602235544p:plain 参考:GitHub - spotbugs/spotbugs-gradle-plugin

2 - 2. includeファイルについて

わたしの環境では include ファイルにバグ・カテゴリーの設定を書いています
画面側で設定したバグ・カテゴリーと同じものを Gradle 側でも設定したいので使っています。
spotbugs-include-filter.xml の内容例

<?xml version="1.0"?>
<FindBugsFilter>
    <Match><Bug category="BAD_PRACTICE,MALICIOUS_CODE,CORRECTNESS,PERFORMANCE,SECURITY,STYLE,EXPERIMENTAL,MT_CORRECTNESS,I18N" /></Match>
</FindBugsFilter>

FindBugsFilterタグの中にMatchタグとBugタグを追加して、category= で報告させたいバグ・カテゴリーをカンマ区切りで設定します
すべてのクラスファイルをチェックしたいので、Matchタグの中にはなにも条件を書いていません
BAD_PRACTICEは「悪い記述」のことで MALICIOUS_CODE は「悪意があるコードの脆弱性」のことです
これらのキーワードがどのカテゴリに該当するかはSpotBugsのドキュメントに書いてありました。 下のページの赤い部分です
f:id:tetsufuru:20210603004559p:plain 参考:Bug descriptions — spotbugs 4.2.3 documentation

あとこのファイルの置き場所は下記フォルダ内にしています。

[プロジェクト直下]/config/spotbugs/

2 - 3. Gradle で check コマンドを実行

これで一通りの設定が終わりました。
では check コマンドを実行してみます、プロジェクト直下で gradle のラッパーのコマンドを実行します。

gradlew check

実行結果はプロジェクト直下/build/reports/spotbugs/ に html ファイルが出力されます。
main.html を開いてみます、下のようなイメージになります。

レポートの例
f:id:tetsufuru:20210603005541p:plain

感想


おつかれさまでした。
今回はSpotBugsついて書きました。
SpotBugsはクラスの作りなど根本から変えないと対応できないような指摘をよくしてくるので あとから導入すると結構な目にあいます。
できるだけ早いタイミングで導入したほうが楽だとおもい書きました
少しでも皆さんの参考になればと思います。

それではまた😎

いまだにJava 8 備忘録 - CheckstyleとFormatterの使い方(Google Styleguide)

今日すること

こんにちはふるてつです。

きょうはCheckstyleFormatterのおはなしをします。
f:id:tetsufuru:20210314215338p:plain
わたしは客先に常駐するタイプの仕事をしていてどこの客先でもCheckstyleは見かけます。
慣れているはずですが自分でいちから設定するといつもCheckstyle設定とFormatterとで、つじつまが合わなくなり途中でやめてしまいます。

みなさんそのような経験はないですか?😑

わたしのような不器用なかたはCheckstyleFormatterの両方ともに「google提供の設定ファイル」を使うのがおすすめです。
わたしは EclipseGradleを使っていますので、GradleでのCheckstyle設定についても最後の方に書きたいと思います。

1. EclipseCheckstyle pluginの設定

Checkstyle pluginを使うと Eclipse の画面上でチェック結果を確認できます。
一番なじみのある使い方だと思います。

1 - 1. Checkstyle Plugin をインストール

まずは EclipseCheckstyle Pluginをインストールします。
Eclipse のメニューから「ヘルプ」-「Eclipseマーケットプレイス」を選びます。
f:id:tetsufuru:20210318015343p:plain

そして「Eclipseマーケットプレイス」の画面で「checkstyle」などのキーワードで検索して下記のようにインストールします。
いまバージョンは 8.36.1 が最新のようです。
f:id:tetsufuru:20210317005211p:plain

※ここで注意点ですが、おなじEclipseでも Pleiades のオールインワンを使っているかたは、すでにビルトインでCheckstyle Pluginが入っています。
マーケットプレイスからさらにインストールすると競合して動かなくなるので注意しましょう。

いまの環境にCheckstyle Pluginが入っているかどうかは設定画面で確認します。
下記の左側メニューに「Checkstyle」が表示されている場合はすでに入っていますのでインストールする必要はありません。
f:id:tetsufuru:20210318015651p:plain

1 - 2. Checkstyleの設定ファイル「google_checks.xml」をダウンロード

次にCheckstyleの git から googleCheckstyleの設定ファイルを入手します。

Checkstyleの git はこちらになります。
f:id:tetsufuru:20210318020751p:plain
https://github.com/checkstyle/checkstyle

チェックスタイル本体とプラグインのバージョンは若干づれていてプラグインが少し遅れ気味です。
ダウンロードする際はバージョンに気をつけてください。
Eclipseにインストールしたプラグインより上のバージョンのファイルを使うとCheckstyleが動かないことがあります。

そこで上の git の写真の右側にある「Releases」のところからプラグインと同じバージョンの checkstyle-8.36.1 を選んで「Source code(zip)」をダウンロードして中身を展開しておきます。
f:id:tetsufuru:20210317011620p:plain
https://github.com/checkstyle/checkstyle/releases/tag/checkstyle-8.36.1

1 - 3. Eclipse に設定ファイル「google_checks.xml」を配置

次に Eclipse のプロジェクト直下にCheckstyleの設定ファイル用のフォルダを作成します。
置き場所はデフォルトで名前が決まっていて下記になります。

[プロジェクト直下]/config/checkstyle/

こちらのフォルダに checkstyle-8.36.1 中の src/main/resources/ から「google_checks.xml」をコピーします。

checkstyle-8.36.1の中(コピー元です)
f:id:tetsufuru:20210317092937p:plain

Eclipseのworkspaceの中(コピー先です)
f:id:tetsufuru:20210317095657p:plain

1 - 4. Checkstyleの新規構成を作成

では設定を取り込みます。
Eclipse のメニューから「ウィンドウ」-「設定」を選んで設定画面を開きます。
設定画面の左側メニューから「Checkstyle」をクリックすると下記の画面になります。
f:id:tetsufuru:20210318021359p:plain

すでにグローバルチェック構成のところに「Google Checks」はビルトインで入っていますが、Gradle側と設定を共有したいのでビルトインは使いません。
「新規」ボタンをクリックして
「型」は外部構成ファイル
「名前」は小文字の google_checks
「ロケーション」はEclipseのプロジェクトに置いた「google_checks.xml」を選びます。
f:id:tetsufuru:20210317102130p:plain

OKをクリックすると警告が出ますがそのまま「続行」をクリックして新規作成を完了します。
f:id:tetsufuru:20210318021534p:plain

新しく小文字の google_checks が増えたのでこれを「デフォルトとして設定」しておきます。
下記のような感じになります。
f:id:tetsufuru:20210318021646p:plain

これでCheckstyleのルール設定が新しくなりました。
あとは自動で検査が始まって、Checkstyle違反があるとパッケージエクスプローラーにマークがでると思います。
エラーの例(工事中のような!アイコンがでてきます)
f:id:tetsufuru:20210318022813p:plain

なにも出てこなければパッケージエクスプローラーでプロジェクトを「右クリック」-「Checkstyle」-「Checkstyle でコードチェック」を手動でおこなってみてください。
f:id:tetsufuru:20210318024327p:plain

あとメニューから「ビュー」の表示で「Checkstyle 違反」も見れるようにしておくといいと思います。
f:id:tetsufuru:20210317104003p:plain

1 - 5. Checkstyleの除外設定

Checkstyle Pluginの除外設定について説明しておきます。
除外設定は画面から行います。
設定方法ですが、まずパッケージエクスプローラーでプロジェクトを「右クリック」-「プロパティ」を開きます。
プロパティ画面左側のメニューで「Checkstyle」を選ぶと下記の画面のようにCheckstyle画面が表示されます。
※「ウィンドウ」-「設定」から表示される画面と似ているので間違えないようにしてください。
f:id:tetsufuru:20210318025231p:plain

上記画面で「パッケージのファイル」をクリック、「変更」ボタンをクリックします。
フィルターの画面が表示されるので除外するパッケージをチェックします。
f:id:tetsufuru:20210318025804p:plain

こちらはわたしの例になりますがMyBatisで自動作成した entity や repository のパッケージを除外しています。

2. Formatter設定

2 - 1. 設定ファイルのダウンロードとライセンスについて

次はFormatterの設定を行います。
Googleが提供しているStyleguideを使用します。
こちらのサイトです。
f:id:tetsufuru:20210319021230p:plain https://github.com/google/styleguide

このなかの「eclipse-java-google-style.xml」をダウンロードしてきます。
このファイルは最終更新日が3年前になっています、だいぶ以前からあったようです。
わたしもっと早く気づけばよかったですねぇ😎

ファイルの置き場所はCheckstyleに合わせて config の下に formatter のフォルダを作ってそこに置きます。

[プロジェクト直下]/config/formatter/

f:id:tetsufuru:20210319032414p:plain

ちなみにStyleguideの README にライセンスのことが書いてありこのファイルは「CC-By 3.0 License」とのことです。
f:id:tetsufuru:20210319031157p:plain

これは「クリエイティブコモンズライセンス」といいます。
大まかに説明すると、原作者のクレジット(氏名、作品タイトルなど)を表示すれば、自由にしていいライセンスです。
商業的にも問題ありませんし、修正も可能です。

そこでクレジットを「eclipse-java-google-style.xml」ファイルに追加します。
下記のように2行目にコメントで追加しておきます。
名前と取得元のURLを英語で書いた感じで良いと思います、これでライセンス的に問題なく使えるはずです。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- ここから
    Used this style from Google Style Guides.
    That can be found at https://github.com/google/styleguide
-->  ここまで
<profiles version="13">
以下省略

ちなみにもしソースを git で公開する場合は READMEに使っている旨をひとこと書いた方が良いと思います。
あと補足になりますが、クリエイティブコモンズライセンスについて詳しくは下記を参照ください。
クリエイティブ・コモンズ・ライセンスとは | クリエイティブ・コモンズ・ジャパン

2 - 2. 設定ファイルをインポート

では次に保存した設定ファイル「eclipse-java-google-style.xml」をインポートします。
Eclipse のメニューから「ウィンドウ」-「設定」を選んで設定画面を開きます。
設定画面の左側メニューで「Java」- 「コードスタイル」-「フォーマッター」を選びます。
f:id:tetsufuru:20210319033802p:plain

フォーマッターの画面で「インポート」ボタンをクリックして「eclipse-java-google-style.xml」をインポートします。
インポートが終わると「アクティブなプロファイル」の欄に「GoogleStyle」と表示されます。
f:id:tetsufuru:20210319034650p:plain

2 - 3. 保存アクションの設定

いったんインポートは終わりですが、「保存アクション」の設定も変えておきます。
設定画面の左側メニューで「Java」- 「エディター」「保存アクション」を選びます。
f:id:tetsufuru:20210319035535p:plain

上記のように保存アクション画面で保存時にソースコードのフォーマットを行うように設定します。
これで保存時に毎回Formatterソースコードをフォーマットするようになりました。
補足になりますが、わたしは「インポートの構成」もチェックONにしています。
「インポートの構成」をONにするとソースコードの保存時に Eclipse が自動でインポート文の並び順を調整します。
しかし今度はCheckstyleと合わなくなり、結局Checkstyleの設定ファイルを修正してCustomImportOrderの定義をコメントアウトしました。
同じように設定する場合、参考にしてください。

google_checks.xml の編集内容

<!--
<module name="CustomImportOrder"> ↓ ここからをコメントアウト
    <property name="sortImportsInGroupAlphabetically" value="true"/>
    <property name="separateLineBetweenGroups" value="true"/>
    <property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
    <property name="tokens" value="IMPORT, STATIC_IMPORT, PACKAGE_DEF"/>
</module> ↑ ここまでをコメントアウト
-->

3. GradleでのCheckstyle設定

ではGradleでのCheckstyle設定を説明します。
同じCheckstyleですが、最初に書いた「Eclipseマーケットプレイス」からインストールしたリアルタイムで動くほうのCheckstyle Pluginとは別物と思ってください。
こちらはリアルタイムではなくGradlebuildコマンドを実行したときにバッチ実行されてソースをチェックします。
ビルドをせずチェックのみをおこないたい場合はcheckコマンドで単独で実行できます。

Gradleで使うCheckstyleのサイトはこちらになります。
f:id:tetsufuru:20210319210428p:plain
https://docs.gradle.org/current/userguide/checkstyle_plugin.html

3 - 1. Gradleの設定ファイルにcheckstyleを追加する

ではまず build.gradle ファイルにcheckstyleの設定を追加します。

シングルプロジェクトの場合

プロジェクトが一つだけの普通のプロジェクトの場合はあたらしい書き方ができます。
バージョンや設定ファイルの定義は plugins{ } の下の checkstyle { } の中の Extension 部分に書きます。
プラグインのバージョンは toolVersion で定義します。
Gradleで使うcheckstyleの最新バージョンは現在 '8.41' ですが、わたしはリアルタイムで動くほうのCheckstyle Pluginと同じに合わせたかったので '8.36.1'にしています。
設定ファイルも同一ファイルを使いたいので同じ「google_checks.xml」を指定します。

build.gradle の内容

plugins {
    id 'checkstyle'
}
checkstyle {
    toolVersion = '8.36.1'
    configFile = rootProject.file('config/checkstyle/google_checks.xml')
}

マルチプロジェクトの場合

Gradleでは複数のプロジェクトまとめることができます。
この場合はあたらしい書き方にはまだできないので少し古い書き方になり、subprojects の中に追加します。
内容を少し簡略化していますが下記のような感じになります。
バージョンや設定ファイルはシングルプロジェクトと同じ書き方になります。

build.gradle の内容

subprojects {
    apply plugin: 'checkstyle'

    checkstyle {
        toolVersion = '8.36.1'
        configFile = rootProject.file('config/checkstyle/google_checks.xml')
    }
}
project(':biz') {}

project(':example_project') {
    dependencies {
        implementation project(':biz')
    }
}

3 - 2. チェックスタイルの除外設定ファイルを作成

Gradleで動かすcheckstyleの除外設定はファイルで行います。
設定のサンプルがcheckstyleの git にあります、こちらです。
f:id:tetsufuru:20210319214655p:plain
https://github.com/checkstyle/checkstyle/blob/master/config/suppressions.xml

これをひな型にしてチェックから除外したいパッケージやファイル名を指定します。
具体的は内容ですが例えばわたしはMyBatisで自動作成した entity や repository のパッケージを除外しています。
ほかにはMyBatis Generator の設定ファイルも除外しています「generationConfig.xml
下記を参考にしてみてください。

suppressions.xml の内容例

<?xml version="1.0"?>

<!DOCTYPE suppressions PUBLIC
    "-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
    "https://checkstyle.org/dtds/suppressions_1_2.dtd">

<suppressions>
    <suppress checks=".*" files="com[\\/]example[\\/]demo[\\/]entity[\\/]domain[\\/]"/>
    <suppress checks=".*" files="com[\\/]example[\\/]demo[\\/]repository[\\/]"/>
    <suppress checks=".*" files="generationConfig.xml"/>
</suppressions>

あと除外ファイルの置き場所は「google_checks.xml」と同じ場所にしたいので下記フォルダ内にします。

[プロジェクト直下]/config/checkstyle/

名前もそのまま「suppressions.xml」として、下図のように保存しておきます。
f:id:tetsufuru:20210319221157p:plain

3 - 3. google_checks.xml ファイルを再編集

最後に「google_checks.xml」ファイルの中を再び編集して「suppressions.xml 」とリンクさせます。
module name = "Checker" のタグの中に、module name="SuppressionFilter" と書いてあるタグがあります。
そこの property の file を書き換えます。
google_checks.xmlの変更前

<!-- https://checkstyle.org/config_filters.html#SuppressionFilter -->
<module name="SuppressionFilter">
    <property name="file" value="${org.checkstyle.google.suppressionfilter.config}"
              default="checkstyle-suppressions.xml" />
    <property name="optional" value="true"/>
</module>

google_checks.xmlの変更後

<!-- https://checkstyle.org/config_filters.html#SuppressionFilter -->
<module name="SuppressionFilter">
    <property name="file" value="${config_loc}/suppressions.xml"/>   <!-- この行を書き換え -->
    <property name="optional" value="true"/>
</module>

${config_loc}Checkstyleのビルトインのプロパティでプロジェクト直下のconfigフォルダの意味になります。

3 - 4. Gradle で build コマンドを実行

これで一通りの設定が終わりました。
では build コマンドを実行してみます、プロジェクト直下で gradle のラッパーのコマンドを実行します。

gradlew build

実行結果はプロジェクト直下/build/reports/checkstyle/ に html ファイルが出力されます。
main.html を開いてみます、だいぶ割愛していますが下のようなイメージになります。

Checkstyleレポートの例
f:id:tetsufuru:20210319224317p:plain

感想


おつかれさまでした。
今回はCheckstyleFormatterについて書きました。
わたしはCheckstyleが長いこと苦手で、いままで避けてとおってきましたがブログにこうやって書くことで整理でき、やっと苦手を克服できたかなと思います。
Checkstyleをいちから設定する機会はそれほど多くはないと思いますので、少しでも皆さんの参考になればと思います。

それではまた😎

Angular11でWebアプリを作ろう - V11にバージョンアップ - ng update

今日すること

こんにちは、ふるてつです。
もう年末ですね。
Angularがバージョン 11 にあがりました。
9から10に上がるときはそれほど修正はなかったのでブログを書かなかったのですが、今回はすこし修正が多く感じたのでその内容を書こうと思います。

バージョンアップ情報の確認

まずはこちらのサイトで手順やその他情報をチェックします。
https://update.angular.io/?v=10.0-11.0
わたしの場合は、Materialを使っているので「I use Angular Material」はチェックONにします。
f:id:tetsufuru:20201215161520p:plain:w800 特に大きな変更とか書いてないですね、楽にバージョンアップできそうです。

ng updateコマンド

早速ですがAngularCLIng updateコマンドを実行します。
すると下記のリストがでてきました。
今回はcdkclicorematerialrxjsのほぼ全部のバージョンアップが必要そうです。

ng update
The installed local Angular CLI version is older than the latest stable version.
Installing a temporary version to perform the update.
Installing packages for tooling via npm.
Installed packages for tooling via npm.
Using package manager: 'npm'
Collecting installed dependencies...
Found 39 dependencies.
    We analyzed your package.json, there are some packages to update:

      Name                               Version                  Command to update
     --------------------------------------------------------------------------------
      @angular/cdk                       10.1.0 -> 11.0.0         ng update @angular/cdk
      @angular/cli                       10.0.3 -> 11.0.2         ng update @angular/cli
      @angular/core                      10.0.4 -> 11.0.2         ng update @angular/core
      @angular/material                  10.1.0 -> 11.0.0         ng update @angular/material
      rxjs                               6.6.0 -> 6.6.3           ng update rxjs

    There might be additional packages which don't provide 'ng update' capabilities that are outdated.
    You can update the addition packages by running the update command of your package manager.

実際にバージョンアップするコマンドですが、上のリストの最後右端に書いてあります。
コマンドを実行する順番はcoreが最初で、次clicdk、そしてmaterial、最後にrxjsがこれまでの経験的に良い気がします。
しかし今回は最初にcoreだと警告が出てしまい、順番を変えてcliから先にしました。
この辺の違いをわたしよくわかっていないですねぇ。

ちなみに下がcoreの時の警告です。
こういうメッセージがでたら焦らずに皆さんもupdateの順番を変えてみてください。
--forceのオプションをつけて実行しろとも書いてありますが、どうしてもできないときでいいと思います。

ng update @angular/core
The installed local Angular CLI version is older than the latest stable version.
Installing a temporary version to perform the update.
Installing packages for tooling via npm.
Installed packages for tooling via npm.
Using package manager: 'npm'
Collecting installed dependencies...
Found 39 dependencies.
Fetching dependency metadata from registry...
                  Package "@angular-devkit/build-angular" has an incompatible peer dependency to "typescript" (requires ">=3.9 < 3.10", would install "4.0.5").
× Migration failed: Incompatible peer dependencies found.
Peer dependency warnings when installing dependencies means that those dependencies might not work correctly together.
You can use the '--force' option to ignore incompatible peer dependencies and instead address these warnings later.   
  See "C:\Users\tetsu\AppData\Local\Temp\ng-KpdYdZ\angular-errors.log" for further details.

バージョンアップ実施

angular/cliのバージョンアップ

まず最初にCLIをバージョンアップします。
ng update @angular/cli

ng update @angular/cli
The installed local Angular CLI version is older than the latest stable version.
Installing a temporary version to perform the update.
Installing packages for tooling via npm.
Installed packages for tooling via npm.
Using package manager: 'npm'
Collecting installed dependencies...
Found 39 dependencies.
Fetching dependency metadata from registry...
    Updating package.json with dependency @angular-devkit/build-angular @ "0.1100.2" (was "0.1000.3")...
    Updating package.json with dependency @angular/cli @ "11.0.2" (was "10.0.3")...
    Updating package.json with dependency @angular/compiler-cli @ "11.0.2" (was "10.0.4")...
    Updating package.json with dependency @angular/language-service @ "11.0.2" (was "10.0.4")...
    Updating package.json with dependency karma @ "5.1.1" (was "5.0.9")...
    Updating package.json with dependency typescript @ "4.0.5" (was "3.9.7")...
    Updating package.json with dependency @angular/animations @ "11.0.2" (was "10.0.4")...
    Updating package.json with dependency @angular/common @ "11.0.2" (was "10.0.4")...
    Updating package.json with dependency @angular/compiler @ "11.0.2" (was "10.0.4")...
    Updating package.json with dependency @angular/core @ "11.0.2" (was "10.0.4")...
    Updating package.json with dependency @angular/forms @ "11.0.2" (was "10.0.4")...
    Updating package.json with dependency @angular/platform-browser @ "11.0.2" (was "10.0.4")...
    Updating package.json with dependency @angular/platform-browser-dynamic @ "11.0.2" (was "10.0.4")...
    Updating package.json with dependency @angular/router @ "11.0.2" (was "10.0.4")...
  UPDATE package.json (1684 bytes)
√ Packages installed successfully.
** Executing migrations of package '@angular/cli' **

> Removing "Solution Style" TypeScript configuration file support.
  DELETE tsconfig.base.json
  UPDATE tsconfig.json (569 bytes)
  UPDATE tsconfig.app.json (290 bytes)
  UPDATE tsconfig.spec.json (288 bytes)
  UPDATE e2e/tsconfig.json (230 bytes)
  Migration completed.

> Replace deprecated library builder '@angular-devkit/build-ng-packagr'.
  Migration completed.

> Add 'declarationMap' compiler options for non production library builds.
  Migration completed.

> Remove deprecated options from 'angular.json' that are no longer present in v11.
  UPDATE angular.json (4278 bytes)
  Migration completed.

> Update workspace dependencies to match a new v11 project.
  UPDATE package.json (1685 bytes)
√ Packages installed successfully.
  Migration completed.

** Executing migrations of package '@angular/core' **

> In Angular version 11, the type of `AbstractControl.parent` can be `null` to reflect the runtime value more accurately.
  This migration automatically adds non-null assertions to existing accesses of the `parent` property on types like `FormControl`, `FormArray` and `FormGroup`.
  Migration completed.

> ViewEncapsulation.Native has been removed as of Angular version 11.
  This migration replaces any usages with ViewEncapsulation.ShadowDom.
  Migration completed.

> NavigationExtras omissions migration.
  In version 11, some unsupported properties were omitted from the `extras` parameter of the `Router.navigateByUrl` and `Router.createUrlTree` methods.
  Migration completed.

> Updates the `initialNavigation` property for `RouterModule.forRoot`.
  Migration completed.

> NavigationExtras.preserveQueryParams has been removed as of Angular version 11.
   This migration replaces any usages with the appropriate assignment of the queryParamsHandling key.
  Migration completed.

> The default value for `relativeLinkResolution` is changing from 'legacy' to 'corrected'.
This migration updates `RouterModule` configurations that use the default value to 
now specifically use 'legacy' to prevent breakages when updating.
  UPDATE src/app/app-routing.module.ts (2120 bytes)
  Migration completed.

> `async` to `waitForAsync` migration.
  The `async` testing function has been renamed to `waitForAsync` to avoid confusion with the native `async` keyword.
  UPDATE src/app/app.component.spec.ts (2120 bytes)
  UPDATE src/app/core/components/error-messaging/error-messaging.component.spec.ts (2219 bytes)
  UPDATE src/app/core/components/loading/loading.component.spec.ts (1732 bytes)
  UPDATE src/app/core/components/mat-date-picker/mat-date-picker.component.spec.ts (3059 bytes)
  UPDATE src/app/core/components/success-messaging/success-messaging.component.spec.ts (2018 bytes)
  UPDATE src/app/core/components/yes-no-dialog/yes-no-dialog.component.spec.ts (1112 bytes)
  UPDATE src/app/pages/components/product-listing-page/product-listing-page.component.spec.ts (17363 bytes)
  UPDATE src/app/pages/components/product-registering-page/product-registering-page-edit.component.spec.ts (25442 bytes)
  UPDATE src/app/pages/components/product-registering-page/product-registering-page-new.component.spec.ts (28742 bytes)
  UPDATE src/app/pages/components/purchase-history-listing-page/purchase-history-listing-page.component-clear.spec.ts (7167 bytes)
  UPDATE src/app/pages/components/purchase-history-listing-page/purchase-history-listing-page.component.spec.ts (13654 bytes)
  UPDATE src/app/pages/components/sign-in-page/sign-in-page.component.spec.ts (7720 bytes)
  UPDATE src/app/pages/components/stock-registering-page/stock-registering-page.component.spec.ts (21791 bytes)
  UPDATE src/app/shared/components/footer/footer.component.spec.ts (795 bytes)
  UPDATE src/app/shared/components/header/header.component.spec.ts (4596 bytes)
  UPDATE src/app/shared/components/sidenav/sidenav.component.spec.ts (3605 bytes)
  UPDATE src/app/super-user-pages/dummy-purchasing-page/dummy-purchasing-page.component.spec.ts (25496 bytes)
  Migration completed.

CLIのバージョンアップでの変更点

CLIをバージョンアップすると2種類のコードが変更されました。
1つ目は「AppRoutingModule」です。
バージョンアップ前

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})

バージョンアップ後

@NgModule({
  imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })],
  exports: [RouterModule]
})

 importsの中に{ relativeLinkResolution: 'legacy' }が追加になりました。
日本語リファレンスを見るとRouterバグフィックスとのこと。
必要かどうかV11で新規プロジェクトを作ってみたところCLIが自動で作った「AppRoutingModule」には { relativeLinkResolution: 'legacy' }は含まれていませんでした。
特に動作に問題が出ていなければ不要と思い、上段の方のコードに戻しました。

もう一つはcomponentのテストクラスです。

beforeEach(async(() => {

のところが、バージョンアップした後は下記のようになりました。

beforeEach(waitForAsync(() => {

使用するメソッドがasync()からwaitForAsync()に変わったようです。
こちらもV11でコマンドで新規にcomponentを作ってみます。
すると下記のようにasync ()はまだ使われていて、下の行にawaitが追加されたcomponentのテストコードができました。
あとかっこのネストが一つ減りました。

beforeEach(async () => {
  await TestBed.configureTestingModule({
    declarations: [ExampleComponent]
  }).compileComponents();
  router = TestBed.inject(Router);
});

というわけでV11で新規に作った場合に合わせることにして、waitForAsyncに変わったところはasync ()awaitの組み合わせに変えました。

angular/coreのバージョンアップ

次は最初に警告が出たcoreです。
ng update @angular/core

ng update @angular/core
Using package manager: 'npm'
Collecting installed dependencies...
Found 39 dependencies.
Fetching dependency metadata from registry...
Package '@angular/core' is already up to date.

今度は問題なく上がりました。

angular/materialのバージョンアップ

次はmaterialを上げます。
ng update @angular/material

ng update @angular/material
Using package manager: 'npm'
Collecting installed dependencies...
Found 39 dependencies.
Fetching dependency metadata from registry...
    Updating package.json with dependency @angular/cdk @ "11.0.0" (was "10.1.0")...
    Updating package.json with dependency @angular/material @ "11.0.0" (was "10.1.0")...
  UPDATE package.json (1684 bytes)
√ Packages installed successfully.
** Executing migrations of package '@angular/cdk' **

> Updates the Angular CDK to v11.
    
      ✓  Updated Angular CDK to version 11

  Migration completed.

** Executing migrations of package '@angular/material' **

> Updates Angular Material to v11.
    
    ⚠  General notice: The HammerJS v9 migration for Angular Components is not able to migrate tests. Please manually clean up tests in your project if they rely on HammerJS.
    Read more about migrating tests: https://git.io/ng-material-v9-hammer-migrate-tests
    
      ✓  Updated Angular Material to version 11

  Migration completed.

こちらも良い感じです。

angular/cdkのバージョンアップ

次はcdkを上げます。
ng update @angular/cdk

ng update @angular/cdk
Using package manager: 'npm'
Collecting installed dependencies...
Found 39 dependencies.
Fetching dependency metadata from registry...
Package '@angular/cdk' is already up to date.

もうバージョンが上がっているとのこと、特に問題ないです。

rxjsのバージョンアップ

最後にrxjsを上げます。
ng update rxjs

ng update rxjs
Using package manager: 'npm'
Collecting installed dependencies...
Found 39 dependencies.
Fetching dependency metadata from registry...
    Updating package.json with dependency rxjs @ "6.6.3" (was "6.6.0")...
  UPDATE package.json (1684 bytes)
√ Packages installed successfully.

こちらも問題なしですね。

GlobalのAngularをバージョンアップ

わたしはGlobalの方のAngularをバージョンアップするのをしばしば忘れます。
localだけで動くAngularをお使いの方は関係ありませんが、Globalもある方はお忘れなく😎
わたしはいったん消して最新をインストールしなおす感じです。
真ん中のnpm cache verifyの代わりにnpm cache clean --forceコマンドを実行する方もいるようです。

npm uninstall -g @angular/cli
npm cache verify
npm install -g @angular/cli@latest

動作確認(Unitテスト)

バージョンアップが終わったのでUnitテストを実行しようとしたらエラーで動きません。
カバレッジ関連のようですが、これまでに使っていたkarma-coverage-istanbul-reporterが非推奨になり、 バージョン11からはkarma-coverageを使うようになってました。
あとkarma.conf.jsの内容も変わるようです。

ng test
'karma-coverage-istanbul-reporter' usage has been deprecated since version 11.
Please install 'karma-coverage' and update 'karma.conf.js.' For more info, see https://github.com/karma-runner/karma-coverage/blob/master/README.md
Compiling @angular/core : es2015 as esm2015
Compiling @angular/cdk/keycodes : es2015 as esm2015
Compiling @angular/animations : es2015 as esm2015
Compiling @angular/compiler/testing : es2015 as esm2015
Compiling @angular/common : es2015 as esm2015
Compiling @angular/animations/browser : es2015 as esm2015
Compiling @angular/cdk/observers : es2015 as esm2015
~ 以下略 ~

karma-coverageのインストール

そこでカルマカバレッジをインストールします。
こちらのコマンドです。
npm install karma karma-coverage --save-dev

npm install karma karma-coverage --save-dev
npm WARN codelyzer@6.0.0 requires a peer of @angular/compiler@>=2.3.1 <11.0.0 || >9.0.0-beta <11.0.0 || >9.1.0-beta <11.0.0 || >9.2.0-beta <11.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN codelyzer@6.0.0 requires a peer of @angular/core@>=2.3.1 <11.0.0 || >9.0.0-beta <11.0.0 || >9.1.0-beta <11.0.0 || >9.2.0-beta <11.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN ngx-translate-testing@5.0.0 requires a peer of @angular/common@^10.0.0-rc.0 || >=10.0.0 <11.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN ngx-translate-testing@5.0.0 requires a peer of @angular/core@^10.0.0-rc.0 || >=10.0.0 <11.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.1.3 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.1.3: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.13 (node_modules\watchpack-chokidar2\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.13 (node_modules\webpack-dev-server\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ karma@5.1.1
+ karma-coverage@2.0.3
added 3 packages from 95 contributors, removed 1 package, updated 1 package and audited 1540 packages in 19.373s

77 packages are looking for funding
  run `npm fund` for details

karma.conf.jsの変更点

こちらは変更前の「karma.conf.js」です。
「karma-coverage-istanbul」の関連がV11でなくなった模様です。

// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html

module.exports = function (config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine', '@angular-devkit/build-angular'],
    plugins: [
      require('karma-jasmine'),
      require('karma-chrome-launcher'),
      require('karma-jasmine-html-reporter'),
      require('karma-coverage-istanbul-reporter'), // ←なくなる
      require('@angular-devkit/build-angular/plugins/karma')
    ],
    client: {
      clearContext: false // leave Jasmine Spec Runner output visible in browser
    },
    coverageIstanbulReporter: { // ←なくなる
      dir: require('path').join(__dirname, './coverage/product-manage-site-for-hands-on'), // ←なくなる
      reports: ['html', 'lcovonly', 'text-summary'], // ←なくなる
      fixWebpackSourcePaths: true // ←なくなる
    }, // ←なくなる
    reporters: ['progress', 'kjhtml'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome'],
    singleRun: false,
    restartOnFileChange: true
  });
};

変更後の「karma.conf.js」はこちらです。
V11で新規プロジェクトを作成してCLIが自動作成した「karma.conf.js」をそのまま自分のに上書きしました✨

// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html

module.exports = function (config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine', '@angular-devkit/build-angular'],
    plugins: [
      require('karma-jasmine'),
      require('karma-chrome-launcher'),
      require('karma-jasmine-html-reporter'),
      require('karma-coverage'),
      require('@angular-devkit/build-angular/plugins/karma')
    ],
    client: {
      clearContext: false // leave Jasmine Spec Runner output visible in browser
    },
    jasmineHtmlReporter: { // ←追加される
      suppressAll: true // removes the duplicated traces // ←追加される
    }, // ←追加される
    coverageReporter: { // ←追加される
      dir: require('path').join(__dirname, './coverage/product-manage-site-for-hands-on'), // ←追加される
      subdir: '.', // ←追加される
      reporters: [ // ←追加される
        { type: 'html' }, // ←追加される
        { type: 'text-summary' } // ←追加される
      ] // ←追加される
    }, // ←追加される
    reporters: ['progress', 'kjhtml'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome'],
    singleRun: false,
    restartOnFileChange: true
  });
};

E2Eテストの確認

E2Eテストも問題なく動きました。

バージョン確認

最後にAngularのバージョンを確認してみます。
ng version

ng version

     _                      _                 ____ _     ___ 
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | | 
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | | 
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI: 11.0.2
Node: 12.18.1
OS: win32 x64

Angular: 11.0.2
... animations, cli, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router
Ivy Workspace: Yes

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1100.2
@angular-devkit/build-angular   0.1100.2
@angular-devkit/core            11.0.2
@angular-devkit/schematics      11.0.2
@angular/cdk                    11.0.1
@angular/material               11.0.1
@schematics/angular             11.0.2
@schematics/update              0.1100.2
rxjs                            6.6.3
typescript                      4.0.5

良かったです、11 に上がってます。

今日の感想


V4あたりのころからAngularのバージョンアップに悩まされてきたので、ちょっとやそっとのことでは困らなくなってきました。
V11ではすこし既存ソースの修正が多かったように感じます。
それでもUnitテストとE2Eテストを準備しておけば、それなりに安心してバージョンアップできるかなぁと思います。

もう年末ですね、わたしは今年の3月から在宅勤務中ですが、来年もしばらくは在宅のままになりそうです。 では来年もよろしくお願いします。

いまだにJava 8 備忘録 - sonarqubeの使い方

今日すること

こんにちはふるてつです。

今回はJava自体のお話ではありません。
sonarqubeのお話になります。
sonarqubeソースコードの静的解析ツールになります。

f:id:tetsufuru:20201009020250p:plain:w300

常駐先のプロジェクトでも使用していますが、すでに設定されたものを使用するだけで、自分でいちから設定することはまずないです。
最近その常駐先で同僚にsonarqubeを教えようとしたところ「あら、どうやるんだっけ??」となりました。

そこで基礎から設定をやり直そうと思いブログにも書いた次第です。
Qiitaなどで話しつくされた感はかなりありますが…

1. sonarqubeの作業

まずsonarqubeダウンロードサイトを見てみます。
f:id:tetsufuru:20201009020730p:plain https://www.sonarqube.org/

コミュニティ版とデベロッパー版があるようですが、 ローカルにインストールして使用する場合は左のコミュニティ版を選ぶようです。
しかしダウンロードしているうちに下記のドキュメントにDocker版もあるというのを見つけまして、途中でダウンロードはやめて今回はDocker版を使うことにしました。

f:id:tetsufuru:20201009022548p:plain https://docs.sonarqube.org/latest/setup/get-started-2-minutes/

sonarqubeの起動

まずはsonarqubeを起動します、Dockerの起動コマンドはこちらです。

docker run -d --name sonarqube -e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true -p 9000:9000 sonarqube:latest

プロセスが起動したら下記のURLを参照します、ローカルホストの9000番ポートです。
f:id:tetsufuru:20201009024237p:plain http://localhost:9000

これでsonarqubeが起動しました。

プロジェクトの登録

では解析するプロジェクトを追加していきます。
まずはサインインします、login、password はともにデフォルトで admin です。
f:id:tetsufuru:20201009024350p:plain

サインインすると「Create new project」ボタンがまんなかに出てきます。
f:id:tetsufuru:20201009025218p:plain

「Create new project」ボタンをクリックするとProject keyとDisplay nameを入力しろと出てきます。
Project keyは解析するプロジェクトとは関係なくどんな名前でも大丈夫なようです。
わたしはとりあえず my_sonar_qube_test にしてみました、Display nameも一緒です。 f:id:tetsufuru:20201009025414p:plain

「Set Up」ボタンをクリックすると次は token を入力しろと出てきました。
こちらもとりあえずの値 my_sonar_qube_token と入れました。
f:id:tetsufuru:20201009025833p:plain

「Generate」ボタンをクリックしてそのあと先に進むと下の画面にたどり着きます。
わたしの環境の場合、言語はJavaで、ビルドはGradleを選択しています。
他にもC#JSも解析できるようです。
f:id:tetsufuru:20201009135424p:plain

画面の下に表示されている内容のうちひとつは解析したいソースの build.gradle ファイルに追加する内容です。
もう一つはsonarqubeに解析したいソースを登録するコマンドになります。

2. 解析されるソース(Java)での作業

Gradleにプラグイン追加

下記のプラグインを build.gradle ファイルに追加します。

plugins {
    id "org.sonarqube" version "2.7"
}

わたしの環境はGradleのマルチプロジェクトで、フロント側とBiz側の二つの子供のプロジェクトがあります。
下記のような感じで2つともに追加しました。
(バージョンは変数にして別ファイルで管理してます)

フロント側の build.gradle

f:id:tetsufuru:20201009034647p:plain

Biz側の build.gradle

f:id:tetsufuru:20201009034750p:plain

sonarqubeに解析したいソースの登録

次に以下のコマンドを解析したいソースのディレクトリ直下で実行します。
補足になりますが、わたしはWindows環境なので-Dの書き方を変えないとエラーになりました。
最初の./gradlewのところも、Dosコマンドを使う方は./が不要と思います。

./gradlew sonarqube -D "sonar.projectKey=myTestProject" -D "sonar.host.url=http://localhost:9000" -D "sonar.login=ea493af063a0129adf716dae2e6e41000c9a9379"
}

このコマンドが正常に終了するとsonarqubeで解析結果を見れるようになります。

3. 解析結果の確認

今回の解析結果は下記になります。
解析対象のソースはわたしが個人的にJavaで作ったもので、これまでにCheckstyleSpotBugsなどのツールは一切使っていません。
たくさん指摘がでると思いますー
f:id:tetsufuru:20201009040046p:plain

Bugs

まず一番上のBugsをみると1件出ていますね。
そんなふうには作ってないのですが…クリックして中身を確認します。 f:id:tetsufuru:20201009140201p:plain IntegerIntegerをかけ合わせてそのあとにLongに変えている箇所に指摘がありますね。
「かけ合わせる前にそれぞれをキャストしろ」と言われているようです。

Vulnerabilities

脆弱性は0件です。これはよし。

Security Hotspots

Security Hotspotsは1件あります。
Security Hotspotsは脆弱性とは違うようですね。
「ここリスクが高いから確認しておけよ」という感じでしょうか。
こちらも中を確認します。
f:id:tetsufuru:20201009141230p:plain Spring Securityの security config が指摘されていました。 わたしの環境ではSPAでサインインするときにSpringのBasic認証を使っているのですがそのあたりです。
そうですね、もう一度自分の意図した設定とあっているか確認しておきます。

Debt

これは負債を解消するためのコストらしいのですが、2h 38min ならいいかなと思いここはスキップします。

Code Smells

Code Smells 「あやしいにおい」ですね、ここは29件もあります。
f:id:tetsufuru:20201009142108p:plain 中身を確認してみます。

a. コードの消し忘れ

f:id:tetsufuru:20201009142749p:plain コメントアウトしたままコードを消していなかったところが指摘されています…

b. シリアライズ

f:id:tetsufuru:20201009142932p:plain 「ここではシリアライズできるクラスを使ってくれ」といったことを指摘してますね、そうですか…

c. その変数は省略できます

f:id:tetsufuru:20201009144938p:plain 「その変数は省略できます」というヤツですね、同じ内容が多数指摘されました。
わたしは節目節目で変数にセットしたいタイプなのですが修正した方がいいでしょうねー
あと同じところにもう一つ指摘があり「lambda -> はメソッド参照 :: に変えましょう」というのもありました。
そうなんですね、これも修正せねば。

d. リストの中が空かどうか

f:id:tetsufuru:20201009145837p:plain 「リストの中が空かどうかは size() メソッドを使うのではなく、isEmpty() メソッドを使いましょう」という指摘です。
すみません…

感想


今回はsonarqubeの初期設定について書きました。
いちおう最後までできました。 今回指摘された内容は別途修正します。

実際に自分でやろうとするとうまくできないことがおおいですねぇ
でもこれで常駐先の同僚にちゃんと説明できると思います。どや顔で。

それではまた😎

いまだにJava 8 備忘録 - MyBatis Genarator と LocalDateTime

今日すること

こんにちはふるてつです。
今日もJavaの備忘録になります。

f:id:tetsufuru:20191019103646p:plain

MyBatisとは昔からあるO/Rマッパーの事です。
今日もそのあたりのお話をしたいと思います。

f:id:tetsufuru:20191019103308p:plain
https://mybatis.org/mybatis-3/ja/index.html

以前MyBatis GeneratorMapperやらEntityやらを自動で作りましたが、 datetime型でddl定義されているテーブルカラムから作られるEntityjava.util.Date型になってしまいます。
それではちょっと古いのでやはりLocaldateTime型に変えたいところです。
その変え方をまた備忘録として書いておこうと思います。
(わりとすぐにできます)

普通に作ったEntity

まずは最初に普通に作ったEntityの例です。
内容とは特に関係ないので一番単純そうなEntityを例に書きます(MenuMst.java
下のような感じで日付の項目がJavaDate型で宣言されています。

package com.example.demo.entity.domain;

import java.time.LocalDateTime;
import java.util.Date;

public class MenuMst {

    /**
     * This field was generated by MyBatis Generator. This field corresponds to the database column MENU_MST.MENU_SEQ
     * @mbg.generated  Sun Jun 07 15:23:08 JST 2020
     */
    private Long menuSeq;
    /**
     * This field was generated by MyBatis Generator. This field corresponds to the database column MENU_MST.MENU_CODE
     * @mbg.generated  Sun Jun 07 15:23:08 JST 2020
     */
    private String menuCode;

    ~ 途中は省略します ~

    /**
     * This method was generated by MyBatis Generator. This method returns the value of the database column MENU_MST.ENTER_DATE
     * @return  the value of MENU_MST.ENTER_DATE
     * @mbg.generated  Sun Jun 07 15:23:08 JST 2020
     */
        // ↓ 登録日時 ゲッターの戻り値が Date型になっている
    public Date getEnterDate() {
        return enterDate;
    }

    /**
     * This method was generated by MyBatis Generator. This method sets the value of the database column MENU_MST.ENTER_DATE
     * @param enterDate  the value for MENU_MST.ENTER_DATE
     * @mbg.generated  Sun Jun 07 15:23:08 JST 2020
     */
        // ↓ 登録日時 セッターの引数も Date型
    public void setEnterDate(Date enterDate) {
        this.enterDate = enterDate;
    }

    /**
     * This method was generated by MyBatis Generator. This method returns the value of the database column MENU_MST.ENTER_USER
     * @return  the value of MENU_MST.ENTER_USER
     * @mbg.generated  Sun Jun 07 15:23:08 JST 2020
     */
    public String getEnterUser() {
        return enterUser;
    }

    /**
     * This method was generated by MyBatis Generator. This method sets the value of the database column MENU_MST.ENTER_USER
     * @param enterUser  the value for MENU_MST.ENTER_USER
     * @mbg.generated  Sun Jun 07 15:23:08 JST 2020
     */
    public void setEnterUser(String enterUser) {
        this.enterUser = enterUser == null ? null : enterUser.trim();
    }

    /**
     * This method was generated by MyBatis Generator. This method returns the value of the database column MENU_MST.UPDATE_DATE
     * @return  the value of MENU_MST.UPDATE_DATE
     * @mbg.generated  Sun Jun 07 15:23:08 JST 2020
     */
        // ↓ 更新日時 ゲッターの戻り値が Date型になっている
    public Date getUpdateDate() {
        return updateDate;
    }

    /**
     * This method was generated by MyBatis Generator. This method sets the value of the database column MENU_MST.UPDATE_DATE
     * @param updateDate  the value for MENU_MST.UPDATE_DATE
     * @mbg.generated  Sun Jun 07 15:23:08 JST 2020
     */
        // ↓ 更新日時 セッターの引数も Date型
    public void setUpdateDate(Date updateDate) {
        this.updateDate = updateDate;
    }

    /**
     * This method was generated by MyBatis Generator. This method returns the value of the database column MENU_MST.UPDATE_USER
     * @return  the value of MENU_MST.UPDATE_USER
     * @mbg.generated  Sun Jun 07 15:23:08 JST 2020
     */
    public String getUpdateUser() {
        return updateUser;
    }

    /**
     * This method was generated by MyBatis Generator. This method sets the value of the database column MENU_MST.UPDATE_USER
     * @param updateUser  the value for MENU_MST.UPDATE_USER
     * @mbg.generated  Sun Jun 07 15:23:08 JST 2020
     */
    public void setUpdateUser(String updateUser) {
        this.updateUser = updateUser == null ? null : updateUser.trim();
    }
}

GeneratorConfigに設定を追加

変え方はMyBatisのリファレンスのこちらに書いてあります。 https://mybatis.org/generator/configreference/javaTypeResolver.html

MyBatis Generatorのコンフィグファイルに下記を追加すればいいようです。
javaTypeResolveruseJSR310Types=trueをセットする感じですね。

<javaTypeResolver>
    <property name="useJSR310Types" value="true"/>
</javaTypeResolver>

余談ですがこれ以外に BigDecimal を使う設定もありました。
その時はforceBigDecimalsを使うようです。

修正後のGeneratorConfig

下が修正後のMyBatis Generatorのコンフィグファイル(generationConfig.xml)です。
わたしの環境そのままを貼り付けています。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
  <context id="handsOnTables" targetRuntime="MyBatis3">

    <!-- Connection Settings -->
    <jdbcConnection driverClass="org.h2.Driver"
        connectionURL="jdbc:h2:../workspace/product-manage-site-service-for-hands-on/product-manage/h2db/sampledb"
        userId="username"
        password="password">
    </jdbcConnection>

    // ↓ ここに追加してます。
    <javaTypeResolver>
        <property name="useJSR310Types" value="true"/>
    </javaTypeResolver>
    // ↑ ここに追加してます。

    <!-- Entity Models -->
    <javaModelGenerator targetPackage="com.example.demo.entity.domain" targetProject="biz/src/main/java">
      <property name="enableSubPackages" value="true" />
      <property name="trimStrings" value="true" />
    </javaModelGenerator>

    <!-- Sql Xml files -->
    <sqlMapGenerator targetPackage="com.example.demo.repository" targetProject="biz/src/main/resources">
      <property name="enableSubPackages" value="true" />
    </sqlMapGenerator>

    <!-- Mappers -->
    <javaClientGenerator type="XMLMAPPER" targetPackage="com.example.demo.repository" targetProject="biz/src/main/java">
      <property name="enableSubPackages" value="true" />
    </javaClientGenerator>

    <!-- Target tables -->
    <table schema="" tableName="USER_MST">
      <property name="mapUnderscoreToCamelCase" value="true" />
    </table>
    <table schema="" tableName="PRODUCT_MST">
      <property name="mapUnderscoreToCamelCase" value="true" />
    </table>
    <table schema="" tableName="PRODUCT_STOCK_MST">
      <property name="mapUnderscoreToCamelCase" value="true" />
    </table>
    <table schema="" tableName="PRODUCT_PURCHASE_TBL">
      <property name="mapUnderscoreToCamelCase" value="true" />
    </table>
    <table schema="" tableName="PAGE_ROLE_MST">
      <property name="mapUnderscoreToCamelCase" value="true" />
    </table>
    <table schema="" tableName="MENU_MST">
      <property name="mapUnderscoreToCamelCase" value="true" />
    </table>

  </context>
</generatorConfiguration>

修整後のEntity

ではMyBatis Generatorを実行しなおしてみます。
Entityが新しく作り直されました。
上記で上げた例とおなじファイルです(MenuMst.java

package com.example.demo.entity.domain;

import java.time.LocalDateTime;

public class MenuMst {

    /**
     * This field was generated by MyBatis Generator. This field corresponds to the database column MENU_MST.MENU_SEQ
     * @mbg.generated  Sun Jun 07 15:48:11 JST 2020
     */
    private Long menuSeq;
    /**
     * This field was generated by MyBatis Generator. This field corresponds to the database column MENU_MST.MENU_CODE
     * @mbg.generated  Sun Jun 07 15:48:11 JST 2020
     */
    private String menuCode;

    ~ 途中は省略します ~

    /**
     * This method was generated by MyBatis Generator. This method returns the value of the database column MENU_MST.ENTER_DATE
     * @return  the value of MENU_MST.ENTER_DATE
     * @mbg.generated  Sun Jun 07 15:48:11 JST 2020
     */
        // ↓ 登録日時 ゲッターの戻り値が LocalDateTime型に変わりました。
    public LocalDateTime getEnterDate() {
        return enterDate;
    }

    /**
     * This method was generated by MyBatis Generator. This method sets the value of the database column MENU_MST.ENTER_DATE
     * @param enterDate  the value for MENU_MST.ENTER_DATE
     * @mbg.generated  Sun Jun 07 15:48:11 JST 2020
     */
        // ↓ 登録日時 セッターの引数も LocalDateTime型に変わりました。
    public void setEnterDate(LocalDateTime enterDate) {
        this.enterDate = enterDate;
    }

    /**
     * This method was generated by MyBatis Generator. This method returns the value of the database column MENU_MST.ENTER_USER
     * @return  the value of MENU_MST.ENTER_USER
     * @mbg.generated  Sun Jun 07 15:48:11 JST 2020
     */
    public String getEnterUser() {
        return enterUser;
    }

    /**
     * This method was generated by MyBatis Generator. This method sets the value of the database column MENU_MST.ENTER_USER
     * @param enterUser  the value for MENU_MST.ENTER_USER
     * @mbg.generated  Sun Jun 07 15:48:11 JST 2020
     */
    public void setEnterUser(String enterUser) {
        this.enterUser = enterUser == null ? null : enterUser.trim();
    }

    /**
     * This method was generated by MyBatis Generator. This method returns the value of the database column MENU_MST.UPDATE_DATE
     * @return  the value of MENU_MST.UPDATE_DATE
     * @mbg.generated  Sun Jun 07 15:48:11 JST 2020
     */
        // ↓ 更新日時 ゲッターの戻り値が LocalDateTime型に変わりました。
    public LocalDateTime getUpdateDate() {
        return updateDate;
    }

    /**
     * This method was generated by MyBatis Generator. This method sets the value of the database column MENU_MST.UPDATE_DATE
     * @param updateDate  the value for MENU_MST.UPDATE_DATE
     * @mbg.generated  Sun Jun 07 15:48:11 JST 2020
     */
        // ↓ 更新日時 セッターの引数も LocalDateTime型に変わりました。
    public void setUpdateDate(LocalDateTime updateDate) {
        this.updateDate = updateDate;
    }

    /**
     * This method was generated by MyBatis Generator. This method returns the value of the database column MENU_MST.UPDATE_USER
     * @return  the value of MENU_MST.UPDATE_USER
     * @mbg.generated  Sun Jun 07 15:48:11 JST 2020
     */
    public String getUpdateUser() {
        return updateUser;
    }

    /**
     * This method was generated by MyBatis Generator. This method sets the value of the database column MENU_MST.UPDATE_USER
     * @param updateUser  the value for MENU_MST.UPDATE_USER
     * @mbg.generated  Sun Jun 07 15:48:11 JST 2020
     */
    public void setUpdateUser(String updateUser) {
        this.updateUser = updateUser == null ? null : updateUser.trim();
    }
}

このように自動生成したEntityの日付型がLocalDateTime型に変わりました。
ちなみに Mapperは特に変わる所はないです。Entityだけが変わる感じです。

感想


今日はまたMyBatis関連でした。
半年か1年位前に試していた内容なので忘れないうちにと書きました。
これですっきり忘れられます~
これはちょっとした断捨離になっているかもしれません…

それではまた😎

いまだにJava 8 備忘録 - MyBatisで更新日付などを自動設定

今日すること

こんにちはふるてつです。

きょうはJava 8 のMyBatisを使ったAOPのおはなしです。
f:id:tetsufuru:20200607114634p:plain
今回書きたかったのは、各テーブルに共通でもつような項目、
例えば「作成者/作成日時」、「更新者/更新日時」を自動で設定する方法です。
Spring BootSprinrg AOPを使います。

実はQiitaにやりたかったことがほぼ書いてあり、真似させていただいてます😊
作者の方ありがとうございます。
https://qiita.com/kenhori/items/9941a159285f9360e877

AOPのインストール

まずSpring AOPをインストールします。
わたしの環境は Gradle を使っているので Maven Repository で Gradle の書き方例を探します。
下記の例がのっていました。
https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop/2.3.0.RELEASE


compile group: 'org.springframework.boot', name: 'spring-boot-starter-aop', version: '2.3.0.RELEASE'

こちらをbuild.gradleファイルに書き入れます。
しかしすこし書き方はすこし古いようですね。
compile groupimplementationなどに書き換えた方がいい気がします。
参考までにわたしはいまこのように書いています。
(バージョン名は${springbootVersion}のように外出しにしてます)


implementation "org.springframework.boot:spring-boot-starter-aop:${springbootVersion}"

Interceptorを追加

わたしはMyBatisの Generator で Mapper を自動作成しています。
そのため Mapper の名前は必ず xxMapper となり、AOPで動きをキャッチしやすいですね。
ちなみにわたしのMapper周りは下のような構成になっています。
f:id:tetsufuru:20200606201353p:plain

ではソースを紹介します。
MapperInterceptor.javaというクラスになります。
細かく解説を入れましたのでみなさまの参考になればと思います。

package com.example.demo.interceptor;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Objects;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;

import com.example.demo.service.AccountService;

import lombok.RequiredArgsConstructor;

@Aspect
@Component
@RequiredArgsConstructor
public class MapperInterceptor {

    private static final String CONST_INSERT = "insert";
    private static final String CONST_UPDATE = "update";
    private static final String CONST_SET_ENTER_USER = "setEnterUser";
    private static final String CONST_SET_ENTER_DATE = "setEnterDate";
    private static final String CONST_SET_UPDATE_USER = "setUpdateUser";
    private static final String CONST_SET_UPDATE_DATE = "setUpdateDate";

    private final AccountService accountService;

    // ↓ ① Beforeなのでメソッドの実行前に呼び出される
    // ↓ ② executionでマッチするメソッドの条件を書いている
    //     (hogeMapper の #insertHoge() メソッドか、#updateHoge() メソッド)
    @Before("execution(* com.example.demo.repository.*Mapper.insert*(..)) || "
            + "execution(* com.example.demo.repository.*Mapper.update*(..))")
    public void setCommonInformations(JoinPoint joinPoint)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {

        // ③ JoinPoint をもとにいまインターセプトされているメソッドの名前を取得する
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        String methodName = method.getName();

        // ④ 登録、更新日時にセットしたい時刻を変数にいれる(UTC基準の現在時刻)
        LocalDateTime localDateTimeNowUtc = LocalDateTime.now(ZoneOffset.UTC);

        // ⑤ JoinPoint をもとにメソッドの1番目の引数を取得する(テーブル更新用のDTOが必ず最初の引数なので)
        Object[] args = joinPoint.getArgs();
        Object dto = args[0];

        if (methodName.startsWith(CONST_INSERT)) {
            // ⑥ こちらが Insert 系のメソッドの時
            //  「作成者・更新者の情報」と、「登録・更新日時にセットしたい時刻」を dto にセットします。
            //  (作成者・更新者情報は アカウントサービスから取得するようにしています)
            setupEnterInformations(accountService.getUserName(), localDateTimeNowUtc, dto);
            setupUpdateInformations(accountService.getUserName(), localDateTimeNowUtc, dto);

        } else if (methodName.startsWith(CONST_UPDATE)) {
            // ⑦ こちらが Update 系のメソッドの時
            //  「更新者の情報」と、「更新日時にセットしたい時刻」を dto にセットします。
            setupUpdateInformations(accountService.getUserName(), localDateTimeNowUtc, dto);
        }

    }

    // ↓ ⑧ 作成者/作成日時を dto に自動で設定するメソッド
    private void setupEnterInformations(String userName, LocalDateTime utcLocalDateTimeNow, Object dto)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {

        // ↓ ⑧ - 1 リフレクションで dto の #setEnterUser() メソッド(enterUser のセッター)を取得する
        Method setEnterUserMethod = ReflectionUtils.findMethod(dto.getClass(), CONST_SET_ENTER_USER, String.class);
        if (Objects.nonNull(setEnterUserMethod)) {
            // ↓ ⑧ - 2  dto の #setEnterUser() メソッドに userName をセットする
            setEnterUserMethod.invoke(dto, userName);
        }

        // ↓ ⑧ - 3 リフレクションで dto の #setEnterDate() メソッド(enterDateのセッター)を取得する
        Method setEnterDateMethod = ReflectionUtils.findMethod(dto.getClass(), CONST_SET_ENTER_DATE,
                LocalDateTime.class);
        if (Objects.nonNull(setEnterDateMethod)) {
            // ↓ ⑧ - 4  dto の #setEnterDate() メソッドに 現在時刻 をセットする
            setEnterDateMethod.invoke(dto, utcLocalDateTimeNow);
        }

    }

    // ↓ ⑨ 更新者/更新日時を dto に自動で設定するメソッド
    private void setupUpdateInformations(String userName, LocalDateTime utcLocalDateTimeNow, Object dto)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {

        Method setUpdateUserMethod = ReflectionUtils.findMethod(dto.getClass(), CONST_SET_UPDATE_USER, String.class);
        if (Objects.nonNull(setUpdateUserMethod)) {
            setUpdateUserMethod.invoke(dto, userName);
        }

        Method setUpdateDateMethod = ReflectionUtils.findMethod(dto.getClass(), CONST_SET_UPDATE_DATE,
                LocalDateTime.class);
        if (Objects.nonNull(setUpdateDateMethod)) {
            setUpdateDateMethod.invoke(dto, utcLocalDateTimeNow);
        }
    }
}

感想


今回はMyBatisAOPについて書きました。
Qiitaの内容をだいぶ参考にしたのでわたしのオリジナルな部分はかなり少なめですが。
しかしこのあたりの内容は忘れてしまいやすいので一度、自分のブログに書きとめておきたかった次第です。

きょうは久しぶりにブログを書きました。
久しぶりすぎてどうやって書いていたか思い出せずちょっと戸惑いました~

それではまた😎