■Spring Framework と Spring Boot
Spring Frameworkは、Javaのアプリケーションフレームワーク
Webアプリケーションの開発に必要となる様々な機能を提供している
ただし多機能なために設定が複雑であり、環境構築に手間と時間がかかる
そのため、最低限の設定を行うだけで構築できるようにしたものが「Spring Boot」として提供されている
(PHPでの「重厚なSymfonyと手軽なLaravel」のような関係なのかもしれない)
また、アプリケーションの内部にTomcatを内包しているため、アプリケーションサーバの構築も不要となっている
Spring Framework - Wikipedia
https://ja.wikipedia.org/wiki/Spring_Framework
多様化するJavaのフレームワーク問題を解決する、「Spring Boot」とは? (1/3):CodeZine(コードジン)
https://codezine.jp/article/detail/11310
Spring Framework 入門! 特徴やbootとの違い、ダウンロードからインストールまでをわかりやすく解説 | プログラミングを学ぶならトレノキャンプ(TRAINOCAMP)
https://camp.trainocate.co.jp/magazine/spring-framework/
Spring Framwork(Javaのフレームワーク)―入門知識、サンプルコード
https://www.zealseeds.com/SysDevTech/Spring/index.html
Spring BootでサーバーサイドKotlin入門 - Qiita
https://qiita.com/kawasaki_dev/items/1a188878eb6928880256
■基本的なプログラムの作成
※IntelliJ IDEA + Spring Boot での開発
IntelliJ IDEAのインストールについては Java.txt を参照
■プロジェクトの作成
Spring Initializr
https://start.spring.io/
今回は以下のように設定する
Dependenciesにある「Spring Web」にはTomcatが付属しているため、Webアプリケーションサーバを用意しなくてもプログラムを動作させることができる
Project: Maven
Language: Java
Spring Boot: 3.0.1(デフォルト、または「SNAPSHOT」や「M1」などが付いていない最新バージョンを選択するといい)
Group: com.example
Artifact: demo
Name: demo
Description: Demo project for Spring Boot
Package name: com.example.demo
Packaging: Jar
Java: 17
Dependencies: Spring Web(「ADD DEPENDENCIES」ボタンから追加する)
設定したら「GENERATE」ボタンをクリックするとZIPファイルがダウンロードできる
これはプロジェクトの雛形なので、展開して適当なフォルダに配置する
今回は「C:\Users\refirio\IdeaProjects\demo」に配置した
IntelliJ IDEA の初期画面で「開く」から上記プロジェクトを開く
(「プロジェクトを信頼して開きますか?」のダイアログが表示されたら、「プロジェクトを信頼」をクリックする)
Mavenにより必要なものがダウンロードされるので、初回の起動は時間がかかる
pom.xml には以下が記述されている
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>
</project>
main メソッドは src/main/java/com/example/demo/DemoApplication.java にあり、以下が記述されている
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
■コードスタイルの設定
ファイル → 設定 → エディター → コードスタイル → HTML
HTMLのインデントがスペース2つで入力されるため、スペース4に変更した
上記設定上はデフォルトでスペース4となっていたが、そのままOKとすると、次回からスペース4で入力されるようになった
…と思ったが、HTMLファイルの新規作成直後はスペース2つになってしまっている?
ファイルを閉じて再度開くと、設定どおりスペース4つと認識されるみたい?
それでも駄目なら、IntelliJ IDEAを再起動すると認識されるみたい?
■プロジェクトの設定
JDKの設定を行う
メニューバーの「ファイル → プロジェクト構造 → プロジェクト」で、JDKを「17」にする
(デフォルトでそのように設定されていた)
実行構成の設定を行う
メニューバーの「実行 → 実行構成の編集」で、左上の「+」アイコンをクリックし、「アプリケーション」を選択する
名前: Tomcat
SDK: java 17
メインクラス: com.example.demo.DemoApplication
「OK」ボタンを押すと、ツールバーで「現在のファイル」と表示されていたセレクトボックスが「Tomcat」に変わることを確認できる
■プロジェクトの実行
プロジェクトの実行ボタンを押す
実行されると、実行結果として「Spring」のアスキーアートと実行ログが表示される
ログの中に「Tomcat started on port(s): 8080 (http) with context path ''」というメッセージがある
ブラウザで
http://localhost:8080/ にアクセスすると、画面に以下が表示される(エラーとなっているが、表示のためのページを作成していないため)
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Tue Jan 03 17:10:56 JST 2023
There was an unexpected error (type=Not Found, status=404).
プロジェクトを再実行する場合、実行ボタンがあった場所に再実行ボタンが表示されているので押す
「プロセス'Tomcat'は実行中です」と表示されたら、「停止して再実行」でいい
プロジェクトを停止する場合、実行ボタンの右側にある停止ボタンを押す
■プロジェクトの実行エラーと対策
もし以下のメッセージが表示されたら、ポート8080はすでに使用されているために起動できていない
Description:
Web server failed to start. Port 8080 was already in use.
Action:
Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.
この場合、src/main/resources/application.properties を開いて以下の内容を記述することで、8888ポートで起動することができる
(他のポート番号を指定することもできる)
server.port = 8888
■エンドポイントの作成と再実行
com.example.demo 内に HomeRestController を作る
HomeRestController.java が作成されるので、以下の内容を記述する
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
@RestController
public class HomeRestController {
@GetMapping(value = "/")
String index() {
return "Spring Boot";
}
@GetMapping(value = "/now")
String now() {
return "現在時刻は%sです。".formatted(LocalDateTime.now());
}
}
プロジェクトを再実行し、ブラウザで
http://localhost:8080/ にアクセスすると、画面に以下が表示される
Spring Boot
またブラウザで
http://localhost:8080/now にアクセスすると、画面に以下が表示される
現在時刻は2023-01-03T17:22:32.596689400です。
■実行可能なJARファイルを作成
IntelliJ IDEA の画面右端に「Maven」というタブが折りたたまれているので、クリックして表示させる
画面内で「demo → ライフサイクル」内にある「package」を右クリックし、「Mavenビルドの実行」を選択する
実行ウインドウに「BUILD SUCCESS」と表示されれば成功
C:\Users\refirio\IdeaProjects\demo\target 内に「demo-0.0.1-SNAPSHOT.jar」が作成されていることを確認できる
コマンドプロンプトで以下を実行すると、先ほどと同じ画面を表示させることができる
(実行前に、IntelliJ IDEA で実行したものは停止させておく)
java -jar C:\Users\refirio\IdeaProjects\demo\target\demo-0.0.1-SNAPSHOT.jar
プログラムは「Ctrl+C」で終了させることができる
ただしこれは簡易な実行方法で、本番環境ならサービスやNginxを組み合わせて実行することになる
詳細は後述の「Vagrantでアプリケーション(JAR)を起動」を参照
JARファイルについては、後述の「プログラムのパッケージ化について」も参照
■プロパティの上書き(引数)
以下のように起動して、ポート番号を上書き変更できる
src/main/resources/application.properties で指定した値も上書き指定できる
開発環境などで、簡易的に値を変更する場合などに使えそう
java -jar demo-0.0.1-SNAPSHOT.jar --server.port=8888
この場合、以下のようにしてアクセスできる
http://localhost:8888/
http://localhost:8888/task
http://localhost:8888/task_add
Spring-Bootの設定プロパティと環境変数 - Qiita
https://qiita.com/NewGyu/items/d51f527c7199b746c6b6
■プロパティの上書き(src/main/resources/application-xxx.properties)
src/main/resources/application.properties で設定を指定できるが、
src/main/resources/application-production.properties src/main/resources/application-staging.properties のようにファイルを作ることで、環境ごとの設定を指定できる
通常の手順でjarファイルを作成すれば、それを実行する際に以下のようにファイルを指定できる
# java -jar demo-0.0.1-SNAPSHOT.jar --spring.profiles.active=production
本番環境のデータベース接続情報などがリポジトリに含むことになるなら微妙かもしれないが、
「本番環境のファイルのみ .gitignore の対象とする」「本番環境はElasticBeanstalkの仕組みに従って別途指定する」
など色々とやりようはありそう
■プロパティの上書き(config/application.properties)
※プログラムをサービスとして稼働させたときは反映されなかった
引き続き要確認
jarファイルと同じ場所に config/application.properties を作成して以下のように書いておくと、起動時に値が上書きされる
本番環境や検収環境で、接続設定などを変更する場合に使えそう
server.port = 8765
spring.datasource.url=jdbc:mysql://localhost:3306/spring-test
spring.datasource.username=test
spring.datasource.password=test1234
起動の際は、以下のとおりファイルのパスなどを指定する必要は無い
java -jar demo-0.0.1-SNAPSHOT.jar
■プロパティの上書き(環境変数)
※プログラムをサービスとして稼働させたときは反映されなかった
引き続き要確認
~/.bash_profile に環境変数として定義(ファイルの最後に追記)しておくと、起動時に値が上書きされる(恒久的な設定になる)
本番環境や検収環境で、接続設定などを変更する場合に使えそう
$ vi ~/.bash_profile
export SERVER_PORT=8888
export SPRING_DATASOURCE_URL=jdbc:mysql://localhost:3306/spring-test
export SPRING_DATASOURCE_USERNAME=test
export SPRING_DATASOURCE_PASSWORD=test1234
設定後、以下で値を反映する
$ source ~/.bash_profile
以下のようにして反映を確認できる
echo $SERVER_PORT
echo $SPRING_DATASOURCE_URL
echo $SPRING_DATASOURCE_USERNAME
echo $SPRING_DATASOURCE_PASSWORD
いまさらですがLinuxで環境変数を設定したい - Qiita
https://qiita.com/chihiro/items/bb687903ee284766e879
■Webアプリケーションの作成
■HTMLを返すコントローラーの作成
com.example.demo 内に新規Javaクラス HomeController を作る
HomeController.java が作成されるので、以下の内容を記述する
@RestController はレスポンスとして文字列を返すような場合に使用する
@Controller はレスポンスとしてHTMLを返すような場合に使用する
package com.example.demo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.time.LocalDateTime;
@Controller
public class HomeController {
@GetMapping(value = "/")
@ResponseBody
String index() {
return """
<html>
<head>
<title>Spring Boot</title>
</head>
<body>
<h1>Spring Boot</h1>
<p>It works!</p>
</body>
</html>
""";
}
@GetMapping(value = "/now")
@ResponseBody
String now() {
return """
<html>
<head>
<title>Spring Boot</title>
</head>
<body>
<h1>Spring Boot</h1>
<p>現在時刻は%sです。</p>
</body>
</html>
""".formatted(LocalDateTime.now());
}
}
■HTMLテンプレートを利用する
テンプレートエンジンのThymeleaf(タイムリーフ)を利用する
pom.xml に以下を追加し、さらにMavenの変更を読み込む
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
src/main/resources/templates/index.html を作成し、以下の内容を記述する
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Boot</title>
</head>
<body>
<h1>Spring Boot</h1>
<p>It works!</p>
</body>
</html>
src/main/resources/templates/now.html を作成し、以下の内容を記述する
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Boot</title>
</head>
<body>
<h1>Spring Boot</h1>
<p>現在時刻は<span th:text="${time}"></span>です。</p>
</body>
</html>
HomeController.java の内容を以下に変更する
package com.example.demo;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.time.LocalDateTime;
@Controller
public class HomeController {
@GetMapping(value = "/")
String index() {
return "index";
}
@GetMapping(value = "/now")
String now(Model model) {
model.addAttribute("time", LocalDateTime.now());
return "now";
}
}
これでブラウザからアクセスすると、テンプレートの内容に従って画面が表示される
Thymeleafについては、後述の「テンプレート(Thymeleaf)」も参照
■MySQLを利用する(JDBCの場合)
※特に理由がなければ、JDBCではなくSpring Data JPAを使うと良さそう
Spring Data JPAについては、後述の「MySQLを利用する(Spring Data JPAの場合)」を参照
ここでは、MySQLはXAMPPに付属しているものを使用することにする
XAMPPのApacheとMySQLを起動する
phpMyAdminで今回は「spring」というデータベースを作成しておく
データベースの文字コードは「utf8mb4_general_ci」としておく
以下のテーブルを作成する
CREATE TABLE tasks (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(80) NOT NULL,
text TEXT,
PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
MySQLを利用するため、pom.xml に以下を追加する(「mysql-connector-j」を「mysql-connector-java」とするとエラーになった)
追加したら、Mavenの変更を読み込む
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
src/main/resources/application.properties にデータベースへの接続設定を記述する(正しく設定しないと起動できない)
なお昔はドライバに「com.mysql.jdbc.Driver」が指定されたようだが、今はその指定は非推奨らしい
spring.jpa.hibernate.ddl-auto=none
spring.datasource.url=jdbc:mysql://localhost:3306/spring
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
Spring BootでMySQLに接続する | 株式会社CONFRAGE ITソリューション事業部
https://confrage.jp/spring-boot%E3%81%A7mysql%E3%81%AB%E6%8E%A5%E7%B6%9A%E3%81%99%E3%82%8B/
You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone support. | 株式会社CONFRAGE ITソリューション事業部
https://confrage.jp/you-must-configure-either-the-server-or-jdbc-driver-via-the-servertimezone-confi...
src/main/java/com/example/demo/Task.java としてデータクラスを作成する
package com.example.demo;
public class Task {
private String id;
private String title;
private String text;
public Task(String id, String title, String text) {
this.id = id;
this.title = title;
this.text = text;
}
public String id() {
return id;
}
public String title() {
return title;
}
public String text() {
return text;
}
}
src/main/java/com/example/demo/TaskDao.java としてデータベースアクセスクラスを作成する
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
public class TaskDao {
private final JdbcTemplate jdbcTemplate;
@Autowired
TaskDao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void add(Task task) {
SqlParameterSource param = new BeanPropertySqlParameterSource(task);
SimpleJdbcInsert insert = new SimpleJdbcInsert(jdbcTemplate).withTableName("tasks");
insert.execute(param);
}
public List<Task> findAll() {
String query = "SELECT * FROM tasks";
List<Map<String, Object>> result = jdbcTemplate.queryForList(query);
List<Task> tasks = result.stream()
.map((Map<String, Object> row) -> new Task(
row.get("id").toString(),
row.get("title").toString(),
row.get("text").toString()
)).toList();
return tasks;
}
}
src/main/resources/templates/task.html としてデータ一覧のテンプレートを作成する
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Boot</title>
</head>
<body>
<h1>Spring Boot</h1>
<p>タスクは以下のとおりです。</p>
<table>
<tr>
<th>ID</th>
<th>タイトル</th>
<th>テキスト</th>
</tr>
<tr th:each="task : ${tasks}">
<td th:text="${task.id}"></td>
<td th:text="${task.title}"></td>
<td th:text="${task.text}"></td>
</tr>
</table>
</body>
</html>
src/main/resources/templates/task_add.html としてデータ登録完了のテンプレートを作成する
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Boot</title>
</head>
<body>
<h1>Spring Boot</h1>
<p>タスクを登録しました。</p>
</body>
</html>
src/main/java/com/example/demo/HomeController.java を以下のように変更する
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.time.LocalDateTime;
import java.util.List;
@Controller
public class HomeController {
private final TaskDao taskDao;
@Autowired
HomeController(TaskDao taskDao) {
this.taskDao = taskDao;
}
@GetMapping(value = "/")
String index() {
return "index";
}
@GetMapping(value = "/now")
String now(Model model) {
model.addAttribute("time", LocalDateTime.now());
return "now";
}
@GetMapping(value = "/task")
String task(Model model) {
List<Task> tasks = taskDao.findAll();
model.addAttribute("tasks", tasks);
return "task";
}
@GetMapping(value = "/task_add")
String addTask() {
Task task = new Task(null, "テスト", "テスト。\nこれはテストです。");
taskDao.add(task);
return "task_add";
}
}
http://localhost:8080/task_add にアクセスするとタスクが登録される
http://localhost:8080/task にアクセスするとタスクが一覧表示される
データベースの接続情報を環境ごとに用意する方法は、このファイル内の「プロパティの上書き」を参照
■MySQLを利用する(Spring Data JPAの場合)
SpringBoot JPAを使う方法(Spring Data JPA の使用方法) - Web系開発メモ
https://web-dev.hatenablog.com/entry/spring-boot/intro/jpa
Spring Boot入門:MySQLにSpring Boot アプリケーションからアクセスする - あるSEのつぶやき・改
https://www.aruse.net/entry/2019/08/04/003838
Spring Boot+JPAでデータベースに接続する方法 - ITを分かりやすく解説
https://medium-company.com/spring-boot%EF%BC%8Bjpa%E3%81%A7%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%8...
ここでは、MySQLはXAMPPに付属しているものを使用することにする
XAMPPのApacheとMySQLを起動する
phpMyAdminで今回は「spring」というデータベースを作成しておく
データベースの文字コードは「utf8mb4_general_ci」としておく
以下のテーブルを作成する
CREATE TABLE tasks (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(80) NOT NULL,
text TEXT,
PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
MySQLを利用するため、pom.xml に以下を追加する(「mysql-connector-j」を「mysql-connector-java」とするとエラーになった)
追加したら、Mavenの変更を読み込む
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
src/main/resources/application.properties にデータベースへの接続設定を記述する(正しく設定しないと起動できない)
spring.jpa.hibernate.ddl-auto=none
spring.datasource.url=jdbc:mysql://localhost:3306/spring
spring.datasource.username=root
spring.datasource.password=1234
src/main/java/com/example/demo/Task.java としてエンティティクラスを作成する
package com.example.demo;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Table;
import jakarta.persistence.Id;
@Entity
@Table(name="tasks")
public class Task {
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
private String title;
private String text;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
src/main/java/com/example/demo/TaskRepository.java としてリポジトリクラスを作成する
package com.example.demo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface TaskRepository extends JpaRepository<Task, Integer> {
}
src/main/resources/templates/task.html としてデータ一覧のテンプレートを作成する
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Boot</title>
</head>
<body>
<h1>Spring Boot</h1>
<p>タスクは以下のとおりです。</p>
<table>
<tr>
<th>ID</th>
<th>タイトル</th>
<th>テキスト</th>
</tr>
<tr th:each="task : ${tasks}">
<td th:text="${task.id}"></td>
<td th:text="${task.title}"></td>
<td th:text="${task.text}"></td>
</tr>
</table>
</body>
</html>
src/main/resources/templates/task_add.html としてデータ登録完了のテンプレートを作成する
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Boot</title>
</head>
<body>
<h1>Spring Boot</h1>
<p>タスクを登録しました。</p>
</body>
</html>
src/main/java/com/example/demo/HomeController.java を以下のように変更する
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.time.LocalDateTime;
import java.util.List;
@Controller
public class HomeController {
@Autowired
private TaskRepository taskRepository;
@GetMapping(value = "/")
String index() {
return "index";
}
@GetMapping(value = "/now")
String now(Model model) {
model.addAttribute("time", LocalDateTime.now());
return "now";
}
@GetMapping(value = "/task")
String task(Model model) {
List<Task> tasks = taskRepository.findAll();
model.addAttribute("tasks", tasks);
return "task";
}
@GetMapping(value = "/task_add")
String addTask() {
Task task = new Task();
task.setId(null);
task.setTitle("テスト");
task.setText("テスト。\nこれはリポジトリのテストです。");
taskRepository.save(task);
return "task_add";
}
}
これで
http://localhost:8080/task_add にアクセスすると、タスクが登録される
また
http://localhost:8080/task にアクセスすると、タスクが一覧表示される
データベースの接続情報を環境ごとに用意する方法は、このファイル内の「プロパティの上書き」を参照
Spring Data JPAについては、後述の「データベース(Spring Data JPA)」も参照
■処理の階層化
■処理の階層化の実践
Controller → Service → Repository → Entity
という階層化が定番実装方法の一つ。これに従ってプログラムを調整する
(以下では、あわせてテンプレートファイルも配置を調整している)
src/main/java/com/example/demo/entity/Task.java(src/main/java/com/example/demo/Task.java を移動&編集)
package com.example.demo.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Table;
import jakarta.persistence.Id;
@Entity
@Table(name="tasks")
public class Task {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
private String title;
private String text;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
src/main/java/com/example/demo/repository/TaskRepository.java(src/main/java/com/example/demo/TaskRepository.java を移動&編集)
package com.example.demo.repository;
import com.example.demo.entity.Task;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface TaskRepository extends JpaRepository<Task, Integer> {
}
src/main/java/com/example/demo/service/TaskService.java を新規に作成
package com.example.demo.service;
import com.example.demo.entity.Task;
import com.example.demo.repository.TaskRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class TaskService {
@Autowired
private TaskRepository taskRepository;
public List<Task> select() {
List<Task> tasks = taskRepository.findAll();
return tasks;
}
public void create() {
Task task = new Task();
task.setId(null);
task.setTitle("テスト");
task.setText("テスト。\nこれはリポジトリのテストです。");
taskRepository.save(task);
}
}
src/main/java/com/example/demo/controller/HomeController.java(src/main/java/com/example/demo/HomeController.java を移動&編集)
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.time.LocalDateTime;
@Controller
public class HomeController {
@GetMapping(value = "/")
String index() {
return "home/index";
}
@GetMapping(value = "/now")
String now(Model model) {
model.addAttribute("time", LocalDateTime.now());
return "home/now";
}
}
src/main/java/com/example/demo/controller/TaskController.java を新規に作成
package com.example.demo.controller;
import com.example.demo.entity.Task;
import com.example.demo.service.TaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
@Controller
public class TaskController {
@Autowired
private TaskService taskService;
@GetMapping(value = "/task/")
String index(Model model) {
List<Task> tasks = taskService.select();
model.addAttribute("tasks", tasks);
return "task/index";
}
@GetMapping(value = "/task/add")
String add() {
taskService.create();
return "task/add";
}
}
src/main/resources/templates/home/index.html(src/main/resources/templates/index.html を移動&編集 / 内容は変わらず)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Boot</title>
</head>
<body>
<h1>Spring Boot</h1>
<p>It works!</p>
</body>
</html>
src/main/resources/templates/home/now.html(src/main/resources/templates/now.html を移動&編集 / 内容は変わらず)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Boot</title>
</head>
<body>
<h1>Spring Boot</h1>
<p>現在時刻は<span th:text="${time}"></span>です。</p>
</body>
</html>
src/main/resources/templates/task/index.html(src/main/resources/templates/task.html を移動&編集 / 内容は変わらず)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Boot</title>
</head>
<body>
<h1>Spring Boot</h1>
<p>タスクは以下のとおりです。</p>
<table>
<tr>
<th>ID</th>
<th>タイトル</th>
<th>テキスト</th>
</tr>
<tr th:each="task : ${tasks}">
<td th:text="${task.id}"></td>
<td th:text="${task.title}"></td>
<td th:text="${task.text}"></td>
</tr>
</table>
</body>
</html>
src/main/resources/templates/task/add.html(src/main/resources/templates/task_add.html を移動&編集 / 内容は変わらず)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Boot</title>
</head>
<body>
<h1>Spring Boot</h1>
<p>タスクを登録しました。</p>
</body>
</html>
http://localhost:8080/
http://localhost:8080/now
http://localhost:8080/task/
http://localhost:8080/task/add
■参考
以下を参考に、ひととおりの流れを作ってみると良さそう
フォーム投稿&バリデーションについても触れられている
Spring Boot + Thymeleafで新規登録画面を作成する - ITを分かりやすく解説
https://medium-company.com/spring-boot-thymeleaf%E3%81%A7%E6%96%B0%E8%A6%8F%E7%99%BB%E9%8C%B2%E7%94%...
SpringBoot/各レイヤの責務 - KobeSpiral2021
https://cs27.org/wiki/kobespiral2021/?SpringBoot/%E5%90%84%E3%83%AC%E3%82%A4%E3%83%A4%E3%81%AE%E8%B2...
Spring Bootで実装するときに気をつけて欲しいポイントベスト3〜構成編〜 - 自主的20%るぅる
https://www.agent-grow.com/self20percent/2020/04/13/spring-boot-check-point-structure/
Spring Bootアプリケーションのコードレビューポイント - Qiita
https://qiita.com/cross-xross/items/144f8bde2ef6fa4b379f
@Component、@Service、@Repository、@Controllerの違いについて - Qiita
https://qiita.com/KevinFQ/items/abc7369cb07eb4b9ae29
Spring Boot Thymeleaf でリンク(a href)を記載する方法 | ITエンジニアの定時退社Tips
https://www.early2home.com/blog/programming/html/post-1564.html
■プログラムの整理
■EntityのGetter/Setterを自動作成
【Spring Boot】Lombokの導入
https://b1san-blog.com/post/spring/spring-lombok/
SpringBootのプロジェクトにlombok導入する - Qiita
https://qiita.com/kihara-takahiro/items/f616d01be6bf7384dbfc
pom.xml
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
src/main/java/com/example/demo/entity/Task.java
package com.example.demo.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Table;
import jakarta.persistence.Id;
import lombok.Data;
@Entity
@Table(name="tasks")
@Data
public class Task {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
private String title;
private String text;
}
lombok.Dataを使用すると、初回実行時に「アノテーションを有効にするか」を確認されるみたい
■フォームからの値取得とバリデーション
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
src/main/java/com/example/demo/request/TaskRequest.java
package com.example.demo.request;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serializable;
@Data
public class TaskRequest implements Serializable {
@NotEmpty(message = "タイトルを入力してください。")
@Size(max = 80, message = "タイトルは80文字以内で入力してください。")
private String title;
@NotEmpty(message = "テキストを入力してください。")
@Size(max = 80, message = "テキストは80文字以内で入力してください。")
private String text;
}
src/main/java/com/example/demo/service/TaskService.java
public void create(TaskRequest taskRequest) {
Task task = new Task();
task.setId(null);
task.setTitle(taskRequest.getTitle());
task.setText(taskRequest.getText());
taskRepository.save(task);
}
src/main/java/com/example/demo/controller/TaskController.java
@GetMapping(value = "/task/add")
String add(Model model) {
model.addAttribute("taskRequest", new Task());
return "task/add";
}
@PostMapping(value = "/task/create")
String create(@Validated @ModelAttribute TaskRequest taskRequest, BindingResult result) {
if (result.hasErrors()) {
return "task/add";
}
taskService.create(taskRequest);
return "redirect:/task/";
}
src/main/resources/templates/task/add.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Boot</title>
</head>
<body>
<h1>Spring Boot</h1>
<p>タスクを登録します。</p>
<form th:action="@{/task/create}" th:object="${taskRequest}" method="post">
<dl>
<dt>タイトル</dt>
<dd><input type="text" th:field="*{title}"><div th:if="${#fields.hasErrors('title')}" th:errors="*{title}" class="error">Title Error</div></dd>
<dt>テキスト</dt>
<dd><input type="text" th:field="*{text}"><div th:if="${#fields.hasErrors('text')}" th:errors="*{text}" class="error">Text Error</div></dd>
</dl>
<p><input type="submit" value="登録"></p>
</form>
</body>
</html>
「@{}」はURLパスを生成するもの
上の場合は「action="/task/create"」としても同じだが、パラメータを埋め込む場合はシンプルに書くことができる
「th:object」は、その要素内で使用するオブジェクトを設定できる
このオブジェクトからは「*{プロパティ名}」で値を取得できる
ここでは「*{title}」は「taskRequest.getTitle()」と解釈される
「#fields.hasErrors('プロパティ名')」でエラーの有無をチェックできる
「th:errors」はエラーメッセージを取得する。「*{プロパティ名}」の形式で、どのプロパティに対するエラーメッセージなのかを指定できる
http://localhost:8080/task/add にアクセスすると、タスクの登録画面が表示される
http://localhost:8080/task/ にアクセスすると、タスクが一覧表示される
バリデーションは「登録画面用」「編集画面用」など画面ごとに用意する方がいいかと思ったが、
以下によるとバリデーションのグループ化によって「登録時だけ実行するバリデーション」といった制御ができるみたい
【Spring Boot】バリデーション
https://b1san-blog.com/post/spring/spring-validation/
■共通レイアウトの作成
pom.xml
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
src/main/resources/templates/layout/frontend.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="UTF-8">
<title>Demo</title>
</head>
<body>
<header th:fragment="header">
<h1>Demo</h1>
</header>
<th:block layout:fragment="content"></th:block>
<footer th:fragment="footer">
<p><small>This is demo site.</small></p>
</footer>
</body>
</html>
src/main/resources/templates/home/index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/frontend}">
<head>
<title>Demo</title>
</head>
<body>
<th:block layout:fragment="content">
<main>
<p>ホーム。</p>
</main>
</th:block>
</body>
</html>
src/main/resources/templates/home/now.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/frontend}">
<head>
<title>現在時刻 | Demo</title>
</head>
<body>
<th:block layout:fragment="content">
<main>
<p>現在時刻は<span th:text="${time}"></span>です。</p>
</main>
</th:block>
</body>
</html>
src/main/resources/templates/task/index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/frontend}">
<head>
<title>タスク | Demo</title>
</head>
<body>
<th:block layout:fragment="content">
<main>
<p>タスクは以下のとおりです。</p>
<table>
<tr>
<th>ID</th>
<th>タイトル</th>
<th>テキスト</th>
</tr>
<tr th:each="task : ${tasks}">
<td th:text="${task.id}"></td>
<td th:text="${task.title}"></td>
<td th:text="${task.text}"></td>
</tr>
</table>
</main>
</th:block>
</body>
</html>
src/main/resources/templates/task/add.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/frontend}">
<head>
<title>タスク登録 | Demo</title>
</head>
<body>
<th:block layout:fragment="content">
<main>
<p>タスクを登録します。</p>
<form th:action="@{/task/create}" th:object="${taskRequest}" method="post">
<dl>
<dt>タイトル</dt>
<dd><input type="text" th:field="*{title}"><div th:if="${#fields.hasErrors('title')}" th:errors="*{title}" class="error">Title Error</div></dd>
<dt>テキスト</dt>
<dd><input type="text" th:field="*{text}"><div th:if="${#fields.hasErrors('text')}" th:errors="*{text}" class="error">Text Error</div></dd>
</dl>
<p><input type="submit" value="登録"></p>
</form>
</main>
</th:block>
</body>
</html>
共通レイアウトの詳細は、後述の「テンプレート(Thymeleaf)」を参照
■値の取得
■GETリクエストからの取得
コントローラーに以下のメソッドがあるとする
@GetMapping(value = "/dev/profile")
@ResponseBody
String profile() {
return "太郎さんは20歳です。";
}
ブラウザから以下のようにアクセスすると、画面に「太郎さんは20歳です。」と表示される
http://localhost:8080/dev/profile
コントローラーのメソッドを以下のようにすると
@GetMapping(value = "/dev/profile")
@ResponseBody
String profile(@RequestParam("name") String name, @RequestParam("age") int age) {
return "%sさんは%d歳です。".formatted(name, age);
}
ブラウザから以下のようにアクセスすると、画面に「Taroさんは20歳です。」と表示される
http://localhost:8080/dev/profile?name=Taro&age=20
RequestParamと割り当て先の変数名が同じなら、以下のように書くこともできる
@GetMapping(value = "/dev/profile")
@ResponseBody
String profile(String name, int age) {
return "%sさんは%d歳です。".formatted(name, age);
}
■URLパスからの取得
コントローラーのメソッドを以下のようにすると
@GetMapping(value = "/dev/profile/{name}/{age}")
@ResponseBody
String profile(@PathVariable("name") String name, @PathVariable("age") int age) {
return "%sさんは%d歳です。".formatted(name, age);
}
ブラウザから以下のようにアクセスすると、画面に「Taroさんは20歳です。」と表示される
http://localhost:8080/dev/profile/Taro/20
なお @PathVariable の指定を省略すると500エラーになった
■POSTリクエストからの取得
コントローラーのメソッドを以下のようにすると
@GetMapping(value = "/dev/form")
@ResponseBody
String form() {
return """
<html>
<head>
<title>Spring Boot</title>
</head>
<body>
<h1>Spring Boot</h1>
<form action="/dev/profile" method="post">
<dl>
<dt>名前</dt>
<dd><input type="text" name="name"></dd>
<dt>年齢</dt>
<dd><input type="text" name="age"></dd>
</dl>
<p><input type="submit" value="表示"></p>
</form>
</body>
</html>
""";
}
@PostMapping(value = "/dev/profile")
@ResponseBody
String profile(@RequestParam("name") String name, @RequestParam("age") int age) {
return "%sさんは%d歳です。".formatted(name, age);
}
ブラウザから以下のようにアクセスすると入力画面が表示され、その入力内容に従って「Taroさんは20歳です。」などと表示できる
http://localhost:8080/dev/form
RequestParamと割り当て先の変数名が同じなら、以下のように書くこともできる
@PostMapping(value = "/dev/profile")
@ResponseBody
String profile(String name, int age) {
return "%sさんは%d歳です。".formatted(name, age);
}
以下のようにして、フォームから各値を取得できる
@GetMapping(value = "/dev/form")
@ResponseBody
String form() {
return """
<html>
<head>
<title>Spring Boot</title>
</head>
<body>
<h1>Spring Boot</h1>
<form action="/dev/profile" method="post">
<dl>
<dt>名前</dt>
<dd><input type="text" name="name"></dd>
<dt>郵便番号</dt>
<dd><input type="text" name="zipcode"> - <input type="text" name="zipcode"></dd>
<dt>職業</dt>
<dd>
<input type="radio" name="job" value="会社員"> 会社員<br>
<input type="radio" name="job" value="自営業"> 自営業<br>
<input type="radio" name="job" value="学生"> 学生<br>
<input type="radio" name="job" value="無職"> 無職
</dd>
<dt>興味のある分野</dt>
<dd>
<input type="checkbox" name="interest" value="HTML"> HTML<br>
<input type="checkbox" name="interest" value="CSS"> CSS<br>
<input type="checkbox" name="interest" value="JavaScript"> JavaScript
</dd>
<dt>お問い合わせ項目</dt>
<dd>
<select name="subject">
<option value="">選択してください</option>
<option value="商品について">商品について</option>
<option value="返品について">返品について</option>
<option value="その他">その他</option>
</select>
</dd>
<dt>お問い合わせ内容</dt>
<dd><textarea name="inquiry" rows="10" cols="50"></textarea></dd>
</dl>
<p><input type="submit" value="表示"></p>
</form>
</body>
</html>
""";
}
@PostMapping(value = "/dev/profile")
@ResponseBody
String profile(String name, String zipcode[], String job, String interest[], String subject, String inquiry) {
return "名前 = %s、郵便番号 = %s、職業 = %s、興味のある分野 = %s、お問い合わせ項目 = %s、お問い合わせ内容 = %s".formatted(
name,
String.join("-", zipcode),
job,
Arrays.toString(interest),
subject,
inquiry
);
}
■POSTリクエストからの取得(@ModelAttributeを使った取得)
上記「POSTリクエストからの取得」の方法は、入力項目の数が多くなると比例して引数が増えて可読性が下がる
このような場合、@ModelAttribute と lombok.Data を使うことで簡潔に記述できる
まずは以下のように src/main/java/com/example/demo/request/ProfileRequest.java として、リクエストを受け取るためのクラスを作成する
package com.example.demo.request;
import lombok.Data;
@Data
public class ProfileRequest {
private String name;
private String[] zipcode;
private String job;
private String[] interest;
private String subject;
private String inquiry;
}
以下のようにして値を受け取ることができる
@PostMapping(value = "/dev/profile")
@ResponseBody
String profile(@ModelAttribute ProfileRequest profileRequest) {
return "名前 = %s、郵便番号 = %s、職業 = %s、興味のある分野 = %s、お問い合わせ項目 = %s、お問い合わせ内容 = %s".formatted(
profileRequest.getName(),
String.join("-", profileRequest.getZipcode()),
profileRequest.getJob(),
Arrays.toString(profileRequest.getInterest()),
profileRequest.getSubject(),
profileRequest.getInquiry()
);
}
■補足
@GetMapping や @PostMapping は、@RequestMapping と書くことでGET/POSTの両方に対応させることができる
■データベース(Spring Data JPA)
Spring Data JPA について
【Spring Data JPA】基本操作とクエリ実装
https://b1san-blog.com/post/spring/spring-jpa/
[Spring Boot] JPAでデータベースに接続 | DevelopersIO
https://dev.classmethod.jp/articles/use_spring-boot-jpa-jpql/
Spring Data JPAを使用したDBアクセス - アルファテックブログ
https://www.alpha.co.jp/blog/202209_01
Spring Boot入門:MySQLにSpring Boot アプリケーションからアクセスする - あるSEのつぶやき・改
https://www.aruse.net/entry/2019/08/04/003838
■マイグレーション(Flyway)
※未検証
Flywayを導入するのが定番の方法みたい
SpringBoot FlywayでDBのマイグレーションをする方法 - Web系開発メモ
https://web-dev.hatenablog.com/entry/spring-boot/intro/flyway
FlywayでSpringBootプロジェクトのDBマイグレーション - 【Spring Hack】
https://springhack.com/flyway%E3%81%A7springboot%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%...
Spring BootにDBマイグレーションツール「flyway」を導入する - ブログ - 株式会社Smallit(スモーリット)
https://smallit.co.jp/blog/687/
■テンプレート(Thymeleaf)
Thymeleaf(タイムリーフ)について
Thymeleafとは?基本構文・Spring Bootでの使い方を解説 | プログラミングを学ぶならトレノキャンプ(TRAINOCAMP)
https://camp.trainocate.co.jp/magazine/about-thymeleaf/
Thymeleaf if文 条件分岐を行うサンプル | ITSakura
https://itsakura.com/thymeleaf-if-loop
Spring Boot + Thymeleafで詳細画面を作成する - ITを分かりやすく解説
https://medium-company.com/spring-boot-thymeleaf%E3%81%A7%E8%A9%B3%E7%B4%B0%E7%94%BB%E9%9D%A2%E3%82%...
■インラインテキスト
一例だが、以下のようにして変数の内容を出力すると、
<p>現在時刻は<span th:text="${time}"></span>です。</p>
以下のように出力される
<p>現在時刻は<span>19:17:45</span>です。</p>
余計なspanが残されているため、多くの情報を埋め込むと出力HTMLがspanだらけになりそうだが、回避する方法はいくつか用意されている
以下のコードは、いずれもspanを出力しない
<p>現在時刻は<span th:text="${time}" th:remove="tag"></span>です。</p>
<p>現在時刻は<th:block th:text="${time}"></th:block>です。</p>
<p>現在時刻は[[${time}]]です。</p>
上から順に
・「th:remove="tag"」を指定する
・「th:block」という疑似的なタグを使用する
・「[[〜]]」という形式で指定する
という方法になっている
[thymeleaf]spanタグ等で囲まないとテキストが表示できない?
https://teratail.com/questions/2558
Tutorial: Using Thymeleaf (ja)
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf_ja.html#%E6%93%AC%E4%BC%BC%E7%9A%84%E3%81...
Thymeleafとは?基本構文・Spring Bootでの使い方を解説 | プログラミングを学ぶならトレノキャンプ(TRAINOCAMP)
https://camp.trainocate.co.jp/magazine/about-thymeleaf/
Thymeleafチートシート - Qiita
https://qiita.com/NagaokaKenichi/items/c6d1b76090ef5ef39482
また以下のようにすると、値を直接指定して出力できる
<p>現在時刻は<span th:text="'正午'" th:remove="tag"></span>です。</p>
<p>現在時刻は[['正午']]です。</p>
値部分には、以下のようにプログラムを埋め込むことができる
<p>現在時刻は<span th:text="${new java.util.Date().toString()}" th:remove="tag"></span>です。</p>
<p>現在時刻は[[${new java.util.Date().toString()}]]です。</p>
■オブジェクトの値参照
Getterメソッドを用意しておけば、オブジェクトのフィールドを直接扱うことができる
【Spring Boot】「Thymeleaf(タイムリーフ)」で画面にまとまった値を渡そう。 | プログラミングマガジン
http://www.code-magagine.com/?p=2935
■共通化
replaceを使うことで、ヘッダやフッタを共通化して読み込ませることができる
Thymeleafを使ってheaderとheadとfooterの共通化 - エキサイト TechBlog.
https://tech.excite.co.jp/entry/2022/08/08/162241
例えば templates/common/frontend.html として以下を作成すると、
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<header th:fragment="header">
<h1>Demo</h1>
</header>
<footer th:fragment="footer">
<p><small>This is demo site.</small></p>
</footer>
</html>
templates/home/index.html として以下のように呼び出すことができる
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Demo</title>
</head>
<body>
<th:block th:replace="~{common/frontend::header}"></th:block>
<main>
<p>これはトップページです。</p>
</main>
<th:block th:replace="~{common/frontend::footer}"></th:block>
</body>
</html>
「{common/frontend::header}」で指定した部分にはheaderタグの部分が、
「{common/frontend::footer}」で指定した部分にはfooterタグの部分が、それぞれ表示される
■レイアウト
layoutを使うことで、基本のレイアウトを作ることができる
ただしthymeleafの標準機能では対応していないので、追加でthymeleaf-layout-dialectを導入する必要がある
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
共通のレイアウトとして、templates/layout/frontend.html を作成するものとする
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="UTF-8">
<title>Demo</title>
</head>
<body>
<header th:fragment="header">
<h1>Demo</h1>
</header>
<th:block layout:fragment="content"></th:block>
<footer th:fragment="footer">
<p><small>This is demo site.</small></p>
</footer>
</body>
</html>
以下のようにして呼び出すと、「layout:fragment="content"」で指定した部分に組み込まれて表示される
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/frontend}">
<body>
<th:block layout:fragment="content">
<main>
<p>これはトップページです。</p>
</main>
</th:block>
</body>
</html>
以下のようにタイトルを定義すると、それが優先して使用される(ただし「 | Demo」をレイアウト側で管理するなど、もっと効率的な方法がありそうな)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/frontend}">
<head>
<title>概要 | Demo</title>
</head>
<body>
<th:block layout:fragment="content">
<main>
<p>これは概要です。</p>
</main>
</th:block>
</body>
</html>
Spring BootとThymeleafでLayoutを共通化する方法 | ホームページ制作のサカエン Developer's Blog
https://www.saka-en.com/java/spring-boot-thymeleaf-layout/
SpringBoot 共通レイアウトを作成(Thymeleaf) | ITSakura
https://itsakura.com/springboot-layout
『Spring Framework 超入門』のレイアウト化の作成がうまく出てこないときの対処法 - yucatio@システムエンジニア
https://yucatio.hatenablog.com/entry/2022/02/09/225705
■静的コンテンツ
src/main/resources 内に static か public ディレクトリを作成て、静的コンテンツを配置できる
Thymeleafで外部のCSSファイルを読み込む方法 - ミルラク
https://miruraku.com/java/thymeleaf/css/
SpringBoot 静的コンテンツを返す方法(html, js, css など) - Web系開発メモ
https://web-dev.hatenablog.com/entry/spring-boot/intro/response-static-content
■セッション
断片だが、以下のようなコードでセッションによるアクセスカウンタができる
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
@Autowired
private HttpSession session;
@GetMapping(value = "/dev/session")
@ResponseBody
String session() {
int count = 0;
if (session.getAttribute("count") != null) {
count = (int) session.getAttribute("count");
}
session.setAttribute("count", ++count);
return "count=" + count;
}
【Spring Boot】セッションとCookie
https://b1san-blog.com/post/spring/spring-session/
■ログ
「System.out.println("TEST");」だと出力できないみたい
専用のLoggerクラスを使用する
一例だが以下のようにコントローラーで定義し、
public class AdminController {
Logger logger = LoggerFactory.getLogger(AdminController.class);
以下のように使用するとログを出力できる
logger.info("TEST");
【Spring Boot】ログ出力(Logback)
https://b1san-blog.com/post/spring/spring-log/
■セキュリティ
■XSS対策
Thymeleafの標準機能で対応されている
以下のようにすると、HTMLがエスケープして出力される
[[${text}]]
以下のようにすると、HTMLがそのまま出力される
[(${text})]
クロスサイトスクリプティング(XSS)のテスト - Qiita
https://qiita.com/oh_yeah_sayryo/items/4e39995e47e371953480
■CSRF対策
Spring Boot Starter Securityを導入することで対応できる
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
src/main/java/com/example/demo/config/SecurityConfig.java
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll();
return http.build();
}
}
実行すると、コンソールに以下が出力された。これは認証用のパスワードらしいが、詳細は改めて確認しておきたい
Using generated security password: 44294d32-d86a-465c-80a3-7e7405cdd084
This generated password is for development use only. Your security configuration must be updated before running your application in production.
上記は「すべてのページで認証を求めない」という指定だが、この時点でCSRF対策が行われている
投稿画面のformタグに、「_csrf」を送信するためのコードが追加されていることを確認できる
<form action="/dev/task/create" method="post">
↓
<form action="/dev/task/create" method="post"><input type="hidden" name="_csrf" value="JYbAnNEHGeO4mA8MhnQA_s9CKsESkNtfjsTADk3T2mkBKvq6R7T1-edhL4eVrz404lk0mPl2B_gioeNy6POmb3Tku1ljHMOD"/>
SpringSecurityの導入/CSRF対策|Javaの基礎を学び終えたアナタに贈る, SpringBoot/SpringSecurityによる掲示板開発ハンズオン
https://zenn.dev/angelica/books/52be1e365c61ea/viewer/f0683a
Basic認証を使用しない場合、以下の方法でパスワードの出力を無くすことができるらしい
【Spring Security】Basic認証いらないけど、良い感じにデフォルトのレスポンスヘッダーを使いたい時に見る記事 - Qiita
https://qiita.com/akkino_D-En/items/d506fa24349a0cbd414d
以下は参考になるかと思ったが情報が古いか
【逆引き】Spring Security(随時更新) - Qiita
https://qiita.com/mr-hisa-child/items/173289e47b4f21022562
■認証
Spring Boot Starter Securityを導入することで対応できる
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
src/main/java/com/example/demo/config/SecurityConfig.java
package com.example.demo.config;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//http.authorizeRequests().anyRequest().permitAll();
http
.formLogin((form) -> form
// 認証を行うURL
.loginProcessingUrl("/admin/")
// ログインページのURL
.loginPage("/admin/")
// ログインユーザ名の項目
.usernameParameter("username")
// ログインパスワードの項目
.passwordParameter("password")
// 認証に成功したときにリダイレクトするURL
.defaultSuccessUrl("/admin/home")
// 認証に失敗したときにリダイレクトするURL
.failureUrl("/admin/?error")
// すべてのユーザがアクセス可能
.permitAll()
)
.logout((logout) -> logout
// ログアウトページのURL
.logoutUrl("/admin/logout")
// ログアウトしたときにリダイレクトするURL
.logoutSuccessUrl("/admin/")
)
.authorizeHttpRequests((requests) -> requests
// CSSなどはログイン無しでもアクセス可能
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
// ログイン無しでもアクセス可能なURL
.requestMatchers("/", "/now", "/task/**", "/dev/**").permitAll()
// 「/admin/」へのアクセスには「ADMIN」権限が必要
.requestMatchers("/admin/**").hasRole("ADMIN")
// 他のページはすべて認証が必要
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
User.UserBuilder users = User.withDefaultPasswordEncoder();
// 認証情報1
UserDetails admin = users
.username("admin")
.password("abcd1234")
.roles("ADMIN")
.build();
// 認証情報2
UserDetails guest = users
.username("guest")
.password("abcd1234")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(admin, guest);
}
}
src/main/java/com/example/demo/controller/AdminController.java
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class AdminController {
@GetMapping(value = "/admin/")
String index() {
return "admin/index";
}
@GetMapping(value = "/admin/home")
String home() {
return "admin/home";
}
}
templates/layout/backend.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="UTF-8">
<title>Admin</title>
</head>
<body>
<header th:fragment="header">
<h1>Demo</h1>
</header>
<th:block layout:fragment="content"></th:block>
<footer th:fragment="footer">
<p><small>This is demo site.</small></p>
</footer>
</body>
</html>
src/main/resources/templates/admin/index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/backend}">
<head>
<title>ログイン | Admin</title>
</head>
<body>
<th:block layout:fragment="content">
<main>
<p>ログインページ。</p>
<p th:if="${param.error}">ユーザ名またはパスワードが異なります。</p>
<p th:if="${param.logout}">ログアウトしました。</p>
<form th:action="@{/admin/}" method="post">
<dl>
<dt>ユーザ名</dt>
<dd><input type="text" name="username" value=""></dd>
<dt>テキスト</dt>
<dd><input type="password" name="password" value=""></dd>
</dl>
<p><input type="submit" value="ログイン"></p>
</form>
</main>
</th:block>
</body>
</html>
src/main/resources/templates/admin/home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/backend}">
<head>
<title>ホーム | Admin</title>
</head>
<body>
<th:block layout:fragment="content">
<main>
<p>管理画面にログインしています。</p>
<form th:action="@{/admin/logout}" method="post">
<p><input type="submit" value="ログアウト"></p>
</form>
</main>
</th:block>
</body>
</html>
これで
http://localhost:8080/ は通常どおりアクセスでき、
http://localhost:8080/admin/ から先に進もうとすると認証を求められる
認証を求めるページや認証情報は、SecurityConfig.java で指定している
「/admin/**」のようなURL指定については以下を参照
アスタリスク1つだと同階層のみ対象だが、アスタリスク2つだと指定階層以下が対象となる
antMatchersのワイルドカードについて - てのひら
https://www.tenohira.xyz/tech/spring-antmatchers-wildcard/
アクセスできるURLは以下のようにしている
「原則ログインが必要なシステム。ログイン画面など一部ページのみ誰でもアクセスできる」という場合はこれで良さそうだが、
.authorizeHttpRequests((requests) -> requests
// CSSなどはログイン無しでもアクセス可能
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
// ログイン無しでもアクセス可能なURL
.requestMatchers("/", "/now", "/task/**", "/dev/**").permitAll()
// 「/admin/」へのアクセスには「ADMIN」権限が必要
.requestMatchers("/admin/**").hasRole("ADMIN")
// 他のページはすべて認証が必要
.anyRequest().authenticated()
);
「/admin/ は管理者用ページ。他は誰でもアクセスできるページ」という場合、以下くらいの指定でもいいかもしれない
.authorizeHttpRequests((requests) -> requests
// 「/admin/」へのアクセスには「ADMIN」権限が必要
.requestMatchers("/admin/**").hasRole("ADMIN")
// 他のページはすべてアクセス可能
.anyRequest().permitAll()
);
ログイン、ログアウトの実装|Javaの基礎を学び終えたアナタに贈る, SpringBoot/SpringSecurityによる掲示板開発ハンズオン
https://zenn.dev/angelica/books/52be1e365c61ea/viewer/1c1ebe
Spring Boot ログイン画面 - 公式サンプルコード
https://spring.pleiades.io/guides/gs/securing-web/
Spring Boot でSpring Securityを使ってみる(SecurityFilterChain) - Qiita
https://qiita.com/TaikiTkwkbysh/items/9e2a7dfdf9f0bed9e779
インメモリ認証 :: Spring Security - リファレンス
https://spring.pleiades.io/spring-security/reference/servlet/authentication/passwords/in-memory.html
Spring Boot にて Spring Security を使用した直後の認証の仕組みをソースコードとともに確認する
https://zenn.dev/kiyotatakeshi/articles/fc593c768ad7e0
SpringSecurityの導入/CSRF対策|Javaの基礎を学び終えたアナタに贈る, SpringBoot/SpringSecurityによる掲示板開発ハンズオン
https://zenn.dev/angelica/books/52be1e365c61ea/viewer/f0683a
timestamp status 999 error none message no message available - Qiita
https://qiita.com/TBATYOF/items/4e87a6297cc78f43e842
WebSecurityConfigurerAdapter を継承するのは古い方法で、SecurityFilterChain を定義するのが新しい方法らしい
WebSecurityConfigurerAdapterが非推奨になってた
https://volkruss.com/posts/p2691/
Spring Security 5.4〜6.0でセキュリティ設定の書き方が大幅に変わる件 - Qiita
https://qiita.com/suke_masa/items/908805dd45df08ba28d8
最新の6.0で学ぶ!初めてのひとのためのSpring Security | ドクセル
https://www.docswell.com/s/MasatoshiTada/KGVY9K-spring-security-intro
以下は古い書き方だが参考までに
Spring Securityの認証のサンプル(SpringBoot) | ITSakura
https://itsakura.com/spring-security-boot
【SpringBoot入門】Spring Securityで認証機能 - Qiita
https://qiita.com/morioheisei/items/ecfeee4860944d0b4a3b
Spring Boot入門:Spring Securityで認証と認可 | ツチヤの備忘録
https://www.tsuchiya.blog/spring-boot-step7/
■メール送信
Spring Boot Starter Mailを導入することで対応できる
メールを送信するためのSMTP情報は、あらかじめ用意しておく
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
application.properties (ロリポップの場合)
spring.mail.host=smtp.lolipop.jp
spring.mail.port=587
spring.mail.username=ロリポップのメールアドレス
spring.mail.password=ロリポップのメールパスワード
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
application.properties (Gmailの場合)
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=Gmailのメールアドレス
spring.mail.password=Gmailのアプリパスワード
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
src/main/java/com/example/demo/controller/HomeController.java
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.MailException;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.time.LocalDateTime;
@Controller
public class HomeController {
@Autowired
private final MailSender mailSender;
public HomeController(MailSender mailSender) {
this.mailSender = mailSender;
}
@GetMapping(value = "/")
String index() {
return "home/index";
}
@GetMapping(value = "/now")
String now(Model model) {
model.addAttribute("time", LocalDateTime.now());
return "home/now";
}
@GetMapping(value = "/mail")
String mail() {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("from@example.com");
message.setTo("to@example.com");
message.setSubject("SpringBootからのメール送信");
message.setText("テスト。\r\nこれはSpringBootからのメール送信です。");
try {
mailSender.send(message);
} catch (MailException e) {
e.printStackTrace();
}
return "home/mail";
}
}
templates/home/mail.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/frontend}">
<head>
<title>メール | Demo</title>
</head>
<body>
<th:block layout:fragment="content">
<main>
<p>メールを送信しました。</p>
</main>
</th:block>
</body>
</html>
これで
http://localhost:8080/mail にアクセスするとメールが送信される
SpringBoot メール送信のサンプル | ITSakura
https://itsakura.com/sb-mailsend
【Spring Boot】メール送信
https://b1san-blog.com/post/spring/spring-mail/
■IPアドレスを取得
一例だがコントローラーで以下のように取得し、
@GetMapping(value = "/dev/")
String index(Model model, HttpServletRequest request) {
model.addAttribute("remoteAddr", request.getRemoteAddr());
model.addAttribute("xForwardedFor", request.getHeader("X-Forwarded-For"));
return "dev/index";
}
テンプレートで以下のように表示できる
<p>remoteAddrは [[${remoteAddr}]] です。</p>
<p>X-Forwarded-Forは [[${xForwardedFor}]] です。</p>
ローカル開発環境では、以下のように表示される
remoteAddrは 0:0:0:0:0:0:0:1 です。
X-Forwarded-Forは です。
本番環境を想定したNginx+SpringBoot環境では、以下のように表示される(この時点ではX-Forwarded-Forの値が表示されていない)
remoteAddrは 127.0.0.1 です。
X-Forwarded-Forは です。
/etc/nginx/conf.d/https.conf で以下を追加すると
proxy_set_header X-forwarded-For $proxy_add_x_forwarded_for;
以下のように表示される(X-Forwarded-Forの値も表示される)
remoteAddrは 127.0.0.1 です。
X-Forwarded-Forは 203.0.113.1 です。
Spring Bootでリクエストを送ったクライアントのIPアドレスを取得してみた|ITエンジニアとして経験・学習したこと
https://www.purin-it.com/spring-boot-show-ipaddress
Java(Spring)でクライアントのIPアドレスを取得する方法 - yu_memorandumの日記
https://yu-memorandum.hatenablog.com/entry/2020/08/17/004251
こんばんは、X-Forwarded-For警察です - エムスリーテックブログ
https://www.m3tech.blog/entry/x-forwarded-for
■ログイン時にSSLなしになるので調査
「ブラウザ -443-> nginx -8080-> Spring Boot」という環境でログインボタンを押すと、SSLなしのURLにリダイレクトされるので調査
■設定ファイルを調整
application-production.properties に以下を追加するも効果なし
server.tomcat.remoteip.remote-ip-header=x-forwarded-for
server.tomcat.remoteip.protocol-header=x-forwarded-proto
Elastic Beanstalk(Java) + Spring Boot + https - Qiita
https://qiita.com/peko_kun/items/5d33c9660f9b90cdd231
Spring Boot に入門した話〜架空のキャンプ場の予約アプリを Elastic Beanstalk にデプロイするまで〜?デプロイ編 - sgyatto's blog
https://sgyatto.hatenablog.com/entry/2021/08/20/002610
■Beanを設定
Beanを設定するとあるが、具体的にどこに設定するのか判らず
リバースプロキシサーバーがいるときのSpring BootのリダイレクトURL作成 - Qiita
https://qiita.com/nogitsune413/items/f47d07f2250c874e6e3e
Forwarded Headers Example in Spring - Code Tinkering
https://codetinkering.com/spring-forwarded-headers-example/
■nginxでproxy_set_headerを追加
nginxの /etc/nginx/conf.d/https.conf にproxy_set_headerの指定を追加するも効果なし
location / {
proxy_pass
http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
# 以下を追加
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Remote-Addr $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-forwarded-For $proxy_add_x_forwarded_for;
# nginx -t
# systemctl restart nginx
# systemctl status nginx
Spring-bootでSSL(HTTPS)有効にするならnginxを使え〜完結編〜 - Apitore blog
https://blog.apitore.com/2016/07/25/spring-boot-ssl-nginx/
■常にSSLへ転送
vi /etc/nginx/conf.d/https.conf に以下を追加して一応ログインできるようになるが、二重にリダイレクトされている(いったんHTTPに飛ばされ、それからHTTPSにリダイレクトされる)
# ファイルの先頭に以下を追加
server {
listen 80;
listen [::]:80;
return 301 https://$host$request_uri;
}
【SSL対応】AWS EC2 + SpringBoot + Nginx + Let’s EncryptでHTTPS対応 | エンジニアの本気で稼ぐ不労所得戦略
https://1-lifengine.com/ec2_spring_nginx_https
■現状の対応
Spring boot redirect のdefaultはhttpになることを認識しておく - Qiita
https://qiita.com/yukiyoshimura/items/29785c1caa256be69500
application.properties で以下のように定義しておく(開発環境を想定)
application.url=http://localhost:8080
application-production.properties では以下のように定義しておく(本番環境を想定)
application.url=https://refirio.net
リダイレクトを行う個所は、すべて以下のように変更する
return "redirect:/task/";
↓
return "redirect:" + environment.getProperty("application.url") + "/task/";
securityFilterChain は一例だが以下のようにする(リダイレクトのURLを調整している)
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//http.authorizeRequests().anyRequest().permitAll();
http
.formLogin((form) -> form
// 認証を行うURL
.loginProcessingUrl("/admin/login")
// ログインページのURL
.loginPage("/admin/")
// ログインユーザ名の項目
.usernameParameter("username")
// ログインパスワードの項目
.passwordParameter("password")
// 認証に成功したときにリダイレクトするURL
.defaultSuccessUrl(environment.getProperty("application.url") + "/admin/home")
// 認証に失敗したときにリダイレクトするURL
.failureUrl(environment.getProperty("application.url") + "/admin/?error")
// すべてのユーザがアクセス可能
.permitAll()
)
.logout((logout) -> logout
// ログアウトページのURL
.logoutUrl("/admin/logout")
// ログアウトしたときにリダイレクトするURL
.logoutSuccessUrl(environment.getProperty("application.url") + "/admin/?logout")
)
.authorizeHttpRequests((authorize) -> authorize
// CSSなどはログイン無しでもアクセス可能
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
// ログイン無しでもアクセス可能なURL
.requestMatchers("/", "/error", "/dev/**", "/admin/", "/admin/login").permitAll()
// 「/admin/」へのアクセスには「ADMIN」権限が必要
.requestMatchers("/admin/**").hasRole("ADMIN")
// 他のページはすべて認証が必要
.anyRequest().authenticated()
);
return http.build();
}
nginxの /etc/nginx/conf.d/https.conf で、SSLを強制しておく
server {
listen 80;
listen [::]:80;
return 301 https://$host$request_uri;
}
これでSSLを維持できる
※「ログイン無しでもアクセス可能なURL」に「/error」が無いと、ログインを試みる際、
https://refirio.net/error?continue に遷移しようとしてしまう
ただし一度管理画面を表示すると、以降は大丈夫になる
謎な挙動だが、ひとまず上記のように書くことで回避できている
改めて調査したい
↑MySQL側でのユニークエラーが発生したときなども、エラー画面を表示しようとしてログイン画面が表示されることがあるみたい
ひとまず「常に /error を含めておく」とするのが無難か
Spring Boot で 404 Not Found などのエラーが発生した際の表示をカスタマイズする - Qiita
https://qiita.com/niwasawa/items/f3479ef16efa488039fb
もっとスマートな方法があるか、引き続き確認したい
Spring boot redirect のdefaultはhttpになることを認識しておく - Qiita
https://qiita.com/yukiyoshimura/items/29785c1caa256be69500
IPアドレスなどについては、改めて以下などを確認しておきたい
Spring Bootでリダイレクト先のURLを組み立てる - Qiita
https://qiita.com/rubytomato@github/items/8d132dec042f695e50f6
■その他
Spring Bootでリダイレクト先のURLを組み立てる - Qiita
https://qiita.com/rubytomato@github/items/8d132dec042f695e50f6
java - Setup Nginx + SSL + Spring MVC + Security - Stack Overflow
https://stackoverflow.com/questions/66833026/setup-nginx-ssl-spring-mvc-security
Spring Security 18. HTTPS にリダイレクト - リファレンス
https://spring.pleiades.io/spring-security/site/docs/5.1.7.RELEASE/reference/html/webflux-redirect-h...
Spring Boot 本番対応機能 - リファレンスドキュメント
https://spring.pleiades.io/spring-boot/docs/current/reference/html/actuator.html
■引き続き
■データの登録編集削除表示(機能の実装)
以下のテーブルを作成し、登録編集削除表示の各機能を作成してみる
CREATE TABLE authors(
id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '代理キー',
created DATETIME NOT NULL COMMENT '作成日時',
modified DATETIME NOT NULL COMMENT '更新日時',
code VARCHAR(255) NOT NULL UNIQUE COMMENT 'コード',
name VARCHAR(255) NOT NULL COMMENT '名前',
kana VARCHAR(255) COMMENT 'カナ',
PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '著者';
src/main/java/com/example/demo/entity/Author.java を新規に作成
package com.example.demo.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.util.Date;
@Entity
@Table(name="authors")
@Data
public class Author {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false, updatable = false)
private Date created;
@Column(nullable = false)
private Date modified;
@Column(unique = true, nullable = false)
private String code;
@Column(nullable = false)
private String name;
private String kana;
}
src/main/java/com/example/demo/request/AuthorSearchRequest.java を新規に作成
package com.example.demo.request;
import lombok.Data;
import java.io.Serializable;
@Data
public class AuthorSearchRequest implements Serializable {
private String code;
private String name;
}
src/main/java/com/example/demo/request/AuthorSaveRequest.java を新規に作成
package com.example.demo.request;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serializable;
@Data
public class AuthorSaveRequest implements Serializable {
private int id;
@NotEmpty(message = "コードを入力してください。")
@Size(max = 20, message = "コードは20文字以内で入力してください。")
private String code;
@NotEmpty(message = "名前を入力してください。")
@Size(max = 80, message = "名前は80文字以内で入力してください。")
private String name;
@Size(max = 80, message = "カナは80文字以内で入力してください。")
private String kana;
}
src/main/java/com/example/demo/request/AuthorDeleteRequest.java を新規に作成
package com.example.demo.request;
import lombok.Data;
import java.io.Serializable;
@Data
public class AuthorDeleteRequest implements Serializable {
private int id;
}
src/main/java/com/example/demo/repository/AuthorRepository.java を新規に作成
package com.example.demo.repository;
import com.example.demo.entity.Author;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface AuthorRepository extends JpaRepository<Author, Integer> {
Page<Author> findByNameLikeOrKanaLike(String name, String kana, Pageable pageable);
boolean existsByCode(String code);
boolean existsByCodeAndIdNot(String code, Integer id);
}
【Spring Data JPA】自動実装されるメソッドの命名ルール - Qiita
https://qiita.com/shindo_ryo/items/af7d12be264c2cc4b252
src/main/java/com/example/demo/service/AuthorService.java を新規に作成
package com.example.demo.service;
import com.example.demo.entity.Author;
import com.example.demo.repository.AuthorRepository;
import com.example.demo.request.AuthorSaveRequest;
import lombok.AllArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import java.util.Date;
import java.util.List;
@Service
@AllArgsConstructor
public class AuthorService {
private final AuthorRepository authorRepository;
public List<Author> select() {
List<Author> authors = authorRepository.findAll();
return authors;
}
public Page<Author> selectByPage(Pageable pageable) {
Page<Author> authors = authorRepository.findAll(pageable);
return authors;
}
public Author selectById(int id) {
Author author = authorRepository.findById(id).get();
return author;
}
public void create(AuthorSaveRequest authorSaveRequest) {
Author author = new Author();
author.setId(null);
author.setCreated(new Date());
author.setModified(new Date());
author.setCode(authorSaveRequest.getCode());
author.setName(authorSaveRequest.getName());
author.setKana(authorSaveRequest.getKana());
authorRepository.save(author);
}
public void update(AuthorSaveRequest authorSaveRequest) {
Author author = new Author();
author.setId(authorSaveRequest.getId());
author.setModified(new Date());
author.setCode(authorSaveRequest.getCode());
author.setName(authorSaveRequest.getName());
author.setKana(authorSaveRequest.getKana());
authorRepository.saveAndFlush(author);
}
public void deleteById(int id) {
authorRepository.deleteById(id);
}
public Page<Author> searchByPage(String name, Pageable pageable) {
Page<Author> authors = authorRepository.findByNameLikeOrKanaLike("%" + name + "%", "%" + name + "%", pageable);
return authors;
}
public boolean isValid(AuthorSaveRequest authorSaveRequest, BindingResult result) {
boolean valid = true;
if (authorSaveRequest.getId() == 0) {
if (authorRepository.existsByCode(authorSaveRequest.getCode())) {
result.addError(new FieldError(result.getObjectName(), "code", "すでに登録されています。"));
valid = false;
}
} else {
if (authorRepository.existsByCodeAndIdNot(authorSaveRequest.getCode(), authorSaveRequest.getId())) {
result.addError(new FieldError(result.getObjectName(), "code", "すでに登録されています。"));
valid = false;
}
}
return valid;
}
}
src/main/resources/templates/author/index.html を新規に作成
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/frontend}">
<head>
<title>著者 | Demo</title>
</head>
<body>
<th:block layout:fragment="content">
<main>
<p><a href="/author/form">著者を追加</a></p>
<form th:action="@{/author/}" th:object="${authorSearchRequest}" method="get">
<dl>
<dt>名前</dt>
<dd><input type="text" size="20" name="name" th:value="*{name}"></dd>
</dl>
<p><input type="submit" value="検索"></p>
</form>
<table>
<tr>
<th>ID</th>
<th>コード</th>
<th>名前</th>
<th>カナ</th>
</tr>
<tr th:each="author : ${authors}">
<td><a th:href="@{/author/view/__${author.id}__}">[[${author.id}]]</a></td>
<td>[[${author.code}]]</td>
<td>[[${author.name}]]</td>
<td>[[${author.kana}]]</td>
</tr>
</table>
<div>
<p th:text="|全${authors.getTotalPages()}ページ中${authors.getNumber() + 1}ページ目を表示中|"></p>
<ul>
<li>
<span th:if="${authors.isFirst()}"><前</span>
<a th:unless="${authors.isFirst()}" th:href="@{/author/(name=${authorSearchRequest.name}, page=${authors.getNumber() - 1})}"><前</a>
</li>
<li th:each="i : ${#numbers.sequence(0, authors.getTotalPages() - 1)}">
<span th:if="${i == authors.getNumber()}" th:text="${i + 1}"></span>
<a th:if="${i != authors.getNumber()}" th:href="@{/author/(name=${authorSearchRequest.name}, page=${i})}" th:text="${i + 1}"></a>
</li>
<li>
<span th:if="${authors.isLast()}">次></span>
<a th:unless="${authors.isLast()}" th:href="@{/author/(name=${authorSearchRequest.name}, page=(${authors.getNumber() + 1}))}">次></a>
</li>
</ul>
</div>
</main>
</th:block>
</body>
</html>
「__${author.id}__」は値を埋め込むための書き方
これで「author.id」が「1」なら「/author/view/1」というURLが作成される
src/main/resources/templates/author/form.html を新規に作成
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/frontend}">
<head>
<title>著者 | Demo</title>
</head>
<body>
<th:block layout:fragment="content">
<main>
<p>著者を登録します。</p>
<form th:action="@{/author/form}" th:object="${authorSaveRequest}" method="post">
<dl>
<dt>コード</dt>
<dd><input type="text" name="code" size="10" th:value="*{code}"><div th:if="${#fields.hasErrors('code')}" th:errors="*{code}" class="error">Code Error</div></dd>
<dt>名前</dt>
<dd><input type="text" name="name" size="20" th:value="*{name}"><div th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="error">Name Error</div></dd>
<dt>カナ</dt>
<dd><input type="text" name="kana" size="20" th:value="*{kana}"><div th:if="${#fields.hasErrors('kana')}" th:errors="*{kana}" class="error">Kana Error</div></dd>
</dl>
<p><input type="submit" value="登録"></p>
</form>
</main>
</th:block>
</body>
</html>
src/main/resources/templates/author/view.html を新規に作成
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/frontend}">
<head>
<title>著者 | Demo</title>
</head>
<body>
<th:block layout:fragment="content">
<main>
<p>著者を編集します。</p>
<form th:action="@{/author/view/__*{id}__}" th:object="${authorSaveRequest}" method="post">
<input type="hidden" name="id" th:value="*{id}">
<dl>
<dt>コード</dt>
<dd><input type="text" name="code" size="10" th:value="*{code}"><div th:if="${#fields.hasErrors('code')}" th:errors="*{code}" class="error">Code Error</div></dd>
<dt>名前</dt>
<dd><input type="text" name="name" size="20" th:value="*{name}"><div th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="error">Name Error</div></dd>
<dt>カナ</dt>
<dd><input type="text" name="kana" size="20" th:value="*{kana}"><div th:if="${#fields.hasErrors('kana')}" th:errors="*{kana}" class="error">Kana Error</div></dd>
</dl>
<p><input type="submit" value="編集"></p>
</form>
<p>著者を削除します。</p>
<form th:action="@{/author/delete/__*{id}__}" th:object="${authorSaveRequest}" method="post">
<input type="hidden" th:field="*{id}">
<p><input type="submit" value="削除"></p>
</form>
</main>
</th:block>
</body>
</html>
src/main/java/com/example/demo/controller/AuthorController.java を新規に作成
package com.example.demo.controller;
import com.example.demo.entity.Author;
import com.example.demo.request.AuthorDeleteRequest;
import com.example.demo.request.AuthorSaveRequest;
import com.example.demo.request.AuthorSearchRequest;
import com.example.demo.service.AuthorService;
import lombok.AllArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
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.*;
@Controller
@AllArgsConstructor
public class AuthorController {
private final AuthorService authorService;
@GetMapping(value = "/author/")
String index(@ModelAttribute AuthorSearchRequest authorSearchRequest, @PageableDefault(page = 0, size = 5, sort = "id") Pageable pageable, Model model) {
if (authorSearchRequest.getName() == null) {
Page<Author> authors = authorService.selectByPage(pageable);
model.addAttribute("authors", authors);
} else {
Page<Author> authors = authorService.searchByPage(authorSearchRequest.getName(), pageable);
model.addAttribute("authors", authors);
}
return "author/index";
}
@GetMapping(value = "/author/form")
String form(Model model) {
model.addAttribute("authorSaveRequest", new Author());
return "author/form";
}
@PostMapping(value = "/author/form")
String form(@Validated @ModelAttribute AuthorSaveRequest authorSaveRequest, BindingResult result) {
boolean valid = authorService.isValid(authorSaveRequest, result);
if (result.hasErrors() || !valid) {
return "author/form";
}
authorService.create(authorSaveRequest);
return "redirect:/author/";
}
@GetMapping(value = "/author/view/{id}")
String view(@PathVariable("id") int id, Model model) {
Author author = authorService.selectById(id);
model.addAttribute("authorSaveRequest", author);
return "author/view";
}
@PostMapping(value = "/author/view/{id}")
String view(@Validated @ModelAttribute AuthorSaveRequest authorSaveRequest, BindingResult result) {
boolean valid = authorService.isValid(authorSaveRequest, result);
if (result.hasErrors() || !valid) {
return "author/view";
}
authorService.update(authorSaveRequest);
return "redirect:/author/";
}
@PostMapping(value = "/author/delete/{id}")
String delete(@ModelAttribute AuthorDeleteRequest authorDeleteRequest) {
authorService.deleteById(authorDeleteRequest.getId());
return "redirect:/author/";
}
}
表示確認用に、authors テーブルには適当にデータを登録しておく
また SecurityConfig.java を調整して、/author/ はログインを求めないようにしておく
※重複エラーのバリデーションはアノテーションで対応できるようにするか
■データの登録編集削除表示(ページャーの実装について補足)
String index(@ModelAttribute AuthorSearchRequest authorSearchRequest, Model model) {
List<Author> authors = authorRepository.findAll();
上記のようなコードを以下のようにすると、ページャーを利用できる
String index(@ModelAttribute AuthorSearchRequest authorSearchRequest, @PageableDefault(page = 0, size = 5, sort = "id") Pageable pageable, Model model) {
Page<Author> authors = authorRepository.findAll(pageable);
ページャーは、一例だが以下のようなコードで表示できる
<div>
<span th:text="|全${authors.getTotalPages()}ページ中${authors.getNumber() + 1}ページ目を表示中|"></span>
<ul>
<li>
<span th:if="${authors.isFirst()}"><前</span>
<a th:unless="${authors.isFirst()}" th:href="@{/author/(page=${authors.getNumber() - 1})}"><前</a>
</li>
<li th:each="i : ${#numbers.sequence(0, authors.getTotalPages() - 1)}">
<span th:if="${i == authors.getNumber()}" th:text="${i + 1}"></span>
<a th:if="${i != authors.getNumber()}" th:href="@{/author/(page=${i})}" th:text="${i + 1}"></a>
</li>
<li>
<span th:if="${authors.isLast()}">次></span>
<a th:unless="${authors.isLast()}" th:href="@{/author/(page=(${authors.getNumber() + 1}))}">次></a>
</li>
</ul>
</div>
■データの登録編集削除表示(テンプレートのvalueとfieldについて補足)
入力画面の「name="code" th:value="*{code}"」は「th:field="*{code}」の状態だと、
サービスでの独自入力チェックでエラーになったとき、入力内容を保持できなかった
■データの登録編集削除表示(登録画面と編集画面のURLについて補足)
以下のように、入力画面と登録実行時のURLを同じものにしている
@GetMapping(value = "/author/form")
String form(Model model) {
@PostMapping(value = "/author/form")
String form(@Validated @ModelAttribute AuthorSaveRequest authorSaveRequest, BindingResult result) {
■データの登録編集削除表示(コンストラクタインジェクションについて補足)
コントローラーで @Autowired を使うのはフィールドインジェクションという方法
この場合、以下のような記述になる
@Controller
public class AuthorController {
@Autowired
private AuthorService authorService;
ただし現在はコンストラクタインジェクションが推奨されている
@Controller
public class AuthorController {
private final AuthorService authorService;
@Autowired
AuthorController(AuthorService authorService) {
this.authorService = authorService;
}
しかしこれだとフィールドインジェクションに比べて行数が増える
そこで「コンストラクタが1つしかない場合、@Autowired は省略できる」というSpringBootのルールを利用する
またこのコンストラクタは、クラスの全インスタンス(1つしか無いが)に変数をセットしている
この場合、@AllArgsConstructor を使うことでコンストラクタも省略できる
@Controller
@AllArgsConstructor
public class AuthorController {
private final AuthorService authorService;
これで今回のコードと同じになる
■データの紐づけ(紐づけ前の準備)
上記が作成できたら以下のテーブルを作成し、記事に対して著者を登録できるようにするか
CREATE TABLE entries(
id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '代理キー',
created DATETIME NOT NULL COMMENT '作成日時',
modified DATETIME NOT NULL COMMENT '更新日時',
datetime DATETIME NOT NULL COMMENT '日時',
title VARCHAR(255) NOT NULL COMMENT 'タイトル',
text TEXT COMMENT '本文',
author_id INT UNSIGNED NOT NULL COMMENT '外部キー 著者',
PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '記事';
src/main/java/com/example/demo/entity/Entry.java を新規に作成
package com.example.demo.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.util.Date;
@Entity
@Table(name="entries")
@Data
public class Entry {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false, updatable = false)
private Date created;
@Column(nullable = false)
private Date modified;
@Column(nullable = false)
private Date datetime;
@Column(nullable = false)
private String title;
private String text;
@Column(name = "author_id", nullable = false)
private Integer authorId;
}
src/main/java/com/example/demo/request/EntrySearchRequest.java を新規に作成
package com.example.demo.request;
import lombok.Data;
import java.io.Serializable;
@Data
public class EntrySearchRequest implements Serializable {
private String keyword;
}
src/main/java/com/example/demo/request/EntrySaveRequest.java を新規に作成
package com.example.demo.request;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.beans.ConstructorProperties;
import java.io.Serializable;
@Data
public class EntrySaveRequest implements Serializable {
private int id;
@NotEmpty(message = "日時を入力してください。")
private String datetime;
@NotEmpty(message = "タイトルを入力してください。")
@Size(max = 80, message = "タイトルは80文字以内で入力してください。")
private String title;
@NotEmpty(message = "本文を入力してください。")
private String text;
@NotEmpty(message = "著者を入力してください。")
private String authorId;
@ConstructorProperties({"author_id"})
public EntrySaveRequest(String author_id) {
// フォームから「name="author_id"」で送られた値を「authorId」という名前で扱う
this.authorId = author_id;
}
}
SpringBootでスネークケースのリクエストパラメータを受け取る方法 - エキサイト TechBlog.
https://tech.excite.co.jp/entry/2021/10/07/082811
src/main/java/com/example/demo/request/EntryDeleteRequest.java を新規に作成
package com.example.demo.request;
import lombok.Data;
import java.io.Serializable;
@Data
public class EntryDeleteRequest implements Serializable {
private int id;
}
src/main/java/com/example/demo/repository/EntryRepository.java を新規に作成
package com.example.demo.repository;
import com.example.demo.entity.Entry;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface EntryRepository extends JpaRepository<Entry, Integer> {
Page<Entry> findByTitleLikeOrTextLike(String title, String text, Pageable pageable);
}
src/main/java/com/example/demo/service/EntryService.java を新規に作成
package com.example.demo.service;
import com.example.demo.entity.Entry;
import com.example.demo.repository.EntryRepository;
import com.example.demo.request.EntrySaveRequest;
import lombok.AllArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
@AllArgsConstructor
public class EntryService {
private final EntryRepository entryRepository;
public List<Entry> select() {
List<Entry> entries = entryRepository.findAll();
return entries;
}
public Page<Entry> selectByPage(Pageable pageable) {
Page<Entry> entries = entryRepository.findAll(pageable);
return entries;
}
public Entry selectById(int id) {
Entry entry = entryRepository.findById(id).get();
return entry;
}
public void create(EntrySaveRequest entrySaveRequest) {
SimpleDateFormat sdFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
Date datetime = null;
try {
datetime = sdFormat.parse(entrySaveRequest.getDatetime());
} catch (ParseException e) {
throw new RuntimeException(e);
}
Entry entry = new Entry();
entry.setId(null);
entry.setCreated(new Date());
entry.setModified(new Date());
entry.setDatetime(datetime);
entry.setTitle(entrySaveRequest.getTitle());
entry.setText(entrySaveRequest.getText());
entry.setAuthorId(Integer.valueOf(entrySaveRequest.getAuthorId()));
entryRepository.save(entry);
}
public void update(EntrySaveRequest entrySaveRequest) {
SimpleDateFormat sdFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
Date datetime = null;
try {
datetime = sdFormat.parse(entrySaveRequest.getDatetime());
} catch (ParseException e) {
throw new RuntimeException(e);
}
Entry entry = new Entry();
entry.setId(entrySaveRequest.getId());
entry.setModified(new Date());
entry.setDatetime(datetime);
entry.setTitle(entrySaveRequest.getTitle());
entry.setText(entrySaveRequest.getText());
entry.setAuthorId(Integer.valueOf(entrySaveRequest.getAuthorId()));
entryRepository.saveAndFlush(entry);
}
public void deleteById(int id) {
entryRepository.deleteById(id);
}
public Page<Entry> search(String keyword, Pageable pageable) {
Page<Entry> entries = entryRepository.findByTitleLikeOrTextLike("%" + keyword + "%", "%" + keyword + "%", pageable);
return entries;
}
public boolean isValid(EntrySaveRequest entrySaveRequest, BindingResult result) {
boolean valid = true;
Pattern pattern = Pattern.compile("^\\d\\d\\d\\d-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d$");
Matcher matcher = pattern.matcher(entrySaveRequest.getDatetime());
if (!matcher.find()) {
result.addError(new FieldError(result.getObjectName(), "datetime", "日時の形式が不正です。"));
valid = false;
}
return valid;
}
}
src/main/resources/templates/entry/index.html を新規に作成
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/frontend}">
<head>
<title>記事 | Demo</title>
</head>
<body>
<th:block layout:fragment="content">
<main>
<p><a href="/entry/form">記事を追加</a></p>
<form th:action="@{/entry/}" th:object="${entrySearchRequest}" method="get">
<dl>
<dt>キーワード</dt>
<dd><input type="text" size="20" name="keyword" th:value="*{keyword}"></dd>
</dl>
<p><input type="submit" value="検索"></p>
</form>
<table>
<tr>
<th>ID</th>
<th>日時</th>
<th>タイトル</th>
<th>本文</th>
</tr>
<tr th:each="entry : ${entries}">
<td><a th:href="@{/entry/view/__${entry.id}__}">[[${entry.id}]]</a></td>
<td>[[${#dates.format(entry.datetime, 'yyyy-MM-dd HH:mm:ss')}]]</td>
<td>[[${entry.title}]]</td>
<td>[[${entry.text}]]</td>
</tr>
</table>
<div>
<p th:text="|全${entries.getTotalPages()}ページ中${entries.getNumber() + 1}ページ目を表示中|"></p>
<ul>
<li>
<span th:if="${entries.isFirst()}"><前</span>
<a th:unless="${entries.isFirst()}" th:href="@{/entry/(keyword=${entrySearchRequest.keyword}, page=${entries.getNumber() - 1})}"><前</a>
</li>
<li th:each="i : ${#numbers.sequence(0, entries.getTotalPages() - 1)}">
<span th:if="${i == entries.getNumber()}" th:text="${i + 1}"></span>
<a th:if="${i != entries.getNumber()}" th:href="@{/entry/(keyword=${entrySearchRequest.keyword}, page=${i})}" th:text="${i + 1}"></a>
</li>
<li>
<span th:if="${entries.isLast()}">次></span>
<a th:unless="${entries.isLast()}" th:href="@{/entry/(keyword=${entrySearchRequest.keyword}, page=(${entries.getNumber() + 1}))}">次></a>
</li>
</ul>
</div>
</main>
</th:block>
</body>
</html>
src/main/resources/templates/entry/form.html を新規に作成
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/frontend}">
<head>
<title>記事 | Demo</title>
</head>
<body>
<th:block layout:fragment="content">
<main>
<p>記事を登録します。</p>
<form th:action="@{/entry/form}" th:object="${entrySaveRequest}" method="post">
<dl>
<dt>日時</dt>
<dd><input type="text" name="datetime" size="20" th:value="*{datetime} ? *{datetime} : ${#dates.format(#dates.createNow(), 'yyyy-MM-dd HH:mm:ss')}"><div th:if="${#fields.hasErrors('datetime')}" th:errors="*{datetime}" class="error">Datetime Error</div></dd>
<dt>タイトル</dt>
<dd><input type="text" name="title" size="20" th:value="*{title}"><div th:if="${#fields.hasErrors('title')}" th:errors="*{title}" class="error">Title Error</div></dd>
<dt>本文</dt>
<dd><textarea name="text" rows="10" cols="50">[[*{text}]]</textarea><div th:if="${#fields.hasErrors('text')}" th:errors="*{text}" class="error">Text Error</div></dd>
<dt>著者</dt>
<dd>
<select name="author_id">
<option value=""></option>
<option
th:each="author : ${authors}"
th:value="${author.id}"
th:text="${author.name}"
th:selected="${author.id} == *{authorId}">
</option>
</select>
<div th:if="${#fields.hasErrors('authorId')}" th:errors="*{authorId}" class="error">AuthorId Error</div>
</dd>
</dl>
<p><input type="submit" value="登録"></p>
</form>
</main>
</th:block>
</body>
</html>
【Spring Boot】Thymeleafでプルダウンを作成する方法 - ITを分かりやすく解説
https://medium-company.com/thymeleaf-%E3%83%97%E3%83%AB%E3%83%80%E3%82%A6%E3%83%B3/
src/main/resources/templates/entry/view.html を新規に作成
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/frontend}">
<head>
<title>記事 | Demo</title>
</head>
<body>
<th:block layout:fragment="content">
<main>
<p>記事を編集します。</p>
<form th:action="@{/entry/view/__*{id}__}" th:object="${entrySaveRequest}" method="post">
<input type="hidden" name="id" th:value="*{id}">
<dl>
<dt>日時</dt>
<dd><input type="text" name="datetime" size="20" th:value="*{#strings.replace(datetime, '.0', '')}"><div th:if="${#fields.hasErrors('datetime')}" th:errors="*{datetime}" class="error">Datetime Error</div></dd>
<dt>タイトル</dt>
<dd><input type="text" name="title" size="20" th:value="*{title}"><div th:if="${#fields.hasErrors('title')}" th:errors="*{title}" class="error">Title Error</div></dd>
<dt>本文</dt>
<dd><textarea name="text" rows="10" cols="50">[[*{text}]]</textarea><div th:if="${#fields.hasErrors('text')}" th:errors="*{text}" class="error">Text Error</div></dd>
<dt>著者</dt>
<dd>
<select name="author_id">
<option value=""></option>
<option
th:each="author : ${authors}"
th:value="${author.id}"
th:text="${author.name}"
th:selected="${author.id} == *{authorId}">
</option>
</select>
<div th:if="${#fields.hasErrors('authorId')}" th:errors="*{authorId}" class="error">AuthorId Error</div>
</dd>
</dl>
<p><input type="submit" value="編集"></p>
</form>
<p>記事を削除します。</p>
<form th:action="@{/entry/delete/__*{id}__}" th:object="${entrySaveRequest}" method="post">
<input type="hidden" th:field="*{id}">
<p><input type="submit" value="削除"></p>
</form>
</main>
</th:block>
</body>
</html>
src/main/java/com/example/demo/controller/EntryController.java を新規に作成
package com.example.demo.controller;
import com.example.demo.entity.Entry;
import com.example.demo.request.EntryDeleteRequest;
import com.example.demo.request.EntrySaveRequest;
import com.example.demo.request.EntrySearchRequest;
import com.example.demo.service.AuthorService;
import com.example.demo.service.EntryService;
import lombok.AllArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
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.*;
@Controller
@AllArgsConstructor
public class EntryController {
private final EntryService entryService;
private final AuthorService authorService;
@GetMapping(value = "/entry/")
String index(@ModelAttribute EntrySearchRequest entrySearchRequest, @PageableDefault(page = 0, size = 5, sort = "id") Pageable pageable, Model model) {
if (entrySearchRequest.getKeyword() == null) {
Page<Entry> entries = entryService.selectByPage(pageable);
model.addAttribute("entries", entries);
} else {
Page<Entry> entries = entryService.search(entrySearchRequest.getKeyword(), pageable);
model.addAttribute("entries", entries);
}
return "entry/index";
}
@GetMapping(value = "/entry/form")
String form(Model model) {
model.addAttribute("entrySaveRequest", new Entry());
model.addAttribute("authors", authorService.select());
return "entry/form";
}
@PostMapping(value = "/entry/form")
String form(@Validated @ModelAttribute EntrySaveRequest entrySaveRequest, BindingResult result, Model model) {
boolean valid = entryService.isValid(entrySaveRequest, result);
if (result.hasErrors() || !valid) {
model.addAttribute("authors", authorService.select());
return "entry/form";
}
entryService.create(entrySaveRequest);
return "redirect:/entry/";
}
@GetMapping(value = "/entry/view/{id}")
String view(@PathVariable("id") int id, Model model) {
Entry entry = entryService.selectById(id);
model.addAttribute("entrySaveRequest", entry);
model.addAttribute("authors", authorService.select());
return "entry/view";
}
@PostMapping(value = "/entry/view/{id}")
String view(@Validated @ModelAttribute EntrySaveRequest entrySaveRequest, BindingResult result, Model model) {
boolean valid = entryService.isValid(entrySaveRequest, result);
if (result.hasErrors() || !valid) {
model.addAttribute("authors", authorService.select());
return "entry/view";
}
entryService.update(entrySaveRequest);
return "redirect:/entry/";
}
@PostMapping(value = "/entry/delete/{id}")
String delete(@ModelAttribute EntryDeleteRequest entryDeleteRequest) {
entryService.deleteById(entryDeleteRequest.getId());
return "redirect:/entry/";
}
}
■データの紐づけ(紐づけの実装)
EntryからAuthorを参照できるように、またその逆も参照できるようにする
src/main/java/com/example/demo/entity/Entry.java
@ManyToOne
@JoinColumn(name = "author_id", insertable = false, updatable = false)
private Author author;
src/main/java/com/example/demo/entity/Author.java
@OneToMany(mappedBy = "author")
@ToString.Exclude
private List<Entry> entities;
@JoinColumn の「insertable = false, updatable = false」については以下を参照
「@Column と @JoinColumn の両方が定義されているとき、『この項目は参照専用に定義したもの』と明示する」ためのもの
今回の場合「private Integer authorId;」と「@JoinColumn(name = "author_id")」の両方があると、「author_idという列が2つある」と解釈されて意図したように更新できない可能性がある
JPA @ManyToOne時の@JoinColumnのパラメータ、insertable = false,updatable = falseについて
https://teratail.com/questions/113950
また「@ToString.Exclude」が無いと、toString() を呼び出したときに循環参照になって「java.lang.StackOverflowError」が発生する
これで相互にデータを参照できる
プログラムから参照する場合、一例だが以下のようなコードになる
List<Author> authors = authorService.select();
for (Author author : authors) {
logger.info(author.getName());
logger.info(String.valueOf(author.getEntities().size()));
logger.info(author.toString());
}
List<Entry> entries = entryService.select();
for (Entry entry : entries) {
logger.info(entry.getTitle());
logger.info(entry.getAuthor().getName());
logger.info(entry.toString());
}
記事一覧に著者名を表示する場合、一例だが以下のようなコードになる
src/main/resources/templates/entry/index.html
<td>[[${entry.author.name}]]</td>
著者一覧に記事数を表示する場合、一例だが以下のようなコードになる
src/main/resources/templates/author/index.html
<td>[[${#lists.size(author.entities)}]]件</td>
なお、実行時に以下のようなエラーになった場合、
Table [entries] contains physical column name [author_id] referred to by multiple logical column names: [author_id], [authorId]
Entityを以下のように調整すれば解消するかもしれない
@Column(nullable = false)
private Integer authorId;
↓
@Column(name = "author_id", nullable = false)
private Integer authorId;
java - A column in a table is referred to by multiple physical column names - Stack Overflow
https://stackoverflow.com/questions/57691377/a-column-in-a-table-is-referred-to-by-multiple-physical...
■データの紐づけ(多対多)
上記が作成できたら以下のテーブルを作成し、記事に対して複数の分類を登録できるようにするか
CREATE TABLE categories(
id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '代理キー',
created DATETIME NOT NULL COMMENT '作成日時',
modified DATETIME NOT NULL COMMENT '更新日時',
name VARCHAR(255) NOT NULL COMMENT '名前',
sort INT UNSIGNED NOT NULL COMMENT '並び順',
PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '分類';
CREATE TABLE category_sets(
category_id INT UNSIGNED NOT NULL COMMENT '外部キー 分類',
member_id INT UNSIGNED NOT NULL COMMENT '外部キー 記事'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '分類 ひも付け';
■その他
以下のページはサンプルが多くて有用そう
Spring Framework | ITSakura
https://itsakura.com/spring-framework
以下のページも有用そう
開発する準備を整えよう|Javaの基礎を学び終えたアナタに贈る, SpringBoot/SpringSecurityによる掲示板開発ハンズオン
https://zenn.dev/angelica/books/52be1e365c61ea/viewer/16ce8d
処理の共通化は以下が参考になりそう
共通処理のまとめ方(Spring) | 全国個人事業主支援協会
https://kojinjigyou.org/22442/
【Spring Boot】Interceptorによる共通処理
https://b1san-blog.com/post/spring/spring-interceptor/
基本的なバリデーションに加えて、グループ化による実行対象制御は検証しておきたい
【Spring Boot】バリデーション
https://b1san-blog.com/post/spring/spring-validation/
■プログラムのパッケージ化について
JAR,WAR,EARそれぞれの違いについて - Java習得を目指すブログ
https://java-wizard.hatenadiary.org/entry/20140311/1394538929
SpringBootで実行可能Jarを作る - 【Spring Hack】
https://springhack.com/springboot%E3%81%A7%E5%AE%9F%E8%A1%8C%E5%8F%AF%E8%83%BDjar%E3%82%92%E4%BD%9C%...
SpringBootアプリケーション(Jar)をCentOS7にデプロイ - Qiita
https://qiita.com/Masahiro_Uemura1234/items/2c60ad1e0365e3b7bdc9
JAR ... Java ARchive
Javaプログラムの実行に必要なクラスファイルや設定ファイルがまとめられているアーカイブ
WAR ... Web ARchive
Java製のWebアプリで利用されるクラスファイル、設定ファイル、JSPやHTMLファイル、JAR形式のライブラリなどがまとめられているアーカイブ
ただしSpringBootで書き出せるJarにはTomcatが含まれていて、単体で実行可能となっている
特に理由がなければJARでパッケージ化するといい(この後のメモもJARを書き出す内容となっている)
■Vagrantでアプリケーション(JAR)を起動
■プログラムの準備
「プロパティの上書き(src/main/resources/application-xxx.properties)」を参考に、環境ごとの設定ファイルを用意しておく
ここでは、Vagrant用に application-test.properties を作成したものとする
■基本設定
# localectl set-locale LANG=ja_JP.UTF-8
# timedatectl set-timezone Asia/Tokyo
# yum install -y
https://cdn.azul.com/zulu/bin/zulu-repo-1.0.0-1.noarch.rpm
# yum install -y zulu17-jdk
# java -version
openjdk version "17.0.5" 2022-10-18 LTS
OpenJDK Runtime Environment Zulu17.38+21-CA (build 17.0.5+8-LTS)
OpenJDK 64-Bit Server VM Zulu17.38+21-CA (build 17.0.5+8-LTS, mixed mode, sharing)
CentOS 7にOpenJDK 17をインストール(Azul Zulu Builds of OpenJDK) - Qiita
https://qiita.com/witchcraze/items/014da8ee2c638737e9bc
Javaのサポートについてのまとめ2018 - Qiita
https://qiita.com/nowokay/items/edb5c5df4dbfc4a99ffb
■プログラムの実行(MySQLなし)
同期フォルダである /var/www に demo-0.0.1-SNAPSHOT.jar を配置
$ nohup java -jar /var/www/demo-0.0.1-SNAPSHOT.jar &
以下でアクセスできることを確認(確認できたら終了しておく)
$ curl
http://localhost:8080/
$ curl
http://localhost:8080/now
■プログラムの実行(MySQLあり)
# curl -sS
https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash
# yum -y install MariaDB-server MariaDB-client
# systemctl start mariadb
# systemctl enable mariadb
# mariadb-secure-installation
$ mysql -u root -p
mysql> CREATE DATABASE spring DEFAULT CHARACTER SET utf8;
mysql> CREATE USER webmaster@localhost IDENTIFIED BY 'abcd1234';
mysql> GRANT ALL PRIVILEGES ON spring.* TO webmaster@localhost;
mysql> QUIT
$ mysql -u webmaster -p
mysql> USE spring
mysql> CREATE TABLE ... (必要なテーブルを作成する)
mysql> QUIT
■サービスの設定
# vi /etc/systemd/system/spring.service
[Unit]
Description = Spring Boot Application
[Service]
ExecStart = /usr/bin/java -jar /var/www/demo-0.0.1-SNAPSHOT.jar --spring.profiles.active=test … 実行したいjarファイルの場所とその環境を指定(jarファイルとjavaは絶対パスで指定)
Restart = always
Type = simple
User = vagrant … 環境に合わせてユーザ名を指定する
Group = vagrant … 環境に合わせてユーザ名を指定する
SuccessExitStatus = 143
[Install]
WantedBy = multi-user.target
UserとGroupに専用のユーザを指定する場合、「useradd xxxxx」のようにしてユーザを作成しておく
また、このファイルがjarファイルを読み取りできるようにしておく
# systemctl list-unit-files -t service | grep spring
… サービスの状態を確認(Unitがサービスとして認識されたか確認)
spring.service disabled
# systemctl enable spring
… サービスの自動起動を有効化
Created symlink from /etc/systemd/system/multi-user.target.wants/spring.service to /etc/systemd/system/spring.service.
# systemctl list-unit-files -t service | grep spring
… サービスの自動起動を確認
spring.service enabled
# systemctl start spring
… サービスを起動
# systemctl status spring
… サービスの起動を確認
● spring.service - Spring Boot Application
Loaded: loaded (/etc/systemd/system/spring.service; enabled; vendor preset: disabled)
Active: failed (Result: start-limit) since 金 2023-01-06 14:47:44 JST; 1s ago
Process: 3212 ExecStart=/var/www/demo-0.0.1-SNAPSHOT.jar (code=exited, status=203/EXEC)
Main PID: 3212 (code=exited, status=203/EXEC)
1月 06 14:47:43 localhost.localdomain systemd[1]: spring.service: main process exited, code=exited, status=203/EXEC
1月 06 14:47:43 localhost.localdomain systemd[1]: Unit spring.service entered failed state.
1月 06 14:47:43 localhost.localdomain systemd[1]: spring.service failed.
1月 06 14:47:44 localhost.localdomain systemd[1]: spring.service holdoff time over, scheduling restart.
1月 06 14:47:44 localhost.localdomain systemd[1]: Stopped Spring Boot Application.
1月 06 14:47:44 localhost.localdomain systemd[1]: start request repeated too quickly for spring.service
1月 06 14:47:44 localhost.localdomain systemd[1]: Failed to start Spring Boot Application.
1月 06 14:47:44 localhost.localdomain systemd[1]: Unit spring.service entered failed state.
1月 06 14:47:44 localhost.localdomain systemd[1]: spring.service failed.
# systemctl stop spring
… サービスを停止させる場合
サービスとしての稼働については、
Dropbox\サーバ\Etcetera.txt
内にある「バックグラウンドで処理を実行する(サービス)」も参照
■Nginxの導入
※あらかじめ、SELinuxを無効化しておく
# yum -y install epel-release
# yum -y install nginx
# vi /etc/nginx/nginx.conf
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
#root /usr/share/nginx/html;
… 公開ディレクトリをコメントアウト
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
proxy_pass
http://localhost:8080; … ブロックに内容を追加
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
# systemctl start nginx
# systemctl enable nginx
http://192.168.33.10/
http://192.168.33.10/now
■トラブルシューティング
起動時のログやエラー時のログは /var/log/message に出力される
例えば「--spring.profiles.active=test」による環境の指定を行った場合、以下が出力されていた
2023-01-19T12:18:53.234+09:00 INFO 3207 --- [ main] com.example.demo.DemoApplication : The following 1 profile is active: "test"
データベースに接続できないとき、以下が出力されていた
Jan 19 11:45:41 localhost mariadbd: 2023-01-19 11:45:41 3 [Warning] Access denied for user 'root'@'localhost'
Jan 19 11:45:42 localhost java: 2023-01-19T11:45:42.454+09:00 ERROR 3011 --- [nio-8888-exec-3] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Exception during pool initialization.
Jan 19 11:45:42 localhost java: java.sql.SQLException: Access denied for user 'root'@'localhost'
データベースにテーブルが作成されていないとき、以下が出力されていた
Windows環境ではテーブルの大文字と小文字は区別されないようだが、Linux(Vagrant)環境では区別されるので注意
2023-01-18T17:51:21.182+09:00 ERROR 3560 --- [nio-8080-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.jdbc.BadSqlGrammarException: StatementCallback; bad SQL grammar [SELECT * FROM tasks]] with root cause
java.sql.SQLSyntaxErrorException: Table 'spring.tasks' doesn't exist
■EC2でアプリケーション(JAR)を起動
■プログラムの準備
前述の「Vagrantでアプリケーション(JAR)を起動」の同項目を参照
■EC2の起動
通常の手順でEC2を作成
Amazon Linux 2 の t2.micro を選択。EIPは無し
セキュリティグループで8080番ポートを空けておく
SSHで接続して以下を行なう
・言語を設定
・タイムゾーンを設定
・パッケージ管理システムを設定
・各種ツールをインストール
ここまでは通常の立ち上げと同じ
■Javaのインストール
Amazon Linux には始めからJavaがインストールされていたようだが、
Amazon Linux 2 にはインストールされていない
$ java -version
… Amazon Linux 2 にはJavaはインストールされていない
-bash: java: command not found
エクストラリポジトリには存在するが、Java11と少し古い(今回はJava17をインストールしたい)
# amazon-linux-extras list | grep java
33 java-openjdk11 available [ =11 =stable ]
以下の記事を参考にインストールする
CorrettoはAWSが用意しているJDKで、JavaSE標準との互換性があるとされている
【EC2】minecraft1.18のサーバーをEC2で立ち上げてみた - Qiita
https://qiita.com/nahiro_tus/items/021d095f49cb65404a2c
Amazon Corretto(本番環境に対応したOpenJDKディストリビューション)| AWS
https://aws.amazon.com/jp/corretto/
# yum install -y
https://corretto.aws/downloads/latest/amazon-corretto-17-x64-al2-jre.rpm
# java -version
openjdk version "17.0.5" 2022-10-18 LTS
OpenJDK Runtime Environment Corretto-17.0.5.8.1 (build 17.0.5+8-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.5.8.1 (build 17.0.5+8-LTS, mixed mode, sharing)
■プログラムの実行
AWS EC2上で Spring Bootアプリ起動 - 闘うITエンジニアの覚え書き
https://www.magata.net/memo/index.php?AWS%20EC2%BE%E5%A4%C7%20Spring%20Boot%A5%A2%A5%D7%A5%EA%B5%AF%...
AWSクラウド環境の構築からSpring Bootアプリのデプロイまで(初心者向け) - Qiita
https://qiita.com/KevinFQ/items/119521ebd12bb7890761
SFTPなどで /home/ec2-user/demo-0.0.1-SNAPSHOT.jar に配置する
$ java -jar /home/ec2-user/demo-0.0.1-SNAPSHOT.jar
EC2のIPアドレスが 54.178.199.100 の場合、以下にアクセスすると Spring Boot で作成したアプリケーションの画面が表示される
アクセスできない場合、8080番ポートを空けているか確認する(確認できたら終了しておく)
http://54.178.199.100:8080/
■サービスの設定
前述の「Vagrantでアプリケーション(JAR)を起動」の同項目を参照
■Nginxの導入
インストール自体は以下で行える
# amazon-linux-extras install -y nginx1
以降の設定については、前述の「Vagrantでアプリケーション(JAR)を起動」の同項目を参照
■その他参考になりそうなページ
EC2+RDS+Spring bootで簡単なAPIを作ってみる? - Qiita
https://qiita.com/yseki_/items/81c84d78895b009c2aa6
【AWS】入門その? EC2とRDSを利用して簡易Webアプリを構築してみる - SEワンタンの独学備忘録
https://www.wantanblog.com/entry/2019/09/21/190819
■トラブルシューティング
前述の「Vagrantでアプリケーション(JAR)を起動」の同項目を参照
■Elastic Beanstalk でアプリケーション(JAR)を起動
サーバメモの AWS.txt の「Elastic Beanstalk」を参照
■Dockerで起動
以下などが参考になりそう
Spring Boot Docker - 公式サンプルコード
https://spring.pleiades.io/guides/topicals/spring-boot-docker/
Spring BootプロジェクトをDocker上で動かす
https://zenn.dev/nishiharu/articles/7f27b8c580f896
SpringBootをAWS ECSで動かす - 【Spring Hack】
https://springhack.com/springboot%E3%82%92aws-ecs%E3%81%A7%E5%8B%95%E3%81%8B%E3%81%99/
■旧メモ: 開発環境インストール
※Spring Toolsで開発したときのメモ
Spring Tools 4
http://spring.io/tools
SpringBootに入門する為の助走本(随時更新) - Qiita
https://qiita.com/sugaryo/items/5695bfcc21365f429767
■JDKインストール
Java環境構築(Windows版) JDKインストール | ITエンジニア"が作るメディア Tech Fun Magazine
https://techfun.cc/java/windows-jdk-install.html
Java環境構築(Windows版) パスの設定 | ITエンジニア"が作るメディア Tech Fun Magazine
https://techfun.cc/java/windows-jdk-pathset.html
コマンドプロンプトで以下のように表示できれば成功
>java -version
java version "14.0.1" 2020-04-14
Java(TM) SE Runtime Environment (build 14.0.1+7)
Java HotSpot(TM) 64-Bit Server VM (build 14.0.1+7, mixed mode, sharing)
■Mavenインストール
WindowsにSpring Tool Suiteを導入する - Qiita
https://qiita.com/segur/items/4cd283d9763a1953f336
C:\apache-maven-3.6.3
に配置
■Spring Toolsインストール
【第一回】Spring Frameworkを使ってみる 〜Spring Framework概要と準備〜 | TECH Projin
https://tech.pjin.jp/blog/2016/04/28/springframework1/
上記ページの解説で触れられている
Spring Tools 4 for Eclipse
を試してみる
Download STS4 (for Windows 64-bit)
をダウンロード
※2020年5月16日現在、多くの解説にあるようにzipファイルはダウンロードできない
代わりにjarファイルをダウンロードできるようになっている
JDKをインストール済みなら、ファイルを実行すれば解答できる
C:\sts4
に配置
C:\sts4\SpringToolSuite4.exe
を実行して起動
Select a directory as workspace
C:\Users\refirio\sts4
に設定して「Launch」
日本語化のため、いったん終了させる
■日本語化
WindowsにSpring Tool Suiteを導入する - Qiita
https://qiita.com/segur/items/4cd283d9763a1953f336
setup.exe
を実行
C:\sts4\SpringToolSuite4.exe
を選択して「日本語化する」ボタンを押す
SpringToolSuite4.exe
を実行して起動。日本語化されていることを確認する
■旧メモ: 基本的なプログラムの作成
※Spring Toolsで開発したときのメモ
■プロジェクトの作成
【第二回】Spring Frameworkを使ってみる 〜プロジェクト作成からビルドまで〜 | TECH Projin
https://tech.pjin.jp/blog/2016/04/28/springframework2/
「Spring Legacy Project」が見当たらない
色々変更されているようなので以下を参考に進める
SpringBootに入門する為の助走本(随時更新) - Qiita
https://qiita.com/sugaryo/items/5695bfcc21365f429767
ファイル → 新規 → Spring スターター・プロジェクト
を実行
「新規 Spring スターター・プロジェクト」ダイアログが開くので、そのまま「次へ」をクリック
「新規 Spring スターター・プロジェクト依存関係」が表示されるので、「テンプレートエンジン → Thymeleaf」と「Web → Spring Web」にチェックを入れて「次へ」をクリック
「新規 Spring スターター・プロジェクト」ダイアログが開くので、そのまま「完了」をクリック
で設定完了
■プログラムの作成
[Java] Spring BootでHello World!(入門編) | マリンロード
https://www.marineroad.com/staff-blog/16785.html
ファイル → 新規 → クラス
ソース・フォルダー: demo/src/main/java
パッケージ: com.example.demo
名前: HelloController
package com.example.demo;
public class HelloController {
}
が作成されるので、以下のように変更する
package com.example.demo;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "Hello World !!";
}
}
プロジェクトを右クリックし、
実行 → Spring Boot アプリケーション
を選択。コンソールに色々表示され、8080番ポートでTomcatが起動し、アプリケーションが開始したと表示される
以下にアクセスすると「Hello World !!」と表示される
http://localhost:8080/hello
プログラムを編集し、ツールバーの赤い四角で停止し、緑の再生ボタンで実行すると、編集内容が反映された
■リクエストマッピングの追加
以下のようにすると / と /hello と /goodbye でそれぞれテキストが表示されるようになる
package com.example.demo;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
@RestController
public class HelloController {
@RequestMapping("/")
public String index() {
return "Demo";
}
@RequestMapping("/hello")
public String hello() {
return "Hello World.";
}
@RequestMapping("/goodbye")
public String bye() {
return "Good Bye World !!";
}
}
■JARファイルの作成
※Spring Boot のJARファイルはTomcatも含まれているので、WARよりもJARを書き出す方が環境構築が容易になる
Spring Boot をはじめてみるよ - Multi Vitamin & Mineral
http://hiranoon.hatenablog.com/entry/2015/12/30/111829#jar-%E3%81%AE%E4%BD%9C%E6%88%90%E3%81%A8%E5%A...
プロジェクト直下の pom.xml を開き、以下のように編集する
groupIdは作成しているプログラムのパッケージ名に合わせる…と思ったが、この編集は行わなくても大丈夫だった
<groupId>com.example</groupId>
↓
<groupId>com.example.demo</groupId>
プロジェクトを右クリックし、
実行 → 5 Maven Install
を選択。しばらく待つと、以下にファイルが作成された
C:\Users\refirio\sts4\demo\target\demo-0.0.1-SNAPSHOT.jar
■その他参考になりそうなページ
Spring Tool Suite インストールと日本語化 for Windows - Qiita
https://qiita.com/andna0410/items/e9cc07c9e2e34a542245
Spring Tool Suite (STS)と Spring Boot で始める Web アプリケーション開発(1) TECHSCORE BLOG
http://www.techscore.com/blog/2016/11/22/start-with-sts-and-spring-boot-1/
AWSクラウド環境の構築からSpring Bootアプリのデプロイまで(初心者向け) - Qiita
https://qiita.com/KevinFQ/items/119521ebd12bb7890761
Spring Bootで作ったJarをAWSのElastic BeanstalkですぐにWebにアップする - Qiita
https://qiita.com/haruto167/items/0c9234d6ef77f3a5a09c
Spring BootをElastic Beanstalk上で動かしてみた - Qiita
https://qiita.com/takexi/items/3cc428e603653bbf2e8b
Spring bootでMySQL接続 - Qiita
https://qiita.com/uk-liverpool/items/5878d3c509c503ff5b29
Spring Boot MySQL を使用したデータへのアクセス - コードサンプル
https://spring.pleiades.io/guides/gs/accessing-data-mysql/
【Java】【AWS】Amazon EC2でJavaのWebアプリケーション動作環境を構築してデプロイするまで - 独り言
https://case10.hateblo.jp/entry/2020/10/06/165543
EC2-1インスタンスでnginx-SpringBoot-MySQL-redisを構築 - 【Spring Hack】
https://springhack.com/1%E5%B1%A4%E3%81%AE%E6%A7%8B%E7%AF%89/
SpringBootをサービス化(デーモン化)して常駐させる - 【Spring Hack】
https://springhack.com/springboot%e3%82%92%e3%82%b5%e3%83%bc%e3%83%93%e3%82%b9%e5%8c%96%ef%bc%88%e3%...
EC2+RDS+Spring bootで簡単なAPIを作ってみる? - Qiita
https://qiita.com/yseki_/items/81c84d78895b009c2aa6
AWS(EC2)上にSpringBootアプリをデプロイする - 好奇心の赴くままに
https://writerman.hatenablog.jp/entry/2019/12/22/215749
Spring BootでMySQLに接続する | 株式会社CONFRAGE ITソリューション事業部
https://confrage.jp/spring-boot%E3%81%A7mysql%E3%81%AB%E6%8E%A5%E7%B6%9A%E3%81%99%E3%82%8B/
以下はMinecraftの環境を作るための記事だが、Java環境の構築なので参考になるかも
【#Minecraft for AWS】EC2インスタンスでMinecraftサーバーを構築 Ver1.19 | DevelopersIO
https://dev.classmethod.jp/articles/new-minecraft-for-aws_ec2-instance/
【#Minecraft for AWS】EC2インスタンスでMinecraftサーバーを構築 Ver1.17 | DevelopersIO
https://dev.classmethod.jp/articles/minecraft-for-aws_ec2-instance/
第1回 AWS で Minecraft サーバー構築【1.19.2】〜バニラサーバー構築〜
https://staff-blog.faith-sol-tech.com/%E7%AC%AC%EF%BC%91%E5%9B%9E-aws-%E3%81%A7-minecraft-%E3%82%B5%...