ハンドラメタ(HandlerMeta)
ルートハンドラのメタデータ。
概要
HandlerMetaは、実行するControllerメソッドのメタデータを含む構造体です。 Routerが要求パスを一致するとHandlerMetaが返され、Pipelineはこの情報を使用して実際のメソッドを呼び出します。
HandlerMeta構造体
// core/handler_meta.go
type HandlerMeta struct {
// コントローラ型 (Container Resolve 対象)
ControllerType reflect.Type
// 呼び出すメソッド
Method reflect.Method
// ハンドラに適用されたインターセプタ
Interceptors []Interceptor
}フィールドの説明
ControllerType
Controller のポインタタイプです。 IoC ContainerでインスタンスをResolveするときに使用されます。
meta.ControllerType // reflect.Type of *UserControllerMethod
呼び出すメソッドのリフレクション情報です。メソッド名、シグネチャ、関数ポインタを含みます。
meta.Method.Name // "GetUser"
meta.Method.Type // func(*UserController, path.Int) (User, error)
meta.Method.Func // 呼び出し 可能な reflect.ValueInterceptors
そのルートにのみ適用されるインターセプタのリスト。グローバルインターセプタとは別に、ルート単位で交差点を適用できます。
meta.Interceptors // []core.Interceptor (ルート レベル)HandlerMetaの生成
メソッド式
Spineはメソッド式(Method Expression)を使用してハンドラを登録します。
// cmd/demo/main.go
app.Route(
"GET",
"/users/:id",
(*UserController).GetUser, // メソッド式
)メソッド式(*UserController).GetUserは一般関数として扱われます。
// メソッド式の 実際の 型
func(*UserController, path.Int) (User, error)
// ↑ レシーバが最初の引数に変換されますRouteOptionでルートインターセプタを適用する
route.WithInterceptorsを使用してルート単位のインターセプタを指定できます。
app.Route(
"GET",
"/users/:id",
(*UserController).GetUser,
route.WithInterceptors(&AuthInterceptor{}),
)// pkg/route/route_options.go
func WithInterceptors(interceptors ...core.Interceptor) router.RouteOption {
return func(rs *router.RouteSpec) {
rs.Interceptors = append(rs.Interceptors, interceptors...)
}
}NewHandlerMeta関数
メソッド式を分析してHandlerMetaを生成します。
// internal/router/handler_meta.go
func NewHandlerMeta(handler any) (core.HandlerMeta, error) {
t := reflect.TypeOf(handler)
v := reflect.ValueOf(handler)
// 1. 関数かどうか 検証
if t.Kind() != reflect.Func {
return core.HandlerMeta{}, fmt.Errorf("handlerは 関数である必要があります")
}
// 2. メソッド式かどうか 検証 (最初の 引数が レシーバ)
if t.NumIn() < 1 {
return core.HandlerMeta{}, fmt.Errorf("handlerは メソッド式である必要があります")
}
// 3. レシーバが ポインタ 型かどうか 検証
レシーバType := t.In(0)
if レシーバType.Kind() != reflect.Ptr {
return core.HandlerMeta{}, fmt.Errorf("handlerの レシーバは ポインタ 型である必要があります")
}
// 4. メソッド 名前 抽出
fn := runtime.FuncForPC(v.Pointer())
if fn == nil {
return core.HandlerMeta{}, fmt.Errorf("メソッド 情報を抽出できません")
}
fullName := fn.Name()
// 例: github.com/NARUBROWN/spine-demo.(*UserController).GetUser
lastDot := strings.LastIndex(fullName, ".")
if lastDot == -1 {
return core.HandlerMeta{}, fmt.Errorf("メソッド 名前 解析失敗: %s", fullName)
}
methodName := fullName[lastDot+1:]
// 5. リフレクションでメソッド情報を取得
method, ok := レシーバType.MethodByName(methodName)
if !ok {
return core.HandlerMeta{}, fmt.Errorf("メソッドを 見つかりません: %s", methodName)
}
return core.HandlerMeta{
ControllerType: レシーバType,
Method: method,
}, nil
}生成プロセスの詳細
Step 1: 関数の検証
t := reflect.TypeOf((*UserController).GetUser)
t.Kind() // reflect.Func ✓Step 2: メソッド式の検証
t.NumIn() // 2 (レシーバ + path.Int)
t.In(0) // *UserController (レシーバ)
t.In(1) // path.IntStep 3: メソッド名の抽出
runtime.FuncForPCで関数のフルパスを取得し、最後の.以降の文字列がメソッド名です。 lastDot == -1の場合、解析失敗エラーを返します。
fn.Name() // "github.com/NARUBROWN/spine-demo.(*UserController).GetUser"
// ↑ methodNameStep 4: Method 獲得
method, _ := reflect.TypeOf(&UserController{}).MethodByName("GetUser")
// method.Name: "GetUser"
// method.Type: func(*UserController, path.Int) (User, error)
// method.Func: 呼び出し 可能な reflect.ValueRouterでの使用
Route 登録
// internal/router/router.go
type Route struct {
Method string // HTTP メソッド
Path string // URL パターン
Meta core.HandlerMeta // ハンドラー メタデータ (Interceptors 含む)
}
func (r *DefaultRouter) Register(method string, path string, meta core.HandlerMeta) {
r.routes = append(r.routes, Route{
Method: method,
Path: path,
Meta: meta,
})
}Route マッチング
func (r *DefaultRouter) Route(ctx core.ExecutionContext) (core.HandlerMeta, error) {
for _, route := range r.routes {
if route.Method != ctx.Method() {
continue
}
ok, params, keys := matchPath(route.Path, ctx.Path())
if !ok {
continue
}
ctx.Set("spine.params", params)
ctx.Set("spine.pathKeys", keys)
return route.Meta, nil // HandlerMeta 返却 (Interceptors 含む)
}
return core.HandlerMeta{}, httperr.NotFound("ハンドラーが ありません.")
}Pipelineでの使用
グローバル+ルートインターセプタ実行フロー
Pipelineは、グローバルインターセプタとルートインターセプタを分離して実行します。
// internal/pipeline/pipeline.go
func (p *Pipeline) Execute(ctx core.ExecutionContext) (finalErr error) {
globalMeta := core.HandlerMeta{}
// AfterCompletionは 成功/失敗と に関係なく 保証
defer func() {
for i := len(p.interceptors) - 1; i >= 0; i-- {
p.interceptors[i].AfterCompletion(ctx, globalMeta, finalErr)
}
}()
// 1. グローバル Interceptor PreHandle (ルーティング前)
for _, it := range p.interceptors {
if err := it.PreHandle(ctx, globalMeta); err != nil {
if errors.Is(err, core.ErrAbortPipeline) {
return nil
}
return err
}
}
// 2. Routerが 実行 対象を 決定
meta, err := p.router.Route(ctx)
if err != nil {
return err
}
routeInterceptors := meta.Interceptors
// ルート Interceptor AfterCompletionは 必ず 保証
defer func() {
for i := len(routeInterceptors) - 1; i >= 0; i-- {
routeInterceptors[i].AfterCompletion(ctx, meta, finalErr)
}
}()
// 3. ArgumentResolver チェーン 実行
paramMetas := buildParameterMeta(meta.Method, ctx)
args, err := p.resolveArguments(ctx, paramMetas)
if err != nil {
return err
}
// 4. ルート Interceptor PreHandle
for _, it := range routeInterceptors {
if err := it.PreHandle(ctx, meta); err != nil {
if errors.Is(err, core.ErrAbortPipeline) {
return nil
}
return err
}
}
// 5. Controller Method 呼び出し
results, err := p.invoker.Invoke(meta.ControllerType, meta.Method, args)
if err != nil {
return err
}
// 6. ReturnValueHandler 処理
returnError := p.handleReturn(ctx, results)
// 7. PostExecutionHook (イベント 発行 など)
for _, hook := range p.postHooks {
hook.AfterExecution(ctx, results, returnError)
}
if returnError != nil {
return returnError
}
// 8. ルート Interceptor PostHandle (逆順)
for i := len(routeInterceptors) - 1; i >= 0; i-- {
routeInterceptors[i].PostHandle(ctx, meta)
}
// 9. グローバル Interceptor PostHandle (逆順)
for i := len(p.interceptors) - 1; i >= 0; i-- {
p.interceptors[i].PostHandle(ctx, meta)
}
return nil
}ParameterMetaの生成
HandlerMeta.Methodを分析して各パラメータのメタ情報を生成します。
// internal/pipeline/pipeline.go
func buildParameterMeta(method reflect.Method, ctx core.ExecutionContext) []resolver.ParameterMeta {
pathKeys := ctx.PathKeys()
pathIdx := 0
var metas []resolver.ParameterMeta
// method.Type.NumIn()は レシーバ 含む
// i=0は レシーバために i=1から 時作
for i := 1; i < method.Type.NumIn(); i++ {
pt := method.Type.In(i)
pm := resolver.ParameterMeta{
Index: i - 1,
Type: pt,
}
if isPathType(pt) {
if pathIdx >= len(pathKeys) {
pm.PathKey = ""
} else {
pm.PathKey = pathKeys[pathIdx]
}
pathIdx++
}
metas = append(metas, pm)
}
return metas
}
func isPathType(pt reflect.Type) bool {
pathPkg := reflect.TypeFor[path.Int]().PkgPath()
return pt.PkgPath() == pathPkg
}Controller呼び出し
// internal/invoker/invoker.go
func (i *Invoker) Invoke(controllerType reflect.Type, method reflect.Method, args []any) ([]any, error) {
// 1. Containerで Controller インスタンス Resolve
controller, err := i.container.Resolve(controllerType)
if err != nil {
return nil, err
}
// 2. 呼び出し 引数 構成 (レシーバ + args)
values := make([]reflect.Value, len(args)+1)
values[0] = reflect.ValueOf(controller) // レシーバ
for idx, arg := range args {
values[idx+1] = reflect.ValueOf(arg)
}
// 3. リフレクションにに メソッド 呼び出し
results := method.Func.Call(values)
// 4. 結と 変換
out := make([]any, len(results))
for i, result := range results {
out[i] = result.Interface()
}
return out, nil
}Interceptor 渡し
InterceptorのすべてのメソッドはHandlerMetaを受け取り、実行先情報にアクセスできます。
// cmd/demo/logging_interceptor.go
func (i *LoggingInterceptor) PreHandle(ctx core.ExecutionContext, meta core.HandlerMeta) error {
log.Printf(
"[REQ] %s %s -> %s.%s",
ctx.Method(),
ctx.Path(),
meta.ControllerType.Name(), // "UserController"
meta.Method.Name, // "GetUser"
)
return nil
}##ブートストラッププロセス
1. Route宣言
// cmd/demo/main.go
app.Route("GET", "/users/:id", (*UserController).GetUser)
// ルートインターセプタと とともに
app.Route("GET", "/admin/users/:id", (*AdminController).GetUser,
route.WithInterceptors((*AuthInterceptor)(nil)), // nil ポインタ → Containerで Resolve
)2. RouteSpec コレクション
// app.go
func (a *app) Route(method string, path string, handler any, opts ...router.RouteOption) {
// HTTP メソッドを 大文字に 変換し 大文字小文字の不一致 防止
method = strings.ToUpper(strings.TrimSpace(method))
spec := router.RouteSpec{
Method: method,
Path: path,
Handler: handler,
}
for _, opt := range opts {
opt(&spec)
}
a.routes = append(a.routes, spec)
}3. HandlerMetaの生成とルートインターセプタResolve
ブートストラップ時にNewHandlerMetaでメタデータを作成し、ルートインターセプタを処理します。 nilポインタに渡されたインターセプタは、IoC ContainerでResolveされます。
// internal/bootstrap/bootstrap.go
router := spineRouter.NewRouter()
for _, route := range config.Routes {
// メソッド式 → HandlerMeta 変換
meta, err := spineRouter.NewHandlerMeta(route.Handler)
if err != nil {
return err
}
// ルートインターセプタ Resolve
resolved := make([]core.Interceptor, len(route.Interceptors))
for i, interceptor := range route.Interceptors {
interceptorType := reflect.TypeOf(interceptor)
value := reflect.ValueOf(interceptor)
if interceptorType.Kind() == reflect.Pointer && value.IsNil() {
// nil ポインタ → Containerで Resolve
inst, err := container.Resolve(interceptorType)
if err != nil {
panic(err)
}
resolved[i] = inst.(core.Interceptor)
} else {
// インスタンス 直接 使用
resolved[i] = interceptor
}
}
meta.Interceptors = resolved
fullPath := joinPath(prefix, route.Path)
router.Register(route.Method, fullPath, meta)
}4. Controllerタイプの収集
Routerに登録されているすべてのControllerタイプを収集します。
// internal/router/router.go
func (r *DefaultRouter) ControllerTypes() []reflect.Type {
seen := map[reflect.Type]struct{}{}
var result []reflect.Type
for _, route := range r.routes {
t := route.Meta.ControllerType
if _, ok := seen[t]; ok {
continue
}
seen[t] = struct{}{}
result = append(result, t)
}
return result
}5. Warm-Up
ブートストラップ時にすべてのControllerを事前にインスタンス化します。
// internal/bootstrap/bootstrap.go
if err := container.WarmUp(router.ControllerTypes()); err != nil {
panic(err)
}フルフローサマリー
設計原則
1. メソッド式の強制
通常の関数やクロージャではなく、メソッド式のみを受け入れます。
// ✓ メソッド式
app.Route("GET", "/users/:id", (*UserController).GetUser)
// ❌ 通常の関数 (サポートしない)
app.Route("GET", "/users/:id", func(id path.Int) User { ... })
// ❌ インスタンス メソッド (サポートしない)
ctrl := &UserController{}
app.Route("GET", "/users/:id", ctrl.GetUser)2. ポインタレシーバを強制
値レシーバーはサポートされていません。
// ✓ ポインタ レシーバ
func (c *UserController) GetUser(id path.Int) User
// ❌ 値 レシーバ (サポートしない)
func (c UserController) GetUser(id path.Int) User3. ブートストラップの検証
NewHandlerMetaはブートストラップの時点で呼び出されるため、無効なハンドラ登録はサーバーの起動前に失敗します。
// 不正な ハンドラー 登録 時 ブートストラップ 失敗
meta, err := spineRouter.NewHandlerMeta(invalidHandler)
if err != nil {
return err // サーバー起動 前 エラー
}4. グローバル vs ルートインターセプタの分離
グローバルインターセプタはapp.Interceptor()として登録し、ルートインターセプタはroute.WithInterceptors()として登録します。 Pipelineでは実行順序が異なります。
// グローバル: すべて リクエストに 適用 (ルーティング前 実行)
app.Interceptor(&CORSInterceptor{})
// ルート: 特定の ハンドラーにだけ 適用 (ルーティング 後, Controller 呼び出し 前 実行)
app.Route("GET", "/admin/:id", (*AdminController).Get,
route.WithInterceptors(&AuthInterceptor{}),
)まとめ
|コンポーネント役割||----------|------| | HandlerMeta |コントローラタイプ、メソッド情報、ルートインターセプタを含むメタデータ | NewHandlerMeta() |メソッド式→HandlerMeta変換| | RouteOption / WithInterceptors() |ルート単位インターセプタの指定| | Router |要求マッチング時にHandlerMetaを返します(Interceptorsを含む) | Invoker | HandlerMetaによるControllerインスタンスのresolveとメソッドの呼び出し | Interceptor | HandlerMetaでの実行対象情報へのアクセス 核心: HandlerMetaは「何を実行するのか」に関するメタデータです。ブートストラップ時に生成され、ランタイムに使用され、実行モデルとビジネスロジックを結ぶためのコアリングとして機能します。ルートインターセプタを含めることで、ハンドラ単位の横断関心事の適用もサポートします。
