『意識高い系Java変換器』~クソアプリ制作6日目~

クソアプリを一通り改善してきた。

6日目はデプロイをしてみる。

 

bkcydrd784yv2.hateblo.jp

 

bkcydrd784yv2.hateblo.jp

 

bkcydrd784yv2.hateblo.jp

 

bkcydrd784yv2.hateblo.jp

 

bkcydrd784yv2.hateblo.jp

 

デプロイする方法を調べる

Render がよさそう

zenn.dev

https://qiita.com/sueasen/items/00dfab7c6db7ff6f95b8

 

Dockerfile が必要らしい。

書き方が全く分からないのでGeminiに聞いてみる。

 

Gradle プロジェクト用 Dockerfile

ファイル名 Dockerfile(拡張子なし)を作成し、以下をコピペしてください。

 

Dockerfile
 
# 1. ビルドを行う環境 (JDK 17)
FROM eclipse-temurin:17-jdk-alpine AS build
WORKDIR /app

# Gradleのキャッシュを効かせるために設定ファイルだけ先にコピー
COPY build.gradle settings.gradle gradlew ./
COPY gradle gradle

# gradlewに実行権限を与える(これ重要!)
RUN chmod +x ./gradlew

# 依存関係(ライブラリ)のダウンロードだけ先に行う
# (ソースコード変更のたびにダウンロードし直さないための高速化テクニック)
RUN ./gradlew dependencies --no-daemon || return 0

# ソースコードをコピーしてビルド実行
COPY . .
RUN ./gradlew bootJar --no-daemon

# 2. 実行を行う環境 (軽量なLinux)
FROM eclipse-temurin:17-jdk-alpine
WORKDIR /app

# ビルド成果物(jarファイル)をコピー
# Gradleのビルド結果は build/libs/ に生成されます
# *-SNAPSHOT.jar や *-plain.jar など複数できる場合があるので、
# 確実に実行可能jarを狙うためにワイルドカードを使っていますが、
# もしエラーになる場合は実際のファイル名を確認してください。
COPY --from=build /app/build/libs/*.jar app.jar

# Render用のポート設定
ENV PORT=8080

# アプリ実行
ENTRYPOINT ["java", "-jar", "app.jar"]

 


何が変わったの?(書き方のポイント)

Maven版との違いは主に3点です。

  1. ビルドコマンドの違い:

    • Maven: mvn clean package

    • Gradle: ./gradlew bootJar

    • bootJar は Spring Boot アプリ専用の実行可能な JAR ファイルを作るコマンドです。

  2. 実行権限の付与:

    • RUN chmod +x ./gradlew

    • Windowsで開発していると、Mac/Linux(サーバー)上で gradlew コマンドが「実行禁止」扱いになってしまうことがあるため、この1行で許可を与えています。

  3. 成果物の場所:

    • Maven: target/*.jar

    • Gradle: build/libs/*.jar

とのこと。

 

Java21を使っていたのでちょっと修正。

# 1. ビルドを行う環境
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app

# 先にGradleの設定ファイルだけコピー(キャッシュ用)
COPY build.gradle settings.gradle gradlew ./
COPY gradle gradle

# gradlewに実行権限を与える
RUN chmod +x ./gradlew

# 依存関係のダウンロード(スペルミス修正: deamon -> daemon
RUN ./gradlew dependencies --no-daemon || return 0

# ソースコードを全てコピー
COPY . .

# ★ここが重要!
# COPY . . Windows側の権限情報が上書きされてしまうため、
# もう一度 gradlew に実行権限を与え直す
RUN chmod +x ./gradlew

# ビルド実行(スペルミス修正: deamon -> daemon
RUN ./gradlew bootJar --no-daemon

# 2. 実行を行う環境
FROM eclipse-temurin:21-jdk-alpine
WORKDIR /app

# ビルド成果物をコピー
# Gradleのバージョンによっては build/libs/ 以下のファイル名が変わるため
# ワイルドカードでコピーします
COPY --from=build /app/build/libs/*.jar app.jar

ENV PORT=8080
ENTRYPOINT ["java", "-jar", "app.jar"]

 

次は application.properties を修正するらしい。

手順2. ポート設定の修正(最重要!)

ここが最大の落とし穴です。 Spring Bootはデフォルトで 8080 ポートで動きますが、Renderは「空いているポートをランダムで割り当てて、環境変数 PORT に入れる」という動きをします。

そのため、src/main/resources/application.properties に以下の1行を追加してください。

 

Properties
 
# 環境変数PORTがあればそれを使い、なければ8080を使う設定
server.port=${PORT:8080}

 

これがないと、デプロイしても「アクセスできません」とエラーになって泣くことになります。

とのこと。

 

指示に従い修正。

spring.application.name=ConsciousJavaConverterBoot

# 環境変数PORTがあればそれを使い、なければ8080を使う設定
server.port=${PORT:8080}

google.api.key=${GOOGLE_API_KEY:}

 

次は GitHub にプッシュ。

手順1. Gitの初期化とコミット

まずは手元のファイルを「保存対象」として登録します。

 

Bash
 
# 1. Gitの管理を開始する
git init

# 2. 全てのファイルをステージング(荷造り)する
git add .

# 3. コミットする(セーブする)
git commit -m "first commit"

# 4. ブランチ名を main に変更する(最近の標準)
git branch -M main

 

手順2. GitHubと紐付けてプッシュ

ここで、さっき作ったGitHubリポジトリのURLを使います。 ブラウザでGitHubリポジトリ画面を開くと、https://github.com/ユーザー名/リポジトリ名.git というURLが表示されているはずです。それをコピーしてください。

 

Bash
 
# 5. リポジトリの宛先を登録する
# 【注意】下のURLはあなたのリポジトリのURLに書き換えてください!
git remote add origin https://github.com/あなたのユーザー名/リポジトリ名.git

# 6. GitHubにアップロード(プッシュ)する
git push -u origin main

成功。

 

Render でデプロイする

 

いよいよラストスパート:Renderで公開

コードがGitHubに上がったので、あとはRender側でポチポチするだけでWebアプリとして世界に公開されます。

以前お伝えした手順ですが、ここからが本番です。

  1. Render にログインする。

  2. 右上の [New +][Web Service] をクリック。

  3. [Build and deploy from a Git repository] をクリック。

  4. GitHub連携済みなら、リポジトリ一覧に ConsciousJavaConverterBoot が出ているはずなので [Connect] をクリック。

  5. 設定画面で以下を確認して入力:

    • Name: アプリ名(URLになります。例: conscious-java

    • Region: Singapore

    • Runtime: Docker(←ここ重要!間違えないで!)

    • Environment Variables:APIキーを設定した場合はここで追加)

  6. 一番下の [Create Web Service] をクリック!

これで5分〜10分ほど待てばデプロイ完了です。 もしRender側でエラーが出たら、ログをコピペして教えてください!応援しています!

 

 

Dockerfile を手で打ち込んだ時にスペルミスがあり一度エラーになる。

その後 Dockerfile 修正と GitHub へのプッシュをし成功。

 

変換をかけてみる。

プロンプト変えたら急にそれっぽい変換するようになったな。

 

Gemini 提案のプロンプト

// ★改善ポイント:プロンプトに「具体例(Few-Shot)」と「キャラ設定」を入れる
        String prompt = """
                あなたは「意識の高いJavaエンジニア」です。
                ユーザーが入力した「日常会話」を、無駄に複雑で可読性の低い、しかし文法的に正しそうな「意識高い系Javaコード」に変換してください。
                
                【ルール】
                1. 日本語は絶対に使わず、すべて英語のクラス名やメソッド名にすること。
                2. Singletonパターン、Factoryパターン、Builderパターンなどを無意味に多用すること。
                3. メソッドチェーン(.で繋ぐやつ)を多用してかっこよく見せること。
                4. コード以外の解説や、Markdownの記号(```java 等)は一切出力しないこと。コードのみを返してください。
                
                【変換例】
                入力:コーヒーがおいしい
                出力:BeverageProvider.getInstance().create(Type.COFFEE).consume().isDelicious(true);
                
                入力:今日はもう帰りたい
                出力:WorkerContext.getCurrentUser().getMotivation().validate().shutdown(ExitStatus.IMMEDIATE);
                
                入力:北海道に行きたい
                出力:TravelAgency.getService().planBuilder().setDestination(Location.HOKKAIDO).execute();
                
                【今回の入力】
                %s
                """.formatted(text); // %s の部分に入力テキストが入ります

 

ひとまずデプロイまでやり切った。

 

▼意識高い系Java変換器

https://conscious-java.onrender.com/

『意識高い系Java変換器』~クソアプリ制作5日目~

 

bkcydrd784yv2.hateblo.jp

 

bkcydrd784yv2.hateblo.jp

 

bkcydrd784yv2.hateblo.jp

 

bkcydrd784yv2.hateblo.jp

 

4日目は「そもそも変換をAIに任せちゃおう!」をした。

5日目は「きれいな見た目(レスポンシブ対応)」をする。

 

目指すこと

CSS(Bootstrap)を用いて画面デザインをする

レスポンシブ対応にする

 

Bootstrapを選んだ理由

多くのJavaの現場(業務システムやSIer)で使われているらしい。

HTML に1行追加するだけで使えるので楽らしい。

とりあえず見栄えが及第点なものを作りたいだけなので今回は Bootstrap に決めた。

 

Bootstrap について調べる

getbootstrap.jp

zenn.dev

uha-blog.com

 

入力画面(input.html)から変更する。

HTMLにインポート?して、ボタンを導入してみる

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>input</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
</head>
<body>
<h2>意識高い系Java変換器</h2>
<form th:action="@{/convert}" method="post">
<input type="text" th:name="targetText"/>
<br><br>
<button type="submit" class="btn btn-primary">変換</button>
</form>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
</body>
</html>

たしかにこれは楽だ。

class="btn btn-primary" と書くだけでボタンが変わった。

 

次は入力エリアをもうちょっと広げたいのと、中央に寄せたい。

 

入力エリアの拡大を試す

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>input</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
</head>
<body>
<h2>意識高い系Java変換器</h2>
<div class="mb-3">
<label class="form-label">変換したい文章を入力</label>
<form th:action="@{/convert}" method="post">
<textarea class="form-control w-75" rows="3" type="text" th:name="targetText"></textarea>
<br>
<button type="submit" class="btn btn-primary">変換</button>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
</body>
</html>

ブラウザの横幅を狭くするとこれ

レスポンシブ対応されていることを確認。

 

次は中央に寄せる

 

ちょっとてこずった。

さっきやった横幅を狭くする作業が中央寄せを邪魔していた。

テキストエリアにだけ幅制限をかけたことで、他要素の中央寄せと干渉したっぽい。

 

やり方としては、Bootstrap のグリッドシステム(行・列)を使う。

画面に幅を調整した列(カラム)を配置。

中身を配置(ここでいう中身が form 要素)。

中央寄せを実施。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>input</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
</head>
<body>
<div class="container mt-5">
<div class="text-center mb-4">
<h2>意識高い系Java変換器</h2>
</div>

<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<form th:action="@{/convert}" method="post">
<div class="mb-4">
<textarea class="form-control" rows="5" placeholder="変換したい文章を入力" type="text" th:name="targetText"></textarea>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary px-5">変換</button>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
</body>
</html>

「変換したい文章を入力」はフォームラベルからプレースホルダに変更した。

ブラウザの横幅を縮めるとこんな感じ。


良いのではないか?

続いて出力画面もデザインを反映していく。

 

まずは「戻る」ボタンを設置。

※文言は「戻る」ではないけど。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>output</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
</head>
<body>
<h2>変換前</h2>
<p th:text="${originalText}">ここに変換前の文字列を表示</p>
<h2>変換後</h2>
<p th:text="${convertedText}">ここに変換後の文字列を表示</p>
<form th:action="@{/}" method="get">
<button type="submit" class="btn btn-primary px-5">別の変換を試す</button>
</form>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
</body>
</html>

画面はこんな感じ。

いい感じにボタン配置できた。

 

次は中央寄せをする。

 

入力画面と同様の考えでカラムの箱を作って中身を入れる感じでできそう。

変換前と変換後の文字が枠の中に表示されるみたいにしたい。

 

backgraound-color で背景つけてみたけど、角が角ばっているしなんか微妙なんだよな...と思い Gemini に相談。

 

Bootstrap の card を使うとよいらしい。

class="card" と書くだけで丸角と枠線がつき、

class="card-body" と書くと枠線と表示文字の間にいい感じに余白ができるらしい。

 

さらに、変換後の背景を黒にして白文字で表示させれば、ターミナルぽくなってよりJavaコード風感が増すと提案してくれた。最高すぎる。

 

ということでできたのがこちら。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>output</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<div class="mb-4">
<h4 class="text-center mb-2">変換前</h4>
<div class="card bg-light shadow-sm">
<div class="card-body">
<p class="card-text text-start mb-0" th:text="${originalText}">ここに変換前の文字列を表示</p>
</div>
</div>
</div>
<div class="mb-5">
<h4 class="text-center mb-2">変換後</h4>
<div class="card bg-dark text-white shadow">
<div class="card-body">
<p class="card-text text-start mb-0 font-monospace" th:text="${convertedText}">
ここに変換後の文字列を表示</p>
</div>
</div>
</div>
<div class="text-center">
<a th:href="@{/}" class="btn btn-primary px-5 rounded-pill">別の変換を試す</a>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
crossorigin="anonymous"></script>
</body>
</html>

ブラウザの横幅を縮めたバージョンがこちら。良い。

rounded-pill を付けることでボタンを丸くできるみたいで、こっちのほうが良かったので変えた。入力画面も丸くした。

 

挙動確認

※テキスト生成のプロンプトが雑のためうまく変換しきれていない。

デザインはめちゃいい感じ。

 

振り返り

Bootstrap で .css ファイル作らなくても HTML だけでこれだけの CSS あてられた。

しかもタグに class="" の属性書くだけで簡単だった。

今後つくるクソアプリも Bootstrap でデザインかける。

今回は、ボタン、カラム、カードの機能を使った。

 

次やること

次は「このクソアプリをデプロイしてみる」をする。

『意識高い系Java変換器』~クソアプリ制作4日目~

 

bkcydrd784yv2.hateblo.jp

 

bkcydrd784yv2.hateblo.jp

 

bkcydrd784yv2.hateblo.jp

 

3日目は「正規表現を用いて柔軟に変換できるようにする」に取り組んだ。

4日目の今日は「そもそも変換をAIに任せちゃおう!」をする。

 

目指すこと

フォームから入力された文章をAIがJavaコード風に変換する。

 

やること

AIの連携?について調べる。

API叩けるんだっけかな。あとはプロンプトってどこにどう設定するんだろう。

既存コードだと変換クラス(Convertクラス)を書き換える感じだろうな。

 

 

調べる

Gemini API が無料で使える(無料枠あり)らしい。

ということでGemini API を使ってみる。

ai.google.dev

↑手順はこの通り。とりあえず動かしてみる。

 

環境変数が GEMINI_API_KEY={APIキー} と記載されていたが

GOOGLE_API_KEY={APIキー} にする必要があった。

 

それ以外は問題なく動かしてみることができた。

 

コード

package org.example;

import com.google.genai.Client;
import com.google.genai.types.GenerateContentResponse;

public class Main {
public static void main(String[] args) {
// The client gets the API key from the environment variable `GEMINI_API_KEY`.
Client client = new Client();

GenerateContentResponse response =
client.models.generateContent(
"gemini-2.5-flash",
"Explain how AI works in a few words",
null);

System.out.println(response.text());
}
}

実行結果

 

さて、これをどうクソアプリに組み込んでいくか。

 

仮説。

ベースは上記のコードでOK。

引数を受け取り→処理→戻り値を返すメソッドに変更

プロンプトを変更(Javaコード風の文章に変換して、など)

受け取った引数をプロンプトに反映する

 

最後の1個だけやり方のイメージがつかない。

ひとまず上からできるところまで進めよう。

 

あぁ、なるほど。

String prompt = text + "をなんちゃってJavaコード風に変換してください。";

で先にプロンプトに反映して、

GenerateContentResponse response = client.models.generateContent(

"gemini-2.5-flash",

prompt,

null);

にすればいいのか。

 

ということで変更してみる。

package com.example.ConsciousJavaConverterBoot.Service;

import com.google.genai.Client;
import com.google.genai.types.GenerateContentResponse;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class Convert {

public String textConvert(String text) {

// Gen AI クライアントを作成
Client client = new Client();

// プロンプト作成
String prompt = text + "をなんちゃってJavaコード風に書き換えてください。";

// テキスト生成
GenerateContentResponse response =
client.models.generateContent(
"gemini-2.5-flash",
prompt,
null);

// 変換結果を返す
return response.text();

// // 辞書の準備
// // Key:日本語、Value:意識高いJavaコード
// Map<String, String> dictionary = new HashMap<>();
//
// // ★ここにアイデアを追加
// dictionary.put("今日の", "today");
// dictionary.put("お昼は", "getLunch()");
// dictionary.put("とんかつでした", "isTonkatsu(true)");
// dictionary.put("コーヒー", "coffee()");
// dictionary.put("うまい", "isGood(true)");
// dictionary.put("北海道", "prefecture(hokkaido)");
// dictionary.put("行き", "travel()");
// dictionary.put("たい", "hasHope()");
//
// // 変換処理
// // dictionarymap)の中身を全部回して置換する
// for (Map.Entry<String, String> entry : dictionary.entrySet()) {
// // 値の後ろに「.」を付けて置換する
// // 例:「今日の」today.
// String javaCode = entry.getValue() + ".";
// // textを上書きしていく
// text = text.replace(entry.getKey(), javaCode);
// }
//
// // 不要な助詞を削除
// text = text.replaceAll("[がにはをへ]", "");
//
// // ★最後の「.」を消す
// // もし最後の文字が"."だったら、切り落とす
// if (text.endsWith(".")) {
// // substring(開始, 終了)で開始~終了までを抜き出す = 最後の1文字のみ切り落とす
// text = text.substring(0, text.length() - 1);
// }
//
// // 日本語の句読点(、。)を削除
// text = text.replace("", "");
// text = text.replace("", "");
}
}

めっちゃすっきりしたな。

 

動かしてみる。

 

めっちゃミスった。

回答がそのまんまのってやがる。

回答の形式を制御しないといけないのか。

 

ということでプロンプトを変えてみる。

String prompt = "" + text + "」をなんちゃってJavaコード風に書き換えてください。返答の形式は「{書き換えた結果}」となるように、結果を1つだけ返してください。";

↑にしてみた。

結果がこれ。

うん。いいのではないか?

 

シングルクォーテーション('')を消してほしいとか、日本語は使わないでほしいとか気になることはあるけど、ひとまず形にするという点ではOKだと思う。

 

やはり気になるところを修正する。

String prompt = "" + text + "」をなんちゃってJavaコード風に書き換えてください。返答の形式は{書き換えた結果}1つだけを返してください。また、書き換える時はJavaコード風の文字に日本語を含めないでください。";

結果がこれ。

うん。良いのではないか?OK。

 

 

課題

生成AIの処理が遅い。10秒くらい?待った。

どうやって処理を早くするか、もしくはユーザー体験を損なわないようにするかを県とする必要がある。(一番最初のクソアプリでどこまでやるかは検討だが。)

↓備忘として参考になりそうなサイトを残しておく。

zenn.dev

www.docswell.com

zenn.dev

 

あとAPIキーをコードに直接書いてるから、セキュリティ的に大丈夫なのか?という懸念がある。たぶん大丈夫じゃない。

qiita.com

note.com

ほらね。

 

どうすればいいんだろう。

それっぽいやつを備忘として貼っておこう。

zenn.dev

 

 

振り返り

題材が簡単だからだと思うが、AIを組み込むのは簡単だった。

しかも40行くらいあったコードが15行くらいに収まった。AIすげえ。

また別の難しさも出てきた。それはAIを制御するということ。

プロンプトエンジニアリングってやつ?

AIにほとんど丸投げできるから楽なんだけど、期待通りのアウトプットを出してもらうようにプロンプトを調整することが難しい。

 

次回やること

画面をきれいにする(デザインを設定する)

最低限の見栄えで、レスポンシブ対応してるところまで持っていけたら合格ラインかな。

『意識高い系Java変換器』~クソアプリ制作3日目~

 

bkcydrd784yv2.hateblo.jp

 

bkcydrd784yv2.hateblo.jp

 

2日目で超簡単な画面を実装した。

3日目の今日は「正規表現を用いて柔軟に変換できるようにする」を取り組む。

 

今のところ、

「コーヒーうまい」→「coffee().isGood(true)」

という変換ができるようにしているが、このままの変換しかできない。

例えば、「コーヒーがうまい」を変換かけるとこうなる。

「コーヒーがうまい」→「coffee().がisGood(true)」

コーヒーがうまい、の「が」は変換に対応していないのでそのまま残る。

 

目指すこと

1つのコードで下記すべてのパターンが変換されるようにする。

 

①コーヒーうまい

②コーヒーがうまい

③コーヒーはうまい

coffee().isGood(true);

 

 

①北海道行きたい

②北海道に行きたい

③北海道へ行きたい

prefecture(hokkaido).travel().hasHope();

 

なんかワンパターンな気がするな、、、

一旦このクソアプリに関してはOKにしよう。

 

ではやっていく。

 

変換処理を修正していく感じだろう。

dictionary.put() の箇所を変えればいいのか?

package com.example.ConsciousJavaConverterBoot.Service;

import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class Convert {

public String textConvert(String text) {
// 辞書の準備
// Key:日本語、Value:意識高いJavaコード
Map<String, String> dictionary = new HashMap<>();

// ★ここにアイデアを追加
dictionary.put("今日の", "today");
dictionary.put("お昼は", "getLunch()");
dictionary.put("とんかつでした", "isTonkatsu(true)");
dictionary.put("コーヒー", "coffee()");
dictionary.put("うまい", "isGood(true)");
dictionary.put("北海道", "prefecture(hokkaido)");
dictionary.put("行き", "travel()");
dictionary.put("たい", "hasHope()");

// 変換処理
// dictionarymap)の中身を全部回して置換する
for (Map.Entry<String, String> entry : dictionary.entrySet()) {
// 値の後ろに「.」を付けて置換する
// 例:「今日の」today.
String javaCode = entry.getValue() + ".";
// textを上書きしていく
text = text.replace(entry.getKey(), javaCode);
}

// ★最後の「.」を消す
// もし最後の文字が"."だったら、切り落とす
if (text.endsWith(".")) {
// substring(開始, 終了)で開始~終了までを抜き出す = 最後の1文字のみ切り落とす
text = text.substring(0, text.length() - 1);
}

// 日本語の句読点(、。)を削除
text = text.replace("", "");
text = text.replace("", "");

// 変換結果を返す
return text + "; // Fix logic";
}
}

 

まず、正規表現について調べる。

【Java】正規表現のまとめ #標準ライブラリ - Qiita

 

ふむ。これをどう使えばいいんだ...?

 

10分考えてもとっかかりも見えなかったので Gemini に懇願。

 

辞書登録の内容は変えない。先に助詞を削除して「単語だけ」にして辞書の内容と合わせる。

 

ということを提案してくれた。

 

つまり

 

①コーヒーうまい

②コーヒーがうまい

③コーヒーはうまい

 

がすべて「コーヒーうまい」になるように助詞を削除する、ということ。

 

replaceAll("[がにをはへ]", "");

を前処理に挟むということだ。

 

なるほどなるほど、、、ん?

 

でもこれって、前処理の時に単語の方も消してしまわないか?

 

例えば、「がいこつがいた」という日本語の場合、

「が」を消すとすると

「いこついた」となる。

 

これは望んでいた仕様と違う。

 

「がいこついた」にしたい。

 

 

ではどうすればいいのか?

助詞の削除処理を後に持ってくればいいのだ!

 

コーヒーがうまい

coffee().がisGood(true);

↓(助詞削除)

coffee().isGood(true);

 

これだ。

 

ということで実装してみよう。

package com.example.ConsciousJavaConverterBoot.Service;

import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class Convert {

public String textConvert(String text) {
// 辞書の準備
// Key:日本語、Value:意識高いJavaコード
Map<String, String> dictionary = new HashMap<>();

// ★ここにアイデアを追加
dictionary.put("今日の", "today");
dictionary.put("お昼は", "getLunch()");
dictionary.put("とんかつでした", "isTonkatsu(true)");
dictionary.put("コーヒー", "coffee()");
dictionary.put("うまい", "isGood(true)");
dictionary.put("北海道", "prefecture(hokkaido)");
dictionary.put("行き", "travel()");
dictionary.put("たい", "hasHope()");

// 変換処理
// dictionarymap)の中身を全部回して置換する
for (Map.Entry<String, String> entry : dictionary.entrySet()) {
// 値の後ろに「.」を付けて置換する
// 例:「今日の」today.
String javaCode = entry.getValue() + ".";
// textを上書きしていく
text = text.replace(entry.getKey(), javaCode);
}

// 不要な助詞を削除
text = text.replaceAll("[がにはをへ]", "");

// ★最後の「.」を消す
// もし最後の文字が"."だったら、切り落とす
if (text.endsWith(".")) {
// substring(開始, 終了)で開始~終了までを抜き出す = 最後の1文字のみ切り落とす
text = text.substring(0, text.length() - 1);
}

// 日本語の句読点(、。)を削除
text = text.replace("", "");
text = text.replace("", "");

// 変換結果を返す
return text + "; // Fix logic";
}
}

 

結果:ページ上部で述べた目指すことの実現を達成

コーヒー



北海道旅行

 

振り返り

正規表現は書き方よりも使いどころを考える方が難しかった。

書き方は最悪調べれば出てくるけど、使い方は作ろうとしているものによって変わってくるし決まった正解もない。

正規表現に限らずかもしれないな。どういう処理をするか?以上にどこでその処理をするのか?の方が難しいのかもしれない。

経験ですね。

 

次回やること

・変換をAIにやってもらう

→AIのAPIを使用して、入力に対する変換をAIでやってもらうように実装する。

これは面白そう。

 

次はデザイン整えて、その次はデプロイしてみて完了かなぁ。

『意識高い系Java変換器』~クソアプリ制作2日目~

最低限動く仕組みだけ作ったクソアプリを改善していく。

bkcydrd784yv2.hateblo.jp

 

画面実装

・入力画面、出力画面の2画面を作成

・Spring Boot + HTML/Thymeleaf を使用

 

0から作ろうとすると何をどう着手すればよいかわからない。

理解できていないことを痛感する。

 

さてどうしようか。

Spring MVC, Thymeleaf など思い当たるワードを検索していこう。

 

画面は Viewクラスかな。変換処理はModelクラスで、View と Model を繋げるのが Controllerクラスだったかな。

Modelクラスはビジネスロジック書くところだから、1日目で作った最低限動くコードを書く感じだろう。html(form)で入力値を受け取るからScannerは必要なさそう。

 

<疑問>

入力値の受け取りと処理後の出力値の受け渡しってどこでやるんだっけ?Modelクラス内か?

→Controllerクラスっぽいな。

 

 

まずは固定値で画面を作ってみる。

その後に入力値を変換する処理を組み込む。

 

まず、Controllerクラスを作る。

package com.example.ConsciousJavaConverterBoot.Controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class ConvertController {

// ビューを表示
@GetMapping("/")
public String showView() {
return "input";
}

// 変換後のビューを表示
@PostMapping("/convert")
public String convertView(Model model) {
return "output";
}
}

http://www.localhost:8080/

にアクセスがあったら input.html を表示する。

http://www.localhost:8080/convert

にアクセスがあったら output.html を表示する。

 

次に、入力画面(input.html)を作る

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>input</title>
</head>
<body>
<h2>意識高い系Java変換器</h2>
<form th:action="@{/convert}" method="post">
<input type="text"/>
<br><br>
<input type="submit" value="変換">
</form>
</body>
</html>

<form th:action="@{/convert}" method="post">

フォームが送信されたときに、/convert にアクセスする。

フォームからの入力データの送信のためPOSTメソッドを使用。

テキスト入力なので type="text" を使用。

 

次に、出力画面(output.html)を作る

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>output</title>
</head>
<body>
<h2>変換前</h2>
<p>ここに変換前の文字列を表示</p>
<h2>変換後</h2>
<p>ここに変換後の文字列を表示</p>
</body>
</html>

一旦固定値で作成し、動作確認する。

 

Spring Boot アプリケーションを起動し、http://hocalhost:8080/ にアクセス。

ダミーテキストを入力し「変換」ボタンをクリック。

output.html が表示されることを確認。

固定値での画面実装は完了。

 

 

次に、変換処理を作っていく。

 

Modelクラス として Convertクラスを作る。

テキスト変換を実施する textConvertメソッドを実装。

package com.example.ConsciousJavaConverterBoot.Service;

import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class Convert {

public String textConvert(String text) {
// 辞書の準備
// Key:日本語、Value:意識高いJavaコード
Map<String, String> dictionary = new HashMap<>();

// ★ここにアイデアを追加
dictionary.put("今日の", "today");
dictionary.put("お昼は", "getLunch()");
dictionary.put("とんかつでした", "isTonkatsu(true)");
dictionary.put("コーヒー", "coffee()");
dictionary.put("うまい", "isGood()");
dictionary.put("北海道", "prefecture(hokkaido)");
dictionary.put("行き", "travel()");
dictionary.put("たい", "hasHope()");

// 変換処理
// dictionarymap)の中身を全部回して置換する
for (Map.Entry<String, String> entry : dictionary.entrySet()) {
// 値の後ろに「.」を付けて置換する
// 例:「今日の」today.
String javaCode = entry.getValue() + ".";
// textを上書きしていく
text = text.replace(entry.getKey(), javaCode);
}

// ★最後の「.」を消す
// もし最後の文字が"."だったら、切り落とす
if (text.endsWith(".")) {
// substring(開始, 終了)で開始~終了までを抜き出す = 最後の1文字のみ切り落とす
text = text.substring(0, text.length() - 1);
}

// 日本語の句読点(、。)を削除
text = text.replace("", "");
text = text.replace("", "");

// 変換結果を返す
return text + "; // Fix logic";
}
}

@Service を付けることでSpringがDIを実施(戻り値の受け渡し)してくれる。

 

Controllerクラスにメソッド呼び出しを追加。

(convertView() を編集)

package com.example.ConsciousJavaConverterBoot.Controller;

import com.example.ConsciousJavaConverterBoot.Service.Convert;
import lombok.AllArgsConstructor;
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 org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@RequestMapping
@Controller
@AllArgsConstructor
public class ConvertController {

private final Convert convert;

// ビューを表示
@GetMapping("/")
public String showView() {
return "input";
}

// 変換後のビューを表示
@PostMapping("/convert")
public String convertView(@RequestParam("targetText") String inputText, // フォームのname値=targetText
Model model) { // 画面にデータを渡す
// 変換処理を実施し result へ格納
String result = convert.textConvert(inputText);

model.addAttribute("convertedText", result);
model.addAttribute("originalText", inputText);

return "output";
}
}

@RequestParam("targetText") String inputText

でname属性が targetText のフォーム入力値を受け取り String型で inputText へ格納。

inputText を変換処理メソッドtextConvert()に渡し変換結果を String型の resurt へ格納。

html の {convertedText} に result の値を渡し、{originalText} に inputText の値を渡す。

 

html の修正

input.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>input</title>
</head>
<body>
<h2>意識高い系Java変換器</h2>
<form th:action="@{/convert}" method="post">
<input type="text" th:name="targetText"/>
<br><br>
<input type="submit" value="変換">
</form>
</body>
</html>

フォーム入力の inputタグにname属性として targetText を設定。

 

output.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>output</title>
</head>
<body>
<h2>変換前</h2>
<p th:text="${originalText}">ここに変換前の文字列を表示</p>
<h2>変換後</h2>
<p th:text="${convertedText}">ここに変換後の文字列を表示</p>
</body>
</html>

th:text で originalText と convertedText を表示する箇所を指定。

 

画面の結果

振り返り

ほぼほぼAIに頼らずに実装できてよかった。

めっちゃ時間かかったけど。。

次やること

次回は正規表現を用いてもうちょっと柔軟に変換できるようにする。

『意識高い系Java変換器』~クソアプリ制作1日目~

ハンズオンで数回Webアプリを制作したことはあるが、全く0から自分で作ったことはないので「クソアプリ」を制作することにした。

 

イデアが浮かばないので Gemini と相談

相談というかほぼ丸投げ。

いくつか挙げてくれたのでとりあえず最初に挙げてくれたものを作ってみる。

 

「意識高い系Java変換器」

概要: 普通の文章を入力すると、「可読性の低いJavaコード風」のそれっぽい文章に変換されるアプリ。
機能:
入力:「今日のお昼はラーメンでした」
出力:today.getLunch().isRamen(true); // Innovation driven lunch

 

割と面白そう。

制約として「まず自分で考える」ということを設定した。

理由は開発能力を鍛えるため。

 

取り掛かりが全くわからん

0から考えるとどう作っていったらいいのか全然わからない。

10分ほど考えてもまとまらなかったので諦めて Gemini に相談。

 

私「Java の現場だと基本設計書があって詳細設計書があって、やっと実装する感じだから設計書作った方がいいんかな?」

Gemi「設計書作るだけで1~2週間かかるから個人開発なら最低限の機能だけメモ残してさっさと作り始めなさい」

 

とのこと。

他にも取り掛かりのポイントを教えてくれた。

 

取り掛かりは小さく簡単に考える

1. 具体的な Before / After を決める(要件の具体化)

「今日のお昼はラーメンでした」と入力したら「today.getLunch().isLamen(true);」が出力される。

↑このように「なんかいい感じで変換する」という抽象的なイメージをコードで実現できるレベルまで具体化する。 

 

2. 「変換の仕組み」を考える(ロジック設計)

「今日の」→「today」

「お昼は」→「getLunch()」

「ラーメンでした」→「isRamen(true)」

のように「何を→何に変換するか」書きだす。この時も抽象的なものではなく、具体的に例を挙げて考えるとよい。

 

次に上記の変換を実現するには実際にどう実装すればいいかコードベースで考える。

今回であれば、replace("変換対象", "変換後") というメソッドが使えそう。

 

つまり、

replace("今日の", "today")

replace("お昼は", "getLunch()")

replace("ラーメンでした", "isRamen(true)")

となる。

 

3. データの持ち方を考える(データ構造)

今回の場合、変換ルール(例:「今日の」→「today」)が数えきれないほど必要。

1つのルールごとに変換処理を書くのは大変すぎるので、コレクションフレームワーク(List、Mapなど)を使用する。

今回であれば、一意で対応するペアでデータを保持できる Map を使用。※「今日の」と「today」が対応しているペアとしてデータを持てる。

 

4. アプリの「入り口」と「出口」を考える(インターフェース)

入力と出力を考える。ただ、まずは小さく作っていくことが大事。

キーボードからの入力に対してコンソールで出力される、という簡単な入口と出口にする。

 

今回であれば、

入力:Scanner

出力:System.out.println

を使用する。

 

これでざっくりとした骨組みができた。

実装してみる

コードはこんな感じ

package org.example;

import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

public class ConsciousJavaConverter {
public static void main(String[] args) {
// 辞書の準備
// Key:日本語、Value:意識高いJavaコード
Map<String, String> dictionary = new HashMap<>();

// ★ここにアイデアを追加
dictionary.put("今日の", "today");
dictionary.put("お昼は", "getLunch()");
dictionary.put("とんかつでした", "isTonkatsu(true)");
dictionary.put("コーヒー", "coffee()");
dictionary.put("うまい", "isGood()");
dictionary.put("北海道", "prefecture(hokkaido)");
dictionary.put("行き", "travel()");
dictionary.put("たい", "hasHope()");

// 入力の受付
Scanner scanner = new Scanner(System.in);
System.out.println("=== 意識高い系Java変換器 ===");
System.out.println("変換したい日本語を入力してください");
System.out.print("> ");

String text = scanner.nextLine();

// 変換処理
// dictionarymap)の中身を全部回して置換する
for (Map.Entry<String, String> entry : dictionary.entrySet()) {
// 値の後ろに「.」を付けて置換する
// 例:「今日の」today.
String javaCode = entry.getValue() + ".";
// textを上書きしていく
text = text.replace(entry.getKey(), javaCode);
}

// ★最後の「.」を消す
// もし最後の文字が"."だったら、切り落とす
if (text.endsWith(".")) {
// substring(開始, 終了)で開始~終了までを抜き出す = 最後の1文字のみ切り落とす
text = text.substring(0, text.length() - 1);
}

// 日本語の句読点(、。)を削除
text = text.replace("", "");
text = text.replace("", "");

// 結果出力
System.out.println("\n=== 変換結果 ===");
System.out.println(text + "; // Fix logic");
System.out.println("==============");

scanner.close();
}
}

クソアプリのしょぼver が完成!

変換して出力されていることを確認

ここを土台に肉付けしていく

改善したいこと

・画面を実装する

正規表現を用いて変換を柔軟にする

・Webアプリとしてデプロイする

・変換をAIにやってもらう

Spring Security カスタマイズ入門

Spring Security カスタマイズ入門

1. 設定クラスを作る

カスタマイズをするには、まず設定用のクラス(Javaファイル)を作成します。 これが「門番への指示書(ルールブック)」になります。

  • @Configuration: これは設定クラスですよ、という印。
  • @EnableWebSecurity: Spring Securityを有効にする印。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    // ここにルールを書いていきます
    
}

2. カスタマイズの3大要素

実際の開発でよく行う設定は、大きく分けて以下の3つです。

  1. 通行許可の設定(認可): 「このページは誰でもOK」「ここは管理者だけ」
  2. ログイン画面の設定: 「自作のデザインを使う」「ログインしたらここへ飛ばす」
  3. パスワードの設定: 「パスワードはどう暗号化するか」

これらをコードでどう書くか見ていきましょう。

実践コード例(Spring Security 6系 / 最新の書き方)

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // ① 通行許可の設定(認可)
            .authorizeHttpRequests(auth -> auth
                // cssフォルダや、ログイン画面自体は誰でもアクセスOK
                .requestMatchers("/css/**", "/login").permitAll()
                // "/admin" から始まるURLは "ADMIN" 権限を持つ人だけ
                .requestMatchers("/admin/**").hasRole("ADMIN")
                // それ以外はすべて「ログイン(認証)」が必要
                .anyRequest().authenticated()
            )
            
            // ② ログイン画面の設定
            .formLogin(login -> login
                // 自作のログイン画面(HTML)のURLを指定
                .loginPage("/login")
                // ログインに成功したらトップページへ
                .defaultSuccessUrl("/", true)
                .permitAll()
            )
            
            // ログアウト設定(おまけ)
            .logout(logout -> logout
                .logoutSuccessUrl("/login?logout")
            );

        return http.build();
    }

    // ③ パスワードの暗号化設定
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 一般的な BCrypt という暗号化方式を使う
        return new BCryptPasswordEncoder();
    }
}

3. 各設定の詳しい意味

authorizeHttpRequests(誰を通していいか)

門番に「場所ごとの通行ルール」を教えます。上から順に評価されるので、順番が大事です。

  • requestMatchers("/..."): URLを指定します。
  • permitAll(): 「顔パスOK」。ログインしていなくても見られます(トップページやLPなど)。
  • hasRole("ADMIN"): 「管理者バッジが必要」。一般ユーザーは弾かれます。
  • authenticated(): 「会員証があればOK」。ログインさえしていれば誰でも見られます。

formLogin(ログインのしかた)

デフォルトのログイン画面はシンプルすぎるので、自分で作ったHTMLを使いたい場合に設定します。

  • loginPage("/login"): コントローラーで /login にアクセスが来たときに表示するHTMLを指定します。これを設定しないと、Spring Securityのデフォルト画面が出ます。
  • defaultSuccessUrl("/"): ログイン成功後にどこに飛ばすかを決めます。

PasswordEncoder(パスワードの守り方)

Spring Securityは「平文(そのままの文字)のパスワード」を嫌います。DBに保存するパスワードも、ログイン時に入力されたパスワードも、暗号化して照合するのがルールです。

  • BCryptPasswordEncoder: 最も標準的な暗号化ツールです。これをBean登録しておくだけで、自動的に使ってくれます。

まとめ

Spring Securityのカスタマイズとは、結局のところ以下のメソッドチェーン(.で繋いでいく書き方)を組み立てることです。

  1. authorizeHttpRequests で「通していいURL」を決める。
  2. formLogin で「ログイン画面の挙動」を決める。
  3. passwordEncoder で「パスワードの扱い」を決める。

最初は「おまじない」に見えるかもしれませんが、「門番への指示書を書いているんだ」と意識すると、コードの意味が見えてきますよ!