网络应用

功能特性

  • Web MVC(模型-视图-控制器)
  • 基于属性配置的自动配置和依赖注入
  • 支持 inject:"" 标签或构造函数注入
  • 基于方法名的自动路由映射
  • 内置验证支持
  • Swagger/OpenAPI 文档支持

Hiboot MVC 简介

Hiboot 旨在隐藏与业务无关的代码,让开发者专注于业务逻辑。

与大多数 Go Web 框架不同,Hiboot 无需手动配置路由。Hiboot 使用反射根据控制器方法名自动构建路由。

项目结构

.
├── config
│   ├── application.yml
│   ├── application-local.yml
│   └── application-dev.yml
├── main.go
├── main_test.go
├── controller
│   ├── user.go
│   └── user_test.go
├── entity
│   └── user.go
└── service
    ├── user.go
    └── user_test.go

应用配置

Hiboot 允许你将配置外部化,以便在不同环境中使用相同的应用程序代码。

属性值可以使用 value:"" 标签直接注入到结构体中:

type MyService struct {
	AppName string `value:"${app.name}"`
	Port    int    `value:"${server.port}"`
}

config/application.yml

app:
  project: myproject
  name: myapp
  profiles:
    include:
    - actuator
    - logging

server:
  port: 8080

logging:
  level: info

应用配置属性参考

字段 描述 示例
app.project 项目名称 myproject
app.name 应用名称 myapp
app.profiles.active 激活的配置文件 dev, test, prod
app.profiles.include 包含的 Starter actuator, logging, swagger
server.port 服务器端口 8080
logging.level 日志级别 debug, info, warn, error

特定环境配置

可以使用命名约定 application-${profile}.yml 定义特定环境的配置。

config/application-local.yml

server:
  port: 8081

logging:
  level: debug

特定环境的配置文件会覆盖基础 application.yml 中的属性。

编写代码

main.go

main 包是 Web 应用程序的入口点:

package main

import (
	"github.com/hidevopsio/hiboot/pkg/app"
	"github.com/hidevopsio/hiboot/pkg/app/web"
	"github.com/hidevopsio/hiboot/pkg/starter/actuator"
	"github.com/hidevopsio/hiboot/pkg/starter/logging"

	_ "myapp/controller"
)

func main() {
	web.NewApplication().
		SetProperty(app.ProfilesInclude, actuator.Profile, logging.Profile).
		Run()
}

控制器 - controller/user.go

控制器处理 HTTP 请求。嵌入 at.RestController 将结构体标记为 REST 控制器:

package controller

import (
	"myapp/entity"
	"myapp/service"
	"net/http"

	"github.com/hidevopsio/hiboot/pkg/app"
	"github.com/hidevopsio/hiboot/pkg/at"
	"github.com/hidevopsio/hiboot/pkg/model"
)

// userController 处理用户相关的 HTTP 请求
type userController struct {
	at.RestController
	at.RequestMapping `value:"/user"`

	userService service.UserService
}

func init() {
	app.Register(newUserController)
}

// newUserController 通过构造函数注入 userService
func newUserController(userService service.UserService) *userController {
	return &userController{
		userService: userService,
	}
}

// Post 处理 POST /user
func (c *userController) Post(request *entity.User) (model.Response, error) {
	err := c.userService.AddUser(request)
	response := new(model.BaseResponse)
	response.SetData(request)
	return response, err
}

// GetById 处理 GET /user/id/{id}
func (c *userController) GetById(id uint64) (response model.Response, err error) {
	user, err := c.userService.GetUser(id)
	response = new(model.BaseResponse)
	if err != nil {
		response.SetCode(http.StatusNotFound)
	} else {
		response.SetData(user)
	}
	return
}

// GetAll 处理 GET /user/all
func (c *userController) GetAll() (response model.Response, err error) {
	users, err := c.userService.GetAll()
	response = new(model.BaseResponse)
	response.SetData(users)
	return
}

// DeleteById 处理 DELETE /user/id/{id}
func (c *userController) DeleteById(id uint64) (response model.Response, err error) {
	err = c.userService.DeleteUser(id)
	response = new(model.BaseResponse)
	return
}

HTTP 方法映射

方法名会自动映射到 HTTP 方法:

方法名 HTTP 方法 路由示例
Get GET GET /user
GetById GET GET /user/id/{id}
GetAll GET GET /user/all
Post POST POST /user
Put PUT PUT /user
PutById PUT PUT /user/id/{id}
Delete DELETE DELETE /user
DeleteById DELETE DELETE /user/id/{id}

实体 - entity/user.go

实体代表业务模型:

package entity

import "github.com/hidevopsio/hiboot/pkg/model"

type User struct {
	model.RequestBody
	Id       uint64 `json:"id"`
	Name     string `json:"name" validate:"required"`
	Username string `json:"username" validate:"required"`
	Password string `json:"password" validate:"required"`
	Email    string `json:"email" validate:"required,email"`
	Age      uint   `json:"age" validate:"gte=0,lte=130"`
	Gender   uint   `json:"gender" validate:"gte=0,lte=2"`
}

func (u *User) TableName() string {
	return "user"
}

服务 - service/user.go

服务实现业务逻辑:

package service

import (
	"errors"
	"myapp/entity"

	"github.com/hidevopsio/hiboot/pkg/app"
	"github.com/hidevopsio/hiboot/pkg/utils/idgen"
)

type UserService interface {
	AddUser(user *entity.User) error
	GetUser(id uint64) (*entity.User, error)
	GetAll() (*[]entity.User, error)
	DeleteUser(id uint64) error
}

type userServiceImpl struct {
	UserService
	users map[uint64]*entity.User
}

func init() {
	app.Register(newUserService)
}

func newUserService() UserService {
	return &userServiceImpl{
		users: make(map[uint64]*entity.User),
	}
}

func (s *userServiceImpl) AddUser(user *entity.User) error {
	if user == nil {
		return errors.New("user 不能为空")
	}
	if user.Id == 0 {
		user.Id, _ = idgen.Next()
	}
	s.users[user.Id] = user
	return nil
}

func (s *userServiceImpl) GetUser(id uint64) (*entity.User, error) {
	user, ok := s.users[id]
	if !ok {
		return nil, errors.New("用户不存在")
	}
	return user, nil
}

func (s *userServiceImpl) GetAll() (*[]entity.User, error) {
	users := make([]entity.User, 0, len(s.users))
	for _, u := range s.users {
		users = append(users, *u)
	}
	return &users, nil
}

func (s *userServiceImpl) DeleteUser(id uint64) error {
	delete(s.users, id)
	return nil
}

使用注解生成 Swagger 文档

使用控制器方法中的注解生成 Swagger/OpenAPI 文档:

package controller

import (
	"github.com/hidevopsio/hiboot/pkg/app"
	"github.com/hidevopsio/hiboot/pkg/at"
)

type userController struct {
	at.RestController
	at.RequestMapping `value:"/user"`
}

func init() {
	app.Register(newUserController)
}

func newUserController() *userController {
	return &userController{}
}

// GetById 处理 GET /user/{id},包含 Swagger 文档
func (c *userController) GetById(
	at struct {
		at.GetMapping `value:"/{id}"`
		at.Operation  `id:"getUserById" description:"根据 ID 获取用户"`
		at.Produces   `values:"application/json"`
		Parameters    struct {
			Id struct {
				at.Parameter `name:"id" in:"path" type:"integer" description:"用户 ID"`
			}
		}
		Responses struct {
			StatusOK struct {
				at.Response `code:"200" description:"成功"`
			}
			NotFound struct {
				at.Response `code:"404" description:"用户不存在"`
			}
		}
	},
	id uint64,
) string {
	return "用户详情"
}

运行应用

go run main.go

输出:

______  ____________             _____
___  / / /__(_)__  /_______________  /_
__  /_/ /__  /__  __ \  __ \  __ \  __/
_  __  / _  / _  /_/ / /_/ / /_/ / /_     Hiboot Application Framework
/_/ /_/  /_/  /_.___/\____/\____/\__/     https://hiboot.hidevops.io

[INFO] Starting Hiboot web application on localhost with PID xxx
[INFO] Mapped "/user" onto controller.userController.Post()
[INFO] Mapped "/user/id/{id}" onto controller.userController.GetById()
[INFO] Mapped "/user/all" onto controller.userController.GetAll()
[INFO] Mapped "/health" onto actuator.healthController.Get()
[INFO] Hiboot started on port(s) http://localhost:8080

发送请求

创建用户

curl -X POST http://localhost:8080/user \
  -H "Content-Type: application/json" \
  -d '{"name":"张三","username":"zhangsan","password":"secret123","email":"zhangsan@example.com","age":30,"gender":1}'

获取所有用户

curl http://localhost:8080/user/all

响应:

{
    "code": 200,
    "data": [
        {
            "id": 209536579658580081,
            "name": "张三",
            "username": "zhangsan",
            "email": "zhangsan@example.com",
            "age": 30,
            "gender": 1
        }
    ],
    "message": "Success"
}

单元测试

main_test.go

package main

import (
	"testing"
	"time"
)

func TestRunMain(t *testing.T) {
	go main()
	time.Sleep(200 * time.Millisecond)
}

使用 Mock 进行控制器测试

使用 Mockery 生成 Mock 实现:

go install github.com/vektra/mockery/v2@latest
mockery --name UserService --dir service --output service/mocks

然后编写测试:

package controller

import (
	"myapp/entity"
	"myapp/service/mocks"
	"net/http"
	"testing"

	"github.com/hidevopsio/hiboot/pkg/app/web"
	"github.com/hidevopsio/hiboot/pkg/utils/idgen"
	"github.com/stretchr/testify/assert"
)

func TestUserController(t *testing.T) {
	mockUserService := new(mocks.UserService)
	userController := newUserController(mockUserService)
	testApp := web.RunTestApplication(t, userController)

	id, err := idgen.Next()
	assert.NoError(t, err)

	testUser := &entity.User{
		Id:       id,
		Name:     "测试用户",
		Username: "testuser",
		Password: "password123",
		Email:    "test@example.com",
		Age:      25,
		Gender:   1,
	}

	mockUserService.On("AddUser", testUser).Return(nil)
	t.Run("POST 请求应该成功添加用户", func(t *testing.T) {
		testApp.Post("/user").
			WithJSON(testUser).
			Expect().Status(http.StatusOK)
	})

	mockUserService.On("GetUser", id).Return(testUser, nil)
	t.Run("GET 请求应该成功获取用户", func(t *testing.T) {
		testApp.Get("/user/id/{id}").
			WithPath("id", id).
			Expect().Status(http.StatusOK)
	})

	mockUserService.AssertExpectations(t)
}

下一步