- 投稿日:2020-11-25T23:55:34+09:00
【Java】URLを入力するとそのページが使っているWordPressのテーマ名を表示するプログラムを作ってみた
はじめに
28歳業界未経験、プログラミング素の人です。
Qiita初投稿!!
現在Javaの研修を受けているので学んだことの備忘録として、
あと同じプログラミング初心者の方々のお役に少しでも立てればと思いQiitaに投稿することにしました。お見苦しい点多々あるかと思いますが、生暖かい目で見てやってください。
この記事について
webページ見てたら「このサイトwordpress使ってんのかな?」「何のテーマ使ってるんやろ?」って思うことが結構あるので、勉強がてら自分なりにJavaで書いてみました。
コマンド上で動く、ブラウザからURLをコピペして実行するだけで結果が表示される簡単な奴です。
方法を考える
HTMLソースコードの<head>内にwp-contentという文字列が含まれているかでWordPressを使っているかチェックします。
テーマ名はwp-content/themes/の後に書かれているので、ソースコード内の一番初めに出てくるwp-content/themes/の部分を取得すれば分かりそうです。html取得 → Listに代入 → for文で一行ずつチェック
という流れでやってみました。
サンプルコード
Sample.javaimport java.io.*; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.List; public class Sample { private List<String> contents; private final String CHARSET = "UTF-8"; public static void main(String[] args) { String inputUrl = ""; BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); Sample sample = new Sample(); try { inputUrl = reader.readLine(); sample.addContents(inputUrl); sample.checkWordpress(); } catch (IOException e) { System.out.println(e); } } // 指定したページがWordpressを使っているか確かめるメソッド public void checkWordpress() { boolean WpCheck = false; for (String content : getContents()) { // headタグ内に "wp-content/themes/" が含まれているかで判定 if (content.contains("wp-content/themes/") == true) { int idx = content.indexOf("wp-content/themes/"); String line = content.substring(idx + 18); // "wp-content/themes/"以降の文字列 idx = line.indexOf("/"); // テーマ名直後の "/" のインデックス番号 System.out.println("\nテーマ名 : " + line.substring(0, idx)); // テーマ名を出力 WpCheck = true; break; } } if (WpCheck == true) { System.out.println("Wordpressを使っています。\n"); } else { System.out.println("\nWordpressは使われていません。"); } } // 指定されたURLのソースコードを取得するメソッド public List<String> download(String url, String charset) throws Exception { URLConnection conn = new URL(url).openConnection(); InputStream is = conn.getInputStream(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, charset))) { ArrayList<String> lineList = new ArrayList<String>(); String line = null; while ((line = reader.readLine()) != null) { lineList.add(line); } return lineList; } } // ダウンロードしたソースコードをListに代入するメソッド public void addContents(String url) { try { this.contents = download(url, getCharset()); } catch (Exception e) { e.printStackTrace(); System.out.println("無効なURLです。"); System.exit(0); } } public List<String> getContents() { return this.contents; } public String getCharset() { return this.CHARSET; } }試しに「lightning」の公式ページのURLを指定しました。
テーマ名が取得できました。
解説
標準入力でURLを指定して、http通信で取得したhtmlソースコードをListに代入しています。
checkWordpressメソッドで、Listに代入したhtmlソースコードを一行ずつfor文でチェックします。
if文で"wp-content/themes/"がソースコードに含まれているか判定。
"wp-content/themes/"のインデックス番号を取得。
テーマ名を取得したいので、"wp-content/themes/"の文字数(18文字)を足したインデックス番号以降の文字列を取得。
テーマ名直後の "/" のインデックス番号を取得して、0番目から"/"までの文字列を抜き出せばテーマ名が分かります。
最後に
もっと効率の良い方法があると思いますが、今の自分にはこれが精一杯でした笑
そもそもブラウザでページのソースを表示したらすぐに分かるんですけど、勉強のためにやってみました。
自分が欲しい機能を作るのは、楽しいし達成感あるしめっちゃ調べるので勉強になります。
終わり
- 投稿日:2020-11-25T23:55:10+09:00
Google App Engine Java 8 から Java11への移行 実践編
はじめに
この記事は、Google App Engine Java 8 から Java11 移行対策 の続きです。
Java11からサーブレット非対応になったGoogle App Engine / Java (GAE/J)でJava8のサーブレットをGAE/J Java11 でも稼働可能なSpring Bootアプリへ置き換える具体的な方法を記述している。Java8 サーブレットから Java11 Spring Bootへ
今回はサンプルとして、GAE/J Java8のガイドに記載されているHello World サーブレットを Spring Boot化してGAE/J Java11にデプロイしてみることにする。
前提条件
この記事は、Java8版のGAE/JのソースをJava11版に書き換えることを主眼とするため、Java 11やCloud SDKのインストールは終了しているものとする。
さらにGAEへデプロイ可能なプロジェクトも作成済みとする。
また、WebフレームワークとしてSpring Bootを使用しているが、Spring FrameworkやSpring Bootの知識もあるものとする。前提とする環境
- Java 11 (OpenJDK 11)
- Apache Maven 3.6
- Google Cloud SDK 319
- Spring Boot 2.4
- Maven 3.6
GAE/J Java8のHello World取得
GAE/J Java8のサンプルは、下記のGitレポジトリーから取得できる
git clone https://github.com/GoogleCloudPlatform/appengine-try-java取得したファイルは以下のようになっている
一部省略|--pom.xml |--src | |--main | | |--java | | | |--myapp | | | | |--DemoServlet.java | | |--webapp | | | |--index.html | | | |--WEB-INF | | | | |--appengine-web.xml | | | | |--web.xmlweb.xmlの中身を見てみよう
web.xml<!DOCTYPE web-app PUBLIC "-//Oracle Corporation//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5"> <servlet> <servlet-name>demo</servlet-name> <servlet-class>myapp.DemoServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>demo</servlet-name> <url-pattern>/demo</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list> </web-app>DemoServletというサーブレットとindex.htmlというHTMLがあるだけということである。
DemoServlet.javapackage myapp; import java.io.IOException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class DemoServlet extends HttpServlet { @Override public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType("text/plain"); resp.getWriter().println("{ \"name\": \"World\" }"); } }index.html<!doctype html> <html> <head> <title>App Engine Demo</title> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> </head> <body> <div id="result">Loading...</div> <script> $(document).ready(function() { $.getJSON('/demo', function(data) { $('#result').html("Hello, " + data.name); }); }); </script> </body> </html>index.htmlがDemoServletを呼び出して
{"name":"World"}
というJSONを受け取り、Hello World
とDIVに表示している。
最後にappengine-web.xmlを確認する。appengine-web.xml<?xml version="1.0" encoding="utf-8"?> <appengine-web-app xmlns="http://appengine.google.com/ns/1.0"> <threadsafe>true</threadsafe> <runtime>java8</runtime> </appengine-web-app>ランタイムがJava8であるということが書かれている。
なお、GAE/JのJava11版のSpring Bootを使用したHello Worldは、下記Gitから取得できる。この中のappengine-java11/springboot-helloworld
が、ほぼほぼ完成形となるので、手っ取り早くスケルトンを確認したい方は、こちらを参照していただきたい。git clone https://github.com/GoogleCloudPlatform/java-docs-samplesGAE/J Java11 と Spring Bootの体裁を整える
まずは、Mavenのコンパイルが通る形にファイル構成の体裁を整えよう
pom.xml 修正
とりあえず、Mavenの依存関係やbuild用のpluginを GAE/JとSpring Bootに対応したものに書き換えてしまおう。
もちろん、本来は自分のアプリケーションに必要な依存関係を追加する必要があるので、適時追加していただきたい。pom.xml<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <version>2.0</version> <groupId>com.google.appengine.demos</groupId> <artifactId>appengine-try-java</artifactId> <properties> <maven.compiler.target>11</maven.compiler.target> <maven.compiler.source>11</maven.compiler.source> </properties> <dependencyManagement> <dependencies> <dependency> <!-- Import dependency management from Spring Boot --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.4.0</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR9</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <!-- Exclude the Tomcat dependency --> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.4.0</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>com.google.cloud.tools</groupId> <artifactId>appengine-maven-plugin</artifactId> <version>2.4.0</version> <configuration> <skip>true</skip> <version>2.0.0</version> </configuration> </plugin> </plugins> </build> </project>
<plugin>com.google.cloud.tools</plugin>
の中の<version>2.0.0</version>
の部分だが、これはGAEにデプロイされるバージョンを表す。このバージョンをJava8版と違う値にしておけば、旧バージョンのGAEに残ったままの状態となり、いざというときに切り戻しができるようになる。
ここで、とりあえずmvn compileしておけば、依存するJarファイルがダウンロードできるはずだ。mvn compileapp.yaml 作成
GAE Java11では、appengine-web.xml は廃止になり、設定はyamlファイルに一本化されたので、app.yamlファイルを作成する。
ファイルの保存場所は以下の通り。
src/main/appengine/app.yaml
必要最小限のapp.yamlファイルの記述は下記の通り。とりあえず、Runtimeが java11であることが書かれていれば後はデフォルト値で稼働する。app.yamlruntime: java11app.yamlの全設定は app.yaml 構成ファイルを参照。
エンドポイント作成
最後にSpring Bootのエンドポイントとなるクラスを作成する。
DemoServlet.javaと同じパッケージに新しいクラスDemoAppを作成しよう。DemoApp.javapackage myapp; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication @RestController public class DemoApp { public static void main(String[] args) { SpringApplication.run(DemoApp.class, args); } }スケルトン完成
とりあえず中身は全くないが、これで体裁は整った。
DemoServlet.javaとWEB-INF以下のファイルは不要なので消してしまおう。
結果的に下記のようなフォルダ構成になるはずだ。
なお、webapp以下のファイルはMavenでビルドすれば、Spring Bootでも静的ファイルとして扱われるのでそのまま放置しておいてよい。|--pom.xml |--src | |--main | | |--appengine | | | |--app.yaml | | |--java | | | |--myapp | | | | |--DemoApp.java | | |--webapp | | | |--index.htmlこれでSpring BootのアプリケーションとしてMavenのコンパイルも通るはずだ。
mvn install
Servlet移行
ここからが本番。いよいよ、サーブレットをSpring Boot対応のクラスに書き換えよう。
DemoServlet 書き換え
DemoAppクラスに下記のようなメソッドを書き加えれば、/demoというGETリクエストに対して、
{"name":"World"}
というJSON文字列を返すエンドポイントを作成することができる。
一目瞭然だが、@GetMapping アノテーションがメソッドがGetリクエストのエンドポイントであることを表しており、引数の"/demo"の部分がURLのマッピングである。
これで完成と言えば完成だ。DemoApp.java(一部)@GetMapping("/demo") public String hello() { return "{ \"name\": \"World\" }"; }ただし、もっとSpring Bootっぽくするならば、戻り値にBeanにすべきである。Beanを戻せば、フレームワークで自動的にJSON文字列のレスポンスを生成してくれる。
これらを踏まえた最終的なDemoApp.javaの全ソースは下記のようになる。DemoApp.javapackage myapp; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication @RestController public class DemoApp { public static void main(String[] args) { SpringApplication.run(DemoApp.class, args); } @GetMapping("/demo") public Hello hello() { Hello hello = new Hello(); hello.name = "World"; return hello; } public class Hello { public String name; } }テストサーバ起動
テストを行うためには、下記のコマンドを実行すれば良い。
ブラウザで http://localhost:8080 を開けば、Hello, Worldの文字が表示されるはずである。mvn spring-boot:runデプロイ
最後にデプロイを行おう。この部分はJava8版と変わりない。
デプロイを行うGclooudのMavenのゴールもプラグインによって読み込まれているので、下記コマンドを実行すればGAEサーバーにデプロイされるはずだ。mvn appengine:deploy最後に
最後のMavenコマンドで、GAEにデプロイできたはずである。
最後により実践的な参考情報を列挙する。サーブレットマッピング
今回はサーブレットをSpring Boot形式に全面的に書き換えるたが、サーブレットクラスには手を加えず、Spring Bootから直接呼び出す方法もある。この方法はSpring Boot Servletマッピング などの記事を参考にしてほしい。
特にJava8の段階で、すでにWeb.xmlではなく@WebServletアノテーションでマッピングをしているのであれば、エンドポイントのクラスに@ServletComponentScanを付加するだけで移行は完了する。リクエストパラメータ/ボディ
今回のサンプルはリクエストパラメータやボディ情報の必要ないサーブレットだったが、実際には必ずパラメータなどの受け取りが必要になるはずだ。
Spring Boot (厳密にはSpring MVCの機能となるが)で、リクエストパラメータやボディを受け取る方法は、Spring MVC コントローラの引数 といった記事が参考になるだろう。
- 投稿日:2020-11-25T23:35:07+09:00
SpringBootことはじめ 2.gradleで実行可能なwarファイルを作る
はじめに
前回、SpringMVCでHelloWorldのREST APIを作成したので
これを実行可能なwarにアーカイブし、実際に実行してみるところまでやってみる。環境
ソフトウェア バージョン OS Windows10 Pro Java JDK12 Eclipse 2020-12 M1 SpringBoot 2.4.0 Spring Tool Suite (STS) 4.8.1 実施
0. 前回作成したHelloWorld REST API アプリ
HelloController.java@RestController public class HelloController { @RequestMapping("/") public String home() { return "Hello World!"; } @RequestMapping("/sb") public String helloSp() { return "Hello SpringBoot!"; } }HelloSpringApplication.javapackage com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class HelloSpringApplication { public static void main(String[] args) { SpringApplication.run(HelloSpringApplication.class, args); } }warを起動してここのエンドポイントにアクセスするのがゴール。
1. Gradleにwarプラグインを追加、bootWarタスク実行
実行可能なwarを作るにはgradleにwarプラグインを追加すればいい。
ついでに生成されるwarファイル名を指定する。
bootWarタスク
にarchiveName
で指定できる。build.gradleplugins { id 'org.springframework.boot' version '2.4.0' id 'io.spring.dependency-management' version '1.0.10.RELEASE' id 'java' id 'eclipse' id 'war' //★ここを追加★ } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = '12' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' } test { useJUnitPlatform() } // ★warファイル名を指定★ bootWar { archiveName 'HelloSpringApp.war' }warプラグインを追加して
gradle tasks
でタスクを確認すると
bootWar
が追加されるのでそれを実行。bootWar実行M:\develop\repository\git\practice2\HelloSpring>gradle bootWar BUILD SUCCESSFUL in 41s 4 actionable tasks: 4 executed成功すればプロジェクトの
build/libs
配下にwarファイルが生成される。2. warファイルの起動
作成したwarファイルを
java -jar {warファイル名}
で起動できる。M:\develop\repository\git\practice2\HelloSpring\build\libs>java -jar HelloSpringApp.war . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.4.0) 2020-11-25 23:19:40.149 INFO 13388 --- [ main] com.example.demo.HelloSpringApplication : Starting HelloSpringApplication using Java 12.0.2 on xxxxx with PID 13388 (M:\develop\repository\git\practice2\HelloSpring\build\libs\HelloSpringApp.war started by xxxx in M:\develop\repository\git\practice2\HelloSpring\build\libs) 2020-11-25 23:19:40.151 INFO 13388 --- [ main] com.example.demo.HelloSpringApplication : No active profile set, falling back to default profiles: default 2020-11-25 23:19:41.212 INFO 13388 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2020-11-25 23:19:41.223 INFO 13388 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2020-11-25 23:19:41.223 INFO 13388 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.39] 2020-11-25 23:19:41.684 INFO 13388 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2020-11-25 23:19:41.685 INFO 13388 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1489 ms 2020-11-25 23:19:41.829 INFO 13388 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2020-11-25 23:19:41.968 INFO 13388 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2020-11-25 23:19:41.976 INFO 13388 --- [ main] com.example.demo.HelloSpringApplication : Started HelloSpringApplication in 2.181 seconds (JVM running for 2.504)この状態で以下にアクセスしてみる。
やったぜ。
tomcatを内包してるのでこのwarアーカイブをそこらへんのサーバにデプロイすれば
そのままWebアプリとして公開できる。
(こんな単純なアプリは無いだろうけど)mainクラスを指定するmanifestはgradleに書かなかったけど
よしなに@SpringBootApplication
のクラスを探してくれるっぽい?まとめ
とりあえずアーカイブ化の方法は取得完了。
後々のCI/CDのCD部分で役に立つ・・・といいなぁ。参考
- 投稿日:2020-11-25T23:18:33+09:00
【Java】インターフェイスを実装するimplementsについて
プログラミング勉強日記
2020年11月25日
インターフェイスを継承してクラスを定義するときに必要なimplementsについてまとめる。implementsとは
implementsの説明をする前にインターフェイスの説明を簡単にする。インターフェイスは、メソッドの戻り値とメソッド名、引数といった使用のみを定義したもので、実際の処理を持たないクラスのようなものである。intefaceを使って型宣言を行うことはできるが、メソッドの定義がないとプログラムは実行できないので、そこでimplementsを使う。つまり、インターフェイスに定義されているメソッドを定義しているクラスであると指定する。
仕様を言い換えると約束事で、約束通りの使い方をすると約束通りの結果になるということをあらかじめ決めておくことで、そのメソッドを使う側は実際の作りを気にしなくてよく、そのメソッドを作る側は仕様さえ守ればいつでも好きなように実装を変えることができる。implementsの書き方
あるクラスにインターフェイスを実装するには下記のように、クラス名の後ろにimplementsを書き、さらにその後ろにインターフェイス名を書く。インターフェイスに抽象メソッドが定義されているならクラスではその抽象メソッドの内容を実際に作る抽象メソッドの実装をしないといけない。(実装しないと、コンパイルエラーになる。)
抽象クラスにインターフェイスを実装する場合は、インターフェイスの抽象メソッドの実装は必要ない。implementsの書き方class クラス名 implements インターフェイス名 { } // 抽象メソッドが定義されている場合 class クラス名 implements インターフェイス名 { インターフェイスの抽象メソッド() { 抽象メソッドの実装; } }インターフェイスを複数implementsすることもでき、その場合はimplementsの後ろに実装したいインターフェイスを
,
(カンマ)で続ける。この場合は、クラスで実装したいインターフェイスにあるすべての抽象メソッドを作成しないといけない。インターフェイスを複数implementsする場合class クラス名 implements インターフェイス名1, インターフェイス名2, ... { インターフェイス1の抽象メソッド() { 抽象メソッドの実装; } インターフェイス2の抽象メソッド() { 抽象メソッドの実装; } }サンプルコード
ボタンをクリックするごとにボタンのテキストの表示が変わるプログラムを作成した。
アクションやイベントの処理に関連するクラスでは、ActionListenerというインターフェイスを実装する。そうしたクラスによって作成されたオブジェクトは、アクションやイベントが発生するとコンポーネントのaddActionListenerメソッドが呼び出される。
package Sample; import java.awt.*; import javax.swing.*; import java.awt.event.*; public class Sample extends JFrame implements ActionListener{ private static final long serialVersionUID = 1L; private JLabel L1; private JButton b1; public Sample() { setLayout(null); this.setSize(200, 200); this.setDefaultCloseOperation(EXIT_ON_CLOSE); L1 = new JLabel("テスト用ボタン"); b1 = new JButton("Click me"); L1.setBounds(50, 10, 100, 20); b1.setBounds(new Rectangle(50, 80, 90, 30)); b1.addActionListener(this); this.add(L1); this.add(b1); this.setVisible(true); } public void actionPerformed(ActionEvent ev) { if(b1.getText() == "ON") b1.setText("OFF"); else b1.setText("ON"); System.out.println("you clicked"); } public static void main(String[] args) { new Sample(); } }参考文献
よくわかる! Javaのインターフェイスを実装する”implements”
インターフェースを実装!Javaでimplementsを使う方法【初心者向け】
- 投稿日:2020-11-25T20:33:24+09:00
CASE式を使って検索項目が空欄の場合に条件を無視する
if文を使わず、SQLのCASE式でnullを無視した検索を実行する
以下のような形で検索に使用する入力項目がいくつもあった場合に、コントローラー側でif文を使って空欄時とそうでないときの場合分けをするとif/elseがずらーっと並ぶ上、同じようなメソッドをいくつも使ってコードが汚くなるので、SQLでどうにかできないかといろいろ調べてみたところ、SQLのCASE式を使えばif文を使わずに、よりスマートにコードがかけることがわかったので備忘録として残します。
repository.java@Query(value = "SELECT * FROM mst_asset" + " WHERE id = CASE WHEN :id = 0 THEN id ELSE :id END" + " AND category_id = CASE WHEN :categoryId = 0 THEN category_id ELSE :categoryId END" + " AND admin_name LIKE concat('%', CASE WHEN :adminName = '' THEN admin_name ELSE :adminName END, '%')" + " AND asset_name LIKE concat('%', CASE WHEN :assetName = '' THEN asset_name ELSE :assetName END, '%')" + " AND delete_flag = false ORDER BY id ASC" + , nativeQuery = true) List<Asset> findByIdAndCategoryIdAndAdminNameAndAssetName( @Param("id") Integer id, @Param("categoryId")Integer categoryId, @Param("adminName") String adminName, @Param("assetName") String assetName);どうやら、WHERE句の[カラム名]=[カラム名]の場合には条件としてカウントしないという性質があるらしくそれを、CASE式に応用することで画面で検索キーワードが入力されなかったパラメータを無視してSQLを実行することができるらしい。
上記のコードでは、Integer型の入力欄のパラメーターに関しては空欄のままだとnullとして受け取ってしまうので、その場合はサービスの中でif文を使って0にセットしてます。String型のパラメーターに関しては、入力された際にはそれをサービスの中でトリミングしてconcatを使って部分一致検索にしてます。
- 投稿日:2020-11-25T20:33:24+09:00
CASE式を使って検索項目が空欄の場合の条件無視にする
if文を使わず、SQLのCASE式でnullを無視した検索を実行する
以下のような形で検索に使用する入力項目がいくつもあった場合に、コントローラー側でif文を使って空欄時とそうでないときの場合分けをするとコードが汚くなるので、SQLでどうにかできないかといろいろ調べてみたところ、SQLのCASE式を使えばif文を使わなくてよりスマートにコードがかけることがわかったので備忘録として残します。
repository.java@Query(value = "SELECT * FROM mst_asset" + " WHERE id = CASE WHEN :id = 0 THEN id ELSE :id END" + " AND category_id = CASE WHEN :categoryId = 0 THEN category_id ELSE :categoryId END" + " AND admin_name LIKE concat('%', CASE WHEN :adminName = '' THEN admin_name ELSE :adminName END, '%')" + " AND asset_name LIKE concat('%', CASE WHEN :assetName = '' THEN asset_name ELSE :assetName END, '%')" + " AND delete_flag = false ORDER BY id ASC" + , nativeQuery = true) List<Asset> findByIdAndCategoryIdAndAdminNameAndAssetName( @Param("id") Integer id, @Param("categoryId")Integer categoryId, @Param("adminName") String adminName, @Param("assetName") String assetName);どうやら、WHERE句の[カラム名]=[カラム名]の場合には条件としてカウントしないという性質があるらしくそれを、CASE式に応用することで画面で検索キーワードが入力されなかったパラメータを無視してSQLを実行することができるらしい。
上記のコードでは、Integer型の入力欄のパラメーターに関しては空欄のままだとnullとして受け取ってしまうので、その場合はサービスの中でif文を使って0にセットしてます。String型のパラメーターに関しては、入力された際にはそれをサービスの中でトリミングしてconcatを使って部分一致検索にしてます。
- 投稿日:2020-11-25T19:20:50+09:00
【Java】アスタリスクのピラミッド
アスタリスクのピラミッド
Javaを使って入力した数の団だけアスタリスクでピラミッドを作成する、「 アスタリスクのピラミッド 」プログラムを作ったメモ。
完成品はこちら
【10段分のピラミッド】 * *** ***** ******* ********* *********** ************* *************** ***************** *******************共通点を探す
このピラミッドがどのように作られているかを考えるために、「ピラミッドが何段になっても変わらない共通点」や、どういう規則で作られているかを考えるため、上記の10段のものに加え、3段で作ったピラミッドを作成して比較してみます。
【3段分のピラミッド】 * *** *****ピラミッドの段数が10段でも、3段でも変わらない部分を考えてみると
- アスタリスクの数は何段になっても奇数
- 最上段は アスタリスク が必ず1個になる
- 最下段は 空白 が必ず0になる
- 一段増えるごとにアスタリスクの数は2つずつ増える
- 一段増えるごとに 空白 の数は 1つずつ減る。
- 一段に入力される空白の数は、(段数 - 1)
という部分が考えられます。
【3段の場合のピラミッドの場合】 * ← 最上段 ( "*" が必ず1個になる) *** ← (一段増えるごとに 空白は1つずつ減り、アスタリスクは2つずつ増える) ***** ← 最下段 ( " " が必ず0になる) 12345 ← 横マスの数やりたいことを文章化
やるべきことが見えたら、それを文章化して
- 空白の数を ( 段数 - 1 ) 個表示させる
- その横に アスタリスクを ( 段数 * 2 - 1 )個表示させる
- 改行する
- 1〜3の流れを段数分繰り返す ※(ただし、段数は前回のループに1つずつ足した数にする。)
これらを実現するために ループ文 を使いました。
つまり上記のリストの 1~3の処理を、段数の数だけループしてあげれば実現できそうです。
またループ内の処理も for文を使うことで効率良く書くことができそうなので、そちらの処理も 「多重for文」を使うと良さそうです。つまり、4のfor文で、1~3の処理をくるむ形で作ることが出来ます。
①. ②~③の処理を 段の数だけループさせるためのループ分を作る。
- n段のピラミッドを作る場合、iは 0から1段のため「 i < n 」と記述。
- ①~②の処理 を内側に入れてループさせる。
for (int i = 0; i < lineCount; i++) { // ①~②の処理... .... .... }②. 空白の数を ( 段数 - 1 ) 個表示させる
見るとピラミッドは 余白( 以下: space ) の数が、上から1つずつ減っていってます。
space は先程の3段ピラミッドを見てみると、「( 段数 - 1 )個出力されている」ことが見えてきます。
上のコードで使用した 「ループを重ねるごとに一つづつ増えていく i 」を使ってやれば、1ずつ減るというわけです。
- space の初期値は 0 のため、予め1を足しておく。
- 空白 (" ")は上から一つずつ減る
【Javaでのコード】
for (int space = 0; space < dan - i ; space++) { System.out.print(" "); }③. その横に アスタリスクを ( 段数 * 2 - 1 )個表示させ、改行する
【書き方】
- その横に アスタリスクを ( 段数 * 2 - 1 )個表示させる
- アスタリスクの数は何段になっても奇数
- 一段増えるごとにアスタリスクの数は2つずつ増える
【Javaでのコード】
for (int j = 0; j <i * 2 - 1; j++) { System.out.print("*"); } System.out.println(); //改行を段数分繰り返す ※(ただし、段数は前回のループに1つずつ足した数にする。)
- 投稿日:2020-11-25T15:10:13+09:00
【Effective Java】抽象概念に適した例外をスローする
Effective Javaの独自解釈です。
第3版の項目73について、自分なりにコード書いたりして解釈してみました。概要
- 下位レイヤーの例外はそのまま出力させるのではなく、上位レイヤーの概念に応じた例外に翻訳することで以下のメリットが得られる。
- ログ調査しやすくなる
- さらに上位のレイヤーでの例外ハンドリングがしやすくなる
- 実装で例外発生を確実に防げるなら、翻訳は考えず例外発生そのものを防ぐ。
説明
以下の環境で実装したコードで説明する。
- Java 11
- Doma 2.19.2
- Spring Boot 2.3.3
- MySQL 5.7
DB挿入時における例外出力の例
DBに
USER
テーブルがあるとし、レコードを新規追加することを考える。
USER
テーブルには主キーとしてid
カラムを持っている。Java側の関連クラスは以下の通り。(importなど略)
Entity
public class User { private String id; private String name; }Dto
public class CreateUser { private String id; private String name; }Daoインタフェース
public interface UserDao { @Insert int insert(User user); }下位レイヤーの例外をそのまま出力させた場合
既に
USER
テーブルに存在するid
を持つcreateUser
を引数に入れ、以下のサービス層のメソッドでinsertを試み例外を発生させる。public User createUser(CreateUser createUser) { User user = new User(); user.setId(createUser.getId()); user.setName(createUser.getName()); userDao.insert(user); return user; }このとき、標準エラーには以下が出力される。
〜略〜 nested exception is org.springframework.dao.DuplicateKeyException: [DOMA2004] 一意制約違反により更新処理が失敗しました。 SQLファイルパス=[null]。 ログ用SQL=[]。 詳しい原因は次のものです。{2}; nested exception is org.seasar.doma.jdbc.UniqueConstraintException: [DOMA2004] 一意制約違反により更新処理が失敗しました。 SQLファイルパス=[null]。 ログ用SQL=[]。 詳しい原因は次のものです。{2}] with root cause java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'A01' for key 'PRIMARY' 〜略〜Daoレイヤーから出力された
DuplicateKeyException
がそのままcreateUser
メソッドを突き抜けてスローされている。弊害としては以下の通り。
- スタックトレースをたどれば
createuser
メソッドで主キー重複例外が出てるから、USER
のID重複エラーだなと一応わかるが、ひと目ではわかりにくい。- 上位レイヤーのコントローラ層で、Entityに応じたエラーハンドリング処理をしたい場合、
DuplicateKeyException
が投げられると、どのEntityに対しての例外なのか判別するために、ややこしい文字列解析などする必要が出てくる。上位レイヤーの例外に置き換えた場合
createUser
メソッドの主な関心事はDBの制約ではなく、User
そのものである。ということでUser
に着目した独自例外を作ってやり、それに翻訳する処理を施す。独自例外
public class UserDuplicatedIdException extends Exception { public UserDuplicatedIdException(String message, Throwable cause) { super(message, cause); } }Primary KeyというDB寄りの言葉は使わず、あくまで
User
のid
というUser
寄りの言葉で例外クラスを作る。
createUser
メソッドは以下のように、DuplicateKeyException
からUserDuplicatedIdException
への翻訳処理を実装する。public User createUser(CreateUser createUser) throws UserDuplicatedIdException { User user = new User(); user.setId(createUser.getId()); user.setName(createUser.getName()); try { userDao.insert(user); } catch (DuplicateKeyException e) { throw new UserDuplicatedIdException("Can't create already existing id user.", e); } return user; }先程の例と同じように
id
重複のUSER
テーブルレコードを挿入しようとすると、以下の標準エラー出力が出力される。〜略〜 nested exception is com.demo.domain.exceptions.UserDuplicatedIdException: Can't create already existing id user.] with root cause java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'A01' for key 'PRIMARY' 〜略〜
createUser
からは、このレイヤーの関心事であるUser
に関する例外UserDuplicatedIdException
が出力される。利点は先程とは逆で以下の通り。
- 例外クラスを見ただけでどのEntityで何が起こったかがひと目で分かる。
- コントローラ層では、catchした例外クラスに応じて楽に分岐処理を実装できる。
また、障害やデバッグ時にログ調査をする際も、目的の例外クラスでgrepをかけられるので捗る。
例外翻訳は乱用しない
先述の例で、
USER
のid
をNull
で登録しようとすると、以下の標準エラーが出力される。〜略〜 nested exception is org.springframework.dao.DataIntegrityViolationException: [DOMA2009] SQLの実行に失敗しました。 SQLファイルパス=[null]。 ログ用SQL=[]。 原因は次のものです。java.sql.SQLIntegrityConstraintViolationException: Column 'id' cannot be null。 根本原因は次のものです。java.sql.SQLIntegrityConstraintViolationException: Column 'id' cannot be null; SQL [insert into USER (id, name) values (?, ?)]; Column 'id' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'id' cannot be null] with root cause java.sql.SQLIntegrityConstraintViolationException: Column 'id' cannot be null 〜略〜なるほど、この場合は
DataIntegrityViolationException
を別の例外に翻訳すればいいんだなと思うかもしれないが、それはNGである。insert時のDBの中身はクライアント側で制御できないが、insertしようとするEntityは制御可能である。
コントローラ層でリクエストのid
指定がない場合はバリデーションで弾くなり、代替値を入れるなりすれば、この例外は100%発生しないので、翻訳処理も必要なくなる。
- 投稿日:2020-11-25T11:53:27+09:00
State Pattern
あるオブジェクトの状態をクラス化する。サンプルでは、水クラスの状態を、氷、液体、気体という状態を想定し、水クラス、液体クラス、気体クラスとする
以下のクラス構成で確認します
クラス 説明 Water.class 水を表すクラス
水の状態は、固体、液体、気体の3つとするwaterState.interface 水の状態を共通化するインターフェイス Ice.class 状態が固体であることを表すクラス Liquid.class 状態が液体であることを表すクラス Air.class 状態が液体であることを表すクラス user(Main.class) 乱数で水の状態を変化させる *user 他の開発者がこのパターンを利用する、という意味合いを含みます
Water.classclass Water{ waterState state = new Liquid(); void setState(waterState state){ this.state=state; } void show(){System.out.println(state.showState());} }waterState_interfaceinterface waterState{String showState();}Ice.classclass Ice implements waterState{ public String showState() {return "Ice";} }Liquid.classclass Liquid implements waterState{ public String showState() {return "Liquid"; } }Air.classclass Air implements waterState{ public String showState() {return "Air";} }user(Main.class)public static void main(String[] args){ Water water = new Water(); Random rand = new Random(); for(int i=0;i<10;i++){ int r = rand.nextInt(30); if(r%5==0){water.setState(new Ice());}else{ if(r%3==0){water.setState(new Liquid());}else{ water.setState(new Air()); }} water.show(); } }}
- 投稿日:2020-11-25T11:42:41+09:00
CFRでclassファイルをデコンパイル
経緯
javaでカバレッジ上げろと言われてもどこを通ってないのかさっぱりわからなかったので、コンパイル時にどう展開されているのか調べたかった。
解決方法
デコンパイルしてclassファイルを確認すればいい。日本語のサイトだとJADというツールが引っかかるが、これは古いのでCFRというデコンパイラを使う。
試してみる
シンプルなtry-with-resourcesがどう展開されているのかを確認する。
- Hoge.java
import java.io.IOException; import java.io.BufferedReader; import java.io.FileReader; import java.util.stream.Collectors; public class Hoge { public String config; private Hoge() { try (BufferedReader br = new BufferedReader(new FileReader("config"))) { config = br.lines().collect(Collectors.joining("\n")); } catch (IOException ex) { ex.printStackTrace(); } } }
- デコンパイルする
$ javac Hoge.java $ java -jar cfr-0.150.jar --tryresources false --decodefinally false Hoge.class > HogeHoge.java
- 結果
/* * Decompiled with CFR 0.150. */ import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.util.stream.Collectors; public class Hoge { public String config; private Hoge() { try { BufferedReader bufferedReader = new BufferedReader(new FileReader("config")); try { this.config = bufferedReader.lines().collect(Collectors.joining("\n")); } catch (Throwable throwable) { try { bufferedReader.close(); } catch (Throwable throwable2) { throwable.addSuppressed(throwable2); } throw throwable; } bufferedReader.close(); } catch (IOException iOException) { iOException.printStackTrace(); } } }余談
カバレッジの確認でjacocoを利用していたが、jacocoのバージョンを上げたら余計な分岐の確認をしなくなったのか、無理してでコンパイルしなくても網羅できるようになった。
- 投稿日:2020-11-25T08:16:03+09:00
【Java・SpringBoot・Thymeleaf】バリデーションエラーメッセージを変更(SpringBootアプリケーション実践編4)
ログインをして、ユーザー一覧を表示するアプリケーションを作成し、
Springでの開発について勉強していきます?
前回のエラーメッセージ実装に引き続き、メッセージプロパティファイルを複数用意して切り替える方法を実装します前回の記事?
【Java・SpringBoot・Thymeleaf】エラーメッセージを実装(SpringBootアプリケーション実践編3)ユーザー登録画面用のフォームクラスにバリデーション実装
- フォームクラスの各フィールドにアノテーションを付けるだけ!で入力チェックが出来る
- NotNull・NotEmpty・NotBlankの違い
アノテーション null 空文字 空白 @NotNull NG OK OK @NotEmpty NG NG OK @NotBlank NG NG NG SignupForm.javapackage com.example.demo.login.domain.model; import java.util.Date; import javax.validation.constraints.AssertFalse; import javax.validation.constraints.Email; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; import org.hibernate.validator.constraints.Length; import org.springframework.format.annotation.DateTimeFormat; import lombok.Data; @Data public class SignupForm { //入力形式、メールアドレス形式 @NotBlank @Email private String userId; //ユーザーID //入力必須、長さ4-100桁まで、半角英数字のみ @NotBlank @Length(min= 4 ,max = 100) @Pattern(regexp = "^[a-zA-Z0-9]+$") private String password; //パスワード //入力必須 @NotBlank private String userName; //ユーザー名 //入力必須 @NotNull @DateTimeFormat(pattern = "yyyy/MM/dd") private Date birthday; //誕生日 //値は20-100 @Min(20) @Max(100) private int age; //年齢 //falseのみ可能 @AssertFalse private boolean marriage; //結婚ステータス }コントローラークラス修正
バリデーションの実施
- 引数のフォームクラスに@Validatedアノテーションを付ける
- バリデーションのチェック結果はBindingResultクラスに入る
- →バリデーションを使う場合、BindingResultクラスを引数に設定する必要がある
public String postSignUp(@ModelAttribute @Validated SignupForm form, BindingResult bindingResult, Model model) {...}@Validatedと@Valid
- @Validated:Spring標準のアノテーション。今回は@Validatedを使用
- @Valid:J2EEから標準で搭載されているBeanValidator(バリデーション用のアノテーション)
メッセージプロパティファイル名、エラーメッセージの書き方が違うSignupController.javapackage com.example.demo.login.controller; import java.util.LinkedHashMap; import java.util.Map; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import com.example.demo.login.domain.model.SignupForm; @Controller public class SignupController { //ラジオボタン用変数 private Map<String, String> radioMarriage; //ラジオボタンの初期化メソッド private Map<String, String> initRadioMarrige() { Map<String, String> radio = new LinkedHashMap<>(); // 既婚、未婚をMapに格納 radio.put("既婚", "true"); radio.put("未婚", "false"); return radio; } //ユーザー登録画面のGET用コントローラ @GetMapping("/signup") public String getSignUp(@ModelAttribute SignupForm form, Model model) { // ラジオボタンの初期化メソッド呼び出し radioMarriage = initRadioMarrige(); // ラジオボタン用のMapをModelに登録 model.addAttribute("radioMarriage", radioMarriage); // signup.htmlに画面遷移 return "login/signup"; } //ユーザー登録画面のPOST用コントローラ //バリデーション実装 @PostMapping("/signup") public String postSignUp(@ModelAttribute @Validated SignupForm form, BindingResult bindingResult, Model model) { // 入力チェックに引っかかった場合、ユーザー登録画面に戻る if (bindingResult.hasErrors()) { // GETリクエスト用のメソッドを呼び出して、ユーザー登録画面に戻る return getSignUp(form, model); } // formの中身をコンソールで確認 System.out.println(form); // login.htmlにリダイレクト return "redirect:/login"; } }SpringBootを起動、新規登録画面を確認!
- http://localhost:8080/login
- ユーザー登録画面で何も入力せずユーザー登録ボタンを押すと、エラーメッセージが表示されました〜〜
- 今のままでは英語のメッセージなので、次回は入力チェックのエラーメッセージを編集して日本語化します^o^
- 投稿日:2020-11-25T00:03:30+09:00