- 投稿日:2021-02-21T23:55:09+09:00
Raspbian GNU/Linux 10 (buster)のJavaをJava 8に入れ替えてRaspberry Pi Zero WをJenkinsのエージェントとして利用する
1. はじめに
Raspberry Pi Zero WをJenkins1のエージェントとして利用しようとしたところ以下のようなメッセージが出力され、ノードの起動ができませんでした。
[02/18/21 22:48:37] [SSH] /usr/bin/javaのJavaバージョンをチェック
/usr/bin/java のJavaバージョンが不明です。
Error occurred during initialization of VM
Server VM is only supported on ARMv7+ VFPRaspberry Pi Zero WのCPUはARMv6でプリインストールされているJavaが対応していないためとわかりました。そこでRaspberry Pi Zero Wで利用可能なJavaに入れ替えました。
2. 環境
筆者が使用しているRaspberry Pi Zero WやRaspbianの諸元を以下に示します。
$ uname -a Linux raspberrypi 5.4.51+ #1333 Mon Aug 10 16:38:02 BST 2020 armv6l GNU/Linux $ cat /proc/device-tree/model Raspberry Pi Zero W Rev 1.1 $ cat /proc/cpuinfo processor : 0 model name : ARMv6-compatible processor rev 7 (v6l) BogoMIPS : 697.95 Features : half thumb fastmult vfp edsp java tls CPU implementer : 0x41 CPU architecture: 7 CPU variant : 0x0 CPU part : 0xb76 CPU revision : 7 Hardware : BCM2835 Revision : 9000c1 Serial : 00000000502df6c1 Model : Raspberry Pi Zero W Rev 1.1 $ lsb_release -a No LSB modules are available. Distributor ID: Raspbian Description: Raspbian GNU/Linux 10 (buster) Release: 10 Codename: buster3. 手順
使用するコマンドは以下の3つです。
$ sudo apt-get remove openjdk-11-jre $ sudo apt-get remove openjdk-11-jre-headless $ sudo apt-get install openjdk-8-jdk4. 動作確認
Javaを利用できるようになりました。
$ java -version openjdk version "1.8.0_212" OpenJDK Runtime Environment (build 1.8.0_212-8u212-b01-1+rpi1-b01) OpenJDK Client VM (build 25.212-b01, mixed mode)Jenkinsのエージェントも起動しました。
[02/18/21 23:01:35] [SSH] javaのJavaバージョンをチェック [02/18/21 23:01:36] [SSH] java -version returned 1.8.0_212. ~略~ Agent successfully connected and online5. 参考にさせていただいた記事
RazpberryPi Zero WでJavaを使おう
Javaを入れ替えできたのはこちらの記事のおかげです。こちらの記事との違いはopenjdk-11-jre-headlessをremoveする前にopenjdk-11-jreをremoveしていることです。付録. コマンド操作のログ
Linux raspberrypi 5.4.51+ #1333 Mon Aug 10 16:38:02 BST 2020 armv6l The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. Last login: Thu Feb 18 22:37:26 2021 pi@raspberrypi:~ $ pi@raspberrypi:~ $ pi@raspberrypi:~ $ java -version Error occurred during initialization of VM Server VM is only supported on ARMv7+ VFP pi@raspberrypi:~ $ pi@raspberrypi:~ $ pi@raspberrypi:~ $ which java /usr/bin/java pi@raspberrypi:~ $ pi@raspberrypi:~ $ pi@raspberrypi:~ $ ls -l /usr/bin/java lrwxrwxrwx 1 root root 22 8月 20 19:45 /usr/bin/java -> /etc/alternatives/java pi@raspberrypi:~ $ pi@raspberrypi:~ $ pi@raspberrypi:~ $ ls -l /etc/alternatives/java lrwxrwxrwx 1 root root 43 8月 20 19:45 /etc/alternatives/java -> /usr/lib/jvm/java-11-openjdk-armhf/bin/java pi@raspberrypi:~ $ pi@raspberrypi:~ $ pi@raspberrypi:~ $ ls -l /usr/lib/jvm/java-11-openjdk-armhf/bin/java -rwxr-xr-x 1 root root 5580 7月 23 2020 /usr/lib/jvm/java-11-openjdk-armhf/bin/java pi@raspberrypi:~ $ pi@raspberrypi:~ $ pi@raspberrypi:~ $ dpkg -S /usr/lib/jvm/java-11-openjdk-armhf/bin/java openjdk-11-jre-headless:armhf: /usr/lib/jvm/java-11-openjdk-armhf/bin/java pi@raspberrypi:~ $ pi@raspberrypi:~ $ pi@raspberrypi:~ $ sudo apt-get remove openjdk-11-jre-headless パッケージリストを読み込んでいます... 完了 依存関係ツリーを作成しています 状態情報を読み取っています... 完了 インストールすることができないパッケージがありました。おそらく、あり得 ない状況を要求したか、(不安定版ディストリビューションを使用しているの であれば) 必要なパッケージがまだ作成されていなかったり Incoming から移 動されていないことが考えられます。 以下の情報がこの問題を解決するために役立つかもしれません: 以下のパッケージには満たせない依存関係があります: default-jre : 依存: default-jre-headless (= 2:1.11-71+b1) しかし、インストールされようとしていません openjdk-11-jre : 依存: openjdk-11-jre-headless (= 11.0.8+10-1~deb10u1) しかし、インストールされようとしていません E: エラー、pkgProblemResolver::Resolve は停止しました。おそらく変更禁止パッケージが原因です。 pi@raspberrypi:~ $ pi@raspberrypi:~ $ pi@raspberrypi:~ $ sudo apt-get remove openjdk-11-jre パッケージリストを読み込んでいます... 完了 依存関係ツリーを作成しています 状態情報を読み取っています... 完了 以下のパッケージが自動でインストールされましたが、もう必要とされていません: libatk-wrapper-java libatk-wrapper-java-jni libbsh-java libexiv2-14 libgfortran3 libglu1-mesa libgmime-2.6-0 libhsqldb1.8.0-java libice-dev libncurses5 libopenjfx-java libopenjfx-jni libpthread-stubs0-dev libsm-dev libssl1.0.2 libx11-dev libxau-dev libxcb1-dev libxdmcp-dev libxt-dev openjfx openjfx-source uuid-dev wolframscript x11proto-core-dev x11proto-dev xorg-sgml-doctools xtrans-dev これを削除するには 'sudo apt autoremove' を利用してください。 以下の追加パッケージがインストールされます: default-jre-headless 提案パッケージ: default-jre 以下のパッケージは「削除」されます: bluej greenfoot-unbundled libreoffice-nlpsolver libreoffice-script-provider-bsh libreoffice-script-provider-js libreoffice-sdbc-hsqldb libreoffice-wiki-publisher openjdk-11-jdk openjdk-11-jre wolfram-engine 以下のパッケージが新たにインストールされます: default-jre-headless アップグレード: 0 個、新規インストール: 1 個、削除: 10 個、保留: 0 個。 11.1 kB のアーカイブを取得する必要があります。 この操作後に 1,148 MB のディスク容量が解放されます。 続行しますか? [Y/n] y 取得:1 http://ftp.tsukuba.wide.ad.jp/Linux/raspbian/raspbian buster/main armhf default-jre-headless armhf 2:1.11-71+b1 [11.1 kB] 11.1 kB を 2秒 で取得しました (5,497 B/s) (データベースを読み込んでいます ... 現在 153744 個のファイルとディレクトリがインストールされています。) bluej (4.2.1) を削除しています ... greenfoot-unbundled (3.6.0) を削除しています ... libreoffice-nlpsolver (0.9+LibO6.1.5-3+rpi1+deb10u6+rpt1) を削除しています ... libreoffice-script-provider-bsh (1:6.1.5-3+rpi1+deb10u6+rpt1) を削除しています ... libreoffice-script-provider-js (1:6.1.5-3+rpi1+deb10u6+rpt1) を削除しています ... libreoffice-sdbc-hsqldb (1:6.1.5-3+rpi1+deb10u6+rpt1) を削除しています ... libreoffice-wiki-publisher (1.2.0+LibO6.1.5-3+rpi1+deb10u6+rpt1) を削除しています ... wolfram-engine (12.0.1+2019062401) を削除しています ... openjdk-11-jdk:armhf (11.0.8+10-1~deb10u1) を削除しています ... openjdk-11-jre:armhf (11.0.8+10-1~deb10u1) を削除しています ... 以前に未選択のパッケージ default-jre-headless を選択しています。 (データベースを読み込んでいます ... 現在 136364 個のファイルとディレクトリがインストールされています。) .../default-jre-headless_2%3a1.11-71+b1_armhf.deb を展開する準備をしています ... default-jre-headless (2:1.11-71+b1) を展開しています... default-jre-headless (2:1.11-71+b1) を設定しています ... gnome-menus (3.31.4-3) のトリガを処理しています ... man-db (2.8.5-2) のトリガを処理しています ... libreoffice-common (1:6.1.5-3+rpi1+deb10u6+rpt1) のトリガを処理しています ... shared-mime-info (1.10-1) のトリガを処理しています ... desktop-file-utils (0.23-4) のトリガを処理しています ... mime-support (3.62) のトリガを処理しています ... hicolor-icon-theme (0.17-2) のトリガを処理しています ... pi@raspberrypi:~ $ pi@raspberrypi:~ $ pi@raspberrypi:~ $ sudo apt-get remove openjdk-11-jre-headless パッケージリストを読み込んでいます... 完了 依存関係ツリーを作成しています 状態情報を読み取っています... 完了 以下のパッケージが自動でインストールされましたが、もう必要とされていません: java-common libactivation-java libaopalliance-java libapache-pom-java libargs4j-java libasm-java libatinject-jsr330-api-java libatk-wrapper-java libatk-wrapper-java-jni libbase-java libbcmail-java libbcpkix-java libbcprov-java libbsh-java libcdi-api-java libcglib-java libcommons-cli-java libcommons-codec-java libcommons-collections3-java libcommons-collections4-java libcommons-compress-java libcommons-io-java libcommons-lang3-java libcommons-logging-java libcommons-math3-java libcommons-parent-java libcurvesapi-java libdom4j-java libdtd-parser-java libehcache-java libel-api-java libexiv2-14 libfastinfoset-java libflute-java libfonts-java libformula-java libgeronimo-annotation-1.3-spec-java libgeronimo-interceptor-3.0-spec-java libgfortran3 libglu1-mesa libgmime-2.6-0 libguava-java libguice-java libhawtjni-runtime-java libhsqldb1.8.0-java libhttpclient-java libhttpcore-java libice-dev libicu4j-java libintellij-annotations-java libitext-java libjansi-java libjansi-native-java libjaxb-api-java libjaxen-java libjcommon-java libjdom1-java libjetbrains-annotations-java libjsoup-java libjsp-api-java libjsr305-java libloader-java liblog4j1.2-java libmail-java libmaven-file-management-java libmaven-parent-java libmaven-resolver-java libmaven-shared-io-java libmaven-shared-utils-java libmaven3-core-java libncurses5 libopenjfx-java libopenjfx-jni libpixie-java libplexus-archiver-java libplexus-cipher-java libplexus-classworlds-java libplexus-component-annotations-java libplexus-interpolation-java libplexus-io-java libplexus-sec-dispatcher-java libplexus-utils2-java libpthread-stubs0-dev librelaxng-datatype-java librepository-java librngom-java libsac-java libsaxonhe-java libservlet-api-java libservlet3.1-java libsisu-guice-java libsisu-inject-java libsisu-ioc-java libsisu-plexus-java libslf4j-java libsm-dev libsnappy-java libsnappy-jni libssl1.0.2 libstax-ex-java libstreambuffer-java libwagon-http-java libwagon-provider-api-java libwebsocket-api-java libx11-dev libxau-dev libxcb1-dev libxdmcp-dev libxerces2-java libxml-commons-external-java libxml-commons-resolver1.1-java libxml-java libxmlbeans-java libxom-java libxsom-java libxt-dev libxz-java openjfx openjfx-source uuid-dev wolframscript x11proto-core-dev x11proto-dev xorg-sgml-doctools xtrans-dev これを削除するには 'sudo apt autoremove' を利用してください。 以下のパッケージは「削除」されます: ant ant-contrib ant-optional ca-certificates-java default-jre-headless libapache-poi-java libcodemodel-java libistack-commons-java libjaxb-java liblayout-java libpentaho-reporting-flow-engine-java libreoffice-report-builder libserializer-java libtxw2-java openjdk-11-jdk-headless openjdk-11-jre-headless アップグレード: 0 個、新規インストール: 0 個、削除: 16 個、保留: 0 個。 この操作後に 361 MB のディスク容量が解放されます。 続行しますか? [Y/n] y (データベースを読み込んでいます ... 現在 136369 個のファイルとディレクトリがインストールされています。) libreoffice-report-builder (1:6.1.5-3+rpi1+deb10u6+rpt1) を削除しています ... libserializer-java (1.1.6-5) を削除しています ... libpentaho-reporting-flow-engine-java (0.9.4-5) を削除しています ... ant-contrib (1.0~b3+svn177-10) を削除しています ... openjdk-11-jdk-headless:armhf (11.0.8+10-1~deb10u1) を削除しています ... default-jre-headless (2:1.11-71+b1) を削除しています ... liblayout-java (0.2.10-3) を削除しています ... libapache-poi-java (4.0.1-1) を削除しています ... libjaxb-java (2.3.0.1-8) を削除しています ... libtxw2-java (2.3.0.1-8) を削除しています ... ca-certificates-java (20190405) を削除しています ... libcodemodel-java (2.6+jaxb2.3.0.1-8) を削除しています ... libistack-commons-java (3.0.6-3) を削除しています ... ant-optional (1.10.5-2) を削除しています ... ant (1.10.5-2) を削除しています ... openjdk-11-jre-headless:armhf (11.0.8+10-1~deb10u1) を削除しています ... ca-certificates (20200601~deb10u1) のトリガを処理しています ... Updating certificates in /etc/ssl/certs... 0 added, 0 removed; done. Running hooks in /etc/ca-certificates/update.d... updates of cacerts keystore disabled. done. libreoffice-common (1:6.1.5-3+rpi1+deb10u6+rpt1) のトリガを処理しています ... man-db (2.8.5-2) のトリガを処理しています ... pi@raspberrypi:~ $ pi@raspberrypi:~ $ pi@raspberrypi:~ $ which java pi@raspberrypi:~ $ pi@raspberrypi:~ $ pi@raspberrypi:~ $ sudo apt-get install openjdk-8-jdk パッケージリストを読み込んでいます... 完了 依存関係ツリーを作成しています 状態情報を読み取っています... 完了 以下のパッケージが自動でインストールされましたが、もう必要とされていません: libactivation-java libaopalliance-java libapache-pom-java libargs4j-java libasm-java libatinject-jsr330-api-java libbase-java libbcmail-java libbcpkix-java libbcprov-java libbsh-java libcdi-api-java libcglib-java libcommons-cli-java libcommons-codec-java libcommons-collections3-java libcommons-collections4-java libcommons-compress-java libcommons-io-java libcommons-lang3-java libcommons-logging-java libcommons-math3-java libcommons-parent-java libcurvesapi-java libdom4j-java libdtd-parser-java libehcache-java libel-api-java libexiv2-14 libfastinfoset-java libflute-java libfonts-java libformula-java libgeronimo-annotation-1.3-spec-java libgeronimo-interceptor-3.0-spec-java libgfortran3 libglu1-mesa libgmime-2.6-0 libguava-java libguice-java libhawtjni-runtime-java libhsqldb1.8.0-java libhttpclient-java libhttpcore-java libicu4j-java libintellij-annotations-java libitext-java libjansi-java libjansi-native-java libjaxb-api-java libjaxen-java libjcommon-java libjdom1-java libjetbrains-annotations-java libjsoup-java libjsp-api-java libjsr305-java libloader-java liblog4j1.2-java libmail-java libmaven-file-management-java libmaven-parent-java libmaven-resolver-java libmaven-shared-io-java libmaven-shared-utils-java libmaven3-core-java libncurses5 libopenjfx-java libopenjfx-jni libpixie-java libplexus-archiver-java libplexus-cipher-java libplexus-classworlds-java libplexus-component-annotations-java libplexus-interpolation-java libplexus-io-java libplexus-sec-dispatcher-java libplexus-utils2-java librelaxng-datatype-java librepository-java librngom-java libsac-java libsaxonhe-java libservlet-api-java libservlet3.1-java libsisu-guice-java libsisu-inject-java libsisu-ioc-java libsisu-plexus-java libslf4j-java libsnappy-java libsnappy-jni libssl1.0.2 libstax-ex-java libstreambuffer-java libwagon-http-java libwagon-provider-api-java libwebsocket-api-java libxerces2-java libxml-commons-external-java libxml-commons-resolver1.1-java libxml-java libxmlbeans-java libxom-java libxsom-java libxz-java openjfx openjfx-source uuid-dev wolframscript これを削除するには 'sudo apt autoremove' を利用してください。 以下の追加パッケージがインストールされます: ca-certificates-java openjdk-8-jdk-headless openjdk-8-jre openjdk-8-jre-headless 提案パッケージ: openjdk-8-demo openjdk-8-source visualvm icedtea-8-plugin fonts-ipafont-gothic fonts-ipafont-mincho fonts-wqy-microhei fonts-wqy-zenhei fonts-indic 以下のパッケージが新たにインストールされます: ca-certificates-java openjdk-8-jdk openjdk-8-jdk-headless openjdk-8-jre openjdk-8-jre-headless アップグレード: 0 個、新規インストール: 5 個、削除: 0 個、保留: 0 個。 32.2 MB のアーカイブを取得する必要があります。 この操作後に追加で 130 MB のディスク容量が消費されます。 続行しますか? [Y/n] y 取得:1 http://ftp.tsukuba.wide.ad.jp/Linux/raspbian/raspbian buster/main armhf openjdk-8-jre-headless armhf 8u212-b01-1+rpi1 [25.5 MB] 取得:2 http://ftp.tsukuba.wide.ad.jp/Linux/raspbian/raspbian buster/main armhf ca-certificates-java all 20190405 [15.7 kB] 取得:3 http://ftp.tsukuba.wide.ad.jp/Linux/raspbian/raspbian buster/main armhf openjdk-8-jre armhf 8u212-b01-1+rpi1 [61.8 kB] 取得:4 http://ftp.tsukuba.wide.ad.jp/Linux/raspbian/raspbian buster/main armhf openjdk-8-jdk-headless armhf 8u212-b01-1+rpi1 [6,299 kB] 取得:5 http://ftp.tsukuba.wide.ad.jp/Linux/raspbian/raspbian buster/main armhf openjdk-8-jdk armhf 8u212-b01-1+rpi1 [382 kB] 32.2 MB を 53秒 で取得しました (608 kB/s) 以前に未選択のパッケージ openjdk-8-jre-headless:armhf を選択しています。 (データベースを読み込んでいます ... 現在 135310 個のファイルとディレクトリがインストールされています。) .../openjdk-8-jre-headless_8u212-b01-1+rpi1_armhf.deb を展開する準備をしています ... openjdk-8-jre-headless:armhf (8u212-b01-1+rpi1) を展開しています... 以前に未選択のパッケージ ca-certificates-java を選択しています。 .../ca-certificates-java_20190405_all.deb を展開する準備をしています ... ca-certificates-java (20190405) を展開しています... 以前に未選択のパッケージ openjdk-8-jre:armhf を選択しています。 .../openjdk-8-jre_8u212-b01-1+rpi1_armhf.deb を展開する準備をしています ... openjdk-8-jre:armhf (8u212-b01-1+rpi1) を展開しています... 以前に未選択のパッケージ openjdk-8-jdk-headless:armhf を選択しています。 .../openjdk-8-jdk-headless_8u212-b01-1+rpi1_armhf.deb を展開する準備をしています ... openjdk-8-jdk-headless:armhf (8u212-b01-1+rpi1) を展開しています... 以前に未選択のパッケージ openjdk-8-jdk:armhf を選択しています。 .../openjdk-8-jdk_8u212-b01-1+rpi1_armhf.deb を展開する準備をしています ... openjdk-8-jdk:armhf (8u212-b01-1+rpi1) を展開しています... openjdk-8-jre-headless:armhf (8u212-b01-1+rpi1) を設定しています ... update-alternatives: /usr/bin/rmid (rmid) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/jre/bin/rmid を使います update-alternatives: /usr/bin/clhsdb (clhsdb) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/jre/bin/clhsdb を使います update-alternatives: /usr/bin/java (java) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/jre/bin/java を使います update-alternatives: /usr/bin/keytool (keytool) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/jre/bin/keytool を使います update-alternatives: /usr/bin/hsdb (hsdb) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/jre/bin/hsdb を使います update-alternatives: /usr/bin/jjs (jjs) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/jre/bin/jjs を使います update-alternatives: /usr/bin/pack200 (pack200) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/jre/bin/pack200 を使います update-alternatives: /usr/bin/rmiregistry (rmiregistry) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/jre/bin/rmiregistry を使います update-alternatives: /usr/bin/unpack200 (unpack200) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/jre/bin/unpack200 を使います update-alternatives: /usr/bin/orbd (orbd) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/jre/bin/orbd を使います update-alternatives: /usr/bin/servertool (servertool) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/jre/bin/servertool を使います update-alternatives: /usr/bin/tnameserv (tnameserv) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/jre/bin/tnameserv を使います update-alternatives: /usr/bin/jexec (jexec) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/jre/lib/jexec を使います ca-certificates-java (20190405) を設定しています ... openjdk-8-jre:armhf (8u212-b01-1+rpi1) を設定しています ... update-alternatives: /usr/bin/policytool (policytool) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/jre/bin/policytool を使います openjdk-8-jdk-headless:armhf (8u212-b01-1+rpi1) を設定しています ... update-alternatives: /usr/bin/jdeps (jdeps) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/jdeps を使います update-alternatives: /usr/bin/wsimport (wsimport) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/wsimport を使います update-alternatives: /usr/bin/jinfo (jinfo) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/jinfo を使います update-alternatives: /usr/bin/jsadebugd (jsadebugd) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/jsadebugd を使います update-alternatives: /usr/bin/native2ascii (native2ascii) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/native2ascii を使います update-alternatives: /usr/bin/jstat (jstat) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/jstat を使います update-alternatives: /usr/bin/javac (javac) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/javac を使います update-alternatives: /usr/bin/javah (javah) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/javah を使います update-alternatives: /usr/bin/idlj (idlj) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/idlj を使います update-alternatives: /usr/bin/jstack (jstack) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/jstack を使います update-alternatives: /usr/bin/jrunscript (jrunscript) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/jrunscript を使います update-alternatives: /usr/bin/javadoc (javadoc) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/javadoc を使います update-alternatives: /usr/bin/jhat (jhat) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/jhat を使います update-alternatives: /usr/bin/javap (javap) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/javap を使います update-alternatives: /usr/bin/jar (jar) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/jar を使います update-alternatives: /usr/bin/xjc (xjc) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/xjc を使います update-alternatives: /usr/bin/schemagen (schemagen) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/schemagen を使います update-alternatives: /usr/bin/jps (jps) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/jps を使います update-alternatives: /usr/bin/extcheck (extcheck) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/extcheck を使います update-alternatives: /usr/bin/rmic (rmic) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/rmic を使います update-alternatives: /usr/bin/jstatd (jstatd) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/jstatd を使います update-alternatives: /usr/bin/jmap (jmap) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/jmap を使います update-alternatives: /usr/bin/jdb (jdb) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/jdb を使います update-alternatives: /usr/bin/serialver (serialver) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/serialver を使います update-alternatives: /usr/bin/wsgen (wsgen) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/wsgen を使います update-alternatives: /usr/bin/jcmd (jcmd) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/jcmd を使います update-alternatives: /usr/bin/jarsigner (jarsigner) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/jarsigner を使います openjdk-8-jdk:armhf (8u212-b01-1+rpi1) を設定しています ... update-alternatives: /usr/bin/appletviewer (appletviewer) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/appletviewer を使います update-alternatives: /usr/bin/jconsole (jconsole) を提供するために自動モードで /usr/lib/jvm/java-8-openjdk-armhf/bin/jconsole を使います desktop-file-utils (0.23-4) のトリガを処理しています ... mime-support (3.62) のトリガを処理しています ... hicolor-icon-theme (0.17-2) のトリガを処理しています ... gnome-menus (3.31.4-3) のトリガを処理しています ... libc-bin (2.28-10+rpi1) のトリガを処理しています ... ca-certificates (20200601~deb10u1) のトリガを処理しています ... Updating certificates in /etc/ssl/certs... 0 added, 0 removed; done. Running hooks in /etc/ca-certificates/update.d... done. done. pi@raspberrypi:~ $ pi@raspberrypi:~ $ pi@raspberrypi:~ $ which java /usr/bin/java pi@raspberrypi:~ $ pi@raspberrypi:~ $ pi@raspberrypi:~ $ java -version openjdk version "1.8.0_212" OpenJDK Runtime Environment (build 1.8.0_212-8u212-b01-1+rpi1-b01) OpenJDK Client VM (build 25.212-b01, mixed mode) pi@raspberrypi:~ $ pi@raspberrypi:~ $
筆者が使用しているJenkinsは2.249.3です。 ↩
- 投稿日:2021-02-21T23:18:21+09:00
SpringBootでToDoアプリを作ってみよう【+jQuery】
はじめに
SpringBootを使ってTodoアプリを作っていきます。
↓以下の記事の続きです。ここに加えていくので、先に作ってみてください。
SpringBootでToDoアプリを作ってみよう【誰でも作れます・初心者向け】前回分のソースファイル
https://github.com/tokio-k/TodoApp-springboot/tree/intermediateこれから、SpringBootの勉強を始めるという人の役に立てれば、いいなと思っています。
基本的にコードはファイル全部を載せるようにします。自分が勉強している時に、書く場所がわからないことがあったので。
既存のものを少し編集するだけの時は省略します。
Ajaxを使った非同期処理で通信を行っていきます。
SpringBootとjQueryのデータ送受信がメインになるかと思います。使う技術
- テンプレートエンジン Thymeleaf
- データベース PostgreSQL
- ORマッパー Mybatis
- フロント部分 jQuery 、Bootstrap
流れ
- CSS・JavaScriptを使う準備
- タスク一覧表示の改修
- 更新処理の改修
- 完了済みの表示表示機能の追加
- 追加機能の表示&追加処理の改修
- 削除機能の表示&削除処理の改修
CSS・JavaScriptを使う準備
Thymeleafのみだったフロンド部分に、CSSやJavaScriptを追加していきます。
src/main/resources/staticの下に記述する必要があります。src/main/resources/staticの下に
「css」フォルダと「js」フォルダを作成します。
この中に、作成したCSSファイルやJsファイルを格納してくことにします。
※今回cssファイルはほとんど使わないです。学習用に1つだけ使います。cssフォルダには、「style.css」を
jsフォルダには、「todo.js」を作成します。index.htmlのheadの中に以下を追加します。(少し下にindex.html全体を載せてます。)
<link rel="stylesheet" th:href="@{/css/style.css}" />index.htmlのbodyの最後に以下を追加します。
<script type="text/javascript" th:src="@{/js/todo.js}"></script>これで、cssフォルダとJsフォルダの中のファイルを適応できます。
今回は、jQueryとBootstrapも使用するので、pom.xmlに以下を追記します。
pom.xml<dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>4.6.0</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.5.1</version> </dependency>(webjarsで依存追加の書き方を確認できます。)
dependenciesタグの中に追加します。index.htmlのheadの中に以下を追加します。
<script src="webjars/jquery/3.5.1/jquery.min.js"></script> <link rel="stylesheet" href="webjars/bootstrap/4.6.0/css/bootstrap.min.css" /> <script src="webjars/bootstrap/4.6.0/js/bootstrap.min.js"></script>これで、jQueryとBootstrapが使えます。
タスク一覧表示の改修
画面を少し整えていきます。
index.htmlを編集します
今回は、formの送信ではなくjQueryで情報を取得して、Controller側に渡すことにします。index.html<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <script src="webjars/jquery/3.5.1/jquery.min.js"></script> <link rel="stylesheet" href="webjars/bootstrap/4.6.0/css/bootstrap.min.css" /> <script src="webjars/bootstrap/4.6.0/js/bootstrap.min.js"></script> <link rel="stylesheet" th:href="@{/css/style.css}" /> <title>TodoApp</title> </head> <body> <h1>TodoList</h1> <h3>マイタスク</h3> <!--↓編集箇所 --> <table class="table table-borderless"> <thead> </thead> <tbody id="todes"> <tr class="todo" th:each="todo : ${todos}"> <td style="width:2rem"> <input type="checkbox"name="done_flg"/> <input type="hidden" name="id" th:value="${todo.id}" /> </td> <td style="width:22rem"><input type="text" style="border:none;width:22rem" name="title" th:value="${todo.title}"/></td> <td><input type="date" style="border:none" name="time_limit" th:value="${todo.time_limit}" /></td> </tr> </tbody> </table> <div style="font-size:20px"> <span id="done_num">1</span> <span class="pr-3">件完了</span> <span class="button_for_show" style="display:inline-block"></span> </div> <table class="table table-borderless"> <thead> </thead> <tbody id="donetodes"> <tr class="todo" th:each="todo : ${doneTodos}"> <td style="width:2rem"> <input type="checkbox"name="done_flg" checked/> <input type="hidden" name="id" th:value="${todo.id}" /> </td> <td style="width:22rem"><input type="text" style="border:none;width:22rem;text-decoration:line-through" name="title" th:value="${todo.title}"/></td> <td><input type="date"style="border:none;display:none" name="time_limit" th:value="${todo.time_limit}" /></td> </tr> </tbody> </table> <!--↑編集箇所 --> <h3>新しいタスクを追加</h3> <form method="post" th:action="@{/add}"> <input type="text" name="title" /> <input type="date" name="time_limit"/> <input type="submit" value="追加" /> </form> <form method="post" th:action="@{/delete}"> <input type="submit" value="完了済みを削除" /> </form> <script type="text/javascript" th:src="@{/js/todo.js}"></script> </body> </html>cssも1つだけ
style.css.button_for_show { border-right : solid 2px #000; border-bottom : solid 2px #000; width:1rem; height:1rem; transform : rotate(45deg); position:relative; bottom:4px }こんな感じになりました。
jsで使うために名前がついてたりしますが、今は気にしないでください。
見出しなど自由にカスタマイズしてみてください。
※写真イメージには、<span id="done_num">2</span>と直接2を書いています。
※上のコードでは2は表示されません。今からJsで実装します完了件数の数を取得して表示します。
todo.jsを編集します<tbody id="donetodes">の子要素の数を数えて表示しています
index.js/** * */ $(function(){ //完了済みの個数取得・表示 let doneCount = $("#donetodes").children("tr").length; $("#done_count").text(doneCount); })更新処理の改修
index.htmlを編集したので、それに合わせて更新をできるようにしていきます。
編集をしたらすぐに更新できるようにします。todo.jsを編集します
ここでやることは以下です。
- 更新があるたびに処理を実行する
- 1つのタスクの情報(id,title,time_limit,done_flg)を取得する
- 取得した値をController側に送って/updateの処理を実行する
- 完了(未完了)ボタンを押した際の処理を実行する
- 完了済み(未完了)へ移動する
- 文字に打ち消し線をつける(消す)
- 日付を隠す(表示する)
- 完了件数を更新する
todo.js$(function(){ //更新処理 $('.todo input').change(function(){ const todo = $(this).parents('.todo'); const id = todo.find('input[name="id"]').val(); const title = todo.find('input[name="title"]').val(); const time_limit = todo.find('input[name="time_limit"]').val(); const is_done = todo.find('input[name="done_flg"]').prop("checked"); let done_flg; if(is_done == true) { done_flg = 1; }else{ done_flg = 0; } const params = { id : id, title : title, time_limit : time_limit, done_flg : done_flg } $.post("/update",params); //完了ボタンを押した際の処理 doneCount = $("#done_count").text(); if($(this).prop('name') == "done_flg"){ if(isDone == true){ $(todo).appendTo("#donetodes"); todo.find('input[name="title"]').css('text-decoration','line-through') todo.find('input[name="time_limit"]').hide(); doneCount ++; }else{ $(todo).appendTo("#todes"); todo.find('input[name="title"]').css('text-decoration','none') todo.find('input[name="time_limit"]').show() doneCount --; } $("#done_count").text(doneCount); } }) })1つ目のconstで<tr>単位の要素(todo単位)を取得しています。
2~5つ目のconstでそれぞれのinputの要素を取得しています。
5つ目のconstでそれぞれの値をオブジェクトに格納し、その後postメソッドでcontrollerに送っています。
※postメソッドに続けて失敗時の処理、controllerからの戻り値を扱う処理等も書くことができます。
※ajaxやpostメソッドなどで調べてみてください。「完了(未完了)ボタンを押した際の処理」以下では、以下の処理を記載しています。
- 完了(未完了)ボタンを押したかどうかで条件分岐
- 完了(未完了)ボタンを押しており完了済みの場合の処理を記載
- 完了(未完了)ボタンを押しており未完了の場合の処理を記載
これで更新ができるようになりました。
TodoController.javaを編集する
非同期処理で画面遷移もなくしているので、Controllerのupdateメソッドも以下のように変更します。TodoController.java@RequestMapping(value="/update") @ResponseBody public void update(Todo todo) { todoMapper.update(todo); }Controllerはビュークラスを返すのが基本ですが、@ResponseBodyをつけることでコンテンツが返せます。
今回は、何も返していないのですが、つけなかったらエラーになったのでつけておきます。
【折り畳み(本文とは関係のない内容)】
フォームの入力チェックでのエラー内容や、更新処理の成功かどうかのデータ等を返すこともあります。更新処理のエラーかどうか返す例
(例)Controller.java@RequestMapping(value="/update") @ResponseBody public String update(Entity entity) { String succes = false; try { Mapper.update(Entity); succes = "true"; } catch(Exception e) { succes = "false"; } return succes; }Mapなどを使って、内容を返したりなどもできます。
String→Map<String,Object> 、 Stringに"errMsg",Objectにエラーメッセージのリスト等
この戻り値はjQueryのpostメソッドに連結させたメソッドの引数となります。完了済みの表示非表示機能の追加
完了しているものは基本的に消しておいて必要な時のみ表示できるようにします。
index.htmlを編集する
2つ目のtableタグ(完了済みを表示している方)にstyleとidをつけます。
※変更がここだけなので、他省略しました。index.html<table class="table table-borderless" style="display:none"id="done_table">これで、完了済みのタスクが表示されなくなりました。
todo.jsを編集する
「完了済みタスク表示/非表示切り替え」以降が編集箇所です。
showStateには、done_tableのdisplayプロパティの値を格納しています。
displayプロパティがnoneの場合は、表示させるための処理を
displayプロパティがnone以外の場合は、非表示にするための処理を書きます
cssを変更して、ボタンの向きと位置も変更しています。todo.js$(function(){ //完了済みの個数取得・表示 let doneCount = $("#donetodes").children("tr").length; $("#done_count").text(doneCount); //更新処理 $('.todo input').change(function(){ const todo = $(this).parents('.todo'); const id = todo.find('input[name="id"]'); const title = todo.find('input[name="title"]'); const timeLimit = todo.find('input[name="time_limit"]'); const isDone = todo.find('input[name="done_flg"]').prop("checked"); let doneFlg; if(isDone == true) { doneFlg = 1; }else{ doneFlg = 0; } const params = { id : id.val(), title : title.val(), time_limit : timeLimit.val(), done_flg : doneFlg } $.post("/update",params); //完了ボタンを押した際の処理 doneCount = $("#done_count").text(); if($(this).prop('name') == "done_flg"){ if(isDone == true){ $(todo).appendTo("#donetodes"); todo.find('input[name="title"]').css('text-decoration','line-through') todo.find('input[name="time_limit"]').hide(); doneCount ++; }else{ $(todo).appendTo("#todes"); todo.find('input[name="title"]').css('text-decoration','none') todo.find('input[name="time_limit"]').show() doneCount --; } $("#done_count").text(doneCount); } }) //完了済みタスク表示/非表示切り替え $('.button_for_show').click(function(){ let showState = $('#done_table').css('display'); if(showState == "none") { $('#done_table').show(); $(this).css({ transform: ' rotate(225deg)','bottom':'-4px' }); }else{ $('#done_table').hide(); $(this).css({ transform: ' rotate(45deg)','bottom':'4px' }); } }) })これで、完了済みタスクの表示/非表示の切り替えができるようになりました。
追加機能の表示と追加処理の改修
index.htmlを編集する
追加機能のフォームには、BootStrapのモーダルを使用します。モーダルは以下のようにすることで作ることができます。
<button data-toggle="modal" data-target="#modal"> モーダルを開くボタン </button> <div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="basicModal" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h3 class="modal-title" id="modalLabelId">タイトル</h3> </div> <div class="modal-body"> <!--ここにbody要素を入れる--> </div> <div class="modal-footer"> <!--ここにfooter要素を入れる--> </div> </div> </div> </div>参考:https://www.fenet.jp/dotnet/column/language/6549/
modal-headerには、タイトルなどのヘッダー要素、
modal-bodyには、内容となるボディ要素、
modal-footerには、ボタンなどのフッター要素、を入れます。今回は、ボディにフォームと追加ボタンを作ります。
タイトルとフッダーは無しにします。
add_formの値をjQueryで取得して、Controller側に送ります。index.html<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <script src="webjars/jquery/3.5.1/jquery.min.js"></script> <link rel="stylesheet" href="webjars/bootstrap/4.6.0/css/bootstrap.min.css" /> <script src="webjars/bootstrap/4.6.0/js/bootstrap.min.js"></script> <link rel="stylesheet" th:href="@{/css/style.css}" /> <title>TodoApp</title> </head> <body> <h1>TodoList</h1> <h3>マイタスク</h3> <table class="table table-borderless"> <thead> </thead> <tbody id="todes"> <tr class="todo" th:each="todo : ${todos}"> <td style="width:2rem"> <input type="checkbox"name="done_flg"/> <input type="hidden" name="id" th:value="${todo.id}" /> </td> <td style="width:22rem"><input type="text" style="border:none;width:22rem" name="title" th:value="${todo.title}"/></td> <td><input type="date" style="border:none" name="time_limit" th:value="${todo.time_limit}" /></td> </tr> </tbody> </table> <div style="font-size:20px"> <span id="done_count"></span> <span class="pr-3">件完了</span> <span class="button_for_show " style="display:inline-block"></span> </div> <table class="table table-borderless" style="display:none"id="done_table"> <thead> </thead> <tbody id="donetodes"> <tr class="todo" th:each="todo : ${doneTodos}"> <td style="width:2rem"> <input type="checkbox"name="done_flg" checked/> <input type="hidden" name="id" th:value="${todo.id}" /> </td> <td style="width:22rem"><input type="text" style="border:none;width:22rem;text-decoration:line-through" name="title" th:value="${todo.title}"/></td> <td><input type="date"style="border:none;display:none" name="time_limit" th:value="${todo.time_limit}" /></td> </tr> </tbody> </table> <!--↓編集箇所 --> <button type="button" class="btn btn-light rounded-circle p-0 text-muted font-weight-bold" data-toggle="modal"data-target="#modal" style="width:2.5rem;height:2.5rem;">+</button> <div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="basicModal" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-body"> <form id="add_form"> <div class="form-group"> <input type="text" name="title" class="form-control"placeholder="新しいタスク"/> </div> <div class="form-group"> <input type="date" name="time_limit"class="form-control" style="width:60%;" /> </div> <button type="button" class="btn btn-primary float-right " data-dismiss="modal" id="add">追加</button> </form> </div> </div> </div> </div> <!--↑編集箇所 --> <form method="post" th:action="@{/delete}"> <input type="submit" value="完了済みを削除" /> </form> <script type="text/javascript" th:src="@{/js/todo.js}"></script> </body> </html>+ボタンを押すと、こんな感じになります。
TodoController.javaを編集する
非同期処理にあわせてControllerを編集していきます。
引数としてTodoのタイトルと期間をわたし、データベースに登録
戻り値として、引数のデータに対応するidを加えたデータを返します。idはデータベース登録時に自動で値が登録されるものなので、その値を取得する必要があります。
TodoController.java@RequestMapping(value="/add") @ResponseBody public Todo add(Todo todo) { todoMapper.add(todo); return todo; }todoMapper.addを実行するとtodoにidが自動で追加されるように、
TodoMapper.xmlを編集していきます。TodoMapper.xmlを編集する
insert時に自動採番されたidを取得できるようにしていきます。
この時、idは戻り値ではなく、引数に渡したオブジェクトにマッピングされます。つまり、戻り値などを受け取りセットする必要もなく、自動でtodoに追加されます。
(戻り値は更新件数になります。ここでは1行insertなので1)TodoMapper.xml<insert id="add" useGeneratedKeys="true" keyProperty="id" parameterType="com.todo.app.entity.Todo"> insert into todo_items (title,time_limit) values (#{title},to_date(#{time_limit},'yy-mm-dd')) </insert>addメソッドの実装をしている、このinsert文に「useGeneratedKeys="true" keyProperty="id"」を設定します。
これで、自動採番された値をidに入れることができるようになりました。(keyPropertyの値はフィールド名)todo.jsを編集する
追加ボタンを押してから、Controllerとデータのやり取りをし、表示するまでを実装していきます。
todo.js$(function(){ //完了済みの個数取得・表示 let doneCount = $('#donetodes').children("tr").length; $('#done_count').text(doneCount); //更新処理 $('.todo input').change(function(){ const todo = $(this).parents('.todo'); const id = todo.find('input[name="id"]'); const title = todo.find('input[name="title"]'); const timeLimit = todo.find('input[name="time_limit"]'); const isDone = todo.find('input[name="done_flg"]').prop("checked"); let doneFlg; if(isDone == true) { doneFlg = 1; }else{ doneFlg = 0; } const params = { id : id.val(), title : title.val(), time_limit : timeLimit.val(), done_flg : doneFlg } $.post("/update",params); //完了ボタンを押した際の処理 doneCount = $('#done_count').text(); if($(this).prop('name') == "done_flg"){ if(isDone == true){ $(todo).appendTo('#donetodes'); todo.find('input[name="title"]').css('text-decoration','line-through') todo.find('input[name="time_limit"]').hide(); doneCount ++; }else{ $(todo).appendTo('#todes'); todo.find('input[name="title"]').css('text-decoration','none') todo.find('input[name="time_limit"]').show() doneCount --; } $("#done_count").text(doneCount); } }) //完了済みタスク表示/非表示切り替え $('.button_for_show').click(function(){ let showState = $('#done_table').css('display'); if(showState == "none") { $('#done_table').show(); $(this).css({ transform: ' rotate(225deg)','bottom':'-4px' }); }else{ $('#done_table').hide(); $(this).css({ transform: ' rotate(45deg)','bottom':'4px' }); } }) //追加処理 $('#add').click(function() { const params = $('#add_form').serializeArray(); $.post("/add",params).done(function(json){ const clone = $('#todes tr:first').clone(true); clone.find('input[name="id"]').val(json.id); clone.find('input[name="title"]').val(json.title); clone.find('input[name="time_limit"]').val(json.time_limit); $('#todes').append(clone[0]); }) }) })追加処理でやっていることは以下です。
- serializeArray()でformの値を取得
- 取得した値をControllerに送る
- Controllerから値を取得
- タスク一覧の1つ目の要素をコピー
- コピーした要素の値をコントローラーから取得した値に変更
- タスク一覧の最後に追加
これで、非同期にデータを追加することができるようになりました。
削除機能の表示&削除処理の改修
最後に削除ボタンも画面遷移なしで実行できるようにしていきます。
index.htmlを編集する
「完了済みを削除」をボタンにするだけです。
ほとんど変更はありません。index.html<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <script src="webjars/jquery/3.5.1/jquery.min.js"></script> <link rel="stylesheet" href="webjars/bootstrap/4.6.0/css/bootstrap.min.css" /> <script src="webjars/bootstrap/4.6.0/js/bootstrap.min.js"></script> <link rel="stylesheet" th:href="@{/css/style.css}" /> <title>TodoApp</title> </head> <body> <h1>TodoList</h1> <h3>マイタスク</h3> <table class="table table-borderless"> <thead> </thead> <tbody id="todes"> <tr class="todo" th:each="todo : ${todos}"> <td style="width:2rem"> <input type="checkbox"name="done_flg"/> <input type="hidden" name="id" th:value="${todo.id}" /> </td> <td style="width:22rem"><input type="text" style="border:none;width:22rem" name="title" th:value="${todo.title}"/></td> <td><input type="date" style="border:none" name="time_limit" th:value="${todo.time_limit}" /></td> </tr> </tbody> </table> <div style="font-size:20px"> <span id="done_count"></span> <span class="pr-3">件完了</span> <span class="button_for_show " style="display:inline-block"></span> </div> <table class="table table-borderless" style="display:none"id="done_table"> <thead> </thead> <tbody id="donetodes"> <tr class="todo" th:each="todo : ${doneTodos}"> <td style="width:2rem"> <input type="checkbox"name="done_flg" checked/> <input type="hidden" name="id" th:value="${todo.id}" /> </td> <td style="width:22rem"><input type="text" style="border:none;width:22rem;text-decoration:line-through" name="title" th:value="${todo.title}"/></td> <td><input type="date"style="border:none;display:none" name="time_limit" th:value="${todo.time_limit}" /></td> </tr> </tbody> </table> <button type="button" class="btn btn-light rounded-circle p-0 text-muted font-weight-bold" data-toggle="modal"data-target="#modal" style="width:2.5rem;height:2.5rem;">+</button> <div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="basicModal" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-body"> <form id="add_form"> <div class="form-group"> <input type="text" name="title" class="form-control"placeholder="新しいタスク"/> </div> <div class="form-group"> <input type="date" name="time_limit"class="form-control" style="width:60%;" /> </div> <button type="button" class="btn btn-primary float-right" data-dismiss="modal" id="add">追加</button> </form> </div> </div> </div> </div> <!--↓編集箇所 --> <button type="button" class="btn btn-outline-secondary" id="delete">完了済みを削除</button> <!--↑編集箇所 --> <script type="text/javascript" th:src="@{/js/todo.js}"></script> </body> </html>TodoController.javaを編集する
TodoController.java@RequestMapping(value="/delete") @ResponseBody public void delete() { todoMapper.delete(); }todo.jsを編集する
行う事は以下です。
- Controllerのdeleteメソッドの実行
- 完了済みタスクの表示削除
- 完了件数のリセット
todo.js$(function(){ //完了済みの個数取得・表示 let doneCount = $('#donetodes').children("tr").length; $('#done_count').text(doneCount); //更新処理 $('.todo input').change(function(){ const todo = $(this).parents('.todo'); const id = todo.find('input[name="id"]'); const title = todo.find('input[name="title"]'); const timeLimit = todo.find('input[name="time_limit"]'); const isDone = todo.find('input[name="done_flg"]').prop("checked"); let doneFlg; if(isDone == true) { doneFlg = 1; }else{ doneFlg = 0; } const params = { id : id.val(), title : title.val(), time_limit : timeLimit.val(), done_flg : doneFlg } $.post("/update",params); //完了ボタンを押した際の処理 doneCount = $('#done_count').text(); if($(this).prop('name') == "done_flg"){ if(isDone == true){ $(todo).appendTo('#donetodes'); todo.find('input[name="title"]').css('text-decoration','line-through') todo.find('input[name="time_limit"]').hide(); doneCount ++; }else{ $(todo).appendTo('#todes'); todo.find('input[name="title"]').css('text-decoration','none') todo.find('input[name="time_limit"]').show() doneCount --; } $("#done_count").text(doneCount); } }) //完了済みタスク表示/非表示切り替え $('.button_for_show').click(function(){ let showState = $('#done_table').css('display'); if(showState == "none") { $('#done_table').show(); $(this).css({ transform: ' rotate(225deg)','bottom':'-4px' }); }else{ $('#done_table').hide(); $(this).css({ transform: ' rotate(45deg)','bottom':'4px' }); } }) //追加処理 $('#add').click(function() { const params = $('#add_form').serializeArray(); $.post("/add",params).done(function(json){ const clone = $('#todes tr:first').clone(true); clone.find('input[name="id"]').val(json.id); clone.find('input[name="title"]').val(json.title); clone.find('input[name="time_limit"]').val(json.time_limit); $('#todes').append(clone[0]); }) }) //削除処理 $('#delete').click(function(){ $.post("/delete").done(function(){ $('#donetodes').empty(); $('#done_count').text(0); }) }) })これで削除もできました。
まとめ
画面遷移なしで、SpringBootとフロント側でデータの送受信を行えるように実装していきました。
それぞれの要素がすべて左寄席1列で並んでいるのでカスタマイズしたり、
例外処理やバリデーションチェック等の機能も実装してみたり、
色々と遊んでみてください。1から作るよりも、何かを改修していく方がハードルが低いと思っています。
「SpringBootの勉強を始めた!」という人の役に立てればいいなと思ってます。
思ったより長くなってしまいました。
慣れないながらも書くの頑張ったので、LGTMもよかったら押してください。
めっちゃ喜びます。↓ソースファイルです
https://github.com/tokio-k/TodoApp-springboot参考文献
https://www.fenet.jp/dotnet/column/language/6549/
https://qiita.com/fukasawah/items/eb0f7f067f8b347cbb2a
https://getbootstrap.jp/docs/4.2/getting-started/introduction/
https://www.webjars.org/
- 投稿日:2021-02-21T22:24:51+09:00
java オブジェクト指向についての備忘録
- 投稿日:2021-02-21T21:04:42+09:00
【初学者】Java入門 Lesson.1
はじめに
スクールではRubyとRailsを学習しましたが、今後は新たな職場で新たな言語を使用しなければならないので、汎用性の高いJavaを学習していこうと思います。
Javaとは
世界中にたくさんの開発者がいる有名な言語。
大規模システム、Webアプリケーション、スマートフォンアプリなど、様々な場所で活躍している。
※JavaとJavaScriptは、メロンとメロンパンくらい無関係。
プログラミング言語求人ランキング
1位 Java (31.1%)
2位 PHP (14.96%)
3位 Ruby (8.24%)
※レバテック プログラミング言語別求人案件ランキング(2019年)Javaの特徴
● コンパイラ言語 ● オブジェクト指向言語 ● OSを選ばない ● JVM言語【コンパイラ言語】
◉「機械語に一括して変換してから実行する」プログラミング言語
◉処理が高速である。【オブジェクト指向言語】
◉オブジェクト指向開発★に適した言語
(★データと処理をワンセットとして組み立てていく開発手法)【OSを選ばない】
◉どのプラットフォームでも可動である
◉理由 → 「JVM」上で動くから
◉「JVM」…Java Virtual Machine (Java仮想マシンのこと)【JVM言語】
◉JVM上で動作する言語のこと
◉「Scala」「Kotlin」「Java」があるが、それぞれに相互運用生がある。Javaプログラムは、JVMがPCに読み取れる機械語へ変換してから実行される。
そのため、どのプラットフォームでも動く。環境構築
【1】 JDK(Java Development Kit)のインストール = プログラムを実行するときに必要な機能がパッケージされたもの1)Gogle検索でJDKと入力
2)「Java DE ダウンロード - Oracle」をクリック
3)インストールする
【2】 ターミナルを起動してJavaのバージョンを確認する。 % java -version【3】 「VScode」 に 「Java Exstension Pack」 をインストールする Java Exstension Pack = Javaの開発に必要な環境をインストールしてくれる実行
【1】 Javaファイルの作成1)デスクトップに「MyJava」というフォルダを作る
2)VScodeでフォルダを開く
3)新規でファイルを作成する。ファイルの拡張子を「◯◯◯.java」でOK【2】 記述【クラス】
Greeting.javaclass Greeting { }◉ファイル名とクラス名は同じにする必要がある
◉クラス名は頭文字を大文字で記述
◉クラス名のあとは波括弧、波括弧内のことは「ブロック」という
◉ブロックに「処理」を記述していく【メソッド】※プログラミングでは「処理」のことを指す。
Greeting.javaclass Greeting { public static void main(String args[]){ } }◉ブロック(波括弧内)に「処理 = メソッド」を記述していく
【コンパイル方法と実行】
Greeting.javaclass Greeting { public static void main(String args[]){ System.out.println("Good morning"); System.out.println("Good afternoon"); System.out.println("Good evening"); } }◉println(プリントライン)
◉処理の終わりにはセミコロン「;」を記述するのがJavaのルール
ターミナルでファイルを実行する
◉コンパイル = JavaのプログラムをPCが読み取れる機械語に変換すること% javac ファイル名.javaすると、「ファイル名.class」が作成される。
続いて、ファイルを実行する% java ファイル名
- 投稿日:2021-02-21T15:40:54+09:00
Javaで標準入力を使った簡単なプログラムを作成
はじめに
Javaの勉強のために、ネット上の記事を参考にして簡単なプログラムを作りました。
備忘録として残りしたいと思います。キーボードからの入力させるプログラム
私は、「Scannerクラス」を用いて入力された数値を取得しました。
構文解析をするためのメソッドです。import java.util.Scanner; #↑Scannerクラスが使用できるようになります。整数を+してくれるプログラムを作ります。
import java.util.Scanner; class main { public static void main(String args[]){ Scanner scan = new Scanner(System.in); //Scannerクラスを初期化 System.out.println("1つ目の数値を入力してください"); int num1 = scan.nextInt(); //入力を受け取る System.out.println("2つ目の数値を入力してください"); int num2 = scan.nextInt(); //入力を受け取る int sum = num1 + num2; System.out.println(num1 + "+" + num2 + "+" + sum); scan.close(); //処理を終わらせる } }小数点を含む数値を+してくれるプログラムをつくります。
データ型を「int」から「double」に変えます。
これで小数点の計算をしてくれます。import java.util.Scanner; class main { public static void main(String args[]){ Scanner scan = new Scanner(System.in); //Scannerクラスを初期化 System.out.println("1つ目の数値を入力してください"); double num1 = scan.nextDouble(); //入力を受け取る System.out.println("2つ目の数値を入力してください"); double num2 = scan.nextDouble(); //入力を受け取る double sum = num1 + num2; System.out.println(num1 + "+" + num2 + "+" + sum); scan.close(); } }さいごに
オープン系のシムテム開発にも携わりたいのでJavaもどんどん勉強していきます!
ここまで読んでいただきありがとうございました!
- 投稿日:2021-02-21T15:37:08+09:00
SpringBootを利用してCLIアプリケーションの起動ができるまで
概要
Springを利用したWEBアプリケーションの開発をしていましたが、Spring Webを利用しないバッチアプリの開発をする事になり、バッチアプリでのDIコンテナの扱い方がなかなか見つからなかったので覚え書きとして今回記事を書くことにしました。
この記事のゴール
Spring Initializrを利用して新規作成したプロジェクトに、以下のような機能を持つコマンドラインアプリケーションを実装する。
- 実行時に引数を受け取り、指定の機能を実行することができる。
- CLIアプリケーションでSpringのBeanを利用する。
環境
- Java 11
- Spring Boot 2.4.3
- Pleiades All in One Eclipse 2020-12
依存関係の一部
Mavenプロジェクトの場合、pom.xmlの記述は
pom.xml<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>Gradleプロジェクトの場合、build.gradleの記述は
build.gradledependencies { implementation 'org.springframework.boot:spring-boot-starter' testImplementation 'org.springframework.boot:spring-boot-starter-test' } test { useJUnitPlatform() }どちらもSpring Initializrを利用して作成したものになります。
以降はこのプロジェクトをEclipseにインポートして作成していきます。今回作成をするものについて
作成するアプリは、起動時の引数に名前を受け取り、受け取った名前に対してコンソール上で挨拶をするものとなります。
引数を受け取らなかった場合は、「Hello World!」と出すようにします。
動作させる上で、起動時に出力されるSpring Bootのバナーが邪魔になってしまうので、application.propertiesでバナーを表示させないようにします。
また、今回コンソールへの出力はLogbackを利用しています。
各設定は以下のようになります。application.propertiesspring.main.banner-mode = offlogback.xml<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE logback> <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%msg%n</pattern> </encoder> </appender> <logger name="console-logger"> <appender-ref ref="STDOUT" /> </logger> </configuration>ファイル階層
CliApplication
├ src/main/java
│ └ com.example
│ ├ controller
│ │ └ CliController.java
│ ├ service
│ │ ├ CliService.java
│ │ └ CliServiceImpl.java
│ └ CliApplication.java
└ src/main/resources
├ application.properties
└ logback.xml実装
長い前置きとなってしまいましたが、まずメインメソッドのあるクラスは以下のようになります。
CliApplication.javaimport org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; import com.example.controller.CliController; @SpringBootApplication public class CliApplication { private final static Logger logger = LoggerFactory.getLogger("console-logger"); public static void main(String[] args) { logger.info("処理開始!"); try (ConfigurableApplicationContext ctx = SpringApplication.run(CliApplication.class, args)) { CliController controller = ctx.getBean(CliController.class); controller.process(args); } catch(Exception e) { e.printStackTrace(); } logger.info("処理終了!"); } }とても重要なのが
ConfigurableApplicationContext ctx = SpringApplication.run(CliApplication.class, args)とした部分で、ここでSpringのDIコンテナを作成しています。
Webアプリケーションの場合は、サーバーを起動した際にDIコンテナにBeanが作成されて、それをいつでも利用できるように待受状態になります。
ところが今回のように、起動をしてから待受状態とならない場合は、呼び出そうとしてもNullPointerExceptionとなってしまうので、自分でDIコンテナを使う準備をする必要があります。
これでCliApplication.javaのあるパッケージ下で定義されたBeanを使用することが出来るようになり、ctx.getBean(CliController.class)とすることでコントローラーを呼び出しています。
try-with-resource文で書かれているのは、メインクラスが実行されたらDIコンテナを使う準備をして、処理が終わったあとに閉じて欲しいのが理由です。次に各コントローラークラスと、サービスクラスは以下のようになります。
CliController.javaimport org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import com.example.service.CliService; @Controller public class CliController { @Autowired private CliService service; public void process(String[] args) { if (args.length == 0) { service.greeting(); } else { service.greeting(args[0]); } } }CliService.javapublic interface CliService { public void greeting(); public void greeting(String arg); }CliServiceImpl.javaimport org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @Service public class CliServiceImpl implements CliService { private final static Logger logger = LoggerFactory.getLogger("console-logger"); public void greeting() { logger.info("Hello! World!"); } public void greeting(String arg) { logger.info(String.format("Hello! %s!", arg)); } }コントローラークラスで、メインクラスと同じように
ConfigurableApplicationContext ctx = SpringApplication.run(CliController.class, args)と呼び出さない理由は、この記述ではDIコンテナを作成することになります。既に上のパッケージで作成済みのDIコンテナがあるのに、新たに重複してDIコンテナを作成してしまうことになります。
メインクラスでDIコンテナを利用する準備をして、そのスコープ内で呼び出されている間はいわゆる待受状態になっています。よって、ここでは@AutowiredでBeanを利用することが出来るという事のようです。動作確認
上記プロジェクトを、引数なしで起動した場合の出力は以下のようになります。
処理開始! Hello! World! 処理終了!また、引数に「Nobunaga」と渡した場合の出力は以下のようになります。
処理開始! Hello! Nobunaga! 処理終了!とても単純な処理になりますので、ここに関しては特に説明することがありません。
今回は起動オプションに関する処理を行っていませんので、Eclipseで実行をする際は実行の構成でANSIコンソール出力のチェックボックスを外さないと、Hello! --spring.output.ansi.enabled=always!となってしまうので注意してください。
- 投稿日:2021-02-21T12:35:30+09:00
JavaScriptでHTMLを操る最低限の知識
エンジニア経験約1年半のTKです。
今までは業務でJavaばっかり書いていてフロント知識を捨てていました。最近、初めてフロントの開発せざるを得ない展開になってしまいました。
嫌だなぁと思いましたが、これを機にちゃんと勉強しようと思いました。今回はJavaScriptを実装した時に思った事を素直に書きつつ、どう対応したかを今後の自分の為に書き記します。
今までサーバーサイドの開発ばっかりやっていてフロントは嫌い!って人向けです。ループ文やif文などの構文についてではありません。(そこはサーバー開発言語の知識があればググってすぐに理解できます)
HTMLとの関係について
僕がJavaScriptを書く際に最初にめんどくさいと思ったのはここです。
全くわからない、どうやってHTMLを操作するの?と純粋に思いました。例えば
そうですね、画面表示する時に処理したいって時。
シンプル.HTML<!DOCTYPE html> <html lang="en"> <script type="text/javascript" src="test.js"></script> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <!--サーバーから渡ってきた日付--> <div id="ymd">21000909</div> </body> </html>実際の業務ではこんな画面あり得ないですが、ご了承ください。
サーバーから日付が8桁で渡ってきて表示する時はスラッシュを補完したい!という要件があるとします。
フロント嫌いの僕からしたらどうやるねん、、、って話です。
JavaScript内で日付文字列にスラッシュを補完するという処理はすぐに思いつきます。が、フロント開発初心者の僕はまずこの値どうやって受け取るんだという疑問から始まります。
しかも、画面表示時に値を受け取るというタイミングもおまけでついてきます。きついですね、、実際にこう書けば仕様通りに動きます。
test.js// ページ読み込み完了後に実行 window.onload = function () { // 対象の要素を取得する const target = document.getElementById("ymd"); // スラッシュを補完し、要素のtextに設定する target.textContent = target.textContent.substr(0, 4) + "/" + target.textContent.substr(4, 2) + "/" + target.textContent.substr(6, 2); }※値の存在チェック等はここでは割愛しております。
やることを箇条書きすると、
・画面表示の際にJavaScriptを呼ぶ
・HTMLのtextを取得し、スラッシュを補完
・HTMLのtextに編集後の値を設定する書いてみるとかなり簡単ですが、僕の勝手な妄想ですがフロント嫌いはここまで辿り着くのに結構時間を要します。
イベント発動→JavaScript実行
JavaScriptの実行タイミングはこれだけと言っても過言ではありません!
この流れがわかってしまえば応用でなんでも出来そうな気がしてきました。
イベントなどは別途また投稿します。(ここでのイベントはざっくり言うと画面表示です。)そういえば、window.onloadは使うべきではないという噂も聞いたことがあります。
同ページで複数のwindow.onloadがあると処理が上書きされてしまう事があるみたいです。updateTest.js// ページ読み込み完了後に実行 window.addEventListener('load', function() { // 対象の要素を取得する const target = document.getElementById("ymd"); // スラッシュを補完し、要素のtextに設定する target.textContent = target.textContent.substr(0, 4) + "/" + target.textContent.substr(4, 2) + "/" + target.textContent.substr(6, 2); }こっちのaddEventListenerを使用すればイベントを追加する為、上書きされないです!
target.textContentも変数に入れるべきだったかな。。。まあいいか。HTMLを操作するための最低限
上記の例のtest.jsに習って記載します。
// 対象の要素を取得する
const target = document.getElementById("ymd");これは、HTMLのid属性が"ymd"の要素を取得し、targetという変数に格納しています。
documentとは何でしょうか?
console.log(document)で表示してみましょう。なるほど!HTMLが表示されています。
つまり、document.何かしらで実際に要素が取得出来る事がわかりました。これがわかれば大体ググれば何とかなります!
よく使うものは下記です。
指定するもの 関数名 例 id getElementById document.getElementById("id名") class getElementsByClassName document.getElementsByClassName("class名") id、class querySelector document.querySelector("idまたはclass名") 次は実際にHTMLに値を設定するところです。
// スラッシュを補完し、要素のtextに設定する
target.textContent = target.textContent.substr(0, 4) + "/" + target.textContent.substr(4, 2) + "/" + target.textContent.substr(6, 2);targetには取得した要素が入っております。
target.関数名=設定する値で設定できます。
設定先の属性によって関数名を使い分ければ良いと言う事です。よく使うものは下記です。
属性 関数名 例 id id target.id=設定する値 class className target.className=設定する値 text textContent target.textContent=設定する値 value value target.value=設定する値 まとめ
とりあえず、JavaScriptが実行できれば後は値を取得する、設定するの知識で結構何とかなる気がします!
- 投稿日:2021-02-21T11:02:55+09:00
リクエストのたびに java.net.http.HttpClient を生成するとスレッドが増え続ける
JDK 11ベースの話です。
背景
WEBアプリケーションで外部のHTTP APIを呼び出す必要があり利用していた。
ただ、そのAPIはWEBサイトに存在していたフォームからのサブミットで呼び出しされるものだった。
APIとして呼び出ししやすいようにパラメーターによってレスポンスがJSONになる位の改修はされていたが、
APIを呼び出すためにはWEBサイトへのログイン(認証)が必要であり、
認証状態はCookieで管理されているというものだった。目的のAPI呼び出しまでに「ログイン」、「目的のAPI呼び出し」の最低2回のリクエストが必要であり、
セッションを破棄するために「ログアウト」もしなければいけない。このAPI呼び出しのためには
java.net.http.HttpClientを使用していたが、
HttpClientでCookieを管理するデフォルトで使用されるjava.net.CookieHandler実装の
java.net.CookieManagerはスレッドセーフ実装になっていない。より厳密には
CookieManagerがCookieデータを格納先に利用するCookieStoreも含めてスレッドセーフ実装ではない。WEBで利用されるCookieはステートなので、スレッドセーフにできないのは道理だと思えた。
API呼び出しのたびに
HttpClientを生成すればよいと考えていたが、
実際に「HttpClientのインスタンス生成」 => 「API呼び出し」を繰り返していくとスレッド数が増え続けていることに気が付いた。そして、そのスレッドはWEBアプリケーションをしばらく稼働していると減少していくことが分かった。
HttpClientが使用するスレッド
HttpClientを生成してAPI呼び出しを行うとスレッドが増えるのは分かった。
なぜスレッドが増えるのかを調査した。非同期タスクを実行する
java.util.concurrent.Executor
java.net.http.HttpClientを生成するときにjava.net.http.HttpClient.Builderを使用する。この時に
HttpClient.Builder#executor(Executor)で内部で実行される非同期タスクと依存タスクを実行するための
java.util.concurrent.Executorを指定することが出来る。ここで
Executorを指定しなかった場合、HttpClientはjava.util.concurrent.ThreadPoolExecutorを
HttpClientのインスタンスごとに生成して使用する。
ThreadPoolExecutorなので、タスクを実行してからもしばらくの間スレッドがプールに残り続けることになる。この仕様は知っていたので、アプリケーション内で
HttpClientには共有ThreadPoolExecutorを渡していたし、
スレッドダンプに出現するスレッド名もThreadPoolExecutorが生成したスレッド名と一致していなかった。今回のスレッド増加の原因は無かった。
デフォルト
Executorの不具合
HttpClientにExecutorを渡さなかった場合に、HttpClientは内部でThreadPoolExecutorを使用するが、
ThreadPoolExecutor#shutdown()が呼び出しされない不具合がある。不具合といっても
HttpClientから発生するタスクの実行にしか使用されないため、
ThreadPoolExecutor#shutdown()が呼び出しされなくても、いずれはスレッドはすべて終了する。問題になるとしたら非同期タスクの中から終了しないタスクが発生したりしたとき位だろうが、
HttpClientの操作から終了しないスレッドが発生したら疑いをかけてもいいかもしれない。この問題はJDK 17で修正されている: [JDK-8258582] HttpClient: the HttpClient doesn't explicitly shutdown its default executor when stopping. - Java Bug System
制御用スレッド
HttpClientの実装にあたるjdk.internal.net.http.HttpClientImplのソースを確認すると
SelectorManagerというクラスが存在する。このクラスは
java.lang.Threadを継承しており、スレッド名にはHttpClient-{ID}-SelectorManagerが割り当てられている。このスレッドが何をしているのか確認すると、
HttpClientからのHTTP接続やHTTPリクエストフローの制御を担う役割を実施しているようだった。そしてこのスレッド、HttpClientImplがガーベージコレクションで解放されるか、
HttpClientImplのタスクカウント(参照カウント)が0になるまでスレッドが終了しない。何故そんな仕組みになっているのかというと、HTTP 2.0のような通信はリクエストとレスポンスの関係が1対1ではないため、双方向の通信を同時に実行しつつ、何時終了するか分からない通信フローを制御し続けるために制御用スレッドとタスクカウントという方式を採ったようだった。
HttpClientImplが使用されなくなっていればガーベージコレクションで解放されるし、
HTTP 2.0のような通信が終了していれば、タスクカウントは0になりスレッドが停止するからという設計のようだ。このスレッドの存在は
HttpClientなどのjavadocに書かれていないし、外部から強制的に終了させるメソッドも公開されていないため、リフレクションを駆使して無理やり停止させることくらいしかできない。対処
問題が発生したWEBアプリケーションはメモリ割り当て量も少なく、
数分でガーベージコレクション発生するガーベージコレクションで制御用スレッドも終了するため様子見ということにした。
java.net.http.HttpRequestに対してCookieを直接していするメソッドが存在しないので、
レスポンスHTTPヘッダーをパースしてCookie取り出し、
リクエストHTTPヘッダーにCookie:を追加するという処理を自作する気にもならなかったというのが正直なところ。しかし、API呼び出しが多い環境やメモリ割り当てが大きくてガーベージコレクションがあまり発生しない環境では
java.net.http.HttpClientを使用しているとスレッド数が増え続ける事象が発生するかもしれない。使用方法によってはApache HttpClientを使用するべきかもしれないと考えさせられた。
- 投稿日:2021-02-21T11:02:55+09:00
リクエストのたびに java.net.http.HttpClient を生成すると制御用スレッドが増える
JDK 11ベースの話です。
背景
WEBアプリケーションで外部のHTTP APIを呼び出す必要があり利用していた。
ただ、そのAPIはWEBサイトに存在していたフォームからのサブミットで呼び出しされるものだった。
APIとして呼び出ししやすいようにパラメーターによってレスポンスがJSONになる位の改修はされていたが、
APIを呼び出すためにはWEBサイトへのログイン(認証)が必要であり、
認証状態はCookieで管理されているというものだった。目的のAPI呼び出しまでに「ログイン」、「目的のAPI呼び出し」の最低2回のリクエストが必要であり、
セッションを破棄するために「ログアウト」もしなければいけない。このAPI呼び出しのためには
java.net.http.HttpClientを使用していたが、
HttpClientでCookieを管理するデフォルトで使用されるjava.net.CookieHandler実装の
java.net.CookieManagerはスレッドセーフ実装になっていない。より厳密には
CookieManagerがCookieデータを格納先に利用するCookieStoreも含めてスレッドセーフ実装ではない。WEBで利用されるCookieはステートなので、スレッドセーフにできないのは道理だと思えた。
API呼び出しのたびに
HttpClientを生成すればよいと考えていたが、
実際に「HttpClientのインスタンス生成」 => 「API呼び出し」を繰り返していくとスレッド数が増え続けていることに気が付いた。そして、そのスレッドはWEBアプリケーションをしばらく稼働していると減少していくことが分かった。
HttpClientが使用するスレッド
HttpClientを生成してAPI呼び出しを行うとスレッドが増えるのは分かった。
なぜスレッドが増えるのかを調査した。非同期タスクを実行する
java.util.concurrent.Executor
java.net.http.HttpClientを生成するときにjava.net.http.HttpClient.Builderを使用する。この時に
HttpClient.Builder#executor(Executor)で内部で実行される非同期タスクと依存タスクを実行するための
java.util.concurrent.Executorを指定することが出来る。ここで
Executorを指定しなかった場合、HttpClientはjava.util.concurrent.ThreadPoolExecutorを
HttpClientのインスタンスごとに生成して使用する。
ThreadPoolExecutorなので、タスクを実行してからもしばらくの間スレッドがプールに残り続けることになる。この仕様は知っていたので、アプリケーション内で
HttpClientには共有ThreadPoolExecutorを渡していたし、
スレッドダンプに出現するスレッド名もThreadPoolExecutorが生成したスレッド名と一致していなかった。今回のスレッド増加の原因は無かった。
デフォルト
Executorの不具合
HttpClientにExecutorを渡さなかった場合に、HttpClientは内部でThreadPoolExecutorを使用するが、
ThreadPoolExecutor#shutdown()が呼び出しされない不具合がある。不具合といっても
HttpClientから発生するタスクの実行にしか使用されないため、
ThreadPoolExecutor#shutdown()が呼び出しされなくても、いずれはスレッドはすべて終了する。問題になるとしたら非同期タスクの中から終了しないタスクが発生したりしたとき位だろうが、
HttpClientの操作から終了しないスレッドが発生したら疑いをかけてもいいかもしれない。この問題はJDK 17で修正されている: [JDK-8258582] HttpClient: the HttpClient doesn't explicitly shutdown its default executor when stopping. - Java Bug System
制御用スレッド
HttpClientの実装にあたるjdk.internal.net.http.HttpClientImplのソースを確認すると
SelectorManagerというクラスが存在する。このクラスは
java.lang.Threadを継承しており、スレッド名にはHttpClient-{ID}-SelectorManagerが割り当てられている。このスレッドが何をしているのか確認すると、
HttpClientからのHTTP接続やHTTPリクエストフローの制御を担う役割を実施しているようだった。そしてこのスレッド、HttpClientImplがガーベージコレクションで解放されるか、
HttpClientImplのタスクカウント(参照カウント)が0になるまでスレッドが終了しない。何故そんな仕組みになっているのかというと、HTTP 2.0のような通信はリクエストとレスポンスの関係が1対1ではないため、双方向の通信を同時に実行しつつ、何時終了するか分からない通信フローを制御し続けるために制御用スレッドとタスクカウントという方式を採ったようだった。
HttpClientImplが使用されなくなっていればガーベージコレクションで解放されるし、
HTTP 2.0のような通信が終了していれば、タスクカウントは0になりスレッドが停止するからという設計のようだ。このスレッドの存在は
HttpClientなどのjavadocに書かれていないし、外部から強制的に終了させるメソッドも公開されていないため、リフレクションを駆使して無理やり停止させることくらいしかできない。対処
問題が発生したWEBアプリケーションはメモリ割り当て量も少なく、
数分でガーベージコレクション発生するガーベージコレクションで制御用スレッドも終了するため様子見ということにした。
java.net.http.HttpRequestに対してCookieを直接していするメソッドが存在しないので、
レスポンスHTTPヘッダーをパースしてCookie取り出し、
リクエストHTTPヘッダーにCookie:を追加するという処理を自作する気にもならなかったというのが正直なところ。しかし、API呼び出しが多い環境やメモリ割り当てが大きくてガーベージコレクションがあまり発生しない環境では
java.net.http.HttpClientを使用しているとスレッド数が増え続ける事象が発生するかもしれない。使用方法によってはApache HttpClientを使用するべきかもしれないと考えさせられた。
- 投稿日:2021-02-21T04:13:17+09:00
AndroidのEditTextで入力された文字がメールアドレスかどうか判定する(Kotlin)
はじめに
初投稿です。Android開発学習中の大学生です。学んだことをまとめて頭の中を整理するために書きます。なので僕と同じ初心者向けの内容です。
背景
Android開発で会員登録の機能を作っているときに、入力された文字列がメールアドレスかどうかの判定方法がわからず困りました。いろいろ調べたり知人のエンジニアの方に教わったことをまとめます。
解決方法
大きく分けて二つの方法があります。
1.正規表現でメールアドレスのパターンを作って、それと入力された文字列が合致するか調べる方法。
2.Androidにデフォルトで存在するPatternsクラスのメソッドを用いて調べる方法。あくまで僕の解釈ですが、1の方が本質的なやり方で2は飛び道具みたいな感じかなと思いました。ただ1の方が正規表現のことを理解していないとできないのでめんどくさめ。あと抜け漏れが発生しやすいかも。まあでも2のやり方も大元では1と同じことやってそう。
1の方法について
まず、正規表現の概念と定型パターンは
概念 : https://userweb.mnet.ne.jp/nakama/
定型パターン : https://qiita.com/grrrr/items/0b35b5c1c98eebfa5128
がわかりやすかったです。入力された文字列がメールアドレスならばtrueそれ以外はfalseを返すサンプルメソッド。
fun isEmailAddress(): Boolean { val mail = editText.text.toString() return mail.matches(Regex("[a-zA-Z0-9._-]+@[a-z]{2,}+\\.+[a-z]{2,}")) }※正規表現の部分は抜け漏れがあるかもしれないので参考程度に思っといてください。
純粋に入力された文字列が正規表現で設定した型とあってるかを見る方法です。
2の方法について
なんも考えず使える、ただただ便利な方法です。抜け漏れも起こりにくいと思います。起きたらAndroidのせいです。
公式のドキュメント : https://developer.android.com/reference/android/util/Patternsfun isEmailAddress(): Boolean { val mail = editText.text.toString() return Patterns.EMAIL_ADDRESS.matcher(mail).matches() }脳死で使いましょう。
結論
多分1の方法を理解したうえで、2の方法を使うっていうのがベストです。
追記
意外と記事書くのが楽しかった。あと内容が間違っている可能性があるので気を付けてください。あとこれはメールアドレスだけでなく、電話番号とかほかにも応用できます。
4:00に深夜テンションで書いたのでもう寝ます。
※コメントで、誤解を招く表記をご指摘頂いたので更新しました。コメント頂いた@sdkeiさんありがとうございます。
2/21 22:00に更新













