クイック エンジニアリングブログ

株式会社クイック Web事業企画開発本部のエンジニアリングチームが運営する技術ブログです。

LaravelエンジニアのためのJava+SpringBootの環境構築

こんにちは、ソフトウェアエンジニアのたろーです。

我々クイックのソフトウェアエンジニアはサーバサイドはPHP+Laravel、 フロントエンドはReactかVueのセットで開発を行う機会が多いです。

業務上ではなかなかPHP以外のサーバサイド言語を触る機会がないため、

Q「PHP以外だったら何勉強する?
A「SpringBoot×Javaじゃない?せっかくIntelliJつかわせてもらってるんだし

くらいのノリでJava、SpringBootにあまり触れてこなかった社内エンジニアに
「こんな感じだよ」の提示も含め、環境構築していこうと思います。

(タイトルに記載しているほどLaravel+PHPとの比較はないかもしれないです。ごめんなさい!)

構築完了の条件

  • SpringBootでAPIを作成しDB疎通を確認する。
  • ローカル端末上にてSpringBootのアプリ経由でReact(Vite)のデフォルトウェルカムページを表示する。

構成

IDE IntelliJ IDEA
バックエンド Java/SpringBoot
フロントエンド TypeScript/React(Vite)
DB MySQL

Dockerにて構築します。以下のミニマム2コンテナ構成です。

  • Webサーバ用コンテナ(フロントエンド+バックエンド)
  • DB用コンテナ

最初の準備

まずはじめに適当にsampleというプロジェクトルートディレクトリを作成します。作成したらIntelliJで開きます。 ここにSpringBootのアプリやReactやコンテナやらの諸々を作成していきます。

SpringBootアプリの初期作成

まずはSpringBootのアプリの雛形を作成します。 Laravelだとcomposerで作成することが多いと思います。 SpringBootだとSpring Initializrで作成するのが楽です。
フレームワーク本体から関連パッケージの依存設定までGUIで実行できます。

Webページ上で作成しダウンロードしてもいいですが、IntelliJでもWEBと同様の操作が可能でIDE上でそのまま開けるのでIntelliJで作成するのがおすすめです。

最初に準備したsampleフォルダを開いた状態で
「File」→「New」→「Project」→「Spring Initializr」を選択します。

各項目については以下のIntelliJのリファレンスがわかりやすいので是非一読してください。 「Spring Initializr プロジェクトウィザード

Spring Initializr-基本構成選択

SpringInitializr-1

今回は以下の構成で作成します。

NameとGroup 任意です。つくりたいアプリの名前やドメインに従って入力してください。
Type(ビルドツール) Gradle-Groovy
スクリプトベースで書けるので好きです。
JDK coretto-21(選択肢にない人はDownload JDKから探してください)。
DockerイメージにAmazon Corretto 21を使いたいので合わせます。
Langage もちろんJava。
せっかくなのでリリースされたばかりの21にします。
Packaging Jar
Javaが初めてという方はJDKとかJarとか混乱してきますよね。Javaをハードル高く感じる理由は用語が多い上に名称が似ていることもあると思います。
とりあえず「Next」を押して次の依存パッケージ選択へ参りましょう。

Spring Initializr-依存パッケージ選択

次は依存パッケージを選びます。

SpringInitializr-2
Lombok アノテーションを書くだけでDIやsetter/getter等オブジェクト操作を実現してくれる超強力なツール。他のライブラリとのかけ合わせで利用したりします。実質必須です。
Spring Boot DevTools JavaはPHPと違って変更→ビルド→実行の流れが必要。
このあたりのプロセスを短縮してくれる。
Spring Web SpringBootでWebアプリを作るのに必要。リクエストハンドリング等をアノテーションで実現する。必須。
Spring Data JPA ORM。これかMybatisが使われることが多い気がする。
LaravelでいうところのEloquentと思っていただければ。
MySQL Driver MySQLドライバーです。
Thymeleaf テンプレートエンジン。LaravelでいうところのBlade。

色々ありすぎて迷いますが今回はこのあたりを選んでおけばとりあえず環境は構築条件は満たせます。選んだらCreateを押しましょう。

バリデーションや認証、ログインセッション管理やGraphQL連携等々、他にも多くのライブラリがありますが、今後必要になったタイミングでbuild.gradleに追加していけば良いと思います。

プロジェクトの確認

Spring Initializrでプロジェクトを作成すると、ルートディレクトリから見て上の図のような構成になっていると思います。
かわいい象さんに癒やされますね。

Dockerコンテナ設定

Webサーバ用のコンテナとMySQLコンテナを作成します。
プロジェクトルートにsample-containerというディレクトリを切って以下の構成でディレクトリ、ファイルを配置します。

Webサーバコンテナ

コンテナベースイメージには「amazoncorretto:21-al2023」を指定しています。 Amazon CorrettoはAWSの提供するOpenJDKビルドです。

amazoncorretto:21-al2023なのでJava21に対応したAmazonLinux2023をベースOSとするコンテナイメージとなります。AmazonLinux2023を使いたかったんです。

他のJavaのDockerイメージであれば「Eclipse Temurin」等が選択肢に上がると思います。

■web/Dockerfile

FROM amazoncorretto:21-al2023
WORKDIR /app
ENV TZ="Asia/Tokyo"

RUN curl -fsSL https://rpm.nodesource.com/setup_18.x | bash - \
    && dnf install nodejs -y \
    && dnf install gcc-c++ make -y

中身自体はnodeのセットアップしかしていません。
nodeのバージョンは必要に応じて修正してください。

MySQLコンテナ

全体的に特筆することはありません。ほぼdocker-composeに記載しています。

■mysql/data
空フォルダです。データ永続化のために作成。

■mysql/init.sql

create table IF NOT EXISTS user (
    id integer auto_increment primary key,
    name varchar(255) not null
);

初期テーブル作成用です。Flyway等のmigrationツールを使ってもよかったですが、とりあえず疎通をしたいだけなのでベタDDLで記載します。

■mysql/mysql.conf

[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
default-authentication-plugin = mysql_native_password

[client]
default-character-set=utf8mb4

my.cnfです。docker-compose側でリネームして配置します。

docker-compose.yml

■docker-compose.yml

services:
  web:
    build:
      context: ./
      dockerfile: web/Dockerfile
    container_name: sample-web
    tty: true
    working_dir: /app
    volumes:
      - ../spring:/app
    ports:
      - 8080:8080
    depends_on:
      - db
    networks:
      - sample-network
  db:
    image: mysql:8.0
    container_name: sample-db
    volumes:
      - ./mysql/data:/var/lib/mysql
      - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
      - ./mysql/mysql.cnf:/etc/mysql/conf.d/my.cnf
    ports:
      - "3306:3306"
    environment:
      MYSQL_DATABASE: sample
      MYSQL_ROOT_PASSWORD: root
      MYSQL_USER: sample-user
      MYSQL_PASSWORD: sample-pswd
      TZ: Asia/Tokyo
    networks:
      - sample-network

networks:
  sample-network:
    driver: bridge

コンテナ立ち上げ&テストデータ挿入

sample-container配下でdocker-composeで立ち上げます。

$ docker-compose up -d
/**割愛*/
Creating sample-db ... done
Creating sample-web ... done

このタイミングでIntelliJ上で立ち上げたmysqlコンテナへの接続情報を作っておきます。

mysqlコンテナへの接続作成

確認用のテストデータも入れておきましょう。

テストデータ

SpringBootアプリの設定

SpringBootアプリのコーディング、実行前に設定変更を行っていきます。

application.properties修正

プロパティ変更を行っていきます。Laravelでいうところの.envやconfig修正の作業に近いです。

■spring/src/main/resources/application.properties

#dev tool(ホットデプロイの有効化)
spring.devtools.remote.restart.enabled=true
spring.devtools.livereload.enabled=true

#thymeleaf(テンプレートやjs、CSSキャッシュの無効化)
spring.thymeleaf.cache=false
spring.web.resources.cache.cachecontrol.no-cache=false

# DB(接続情報)
spring.datasource.url=jdbc:mysql://db/sample
spring.datasource.username=sample-user
spring.datasource.password=sample-pswd

#JPA(利用DB)
spring.jpa.database=MYSQL

こちらのpropertiesファイルについてはyaml形式での記載も可能です。
また、propertiesファイルを指定してのアプリ実行ができるため、staging、production等の環境に合わせたそpropertiesファイルを作成し、管理するのが良いと思います。その際は接続情報はシークレット化しましょう。

build.gradle修正

先の項でも記載しましたが、Javaはモジュールビルド→実行とプロセスを踏む必要があります。PHPの開発になれているとこの辺手間ですよね。このあたりを一括でまとめてやってくれるbootRunの設定を変えておきます。

■spring/build.gradle

//追記
bootRun {
   // build→srcディレクトリを参照へ
    sourceResources sourceSets.main
   // 実行環境モード指定
    jvmArgs = ["-Dspring.profiles.active=develop"]
}

IntelliJのオートコンパイル設定

[Preferences]→ [Build, Execution, Deployment]→[Compiler]
の「Build project automatically」にチェックを入れておきます。

SpringBootアプリのビルド&起動

ここでやっとspringBootアプリの起動です。コンテナに潜り起動します。 もちろんdocker execで直接コマンドラインを渡して実行しても良いです。

$ docker exec -it sample-web /bin/bash
# ./gradlew bootRun

アスキーアートとともに立ち上がります!
アプリとかミドルウェア立ち上げた際のターミナル表示されるアスキーアートってテンション上がりますよね!!!!!大好き

ちなみにビルドと起動をそれぞれ行うのであれば以下のような手順を踏みます。

# ./gradlew build
# java -jar -Dspring.profiles.active=develop build/libs/spring-0.0.1-SNAPSHOT.jar

SpringBootアプリDB疎通確認&API作成

次はDBの疎通確認を踏まえて簡単なGETのAPIを作成します。

API作成

今回はEntity→Repository→Service→Controllerの順で作成していきます。

Entity作成

entityパッケージを切ってEntityクラスを作ります。対象はコンテナ作成の際に同時につくったUserテーブルです。
IntelliJでDBへのコネクションを作成しておくと、DB情報と紐づけてくれるから開発の際に楽になります。

■spring/src/main/java/com/sample/spring/entity/UserEntity.java

package com.sample.spring.entity;

import jakarta.persistence.*;
import lombok.Data;

@Entity
@Data
@Table(name="user")
public class UserEntity {
    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(name = "name")
    private String name;

}

Repository作成

repositoryパッケージを切ってリポジトリの作成を行います。実態はほぼJpaRepository側にあるのでInterfaceのみ作成します。

■spring/src/main/java/com/sample/spring/repository/UserRepository.java

package com.sample.spring.repository;

import com.sample.spring.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<UserEntity,Integer> {
}

Service作成

serviceパッケージを切ってサービスクラスを作成します。
実装するのは全ユーザ情報を返すだけの雑な関数のみにしておきます。

■spring/src/main/java/com/sample/spring/service/UserService.java

package com.sample.spring.service;

import com.sample.spring.entity.UserEntity;
import com.sample.spring.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {

    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public List<UserEntity> getAll() {
        return userRepository.findAll();
    }
}

Controller作成

controllerパッケージを切ってコントローラクラスを作成します。
「/api/user/list」というリクエストのエンドポイントを作成し、全件取得したユーザー情報をEntitlyのListとしてそのまま返却してみます。

■spring/src/main/java/com/sample/spring/controller/api/UserController.java

package com.sample.spring.controller.api;

import com.sample.spring.entity.UserEntity;
import com.sample.spring.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("api/user")
public class UserController {
    private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/list")
    public List<UserEntity> list() {
        return userService.getAll();
    }
}

疎通確認

下記URLをGETでコールして確認をしてみます。 http://localhost:8080/api/user/list

DBに登録されているデータが返却されていることを確認できました!
あんなに雑に突っ込んだEntityのListもしっかりとjson形式で返却してくれています!

SpringBootでの画面作成&表示確認

次はSpringBootとThymeleafを利用して画面作成と表示確認を行っていきます。

画面作成

HTMLテンプレートとCSS作成

spring/src/main/resources/templatesにHTMLテンプレートを配置します。
ついでに先程作成したuserテーブルの情報を表示できるようにしておきます。
後述する自作CSSも外部ファイルとして呼び出しています。

■spring/src/main/resources/templates/sample.html

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <link th:href="@{/css/sample.css}" rel="styleSheet">
    <title>TEST</title>
</head>
<body>
<main>
    <h1>
        Hello SpringBoot
    </h1>
    <table>
        <tr>
            <th>ID</th>
            <th>名前</th>
        </tr>
        <tr th:each="user : ${userList}">
            <td th:text="${user.id}"></td>
            <td th:text="${user.name}"></td>
        </tr>
    </table>

</main>
</body>

</html>

次はspring/src/main/resources/static配下にcssというディレクトリを切ってsample.cssを配置します。CSS、JS等の静的リソースはこちらに配置していきます。

body {
    font-family: 'Hiragino Kaku Gothic ProN W3', Meiryo, Arial, Helvetica, sans-serif;
}
main {
    display: grid;
    place-content: center;
    place-items: center;
}
table {
    border-collapse: collapse;
    width: 100%;
}
th, td {
    border: solid 1px;
    text-align: center;
}

Controller作成

次はコントローラーの作成です。 spring/src/main/java/com/sample/spring/controller配下に画面表示用のコントローラーを作成します。
とりあえず「http://localhost:8080/sample」で表示したとき用のパスマッピングをします。return値にはresources/templatesからみたテンプレート名を記載します。先程作成したuserテーブルの情報を画面に渡せるようにしておきます。

■spring/src/main/java/com/sample/spring/controller/SampleController.java

package com.sample.spring.controller;

import com.sample.spring.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

@Controller
public class SampleController {
    private final UserService userService;

    @Autowired
    public SampleController(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping(path = "/sample", method = RequestMethod.GET)
    public String index(Model model) {
        model.addAttribute("userList", userService.getAll());
        return "sample";
    }
}

表示確認

http://localhost:8080/sampleにアクセスして表示確認を行います。
ページが表示されスタイルも適応されていることまで確認できました。userテーブルのデータも表示されていますね。

React組み込み

ここまででSpringBootの基本的な使い方はわかったのでReactを組み込みます。
SpringBootでアクセスを受け、Reactのウェルカムページを表示させます。

Reactプロジェクト作成

ViteでReact×TypeScriptプロジェクトを作成します。
npm create viteでReact×TypeScriptを選んで作成しただけですのでReactプロジェクトの作成方法は割愛します。
どこに作るか・・・はかなり悩ましいところなのですが、今回はspring/src直下にfrontendというプロジェクト名で作成することにしました。
次に作る際は違う場所に違う名前でプロジェクトcreate機能を使わず、ディレクトリ構成を一から考えて作成する思います。

作成したらnpm installまで実施しておきます。

SpringBootとのつなぎ込み

初期のReactプロジェクトの構成は下記の図のようになっています。
これをSpringBoot、Thymeleafとガッチャンコしていきます。

方針は以下の通りです。

テンプレート(HTML) index.html
front配下からSpringBoot管轄のresources/templatesへ移動。
フロントへのエントリー用のjsパスを固定化する(Viteのビルドで毎回名前が変わらないようにする)。
main.tsx
App.tsx
HTML↔jsのエントリーポイントとして名前固定でトランスパイルしてstatic配下に配置。チャンクファイルもstaticに配置。
CSS styled-jsxでjs化。テンプレート側でcssファイル指定のことを考えないようにする。

AFTERの構成は以下のようになりました。

SpringBoot/Thymeleaf側

ReactSampleControllerを作成し/react/sampleでリクエスト受付をするように設定しています。
frontend側にあったindex.htmlをresources/templates配下へ移動しています。

index.htmlはリンク設定をThymeleaf記述式に変えて(変えなくてもOKです)scriptの参照先をトランスパイル後のエントリー用jsに変更しているのみです。このjsはビルドの度に名前が変わらないようにviteで設定します。

■spring/src/main/resources/templates/index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" th:href="@{/vite.svg}" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" th:src="@{/assets/js/main.js}"></script>
  </body>
</html>

React側

CSSはjsx化してtsxに変更しています。
vite.config.tsは以下のように変更し、エントリー用js(main.js)の固定化とビルド後の出力先の設定を行っています。

■spring/src/frontend/vite.config.ts

import type {UserConfig} from 'vite'
import {splitVendorChunkPlugin} from 'vite'
import react from '@vitejs/plugin-react'
import {resolve} from 'path'

// https://vitejs.dev/config/

const defineConfig: UserConfig = {
    plugins: [react(), splitVendorChunkPlugin()],
    publicDir: 'public',
    build: {
        // watchモード追加
        watch: {},
        outDir: '../main/resources/static',
        rollupOptions: {
            input: {
                // main.jsとして出力させる
                main: resolve(__dirname, 'src/main.tsx'),
            },
            output: {
                // entryファイルはハッシュ化しない
                entryFileNames: `assets/js/[name].js`,
                chunkFileNames: `assets/js/vendor/[hash].js`,
                assetFileNames: `assets/[name]-[hash].[ext]`,
            },

        }
    },

}
export default defineConfig

キャッシュ戦略などにより、entryFileName にもハッシュ値を付与し、index.html を含めて一括でビルドする必要がある場合、

  • frontendプロジェクト内に index.html ファイルを配置(デフォルトの状態)
  • vite.config.js で、入力を index.html に変更
  • ビルドの出力ディレクトリ (outDir) も変更し、それを適切な場所に設定します。
  • SpringBoot アプリケーション側で、Configurationクラスを作成して静的ファイルのパス解析を変更。 等すれば実現できるかなと思いましたが時間切れにより今回は諦めました!
    またそこまで考えるのであればそれも踏まえてフロントのソース構成から練ると思うので、今回はこのあたりで止めておきます。

表示確認

React側をビルドして表示を確認します。

bash-5.2# npm run build -watch

> frontend@0.0.0 build
> tsc && vite build

vite v4.4.11 building for production...

watching for file changes...

build started...
✓ 32 modules transformed.
../main/resources/static/assets/react-35ef61ed.svg       4.13 kB │ gzip:  2.14 kB
../main/resources/static/assets/js/main.js               4.32 kB │ gzip:  1.37 kB
../main/resources/static/assets/js/vendor/58ea80d7.js  141.88 kB │ gzip: 45.45 kB
built in 969ms.

とりあえずビルドは成功しました。ファイルも想定通りの場所に出力されています。
しれっとvite.configに入れ込んだwatchモードオプションでホットデプロイにしています。
これでSpringBoot側と合わせて基本的にソースコードは即時反映されるようになりますね。

ちなみにgradle経由でnpmの実行もできますが、今回は間に合わず設定を入れていません!

ではhttp://localhost:8080/react/sampleでアクセスしてみます。

成功でございます。アニメーションもカウンターも動作しています。

ただ今回のフロント周りの構成は実際にアプリを作る際にはかなり変わると思うので、改めてもっとSpringBoot/Thymeleaf側の構成と合わせてディレクトリ構成から作り直したいとは思います。

最後にLaravel(PHP)と比べて

昔に比べてJavaの開発環境の構築はかなり簡単になったと思いますが、Laravelと比べると構築の難易度は多少高いかなと思います。(Laravelの初期構築が楽すぎるのはありますが)

Laravel SailのようなDocker補助ツールもないですし、Laravel Vite×Bladeの用にフロントフレームワークとのつなぎ込みのお膳立てもしてくれているわけではありません。 そもそも今回はThymeleafを選びましたがテンプレートエンジンから選ばなければなりません。

また、Java関連の用語には似たような名前のものも大量にありますし、取り巻く環境も複雑で結構とっつきにくいと思います。(その辺を気にせず簡単な構築であればできるようになってきていますが)

ただし、開発規模が大きくなってくると静的型付け言語のありがたみはかなり感じますし、非同期処理、アノテーション記載によるコーディング量の軽減や画一化、ライブラリの豊富さ、そしてサポートの長さ(Java21は2031年9月まで・・・!)等々、使うメリットもかなりあると思います。シェア率も高いのでエンジニアを集めやすいという利点もあります。

「結局は適材適所で選べる状況が良いのではないでしょうか」というなんとも締まらない感じの結びになってしまいますが、

とりあえず普段使わないもので何か作ると楽しいので、業務でもラフな感じで色んなもの触ってつくってみる会をするのもありかなと思いました! それでは!


\\『真のユーザーファーストでマーケットを創造する』仲間を募集中です!! //

919.jp