【JAVA】SPRING BOOTでWEBアプリを作成してみる 10日目 AJAXで画像をDB(Blob oid)にアップロード・画面表示 おみくじ機能

2021/9/4



こんにちは。今回はAjaxを使って画面更新をしない非同期での画像登録処理を行います。
Blobの画像は、フォルダではなく、データベースに登録できるようにします。




前準備


イメージをダウンロードするため下記を利用します。

ポイント

  • データベースから取得したバイナリデータをBase64でエンコードして画面に表示させるため、commons-codecを使用します。
  • Mybatisだけでなく、postgresqlのLargeObjectを使用しました。postgresqlのスコープをコメントアウト、<!-- <scope>runtime</scope> -->


pom.xmlの中身を記載します。

/demo/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>2.2.6.RELEASE</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>1.8</java.version>
    <maven-jar-plugin.version>3.1.1</maven-jar-plugin.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-devtools</artifactId>
      <scope>runtime</scope>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.0</version>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <!-- <scope>runtime</scope> -->
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.webjars</groupId>
        <artifactId>bootstrap</artifactId>
        <version>4.1.1</version>
    </dependency>
    <dependency>
        <groupId>org.webjars</groupId>
        <artifactId>jquery</artifactId>
        <version>3.3.1</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-commons</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter-test</artifactId>
        <version>2.1.0</version>
    </dependency>
    <dependency>
        <groupId>com.github.springtestdbunit</groupId>
        <artifactId>spring-test-dbunit</artifactId>
        <version>1.3.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.dbunit</groupId>
        <artifactId>dbunit</artifactId>
        <version>2.5.2</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
    </dependency>
    <!-- なくてもいいですが、便利だから入れました -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
  </dependencies>

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

</project>

Spring Securityに引っかからないよう、"/fortune/**"(おみくじ処理)と"/statics/**"(cssの格納場所)のパスにpermitAll() を追加します。(41行目)

/demo/src/main/java/com/example/demo/configuration/SpringSecurityConfig.java
package com.example.demo.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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 org.springframework.security.web.util.matcher.AntPathRequestMatcher;

/**
*
* Spring Boot 勉強用 Spring Securityの設定
*
* @version   1.0.0
*/
@Configuration
// Spring Security 有効
@EnableWebSecurity
// メソッド単位で認可する @Securedを有効化
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled=true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    public static final String ADMIN = "ADMIN";
    public static final String EMPLOYEE = "EMPLOYEE";
    public static final String ROLE_ADMIN = "ROLE_ADMIN";
    public static final String ROLE_EMPLOYEE = "ROLE_EMPLOYEE";

    @Bean
    PasswordEncoder passwordEncoder() {
        // パスワードをBCryptで扱う
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests() // 認証
                .antMatchers("/", "/prelogin", "/login/add", "/resources", "/webjars/**", "/fortune/**", "/statics/**")
                .permitAll() // ログイン認証なしでアクセス可能
                 // 管理者権限がないとアクセス不可。"ROLE_"の接頭辞は不要。
                 // 社員権限の/users/**より具体的なルールなので先に記述する
                .antMatchers("/users/new").hasRole(ADMIN)
                .antMatchers("/users/**").hasRole(EMPLOYEE) // 社員権限がないとアクセス不可。
                .anyRequest().authenticated() // 上記以外、認証を要求する
                .and()
               .formLogin() // ログイン
                .loginPage("/prelogin") // 許可のない非認証のアクセスの場合、強制的にログイン画面へ遷移させる
                .loginProcessingUrl("/login") // ログイン処理を行うurl。画面から飛ばし、コントローラには書かなくていい
                .usernameParameter("fEmail") // ログイン画面でログイン処理を行うためのメールアドレス
                .passwordParameter("fPassword") // 〃パスワード
                .defaultSuccessUrl("/users") // ログイン成功時に遷移する
                .failureUrl("/login/error") // ログイン失敗時に遷移する
                .permitAll()
                .and()
               .logout() // ログアウト
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) // ログアウトを行うパス
                .logoutSuccessUrl("/prelogin") // ログアウトしてログイン画面へ遷移させる
                .deleteCookies("JSESSIONID") // ログアウト後、CookieのセッションIDを削除
                .invalidateHttpSession(true) // trueでログアウト後、セッションを無効
                .permitAll()
                .and()
               .csrf();//csrf対策
    }

}

おみくじ機能の実装


Ajax


Ajaxは、Asynchronous JavaScript + XMLの略です。

ウェブブラウザ内で非同期通信を行うプログラミング手法で、画面遷移を行わずにサーバーから情報を取得し、動的な処理を行うことができます。
これを使えば画面の一部の表示だけをその場で変えることができるようになります。

今回は、最もメジャーな手法と思われるJqueryからAjaxを利用してみましょう。
フォームとともに送信しない場合、Spring Securityのcsrfも考慮する必要があります。

https://ja.wikipedia.org/wiki/Ajax


postgresのBlob型をoidで登録、取得


postgresには、イメージやXMLといったファイルを保管するためのラージオブジェクトとしてBlob型(Binary large object)が存在します。

Blob型には1ギガバイトまでのファイルを扱えるbyteaとそれ以上のファイルを扱うことのできるoidがあります。

今回はメモリ効率の良いといわれるoidでイメージの登録を行います。

oidで登録した場合、通常のテーブルにはoidというidが登録されます。
実際のデータはpg_largeobjectの方に紐づくoid毎に複数分割されて格納されます。
また、pg_largeobjectには権限がないとアクセスできないようでした。
そこでMybatisだけでなく、postgresのLargeObjectを使用してデータを取得しました。

ラージオブジェクト


シナリオ


まず、Ajaxにてイメージファイル(今回はPNGのみ)をクライアントからサーバーに送り、データベースに登録します。

その後、おみくじボタンを押したときに、ランダムで登録されたイメージとおみくじの結果を表示してみます。



データベース


おみくじの文言格納用にfortuneテーブルを、イメージ格納用にfortune_imageテーブルを作成します。

/demo/src/main/resources/schema.sql
--DDL
--usersテーブルが存在しなければ作成
CREATE TABLE IF NOT EXISTS web_schema.users (
  id serial PRIMARY KEY,
  name VARCHAR(30),
  email  VARCHAR(50),
  address VARCHAR(255),
  sex VARCHAR(1),
  remark VARCHAR(255)
);

--m_selectテーブルが存在しなければ作成
CREATE TABLE IF NOT EXISTS web_schema.m_select (
  name VARCHAR(30),
  value VARCHAR(1)
);

--authoritiesテーブルが存在しなければ作成
CREATE TABLE IF NOT EXISTS web_schema.authorities (
  email VARCHAR(30) PRIMARY KEY,
  password  VARCHAR(80) NOT NULL,
  adminauthority  boolean,
  createdate timestamp DEFAULT CURRENT_TIMESTAMP,
  enabled boolean DEFAULT true,
  accountnonexpired boolean DEFAULT true,
  accountnonlocked boolean DEFAULT true,
  credentialsnonexpired boolean DEFAULT true
);

--fortune_imageテーブルが存在しなければ作成
CREATE TABLE IF NOT EXISTS web_schema.fortune_image (
  id serial PRIMARY KEY,
  image oid
);

--fortuneテーブルが存在しなければ作成
CREATE TABLE IF NOT EXISTS web_schema.fortune (
  id serial PRIMARY KEY,
  word  VARCHAR(80) NOT NULL
);

/demo/src/main/resources/data.sql
--DML
--usersテーブルの削除
DELETE FROM web_schema.users;

--usersテーブルの挿入
INSERT INTO web_schema.users (name,email,address,sex,remark) VALUES ('おとくちゃん','otoku@xxxxx.co.jp','埼玉県','0','特になし');
INSERT INTO web_schema.users (name,email,address,sex,remark) VALUES ('織田信長','nobu@xxxxx.co.jp','尾張','1','天下布武');
INSERT INTO web_schema.users (name,email,address,sex,remark) VALUES ('豊臣秀吉','hide@xxxxx.co.jp','大阪','1','天下統一');
INSERT INTO web_schema.users (name,email,address,sex,remark) VALUES ('徳川家康','ie@xxxxx.co.jp','三河','1','江戸開幕');
INSERT INTO web_schema.users (name,email,address,sex,remark) VALUES ('NYN','nyn@xxxxx.co.jp','東京都','0','お菓子の材料屋さん');
INSERT INTO web_schema.users (name,email,address,sex,remark) VALUES ('ICG','icg@xxxxx.co.jp','北海道','0','お客さん');
INSERT INTO web_schema.users (name,email,address,sex,remark) VALUES ('MUR','mur@xxxxx.co.jp','下北沢','1','大先輩');
INSERT INTO web_schema.users (name,email,address,sex,remark) VALUES ('YJSP','yjsp@xxxxx.co.jp','下北沢','0','空手部');
INSERT INTO web_schema.users (name,email,address,sex,remark) VALUES ('KMR','kmr@xxxxx.co.jp','下北沢','1','空手部');

--m_selectテーブルの削除
DELETE FROM web_schema.m_select;

--m_selectテーブルの挿入
INSERT INTO web_schema.m_select (name,value) VALUES ('-','-');
INSERT INTO web_schema.m_select (name,value) VALUES ('女性','0');
INSERT INTO web_schema.m_select (name,value) VALUES ('男性','1');

--fortuneテーブルの削除
DELETE FROM web_schema.fortune;

--fortuneテーブルの挿入
INSERT INTO web_schema.fortune (id,word) VALUES ('1','大吉。いいことがあるでしょう');
INSERT INTO web_schema.fortune (id,word) VALUES ('2','中吉。普通の人生');
INSERT INTO web_schema.fortune (id,word) VALUES ('3','小吉。そんなに良くない');
INSERT INTO web_schema.fortune (id,word) VALUES ('4','末吉。ぎりぎり');
INSERT INTO web_schema.fortune (id,word) VALUES ('5','凶。残念');


Spring Bootの設定


サーバにあまり大容量のファイルを大量に送られせないように、サーバに送られるファイルの容量を制限します。ここでは5MBに指定
spring.servlet.multipart.max-file-size
spring.servlet.multipart.max-request-size

ファイルアップロード制限の調整
5.17.2.1.1. Servlet3.0のアップロード機能を有効化するための設定

/demo/src/main/resources/application.properties
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/web?web_schema
spring.datasource.username=dev
spring.datasource.password=xxxx

#開発プロファイルの読み込み
spring.profiles.active=dev

#起動するごとにDBを初期化する
spring.datasource.initialization-mode=always

#1ページに表示する行数
pageRowCnt=3

# アップロードするファイルのサイズ指定
spring.servlet.multipart.max-file-size=5242880
spring.servlet.multipart.max-request-size=5242880

[ファイルのサイズ超えしたときにおこるMaltipartExceptionのハンドリング方法]
Spring Boot でファイルアップロードエラーをハンドリングする

これはうまくできませんでした。


サイドバーとログイン画面の修正


ルートパスに設定しているログイン画面のサイドバーにおみくじボタンを表示し、押下時にダイアログ表示して処理を行う。

[サイドバー]
/demo/src/main/resources/templates/layout/sidebar.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>サイドバー</title>
</head>
<body>

  <!--
    th:fragmentでhtmlを部品化
    th:removeの属性tagでdivタグを削除
  -->
  <div class="sidebar-class" th:fragment="fra-sidebar" th:remove="tag">
    <form id="sidebar-form" th:action="@{/users/new}" th:method="post">

      <!-- ROLE_USER権限のときはボタン活性 -->
      <span sec:authorize="hasRole('ADMIN')"><input class="btn btn-primary btn-lg" type="submit" value="新規登録" /></span>
      <!-- ROLE_USER権限でないときはボタン非活性 -->
      <span sec:authorize="!hasRole('ADMIN')"><input class="btn btn-primary btn-lg" type="submit" value="新規登録" disabled /></span>

    </form>

    <!-- なにも権限のないときボタン表示 -->
    <span sec:authorize="!hasRole('ADMIN') and !hasRole('EMPLOYEE') ">
      <input  id="modal-btn" class="btn btn-success btn-lg mt-4" type="button" value="おみくじ" data-toggle="modal" data-target="#modal-fortune" />
    </span>

    <!-- おみくじで表示するイメージの登録 -->
    <div class="modal fade" id="modal-fortune">
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title">Ajax:おみくじ</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="閉じる">
              <span aria-hidden="true">×</span>
            </button>
          </div>
          <div class="modal-body">

            <form id="modal-form" th:action="@{/fortune/image_upload}" th:method="post"
                      enctype="multipart/form-data">

              <div class="form-group">
                <label for="file">おみくじのイメージ選択</label>
                  <div class="custom-file">
                    <input type="file" class="custom-file-input" id="file" name="file" accept="image/png">
                    <label class="custom-file-label" for="file">おみくじで使用するPNGを選択してください</label>
                  </div>
              </div>

              <button type="button" class="btn btn-primary mr-2" id="modal-fortune-add">登録</button>
              <button type="button" class="btn btn-primary" id="modal-fortune-get">おみくじ</button><br>
              <div id="word" class="mt-4"></div>
              <!-- PNGを追加するタグ -->
              <div id="fortune-image"></div>

            </form>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-dismiss="modal">閉じる</button>
          </div>
        </div>
      </div>
    </div>

  </div>

</body>
</html>
23行目 : <span sec:authorize="!hasRole('ADMIN') and !hasRole('EMPLOYEE') ">で最初にログイン画面を開いたときに"おみくじ"ボタンが表示されるようにしました。


28-64行目 : "おみくじ"ボタンが押下されたときにダイアログを表示し、イメージの登録とおみくじ処理を行います。
サーバにファイルを送るには、フォームにenctype="multipart/form-data"を設定します。

ポイント

  • ファイルの送信は、enctype="multipart/form-data"を使う



ログイン画面の修正はajaxに関連するjavascriptの修正が主になります。

[ログイン画面]
/demo/src/main/resources/templates/login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<meta id="_csrf" name="_csrf" th:content="${_csrf.token}"/>
<meta id="_csrf_header" name="_csrf_header" th:content="${_csrf.headerName}"/>
<title>ユーザ登録・ログイン処理</title>
<link rel="stylesheet"
  th:href="@{/webjars/bootstrap/4.1.1/css/bootstrap.min.css}" />
<script th:src="@{/webjars/jquery/3.3.1/jquery.min.js}"></script>
<script th:src="@{/webjars/bootstrap/4.1.1/js/bootstrap.min.js}"></script>
<script src="https://cdn.jsdelivr.net/npm/bs-custom-file-input/dist/bs-custom-file-input.js"></script>
<link th:href="@{/statics/default.css}" rel="stylesheet" type="text/css">
<script>
$(document).ready( function(){
    /* エラー数のカウント */
    var errCnt = $('.is-invalid').length;

    if (errCnt > 0) {
      /* エラーの入力欄にフォーカスを設定 */
      $('.is-invalid').first().focus();
    } else {
      /* 検索条件フォームの名前の入力欄にフォーカスを設定 */
      $('input#name').focus();

      /* 新規登録ボタン押下 */
      $('#btn-add').on('click', function () {
        $('#loginForm').attr('action', '/login/add').submit();
      });

    }

    /* おみくじボタン押下時の初期化 */
    $('#modal-btn').on('click', function () {
        /* アップロードファイルの初期化 */
        $('#image').remove();
        $('#word').text('');
        $('input[type=file]').val('');
        $('.custom-file-label').text('おみくじで使用するPNGを選択してください');
    });

    /* ファイルを選択すると、ファイル名を表示 */
    $('.custom-file-input').on('change',function(){
        $(this).next('.custom-file-label').html($(this)[0].files[0].name);
    });

    /* おみくじイメージの登録 */
    $('#modal-fortune-add').on('click', function () {

        var pos = $('.custom-file-input').val().lastIndexOf('.');
        if (pos === -1) return alert('PNGを選択してください');

        var filename = $('.custom-file-input').val().slice(pos + 1);
        if (filename != 'png') return alert('PNGを選択してください');

        var formData = new FormData($('#modal-form').get(0));

        if (window.FormData) {
          $.ajax({
              type : 'POST',
              url : '/fortune/image_upload',
              dataType : 'text', // 受信するデータのタイプ
              data : formData, // 送信するデータ
              processData : false, // クエリ文字列に変換するか
              contentType : false, // content-typeヘッダの値を変換するか
              cache: false,
              timeout: 600000,
              success : function(data) {
                alert(data);
              },
              error : function(XMLHttpRequest, textStatus, errorThrown) {
                alert('XMLHttpRequest ' + XMLHttpRequest);
                alert('textStatus ' + textStatus);
                alert('errorThrown ' + errorThrown);
              }
          });
        } else {
          alert('FormDataが非対応のブラウザ');
        }
        /* アップロードファイルの初期化 */
        $('input[type=file]').val('');
        $('.custom-file-label').text('おみくじで使用するPNGを選択してください');
    });

    /* おみくじ機能 */
    $('#modal-fortune-get').on('click', function () {

      var token = $('#_csrf').attr('content');
      var header = $('#_csrf_header').attr('content');

      $.ajax({
          type : 'POST',
          url : '/fortune/get_info',
          dataType : 'json',
          cache: false,
          timeout: 600000,
          /* おみくじ機能 */
          beforeSend: function(xhr) {
              xhr.setRequestHeader(header, token);
          },
          success : function(data) {
            $('#word').text(data.word);

            if (data.image) {
              $('#image').remove();
              $('#fortune-image').append('<img id="image" width="300px">')
              $('#image').attr('src', data.image);
            }
          },
          error : function(XMLHttpRequest, textStatus, errorThrown) {
            alert('XMLHttpRequest ' + XMLHttpRequest);
            alert('textStatus ' + textStatus);
            alert('errorThrown ' + errorThrown);
          }
      });
    });

});
</script>
</head>
<body>

  <div class="container">

    <!--ヘッダー -->
    <header class="mt-5">
      <div id="header-container" th:insert="layout/header :: fra-header"></div>
    </header>

    <div class="row mt-5">

      <!--col-smで解像度がスマホ(~767px)以下の時、2列表示 -->

      <!--メニュー・サイドバー -->
      <nav class="col-sm-2">
        <div id="sidebar-container"
          th:insert="layout/sidebar :: fra-sidebar"></div>
      </nav>

      <!--メイン -->
      <section id="main-container" class="offset-sm-1 col-sm-9">

        <h2>ユーザ登録・ログイン処理</h2>
        <br>

        <!--メッセージ -->
        <th:block th:with="message=${message}">
           <span th:text="${message}"></span>
        </th:block>

        <!-- CSRF対策  actionでなく、th:actionで書かないとSpring SecurityはPOST送信をはじく -->
        <form class="mt-4" id="loginForm" method="post" action="" th:action="@{/login}" th:object="${loginForm}">

          <div class="form-group row" th:classappend="${#fields.hasErrors('fEmail')}? has-error">
            <label>ユーザメール(ユーザ名) <span class="badge badge-danger">必須</span></label>
            <input type="email" th:class="${#fields.hasErrors('fEmail')} ? 'form-control is-invalid':'form-control'"
                            id="email" name="fEmail" th:value="*{fEmail}"/>
            <div class="text-danger" th:if="${#fields.hasErrors('fEmail')}" th:errors="*{fEmail}"></div>
          </div>

          <div class="form-group row" th:classappend="${#fields.hasErrors('fPassword')}? has-error">
            <label>パスワード <span class="badge badge-danger">必須</span></label>
            <input type="password" th:class="${#fields.hasErrors('fPassword')} ? 'form-control is-invalid':'form-control'"
                            id="password" name="fPassword" th:value="*{fPassword}"/>
            <div class="text-danger" th:if="${#fields.hasErrors('fPassword')}" th:errors="*{fPassword}"></div>
          </div>

          <div class="mt-4 mb-4">
            <input type="button" class="btn btn-primary" id="btn-add" value="ユーザ登録" />
            <input type="submit" class="btn btn-primary mr-2" id="btn-login" value="ログイン" />
          </div>

          <!--権限選択 -->
          <label>ユーザ登録時の権限を選択</label>
          <div class="form-check mb-4">
            <input class="form-check-input" type="checkbox" name="fAdminAuthority" id="authority" value="on" checked>
            <label class="form-check-label" for="authority">ユーザに管理者権限を付与</label>
          </div>

        </form>

      </section>

    </div>

  </div>

</body>
</html>
5-6、97-99行目 : Ajaxでフォームがないとき、これまでのようにth:actionでcsrfのトークンが自動で入力ということができません。
なので、Spring Securityのcsrfに引っかからないようにトークンを設定します。
Use of Spring CSRF with ajax Rest call and HTML page with Thymeleaf


12-13、42-44行目 : BootStrapのファイルを設定するinputはデザインが良くないので、bs-custom-file-inputを使用します。設定が多少面倒ですが...
カスタムフォームのサンプル


58-66行目 : Ajaxにてサーバに送信して戻り値を受け取る設定をしています。
typeとurlはそのまま、それぞれフォームのmethodとactionに対応します。
data : formDataで、55行目で取得したフォームのデータをサーバ側に送信します。

dataType : 'text'で、サーバから帰ってくる値を文字として評価します。
jsonというサーバとやり取りしやすい形式のフォーマットで受け取りたいときには'json'を指定します。

Ajaxの処理成功時にはsuccess内に処理を書きます。
イメージ登録の68行目ではサーバからの戻り値を表示。
おみくじ表示の方では、101-107行目でおみくじの文言の表示とともに、サーバからイメージを受け取った時には'#fortune-image'タグにイメージのタグを追加し、画像を表示しています。
Ajaxの処理失敗時にはerror内で処理を書きます。

※jquery1.8からsuccessは非推奨で、thenやdoneを使うそうです。
おじさんが若い時はね$.ajax()のオプションでsuccessとかerrorとか指定していたんだよ(追憶)
【Ajax】通信が成功した時の処理「success」と「done」の違いについて


ログイン画面の13行目で読み込んでいるcss。これもbs-custom-file-inputの設定 

/demo/src/main/resources/static/default.css
.custom-file {
  overflow: hidden;
}

.custom-file-label {
  white-space: nowrap;
}


サーバサイド


おみくじ機能に関して、リクエストをハンドリングするためのコントローラを作成します。

[コントローラ]
/demo/src/main/java/com/example/demo/controller/fortuneController.java
package com.example.demo.controller;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.postgresql.largeobject.LargeObject;
import org.postgresql.largeobject.LargeObjectManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.example.demo.json.Fortune;
import com.example.demo.service.FortuneService;

/**
*
* Spring Boot 勉強用 ログインコントローラ
*
* @version   1.0.0
*/
@RestController
@RequestMapping("/fortune")
public class fortuneController {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private FortuneService fortuneService;

    /**
    *
    * イメージを登録する。
    *
    * @param   file  Image
    * @return   成功・失敗
    */
    @PostMapping("image_upload")
    public Object imageUpload(@RequestParam MultipartFile file) {

        if (file.isEmpty()) {
            logger.warn("ファイルが選択されていません");
            return "ファイルが選択されていません";
        }

        try {
            // fortune_imageテーブルにイメージを登録する
            fortuneService.insert(file);
        } catch (Exception e) {
            e.printStackTrace();
            return "登録に失敗しました";
        }

        return "登録に成功しました";
    }

    /**
    *
    * おみくじ情報を取得する。
    *
    * @return   おみくじ情報
    */
    @ResponseBody
    @PostMapping("get_info")
    public Fortune getInfo() {

        StringBuffer data = new StringBuffer();
        // おみくじ情報取得
        Fortune fortune = fortuneService.randamFortune();

        String sOid = fortune.getImage();
        if (StringUtils.isEmpty(sOid)) {
            logger.warn("ファイルが取得されていません");
            return fortune;
        }

        // oidからラージオブジェクト取得 mybatisで取得できなかったので、LargeObjectを利用
        // 時間ができたらConnectionの値を直接記載しないようにすること。ControllerでなくServiceで実装すること
        try (Connection conn = DriverManager.getConnection("jdbc:postgresql://localhost:5432/web?web_schema","dev","xxxx");) {
            conn.setAutoCommit(false);

            LargeObjectManager lobj = ((org.postgresql.PGConnection)conn).getLargeObjectAPI();
            try (@SuppressWarnings("deprecation")
            LargeObject obj = lobj.open(Integer.parseInt(fortune.getImage()), LargeObjectManager.READ);) {

                String base64 = new String(Base64.encodeBase64(obj.read(obj.size())));
                // pngファイルのみ対象
                data.append("data:image/png;base64,");
                data.append(base64);
                fortune.setImage(data.toString());
            }

        } catch (SQLException e) {
            logger.error("error cause = [" + e.getCause() + "] " +
                                "error message = [" + e.getMessage() + "]");
            logger.error("error stackTrace ", e);
            return fortune;
        }

        return fortune;
    }

}
47行目 : 画面から受け取ったファイルは@RequestParam MultipartFileの引数から取得します。

71行目 : サーバから画面に戻すときには、@ResponseBodyを設定するだけで、Jsonとして返すことができます。
SpringMVCで画像の入出力をするだけ

87-98行目 : Mybatisから取得したoidをもとに、postgresのLargeObjectを使ってバイナリデータを取得し、Base64でエンコードして画面に返します。
31.7. バイナリデータの格納

ポイント

  • サーバ側では、ファイルはMultipartFile で受け取る
  • Jsonは@ResponseBodyで返す


コントローラから画面側にデータを渡すJsonに設定するBeanです。

/demo/src/main/java/com/example/demo/json/Fortune.java
package com.example.demo.json;

import java.io.Serializable;

import lombok.Data;

/**
*
* Spring Boot 勉強用 Json返却用オブジェクト
*
* @version   1.0.0
*/
@Data
public class Fortune implements Serializable {

    private static final long serialVersionUID = 7510414307650324968L;

    private String word;
    private String image;

}


[サービス]
/demo/src/main/java/com/example/demo/service/FortuneService.java
package com.example.demo.service;

import org.springframework.web.multipart.MultipartFile;

import com.example.demo.exception.UserException;
import com.example.demo.json.Fortune;

/**
*
* Spring Boot 勉強用 モデル
*
* @version   1.0.0
*/
public interface FortuneService {

    /*
     * fortune_imageテーブルにイメージを登録する
     *
     * @param   file  登録用イメージ
     * @throws  UserException insert失敗
     */
    void insert(MultipartFile file) throws UserException;

    /*
     * fortuneとfortune_imageテーブルから1件データを取得する
     *
     * @return  おみくじ用の単語とイメージ
     */
    Fortune randamFortune();

}
サービス部分の実装の形式を指定しているインターフェースです。
インターフェースに対する実装は下記の通り。
特に注意するところはありません。

/demo/src/main/java/com/example/demo/service/FortuneServiceImpl.java
package com.example.demo.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import com.example.demo.exception.UserException;
import com.example.demo.json.Fortune;
import com.example.demo.mapper.FortuneMapper;

/**
 *
 * Spring Boot 勉強用 モデル
 *
 * @version   1.0.0
 */
@Service
public class FortuneServiceImpl implements FortuneService {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private FortuneMapper fortuneMapper;

    /*
     * {@inheritDoc}
     */
    @Transactional
    @Override
    public void insert(MultipartFile file) throws UserException {

        int result;
        try {
            // fortune_imageテーブルにイメージを登録する
            result = fortuneMapper.insert(file.getInputStream());
        } catch (Exception e) {
            throw new UserException(e);
        }

        // 登録結果をログに出力
        logger.info("[登録結果] " + String.valueOf(result) + "件登録");

    }

    /*
     * {@inheritDoc}
     */
    @Transactional(readOnly = true)
    @Override
    public Fortune randamFortune() {
        return fortuneMapper.randamFortune();
    }

}



[マッパー]
サービスから呼ばれるSQLの処理を指定しているインターフェースです。
oidのイメージ登録にはInputStreamを設定しています。

/demo/src/main/java/com/example/demo/mapper/FortuneMapper.java
package com.example.demo.mapper;

import java.io.InputStream;

import org.apache.ibatis.annotations.Mapper;

import com.example.demo.json.Fortune;

/**
*
* Spring Boot 勉強用 モデル
*
* @version   1.0.0
*/
@Mapper
public interface FortuneMapper {

    /*
     * fortune_imageテーブルにイメージを登録する
     *
     * @param  file  登録用イメージ
     * @return  処理結果
     */
    int insert(InputStream file);

    /*
     * fortuneとfortune_imageテーブルから1件データを取得する
     *
     * @return  おみくじ用の単語とイメージ
     */
    Fortune randamFortune();

}

[mybatisのsqlの設定xml]
oidの取得にjdbcType="BLOB"を設定しています。
ORDER BY random() LIMIT 1でテーブルから1件ランダムにデータを取得しています。
データの取得部分は、内部結合ですので、画像がないとおみくじの文言も取得されません

/demo/src/main/java/com/example/demo/mapper/FortuneMapper.xml
<?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="com.example.demo.mapper.FortuneMapper">

  <!--fortune_imageテーブルにイメージを登録する -->
  <insert id="insert" useGeneratedKeys="true">
    INSERT INTO web_schema.fortune_image (image)
    VALUES (#{file});
  </insert>


  <resultMap id="fortuneResultMap" type="com.example.demo.json.Fortune">
    <result property="word" column="word" />
    <result column="image" property="image" jdbcType="BLOB"/>
  </resultMap>

  <!--fortuneとfortune_imageテーブルから1件データを取得する -->
  <select id="randamFortune"  resultMap="fortuneResultMap">
    SELECT
      a.image AS image,
      b.word AS word
    FROM
       (SELECT
          image
        FROM
          web_schema.fortune_image
        ORDER BY
          random()
        LIMIT
          1) a,
     (SELECT
        word
      FROM
        web_schema.fortune
      ORDER BY
        random()
      LIMIT
        1) b;
  </select>

</mapper>



ここまでできたらプロジェクトを右クリックして、実行 -> Spring Boot アプリケーションを選択。
urlにhttp://localhost:8080/usersを入力して確認します。


ログイン画面の確認



イメージ登録



おみくじ機能の確認




今回はここまでです。
気をつけてはいますが、間違いがあったらごめんなさい。バイバイ

お借りした素材
NYN姉貴.png
ちょっと髪型を変えたNYN姉貴.png
何となく作ったぢゆしPB
NEL姉貴04
クッキー☆背景素材集

JAVA おすすめの本
[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]
スッキリわかるJava入門第2版 [ 中山清喬 ]
価格:2860円(税込、送料無料) (2020/4/12時点)






カテゴリ

このブログを検索

自己紹介

自分の写真
作らなきゃ(使命感)

QooQ