Spring BootでMybatisを使ってみる

GitHubhttps://github.com/n-yata/mybatis-sample.git

pom.xml

pom.xmlは下のような感じ。
mybatis-spring-boot-starterを入れる。

<?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>2.4.5</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>example</groupId>
    <artifactId>Mybatis-sample</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>Mybatis-sample</name>
    <description>Spring sample project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>

        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.properties

DBの接続情報とmybatisの設定を書く。

spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.driver-class-name=org.postgresql.Driver

# MybatisマッピングJavaクラスのパッケージ
mybatis.type-aliases-package=example.mapper
# xmlマッピングファイルの場所
mybatis.mapper-locations=classpath:mapper/*.xml
# アンスコ→キャメルケースに変換
mybatis.configuration.map-underscore-to-camel-case=true

propertiesに書いたJavaファイルとxmlファイルのパスは下のような感じ。
f:id:n-yata:20210425131353p:plain

Modelクラス

User.java
Lombok便利。フィールドだけ書いて簡単実装。

package example.model;

import lombok.Data;

@Data
public class User {
   private String userId;
    private String firstName;
    private String lastName;
    private String password;
    private String roleName;
}

対応するテーブルのcreate文(PostgreSQLを使用)

CREATE TABLE IF NOT EXISTS usr(
  user_id VARCHAR(255) NOT NULL,
  first_name VARCHAR(255) NOT NULL,
  last_name VARCHAR(255) NOT NULL,
  password VARCHAR(255) NOT NULL,
  role_name VARCHAR(255) NOT NULL,
  PRIMARY KEY (user_id)
);

適当にインサート

INSERT INTO usr (user_id, first_name, last_name, role_name, password)
VALUES ('test01', '太郎', '山田', 'USER', 'password');

INSERT INTO usr (user_id, first_name, last_name, role_name, password)
VALUES ('test02', '花子', '田中', 'USER', 'password');

INSERT INTO usr (user_id, first_name, last_name, role_name, password)
VALUES ('test03', '次郎', '佐藤', 'USER', 'password');

JavaxmlファイルのMapper

SampleMybatis.java
インタフェースを書く。@Mapperアノテーション忘れずに。

package example.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import example.model.User;

@Mapper
public interface SampleMybatis {
    List<User> selectAll();
}

SampleMybatis.xml
Javaに書いたメソッドに対応するSQLを書く。
mapperタグのnamespace属性はパッケージ名.クラス名
selectタグ内のresultType属性でモデルクラス指定はパッケージ名.クラス名

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="example.mapper.SampleMybatis">
  <select id="selectAll" resultType="example.model.User">
    select * from usr
  </select>
</mapper>

HomeController

Mapperインタフェース呼び出してSQL実行、画面に表示する。

package example.controller;

import java.util.ArrayList;
import java.util.List;

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 org.springframework.web.bind.annotation.PostMapping;

import example.mapper.SampleMybatis;
import example.model.User;

@Controller
public class HomeController {
    @Autowired
    SampleMybatis mapper;

    @GetMapping(value = "/")
    public String get() {
        return "index";
    }

    @PostMapping(value = "/")
    public String post(Model model) {
        List<User> userList = mapper.selectAll();
        List<String> lines = new ArrayList<>();

        for(User user : userList) {
            lines.add(user.getLastName() + " " + user.getFirstName() + ", " + user.getRoleName());
        }

        model.addAttribute("file_contents", lines);
        return "index";
    }
}

index.html

ボタン押したらSQLの結果を表示

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="https://www.thymeleaf.org"
    xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
    <form method="post" action="/">
        <input type="submit" value="selectAll">
    </form>
    <table>
        <tr th:each="line:${file_contents}">
            <td th:text=${line}></td>
        </tr>
    </table>
</body>
</html>

参考文献

https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/

Spring Bootのapprication.propertiesを環境ごとに用意する

環境変数を使ってローカル環境、開発環境とかで読み込ませるpropertiesファイルを分ける方法。以下のようなファイルを用意する。

  • application.properties 切り分け元
  • application-*.properties 切り分け先

たとえばローカル環境、開発環境で分けるなら下のような感じ。(ファイル名、環境変数名は適当)

  • application.properties
  • application-develop.properties
  • application-production.properties

application.properties

spring.profiles.active=${SPRING_PROFILES}

application-develop.properties

spring.datasource.url=ローカル環境の接続URL
spring.datasource.username=ローカル環境のユーザ名
spring.datasource.password=ローカル環境のパスワード

application-production.properties

spring.datasource.url=開発環境の接続URL
spring.datasource.username=開発環境のユーザ名
spring.datasource.password=開発環境のパスワード

あとは環境変数を各環境ごとに設定する。
上の例ならdevelopproduction

eclipseだったら「実行(起動)の構成」から環境変数を設定できる。

f:id:n-yata:20210423032937p:plain

f:id:n-yata:20210423033101p:plain

Spring BootでローカルからHeroku PostgreSQLに接続

application.propertiesに下のように書く。

spring.datasource.url=jdbc:postgresql://<host>:<port>/<dbname>?sslmode=require&amp;sslfactory=org.postgresql.ssl.NonValidatingFactory
spring.datasource.username=<user>
spring.datasource.password=<password>

<host><port><dbname><user><password>は下のコマンドで確認できる

$ heroku pg:credentials:url

ちなみにHeroku上にアップしたアプリの場合、下記のように書けば
Heroku上に用意された環境変数から接続情報を取得できる。

spring.datasource.url=${JDBC_DATABASE_URL}
spring.datasource.username=${JDBC_DATABASE_USERNAME}
spring.datasource.password=${JDBC_DATABASE_PASSWORD}

Spring BootでTodoアプリ2 -Todo作成-

Todo画面の作成

前回ログイン機能を作成したので、次はメインとなるTodo画面の作成。
下記の記事を参考に作成、少し修正したり機能追加したり。
Spring BootでToDoアプリを作ってみた - かずきのBlog@hatena

Todo用テーブルの作成

前回作成したusrテーブルのuser_idを外部キーとして持つtodoテーブルを作成

DROP TABLE IF EXISTS todoitems CASCADE;
CREATE TABLE IF NOT EXISTS todoitems(
  id SERIAL NOT NULL,
  title VARCHAR(255),
  done BOOLEAN,
  tododate TIMESTAMP NOT NULL,
  user_id VARCHAR(255) NOT NULL,
  PRIMARY KEY (id),
  FOREIGN KEY (user_id) REFERENCES usr
);

Todo画面を作成

前回作成したtodo.htmlを修正する。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta charset="UTF-8">
<title>Todo</title>
</head>
<body>
    <h2>Todo Spring App</h2>
    <p th:inline="text">login user: [[${lastName}]] [[${firstName}]]</p>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
    </form>
    <a th:unless="${todoItemForm.done}" th:href="@{/todo?isDone=true}">完了したアイテムの表示</a>
    <a th:if="${todoItemForm.done}" th:href="@{/todo?isDone=false}">TODOの表示</a>

    <hr />

    <h3>TODOの追加</h3>
    <form method="post" th:action="@{/new}">
        <input type="text" name="title" />
        <input type="submit" value="追加" />
    </form>

    <h3>TODOリスト</h3>
    <table>
        <thead>
            <tr>
                <th>Title</th>
                <th>
                    <form th:if="${todoItemForm.done}" method="post" th:action="@{/deleteAll}">
                        <input type="hidden" name="done" th:value="true" />
                        <input type="submit" value="DeleteAll" />
                    </form>
                    <form th:unless="${todoItemForm.done}" method="post" th:action="@{/doneAll}">
                        <input type="submit" value="DoneAll" />
                    </form>
                </th>
            </tr>
        <tbody>
            <tr th:each="todoItem : ${todoItemForm.todoItems}">
                <td th:text="${todoItem.title}">xxx</td>
                <td>
                    <form th:unless="${todoItemForm.done}" method="post" th:action="@{/done}" th:object="${todoItem}">
                        <input type="hidden" name="id" th:value="*{id}" />
                        <input type="submit" value="Done" />
                    </form>
                </td>
                <td>
                    <form th:if="${todoItemForm.done}" method="post" th:action="@{/restore}" th:object="${todoItem}">
                        <input type="hidden" name="id" th:value="*{id}" />
                        <input type="submit" value="Restore" />
                    </form>
                </td>
                <td>
                    <form th:if="${todoItemForm.done}" method="post" th:action="@{/delete}" th:object="${todoItem}">
                        <input type="hidden" name="id" th:value="*{id}" />
                        <input type="submit" value="Delete" />
                    </form>
                </td>
            </tr>
        </tbody>
    </table>
</body>
</html>

Javaを書いてく

前回作成したログイン機能のみのアプリに下記を加えていく。
/SpringTodo/src/main/java/nyata/app/todo
- HomeController.java
/SpringTodo/src/main/java/nyata/domain/model
- TodoItem.java
/SpringTodo/src/main/java/nyata/domain/repositoriy
- TodoItemRepository.java
/SpringTodo/src/main/java/nyata/domain/service
- TodoItemForm.java

HomeController.java

画面から操作された時のコントローラ。参照したり更新したり。

package nyata.app.todo;

import java.time.LocalDateTime;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import nyata.domain.model.TodoItem;
import nyata.domain.repositoriy.TodoItemRepository;
import nyata.domain.service.TodoItemForm;
import nyata.domain.service.TodoUserDetails;

/**
 * Todo画面のコントローラ
 * @author nyata
 */
@Controller
public class HomeController {
    @Autowired
    TodoItemRepository repository;

    /**
     * 一覧表示
     * @param todoItemForm
     * @param isDone
     * @param userDetails
     * @param model
     */
    @GetMapping(value = "/todo")
    public String todo(@ModelAttribute TodoItemForm todoItemForm, @RequestParam("isDone") Optional<Boolean> isDone,
            @AuthenticationPrincipal TodoUserDetails userDetails, Model model) {
        todoItemForm.setDone(isDone.isPresent() ? isDone.get() : false);
        todoItemForm.setTodoItems(
                this.repository.findByDoneAndUserOrderByTododateAsc(todoItemForm.isDone(), userDetails.getUser()));
        model.addAttribute("firstName", userDetails.getUser().getFirstName());
        model.addAttribute("lastName", userDetails.getUser().getLastName());
        return "todo";
    }

    /**
     * 状態を「完了」に変更
     * @param id
     */
    @PostMapping(value = "/done")
    public String done(@RequestParam("id") long id) {
        TodoItem item = this.repository.getOne(id);
        item.setDone(true);
        this.repository.save(item);
        return "redirect:/todo?isDone=false";
    }

    /**
     * すべてのアイテムの状態を「完了」に変更
     * @param todoItemForm
     * @param userDetails
     */
    @PostMapping(value = "/doneAll")
    public String doneAll(@ModelAttribute TodoItemForm todoItemForm,
            @AuthenticationPrincipal TodoUserDetails userDetails) {
        todoItemForm.setTodoItems(this.repository.findByDoneAndUser(false, userDetails.getUser()));
        for (TodoItem todoitem : todoItemForm.getTodoItems()) {
            todoitem.setDone(true);
            this.repository.save(todoitem);
        }
        return "redirect:/todo?isDone=false";
    }

    /**
     * 状態を「未完」に変更
     * @param id
     */
    @PostMapping(value = "/restore")
    public String RestoreAction(@RequestParam("id") long id) {
        TodoItem item = this.repository.getOne(id);
        item.setDone(false);
        this.repository.save(item);
        return "redirect:/todo?isDone=true";
    }

    /**
     * アイテムを削除
     * @param id
     */
    @PostMapping(value = "/delete")
    public String deleteItem(@RequestParam("id") long id) {
        this.repository.deleteById(id);
        return "redirect:/todo?isDone=true";
    }

    /**
     * 「完了」状態のすべてのアイテムを削除
     * @param done
     * @param userDetails
     */
    @PostMapping(value = "/deleteAll")
    @Transactional
    public String deleteAll(@RequestParam("done") boolean done, @AuthenticationPrincipal TodoUserDetails userDetails) {
        this.repository.deleteByDoneAndUser(done, userDetails.getUser());
        return "redirect:/todo?isDone=true";
    }

    /**
     * 新規アイテムの登録
     * @param item
     * @param userDetails
     */
    @PostMapping(value = "/new")
    public String newItem(TodoItem item, @AuthenticationPrincipal TodoUserDetails userDetails) {
        item.setDone(false);
        item.setTododate(LocalDateTime.now());
        item.setUser(userDetails.getUser());
        this.repository.save(item);
        return "redirect:/todo";
    }
}
TodoItem.java
package nyata.domain.service;

import java.util.List;

import nyata.domain.model.TodoItem;

/**
 * Todoアイテムのフォーム
 * @author nyata
 */
public class TodoItemForm {
    /* 表示リスト(完了・未完)の切替フラグ */
    private boolean isDone;
    /* 表示するTodoリスト */
    private List<TodoItem> todoItems;

    // setter, getter
    public boolean isDone() {
        return isDone;
    }

    public void setDone(boolean isDone) {
        this.isDone = isDone;
    }

    public List<TodoItem> getTodoItems() {
        return todoItems;
    }

    public void setTodoItems(List<TodoItem> todoItems) {
        this.todoItems = todoItems;
    }
}
TodoItemRepository.java

JPAを使う用のリポジトリ。ソートや一括更新のため少しメソッド追加。
ユーザーごとのアイテムリストを表示する。

package nyata.domain.repositoriy;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import nyata.domain.model.TodoItem;
import nyata.domain.model.User;

/**
 * Todoアイテムのリポジトリ
 * @author nyata
 */
public interface TodoItemRepository extends JpaRepository<TodoItem, Long> {
    public List<TodoItem> findByDoneAndUser(boolean done, User user);

    public List<TodoItem> findByDoneAndUserOrderByTododateAsc(boolean done, User user);

    public long deleteByDoneAndUser(boolean done, User user);
}
TodoItemForm.java

画面に表示内容を受け渡す役割

package nyata.domain.service;

import java.util.List;

import nyata.domain.model.TodoItem;

/**
 * Todoアイテムのフォーム
 * @author nyata
 */
public class TodoItemForm {
    /* 表示リスト(完了・未完)の切替フラグ */
    private boolean isDone;
    /* 表示するTodoリスト */
    private List<TodoItem> todoItems;

    // setter, getter
    public boolean isDone() {
        return isDone;
    }

    public void setDone(boolean isDone) {
        this.isDone = isDone;
    }

    public List<TodoItem> getTodoItems() {
        return todoItems;
    }

    public void setTodoItems(List<TodoItem> todoItems) {
        this.todoItems = todoItems;
    }
}

完成

ログイン機能よりすごく簡単に実装できた。フレームワークって簡単にアプリ作れてすごいなと思いました。 もう一つくらい、Springでアプリ作りたいと思ってる。ひとまずUIがしょぼいままなのでフロントの勉強として作成したアプリの見た目を整えていこうと思う。
GitHub - n-yata/ToDoSpring

はまったこと

画面遷移とリダイレクトでパスの書き方が異なる

単純に画面遷移するときは、return "todo";
リダイレクトのときは、 return "redirect:/todo";
頭に"/"をつけたりつけなかったり。ちゃんと区別しないと怒られる。

一括更新の仕方

SQLだったらUPDATE todoitems SET done = 'false' WHERE user_id = 'hogehoge'とかでまとまりのまま一括更新できるのだけど、 JPAで一括UPDATEするときにfor文利用する方法しか見つからなかった。

        todoItemForm.setTodoItems(this.repository.findByDoneAndUser(false, userDetails.getUser()));
        for (TodoItem todoitem : todoItemForm.getTodoItems()) {
            todoitem.setDone(true);
            this.repository.save(todoitem);
        }

なんかSQLの良さを殺してしまうような書き方な気がして気持ち悪いので気が向いたら修正する。

JPAの書き方

日本語の記事少ない。英語ちゃんと勉強しようと思いました・・・

参考文献

Spring BootでToDoアプリを作ってみた - かずきのBlog@hatena
[Spring MVC] パス変数の受け渡し方について - Qiita
Spring Data JPA で遊んでみる 〜その6〜 - Yamkazu's Blog
令和時代に「Spring入門」「Spring徹底入門」を読むとき気をつけるべきN個のこと - Qiita

Spring BootでTodoアプリ1 -ログイン機能作成-

前置き

以前Spring徹底入門を読んでから、業務で触れることなかったのでいったんSpring勉強してなかった。 週末にまとまった勉強時間が取れるようになって、やりたい勉強できるようになったのでSpring Bootでアプリ作ってみた。 相も変わらず作成したアプリはHerokuにアップ。
Spring徹底入門はリファレンスとしてアプリ作りながら読むとすごく勉強になる。

アプリの内容

Todoアプリ。ログイン機能とTodoを作る。
DBはHerokuで使用できるPostgreSQL
本記事ではログイン機能を作成。
f:id:n-yata:20210320061108p:plain

テーブル作成

Todoアプリ用DBを作って、ユーザー管理テーブルを作成。

CREATE DATABASE tododb;

DROP TABLE IF EXISTS usr CASCADE;
CREATE TABLE IF NOT EXISTS usr(
  user_id VARCHAR(255) NOT NULL,
  first_name VARCHAR(255) NOT NULL,
  last_name VARCHAR(255) NOT NULL,
  password VARCHAR(255) NOT NULL,
  role_name VARCHAR(255) NOT NULL,
  PRIMARY KEY (user_id)
);

インサートするデータはこんな感じ。

INSERT INTO usr (user_id, first_name, last_name, role_name, password)
VALUES ('user', '太郎', '山田', 'USER', '$2a$10$ZqU0cizLk7c6YEydWPfpr.3Gmomn5JtYOfBqEExy3muSNKm3sZY7a');

passwordはBCryptでハッシュ値に変換したものを入れとく。
上記のパスワードは「password」をハッシュ化したもの。
ハッシュ化は下記のサイトで計算できる。(ありがたい)
BCrypt ハッシュ値 計算 | tekboy

ストレッチング回数は10回に指定してハッシュ化。 f:id:n-yata:20210320063125p:plain

プロジェクト作成

eclipseで開発。最近のpleiadesならSTS標準で入ってるはず。
Spring Boot 開発環境構築 Eclipse STS インストール - 公式ガイド

プロジェクトを作成。
f:id:n-yata:20210320063714p:plainf:id:n-yata:20210320063725p:plain

pom.xmlのdependenciesタグ内にSpring Securityを追加。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>

application.propertiesにPostgreSQLの接続情報を追加。
ついでにログ出力も設定。

# DB接続設定
spring.datasource.url=jdbc:postgresql://localhost:5432/tododb
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true

# ログ出力の設定
logging.file.name=C:/logs/todoSpring.log
#logging.level.org.springframework.web=DEBUG
logging.level.org.springframework.web=INFO

HTMLの作成

ログイン画面のHTMLを作成。
「/SpringTodo/src/main/resources/templates」内にlogin.htmlを作成。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta charset="UTF-8">
<title>Login Form</title>
</head>
<body>
    <div th:if="${param.error}">
        Invalid username and password.
     </div>
     <div th:if="${param.logout}">
        You have been logged out.
     </div>
     <form th:action="@{/login}" method="post">
        <div><label> User Name : <input type="text" name="username" /></label></div>
        <div><label> Password : <input type="password" name="password" /></label></div>
        <div><input type="submit" value="Sign In"></div>
     </form>
</body>
</html>

ログインが成功したときの遷移先を作成。
todo.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
        <form th:action="@{/logout}" method="post">
            <input type="submit" value="Sign Out"/>
        </form>
    </body>
</html>

Javaを書く

作成するソースは下記。
/SpringTodo/src/main/java/nyata/
- MvcConfig.java
- SpringTodoApplication.java(今回は編集しない)
- WebSecurityConfig.java

/SpringTodo/src/main/java/nyata/domain/model
- User.java
- RoleName.java

/SpringTodo/src/main/java/nyata/domain/repositoriy
- UserRepository.java

/SpringTodo/src/main/java/nyata/domain/service
- TodoUserDetails.java
- TodoUserDetailsService.java

MvcConfig.java

URLの遷移先を管理

package nyata;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * View Controller
 * @author nyata
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
        registry.addViewController("/").setViewName("login");
        registry.addViewController("/todo").setViewName("todo");
    }
}
WebSecurityConfig.java

Spring Securityの設定情報。
パスワードのハッシュ化としてBCryptを使用。

package nyata;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import nyata.domain.service.TodoUserDetailsService;

/**
 * Security Config
 * @author nyata
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    TodoUserDetailsService userDetailsService;

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/js/**", "/css/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .usernameParameter("username")
                .passwordParameter("password")
                .defaultSuccessUrl("/todo", true)
                .permitAll()
                .and()
                .logout()
                .permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}
RoleName.java
package nyata.domain.model;

/**
 * ユーザー権限一覧
 * @author nyata
 */
public enum RoleName {
    ADMIN, USER
}
User.java
package nyata.domain.model;

import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Id;
import javax.persistence.Table;

import org.hibernate.annotations.Proxy;

/**
 * ユーザー情報のエンティティ
 * @author nyata
 */
@Entity
@Proxy(lazy = false)
@Table(name = "usr")
public class User {
    /* ユーザーID */
    @Id
    private String userId;
    /* パスワード */
    private String password;
    /* ファーストネーム */
    private String firstName;
    /* ラストネーム */
    private String lastName;
    /* ユーザー権限 */
    @Enumerated(EnumType.STRING)
    private RoleName roleName;

    // setter, getter
    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public RoleName getRoleName() {
        return roleName;
    }

    public void setRoleName(RoleName roleName) {
        this.roleName = roleName;
    }
}
UserRepository.java

JPAを使用するためのリポジトリ

package nyata.domain.repositoriy;

import org.springframework.data.jpa.repository.JpaRepository;

import nyata.domain.model.User;

/**
 * ユーザー情報のリポジトリ
 * @author nyata
 */
public interface UserRepository extends JpaRepository<User, String> {
}
TodoUserDetails.java

ユーザー認証の設定

package nyata.domain.service;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;

import nyata.domain.model.User;

/**
 * ユーザー認証情報
 * @author nyata
 */
public class TodoUserDetails implements UserDetails {
    private final User user;

    public TodoUserDetails(User user) {
        this.user = user;
    }

    public User getUser() {
        return user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return AuthorityUtils.createAuthorityList("ROLE_" + this.user.getRoleName().name());
    }

    @Override
    public String getPassword() {
        return this.user.getPassword();
    }

    @Override
    public String getUsername() {
        return this.user.getUserId();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
TodoUserDetailsService.java

ユーザー認証処理を受け持つ

package nyata.domain.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import nyata.domain.model.User;
import nyata.domain.repositoriy.UserRepository;

/**
 * ユーザー認証のサービス
 * @author nyata
 */
@Service
public class TodoUserDetailsService implements UserDetailsService {
    @Autowired
    UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.getOne(username);
        if (user == null) {
            throw new UsernameNotFoundException(username + " is not found.");
        }
        return new TodoUserDetails(user);
    }
}

ひとまずこれでログイン機能完成。次はTodo機能を作っていく。

参考文献

Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発(株式会社NTTデータ)|翔泳社の本
Spring Boot ログイン画面 - 公式サンプルコード
BCrypt ハッシュ値 計算 | tekboy
Spring Bootでログを出力する | DevelopersIO
SpringSecurity-AuthenticatedPricipalは非推奨 - Javaer101

WindowsでJIS配列からUS配列キーボードに変更したときに設定したこと

前置き

US配列キーボードには前からなんだか憧れがあった。「できるプログラマならUS配列!」みたいな雰囲気をググってるとよく見てて、それに影響されてUSキーボードを買ってみた。 確かにホームポジションからEnterキーまで距離少し近いとか、コード書いてると、プログラミング言語の構文ってUS配列用にできてるなって感じる。 いったんどうしても解決できない課題があったのでJIS配列に戻したけど、またいずれUS配列に切り替えるようのメモとして残す。

USキー配列への設定変更

ググれば結構出てくるけどおさらい。
下記の画像の通りに進んで再起動すればUS配列用に設定できる

f:id:n-yata:20210228131329p:plain
f:id:n-yata:20210228131335p:plain
f:id:n-yata:20210228131345p:plain
f:id:n-yata:20210228131407p:plain
f:id:n-yata:20210228131413p:plain
f:id:n-yata:20210228131418p:plain

ノートPCのキーボード(JIS配列)を無効化

別に必須じゃないけど。ノートPCのキーボードの上に物理的に外付けキーボード置いて使ったりするので無効にした。下記の記事を参考にした。
qiita.com

VSCodeの挙動用に設定変更

困ったことにVSCodeで「Ctl+`(バッククォート)」が反応しない。
(統合ターミナルを表示するためのショートカット)

下記の記事を参考にWindowsの言語に英語を追加した。
qiita.com

在宅勤務でホストPCにリモート接続は難あり

ホストPCの設定がJIS配列のままだからか、リモートデスクトップ製品の問題か、自宅PC(US配列)からキーボード入力すると、配列の違いから入力ができない文字がある。 残念ながらSESという業態で勤務している以上、ホストPCは勝手に設定できないし、US配列で作業は支障が出るのでいったんJIS配列に戻した。

ラズパイ(Debian 10)にサーバー(Tomcat 9)入れて公開する手前まで

前置き

いずれ自分でサーバー立てたいと思ってたけど、直近ではそんなつもりなかった。 自社の人と会話する機会があって、ラズパイの魅力をたくさん教えてもらって衝動買い。 せっかくなので勉強がてら自分のサーバーを立ててみることにした。 (最後に記載するけど、物理的な障害がありサーバー公開するには至っていない)

買ったやつ

www.amazon.co.jp

ラズパイと周辺機器全部入り。OSもraspbianがSDカードにインストール済み。 チキンなので全部そろってすぐ起動できるやつ買った。初期設定関係はこの記事では省略。

まずは最新状態にしてバージョン確認

ラズパイにインストールされてるもろもろとかを最新状態にする。
セキュリティ的にも定期的に最新状態にする必要あるのだろうなあ(面倒)

# 最新のパッケージをインストール
$ sudo apt update
$ sudo apt upgrade

# OSのアップデート
$ sudo apt-get dist-upgrade

# ラズパイのファームウェアのアップデート
$ sudo rpi-update

# debianのバージョン確認
$ cat /etc/debian_version

raspbianとdebianの違いはよくわかってない。けどサーバーとして使う分には一緒っぽい。 debianのバージョンわかったら「debian 10 tomcat インストール」とかでググる

Tomcatをインストールする

自分の場合はバージョン10だったので、下記のような記事がヒットした。
記事を参考にしながら(というか記事そのまま)必要なツールをインストールしていく
Debian 10にApache Tomcat 9をインストールする方法

Tomcatインストールして設定完了したら、ブラウザに「localhost:8080」とかでTomcatのページ表示されるか確認。 他のPCから同一ネットワーク内のラズパイにアクセスするなら、「ラズパイのIPアドレス:8080」で表示できる。 ラズパイIPアドレスは下記コマンドで調べる。wifiの場合はwlan0のinetってとこ(192.168..とか)。

$ ifconfig

メモとしてlinuxでのusergroupのコマンド
(ユーザーの追加は-aをつけ忘れると追加ではなく置き換えになる。全部グループ消えるので注意!)

# ユーザーのgroupを確認 
$ groups username
# ユーザーの追加
$ sudo usermod -aG groups username

PostgreSQLのインストール

DBは使い慣れているPostgreSQLをインストール。
環境構築:Debian 10(buster) にPostgreSQL 11をインストールし、新規DBを作成する方法

上記記事を参考にPostgre接続用ユーザーを作成した場合、ユーザー名のDBは作成されていないので

$ psql -U 接続用ユーザー名

ってやったら失敗した。

$ psql -U 接続用ユーザー名 -d postgres

とかって作成済みのDB名を指定する必要がある。
(もしくは接続用ユーザー名と同じ名前のDBを作成する。よろしくないだろうけど)

warファイルをデプロイしてみる

アプリ作ってwarで固めたらTomcatにwarファイルを配置する。
tomcatユーザーでないと権限無いのでスイッチする必要ある。

# tomcatユーザーにスイッチ
$ su - tomcat

# warファイルを移動
$ mv appname.war /opt/tomcat/webapps/

少しすると勝手にwarを展開してくれてサービス開始する。 下記はTomcatコントロールする用のコマンド

# 起動
$ sudo systemctl start tomcat

# 停止
$ sudo systemctl stop tomcat

# リスタート
$ sudo systemctl restart tomcat

# ステータス確認
$ sudo systemctl status tomcat

URLを入れて動いてるか確認。
localhost:8080/appname

公開するためにセキュリティ面を整える

下記記事を参考にした。
raspberry piでお手軽自家サーバー webサーバー編 - Qiita

そしていざ公開!

(まだセキュリティダメかもしれないけど)とにかく公開したくて自宅のルーターいじってポート開放! しようとして、さきほどの記事見ながらなんちゃらやってたら、使ってるwifiルーターが公式でポート開放できないようにしてることが判明した。
SoftBank Air における機能制限のお知らせ | インターネット・固定電話 | ソフトバンク

つよつよを目指すためには、物理的な障害も解決しないといけないことを学んだ(完)