- name
- tinystruct-patterns
- description
- tinystruct Java フレームワークで開発する際の専門ガイダンス。tinystruct コードベース、または tinystruct 上に構築されたあらゆるプロジェクトで作業する際に使用します — プロジェクトに存在しない場合の bin/dispatcher・bin/dispatcher.cmd 起動スクリプトの生成、Application クラスの作成、@Action によるルート定義、ユニットテスト、ActionRegistry、HTTP/CLI デュアルモード対応、組み込み HTTP サーバー、イベントシステム、Builder/Builders による JSON 処理、AbstractData によるデータベース永続化、POJO 生成、Server-Sent Events (SSE)、ファイルアップロード、アウトバウンド HTTP 通信を含みます。
- metadata
- {"origin":"ECC","upstream":"tinystruct==1.7.37"}
# tinystruct 開発パターン
**tinystruct** Java フレームワークを使用してモジュールをビルドするためのアーキテクチャと実装パターン。CLI と HTTP を等しく扱う軽量・高性能なフレームワークで、`main()` メソッドを必要とせず、最小限の設定で動作します。
## コア原則
**CLI と HTTP は等しい市民(equal citizens)です。** トランスポート中立な `@Action` は `Mode.DEFAULT` を使用するか `mode` を省略し、HTTP専用の依存関係を持たないものです。このようなアクションはターミナルとWebブラウザの両方から実行できます。`Mode.DEFAULT` はトランスポートの制限をなくすだけで、HTTPに依存するメソッドをCLI対応にするものではありません。`Request` と `Response` の引数はHTTPコンテキスト属性から供給されるため、通常のCLI呼び出しのようにその属性がなければ null のままです。共有ロジックはHTTPオブジェクト、セッション、アップロードに依存させず、必要に応じてトランスポート固有のエントリポイントから呼び出してください。この「デュアルモード」対応能力が tinystruct のコア設計思想です。トランスポート固有のモード(`Mode.CLI`、`Mode.HTTP_GET`、`Mode.HTTP_POST` など)を持つアクションは意図的にそのトランスポートに限定されており、もう一方から呼び出すとフレームワークは "The action is not allowed to be executed." で拒否します。
## 主要な開発ツール:`bin/dispatcher`
**`bin/dispatcher` は tinystruct アプリケーションを開発・実行・テスト・デバッグするためのデフォルトかつ最優先のツールです — IDE の実行構成、手書きの `main()`、curl、ブラウザよりも先にこれを使ってください。** トランスポート中立な `@Action` メソッドは設計上デュアルモードであるため、`bin/dispatcher` を使うことでサーバーやブラウザを必要とせず、ターミナルから直接ルーティング・引数バインディング・ビジネスロジックを、最速のフィードバックループで検証できます。HTTPメソッドに限定されたアクション(`Mode.HTTP_POST`、`Mode.HTTP_GET` など)は `bin/dispatcher` からは呼び出せないため、起動中のサーバーに対してHTTPクライアントでテストしてください。
```bash
# CLI対応の @Action を直接実行して新しいアクションを検証する
# (クラスをインポートする必要があります。--import または application.properties の default.import.applications を参照してください)
bin/dispatcher greet/James --import com.example.MyService
bin/dispatcher echo --words "Praise the Lord"
# Web向けの対応部分を検証する必要があるときに HTTP サーバーを起動
bin/dispatcher start --import org.tinystruct.system.HttpServer
# 今回の実行のために追加の Application/MCP クラスをインポート
# (クラスごとに --import を1つ。カンマ区切りのリストは ClassNotFoundException になる)
bin/dispatcher start --import org.tinystruct.system.HttpServer --import com.example.MyService
# 利用可能な内容を確認
bin/dispatcher --help
bin/dispatcher --version
```
このワークフローをデフォルトにしてください:トランスポート中立な `@Action` を実装したら、`bin/dispatcher <action>` ですぐに実行して CLI モードで正しく動作することを確認し、Web向けのパス(例:`mode = Mode.HTTP_POST`、セッション、ファイルアップロードなど)を検証する必要があるときだけ HTTP サーバー(これも `bin/dispatcher start ...` で)を起動します。HTTP専用のアクションではCLIの手順を省略してHTTPでテストし、`Mode.CLI` のアクションではサーバーは不要です。アプリのエントリポイントとして `main(String[] args)` を決してハードコードしないでください — `bin/dispatcher`(Windows では `bin/dispatcher.cmd`)がすべてのモジュールの唯一のエントリポイントです。
### `bin/dispatcher` と `bin/dispatcher.cmd` の生成
すべての tinystruct プロジェクトは `bin/` に起動スクリプトを必要とします。**フレームワークがこれを生成します — 手で書いたり、他のプロジェクトからコピーしたり、`main()` で代用したりしないでください。** `ApplicationManager.init()` は、起動スクリプトが存在しない場合、tinystruct jar 内のテンプレートからそのjarのバージョンを埋め込んだ、実行中のOS用のスクリプトを作業ディレクトリ配下の `bin/` に書き出します。改行コードと実行権限はすでに正しく設定されています。
**1. プロジェクトがビルド対象とする tinystruct のバージョンを確認する**:`pom.xml` の `<tinystruct.version>` プロパティ、または `org.tinystruct:tinystruct` 依存関係です。jar は `~/.m2/repository/org/tinystruct/tinystruct/<version>/`(なければ `mvn dependency:resolve` を実行)か、プロジェクトの `lib/` に存在している必要があります。
**2. プロジェクトルートから、フレームワークを一度実行します:**
```bash
java -cp ~/.m2/repository/org/tinystruct/tinystruct/<version>/tinystruct-<version>.jar \
org.tinystruct.system.Dispatcher --version
```
`<version>` をステップ1で確認したバージョンに置き換えてください。Windows では `%USERPROFILE%\.m2\repository\org\tinystruct\tinystruct\<version>\tinystruct-<version>.jar` と同じクラスを使用します。これにより**実行したOS用のスクリプトだけ**が、まだ存在しない場合にのみ作成されます:
| 実行環境 | 作成されるもの |
|---|---|
| Linux, macOS | `bin/dispatcher`(実行可能、LF) |
| Windows | `bin\dispatcher.cmd`(CRLF) |
**3. 検証する。** プロジェクトルートから `bin/dispatcher --version`(Windows では `bin\dispatcher.cmd --version`)を実行します。`Dispatcher (cli) (built on tinystruct-<version>)` と表示され、スクリプト内の `VERSION` は jar のバージョンと一致します。続けて `bin/dispatcher --help` を実行すると、コマンド一覧とインポートされたすべてのアクションが表示されます。作成したファイルをユーザーに見せてください。
**4. 両方のスクリプトを取得するには**、各OSでステップ2を実行するか、もう一方のOSでチームメイトやCIに実行させてその結果をコミットしてもらいます。どちらのOSでチェックアウトしても動作するよう、両方をコミットしてください。`-Dos.name=…` で別のOSを偽装しようとしないでください:Windows では `setPosixFilePermissions` の `UnsupportedOperationException` で失敗し、空の `bin/dispatcher` が残ります。チェックアウト時に改行コードが壊れないよう、以下の `.gitattributes` を推奨します:
```
bin/dispatcher text eol=lf
bin/dispatcher.cmd text eol=crlf
```
**tinystruct のアップグレード:** `pom.xml` のバージョンを変更し、古いスクリプトを削除して、新しい jar でステップ2を繰り返します。あるいは `bin/dispatcher update` が Maven Central で最新リリースを確認し、プロジェクトの依存関係をアップグレードしてスクリプトを強制的に再生成します。これにはネットワークが必要で、`pom.xml` を編集し、選択したバージョンではなく*最新*バージョンに移行します。
**これは暗黙的にも発生します。** `ApplicationManager.init()` に到達するコード(dispatcher 自体、またはユニットテスト内の `ApplicationManager.install(app, config)`)は、スクリプトが存在しない場合、*カレントディレクトリ*にそれを作成します。ユニットテストが `bin/dispatcher.cmd` を各モジュールのディレクトリに散らかしてしまう場合は、ビルドディレクトリから実行してください:surefire の設定で `<workingDirectory>${project.build.directory}</workingDirectory>` を設定します。
**スクリプトの動作内容**(ほとんどの問題を説明します):
- 必ず**プロジェクトルートから**実行してください:シェルスクリプトはカレントディレクトリをルートとみなし、`.cmd` は `bin/` の親ディレクトリをルートとみなします。
- クラスパスは `target/classes`、`lib/*.jar`、`WEB-INF/lib/*`、`WEB-INF/classes`、そして tinystruct の jar(`lib/` になければ `~/.m2`)で構成され、その後 `org.tinystruct.system.Dispatcher` を引数付きで実行します。
- 初回実行時、`mvnw` / `mvnw.cmd` が存在しない場合は、プロジェクトルートに Maven Wrapper を展開します(Unix では `unzip`、Windows では PowerShell)。これが起こることをユーザーに伝えてください。ファイルはコミットしても、無視してもかまいません。
- `-D…` や `-X…` 引数を JVM オプションとして扱うのはシェルスクリプトのみです。
**注意点:**
- **Windows では `bin\dispatcher.cmd` を使ってください。** シェルスクリプトはクラスパスを `:` で結合するため、Git Bash/MSYS 上では `ClassNotFoundException: org.tinystruct.system.Dispatcher` で失敗します。WSL か `.cmd` を使用してください。
- **クラスパスに含まれるのは tinystruct の jar のみです。** フレームワークはデフォルトで fat jar を提供しなくなったため、他のモジュール、JDBCドライバ、ライブラリ(jjwt、lettuce など)は `lib/` に置く必要があります:`mvn dependency:copy-dependencies -DoutputDirectory=lib`(`lib/` は git-ignore してください)。マルチモジュールビルドでは、起動スクリプトを `target/classes` と `lib/` を持つディレクトリに置き、兄弟モジュールの jar をその `lib/` にコピーしてください。兄弟モジュールがクラスパスに含まれないディレクトリから実行すると `NoClassDefFoundError` になります。
- **アプリケーションは `--import` または設定でロードしてください。** クラスごとに `--import` を1つ(`--import a.A,b.B` は失敗します。`--import a.A --import b.B` と書いてください)、または `application.properties` に一度だけ列挙します:`default.import.applications=a.A;b.B`(`;` 区切り)。
- **`.cmd` には `JAVA_HOME` の設定が必要です**。設定されていないとエラーで停止します。
- **`bin/` は `target/classes` と `lib/` を持つディレクトリの直下に置いてください。** `.cmd` は `bin/` の親ディレクトリをルートとみなすため、`bin/` が誤った場所にネストしていると、間違ったクラスパスが `Dispatcher` の前に付与されます。
## 使用するタイミング
### 使用する場面
- `bin/dispatcher` を通じて、トランスポート中立または `Mode.CLI` の `@Action` を実行・テスト・デバッグするとき — これは HTTP や IDE の実行構成に頼る前の、tinystruct アプリを扱うデフォルトの方法です。HTTP専用のアクションは、代わりにHTTPでテストします。
- `bin/dispatcher` / `bin/dispatcher.cmd` が存在しないプロジェクトをセットアップするとき — フレームワークに生成させてください(「`bin/dispatcher` と `bin/dispatcher.cmd` の生成」を参照)。
- `AbstractApplication` を拡張して新しい `Application` モジュールを作成するとき。
- `@Action` を使用してルートとコマンドラインアクションを定義するとき。
- `Context` を通じてリクエストごとの状態を処理するとき。
- ネイティブの `Builder` と `Builders` コンポーネントを使用してJSONシリアライゼーションを行うとき。
- `AbstractData` POJO を使用してデータベース永続化を行うとき。
- `generate` コマンドを使用してデータベーステーブルから POJO を生成するとき(XML マッピングファイルまたはアノテーション `--mapping annotation`)。
- リアルタイムプッシュのために Server-Sent Events (SSE) を実装するとき。
- multipart データによるファイルアップロードを処理するとき。
- `URLRequest` と `HTTPHandler` を使用してアウトバウンドHTTPリクエストを行うとき。
- `application.properties` でデータベース接続やシステム設定を構成するとき。
- ルーティングの競合(Action)や CLI 引数解析をデバッグするとき。
## 動作の仕組み
tinystruct フレームワークは、`@Action` でアノテーションされたメソッドをルーティング可能なエンドポイントとして扱います。`mode` を指定しない場合、ディスパッチは単一のトランスポートに制限されませんが、引数と実装は実行環境に対応している必要があります。通常のCLI呼び出しではHTTPの `Request` や `Response` オブジェクトは供給されません。`mode = Mode.CLI` や `Mode.HTTP_POST` などのHTTPモードを指定した場合、ディスパッチはそのトランスポートに限定されます。アプリケーションは `AbstractApplication` を拡張することで作成され、`init()` などのコアライフサイクルフックとリクエスト `Context` へのアクセスが提供されます。
ルーティングは `ActionRegistry` によって処理され、パスセグメントをメソッド引数に自動的にマッピングして依存関係を注入します。データのみのサービスでは、ゼロ依存のフットプリントを維持するために、JSONシリアライゼーションにネイティブの `Builder` と `Builders` コンポーネントを使用すべきです。データベース層は `@Table`/`@Column` アノテーションまたは XML マッピングファイルでテーブルにマッピングされた `AbstractData` POJO を使用し、外部 ORM ライブラリなしで CRUD 操作を行います。不足しているテーブルは `database.autocreate=true` で自動的に作成できます。
## 例
### 基本アプリケーション(MyService)
```java
public class MyService extends AbstractApplication {
@Override
public void init() {
this.setTemplateRequired(false); // データ/APIアプリの .view 参照を無効化
}
@Override public String version() { return "1.0.0"; }
@Action("greet")
public String greet() {
return "Hello from tinystruct!";
}
// パスパラメータ: GET /?q=greet/James または bin/dispatcher greet/James
@Action("greet")
public String greet(String name) {
return "Hello, " + name + "!";
}
}
```
### HTTPモード分岐(login)
以下の長さ制限は UTF-16 コード単位で数えるアプリケーション側の設定例であり、tinystruct のデフォルトではありません。認証サービスの方針に合わせてください。空白だけの認証情報は拒否しますが、認証に渡す値をトリミングしたり変更したりしません。
```java
@Action(value = "login", mode = Mode.HTTP_POST)
public String doLogin(Request<?, ?> request) throws ApplicationException {
String username = request.getParameter("username");
String password = request.getParameter("password");
final int maxUsernameLength = 256;
final int maxPasswordLength = 1024;
if (username == null || password == null
|| username.length() > maxUsernameLength || password.length() > maxPasswordLength
|| username.isBlank() || password.isBlank()) {
throw new ApplicationException("Invalid credentials");
}
// AuthService はアプリケーション独自のクラスであり、tinystruct の一部ではない
String userId = AuthService.getInstance().authenticate(username, password); // 失敗時は null
if (userId == null) {
throw new ApplicationException("Invalid credentials");
}
// 認証に成功した後にのみセッションを操作する
request.getSession().setAttribute("userId", userId);
return "Logged in";
}
```
### ネイティブJSONデータ処理(Builder + Builders)
```java
import org.tinystruct.data.component.Builder;
import org.tinystruct.data.component.Builders;
@Action("api/data")
public String getData() throws ApplicationException {
Builders dataList = new Builders();
Builder item = new Builder();
item.put("id", 1);
item.put("name", "James");
dataList.add(item);
Builder response = new Builder();
response.put("status", "success");
response.put("data", dataList);
return response.toString(); // {"status":"success","data":[{"id":1,"name":"James"}]}
}
```
### SSE(Server-Sent Events)
```java
import org.tinystruct.http.SSEPushManager;
@Action("sse/connect")
public String connect() {
return "{\"type\":\"connect\",\"message\":\"Connected to SSE\"}";
}
// 特定のクライアントへプッシュ
String sessionId = getContext().getId();
Builder msg = new Builder();
msg.put("text", "Hello, user!");
SSEPushManager.getInstance().push(sessionId, msg);
// 全員へブロードキャスト
SSEPushManager.getInstance().broadcast(msg);
```
### ファイルアップロード
```java
import org.tinystruct.data.FileEntity;
@Action(value = "upload", mode = Mode.HTTP_POST)
public String upload(Request<?, ?> request) throws ApplicationException {
List<FileEntity> files = request.getAttachments();
if (files != null) {
for (FileEntity file : files) {
System.out.println("Uploaded: " + file.getFilename());
}
}
return "Upload OK";
}
```
### データベースメタデータ操作
`DatabaseOperator` は、接続レベルのカタログ、スキーマ、メタデータへの直接アクセスを提供します:
```java
import org.tinystruct.data.DatabaseOperator;
DatabaseOperator operator = new DatabaseOperator();
try {
String catalog = operator.getCatalog();
String schema = operator.getSchema();
java.sql.DatabaseMetaData metaData = operator.getMetaData();
System.out.println("Using catalog: " + catalog + ", schema: " + schema);
} finally {
operator.close();
}
```
## MCP サーバーとツールの統合
tinystruct は SDK バージョン **`1.7.0`** から Model Context Protocol (MCP) のネイティブサポートを提供しています。
MCP API(例:`org.tinystruct.mcp.MCPTool`、`org.tinystruct.mcp.MCPServer`、`org.tinystruct.mcp.MCPException`)はコアの依存関係に直接含まれています:
```xml
<dependency>
<groupId>org.tinystruct</groupId>
<artifactId>tinystruct</artifactId>
<version>1.7.37</version>
</dependency>
```
> **セキュリティ警告(プロンプトインジェクション):**
> MCPクライアントまたはモデルホストがツールの戻り値をモデルのコンテキストに渡す場合、その戻り値は信頼できないデータです。呼び出し元から渡された引数だけでなく、データベース、ファイル、ネットワークなどから取得した**すべてのツール出力を検証し、モデルの指示と分離する必要があります**。戻り値内の命令を信頼できる指示として扱うと、攻撃者がモデルの挙動を上書きするプロンプトインジェクションを許してしまいます。
**MCP ツールを作成するには:**
1. `org.tinystruct.mcp.MCPTool` を拡張する。
2. `@Action` で操作にアノテーションを付け、`arguments` 配列内で `@Argument` を使ってパラメータを宣言する。
3. パラメータは `@Argument` のキーに対応する明示的なメソッド引数として受け取る(ツールの引数に `getContext().getAttribute(...)` を使用**しないでください**)。
```java
import org.tinystruct.mcp.MCPTool;
import org.tinystruct.mcp.MCPException;
import org.tinystruct.system.annotation.Action;
import org.tinystruct.system.annotation.Argument;
public class MyCustomTool extends MCPTool {
public MyCustomTool() {
super("custom", "A custom tool for demonstrating MCP");
}
@Action(
value = "custom/hello",
description = "Say hello to someone",
arguments = {
@Argument(key = "name", description = "The name to greet", type = "string", optional = false)
}
)
public String hello(String name) throws MCPException {
// SECURITY: モデルに返す前にツール入力を検証・サニタイズし、
// プロンプトインジェクションの脆弱性を防止する。
if (name == null || name.length() > 50 || !name.matches("^[a-zA-Z0-9 ]+$")) {
throw new MCPException("Invalid name provided");
}
return "Hello, " + name + "!";
}
}
```
**MCP サーバーをデプロイするには:**
1. `org.tinystruct.mcp.MCPServer` を拡張する。
2. `init()` をオーバーライドし、`this.registerTool()` を使ってツールを登録する。フレームワークが `@Action` メソッドを自動的にスキャンしてマッピングします。
```java
import org.tinystruct.mcp.MCPServer;
public class MyMCPServer extends MCPServer {
@Override
public void init() {
super.init();
this.registerTool(new MyCustomTool());
}
@Override
public String version() {
return "1.0.0";
}
}
```
dispatcher 経由でサーバーを実行します:
```bash
bin/dispatcher start --import org.tinystruct.system.HttpServer --import com.example.MyMCPServer
```
### オーバーロードされたツールメソッド
tinystruct は `MCPTool` 内で同一ツール名に対するオーバーロードメソッド(同じツール名を共有しつつ異なるパラメータシグネチャを受け取る)をサポートしています。
- **スキーママージ**:同じ名前を持つすべてのオーバーロードの入力スキーマは、動的に統合されたひとつの JSON スキーマにマージされます。プロパティは和集合として扱われ、必須プロパティは積集合として扱われます(*すべての*オーバーロードで必須とされているフィールドのみが必須として残ります)。
- **実行ルーティング**:ツールを実行する際、サーバーは利用可能なオーバーロードメソッドを順に試行します。各メソッドのスキーマ検証が最初に行われ、提供された引数に対して最初に検証を通過したオーバーロードが実行されます。どのシグネチャも一致・成功しない場合、フレームワークは適切な例外をスローします。
## 設定
設定は `src/main/resources/application.properties` で管理されます。
```properties
# データベース
driver=org.h2.Driver
database.url=jdbc:h2:~/mydb
database.user=sa
database.password=
# 任意:初回利用時にクラスのマッピングから不足しているテーブルを作成する(デフォルトはオフ)
# database.autocreate=true
# サーバー
default.home.page=hello
server.port=8080
default.server.open_browser=true
# 静的ファイルはこれらのディレクトリ(カンマまたはセミコロン区切り)からのみ配信されます。
# ルートや親パスにシンボリックリンクがない公開用の実ディレクトリを使ってください。
# 作業ディレクトリ自体やその祖先を公開しないでください。デフォルト:static
# server.static.dirs=static,public
# ロケール
default.language=en_US
# セッション(クラスタ環境向けの Redis)
# default.session.repository=org.tinystruct.http.RedisSessionRepository
# redis.host=127.0.0.1
# redis.port=6379
# プログラムによるロギング設定
logging.enabled=true
logging.level=INFO
org.tinystruct.level=FINE
# 作成するハンドラ(カンマまたは空白区切り)。デフォルト:ConsoleHandler のみ
View on GitHub