こんにちは、ソフトウェアエンジニアのたろーです。
我々クイックのソフトウェアエンジニアはサーバサイドはPHP+Laravel、 フロントエンドはReactかVueのセットで開発を行う機会が多いです。
業務上ではなかなかPHP以外のサーバサイド言語を触る機会がないため、
Q「PHP以外だったら何勉強する?」
A「SpringBoot×Javaじゃない?せっかくIntelliJつかわせてもらってるんだし」
くらいのノリでJava、SpringBootにあまり触れてこなかった社内エンジニアに
「こんな感じだよ」の提示も含め、環境構築していこうと思います。
(タイトルに記載しているほどLaravel+PHPとの比較はないかもしれないです。ごめんなさい!)
- 構築完了の条件
- 構成
- 最初の準備
- SpringBootアプリの初期作成
- Dockerコンテナ設定
- SpringBootアプリの設定
- SpringBootアプリDB疎通確認&API作成
- SpringBootでの画面作成&表示確認
- React組み込み
- 最後に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-基本構成選択
今回は以下の構成で作成します。
NameとGroup | 任意です。つくりたいアプリの名前やドメインに従って入力してください。 |
Type(ビルドツール) | Gradle-Groovy スクリプトベースで書けるので好きです。 |
JDK | coretto-21(選択肢にない人はDownload JDKから探してください)。 DockerイメージにAmazon Corretto 21を使いたいので合わせます。 |
Langage | もちろんJava。 せっかくなのでリリースされたばかりの21にします。 |
Packaging | Jar |
とりあえず「Next」を押して次の依存パッケージ選択へ参りましょう。
Spring Initializr-依存パッケージ選択
次は依存パッケージを選びます。
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コンテナへの接続情報を作っておきます。
確認用のテストデータも入れておきましょう。
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月まで・・・!)等々、使うメリットもかなりあると思います。シェア率も高いのでエンジニアを集めやすいという利点もあります。
「結局は適材適所で選べる状況が良いのではないでしょうか」というなんとも締まらない感じの結びになってしまいますが、
とりあえず普段使わないもので何か作ると楽しいので、業務でもラフな感じで色んなもの触ってつくってみる会をするのもありかなと思いました! それでは!
\\『真のユーザーファーストでマーケットを創造する』仲間を募集中です!! //