Go vs Java 企业级分层架构全面对比
文档概述
本文档系统对比 Go 与 Java(Spring Boot)在分层架构中的设计差异,涵盖参数校验、身份认证、权限控制、缓存策略、日志系统、异常处理、事务管理等核心模块。
一、层级对照表
| 层级 |
Go 命名 |
Java 命名 |
职责 |
| 入口层 |
handler / controller |
Controller |
接收请求、参数校验、调用服务 |
| 业务层 |
service / usecase |
Service |
业务逻辑、事务编排 |
| 数据层 |
repository / store |
Mapper / DAO / Repository |
数据持久化 |
| 实体层 |
entity / model / domain |
Entity / PO / DO |
数据结构定义 |
| 传输层 |
dto / request / response |
DTO / VO / BO |
数据传输对象 |
| 中间件层 |
middleware |
Filter / Interceptor / Aspect |
认证、鉴权、日志、限流 |
| 配置层 |
config |
Configuration / Properties |
配置管理 |
二、企业级目录结构对比
Go 典型结构
project/
├── cmd/
│ └── server/
│ └── main.go # 程序入口
├── internal/
│ ├── handler/ # HTTP 处理器
│ │ ├── user_handler.go
│ │ ├── order_handler.go
│ │ └── middleware/ # 中间件
│ │ ├── auth.go # 认证中间件
│ │ ├── permission.go # 权限中间件
│ │ ├── logger.go # 日志中间件
│ │ ├── ratelimit.go # 限流中间件
│ │ └── recovery.go # 异常恢复
│ ├── service/ # 业务逻辑
│ │ ├── user_service.go
│ │ └── order_service.go
│ ├── repository/ # 数据访问
│ │ ├── user_repository.go
│ │ ├── order_repository.go
│ │ └── cache/ # 缓存实现
│ │ └── user_cache.go
│ ├── entity/ # 实体定义
│ │ ├── user.go
│ │ └── order.go
│ ├── dto/ # 传输对象
│ │ ├── request/
│ │ │ ├── user_request.go
│ │ │ └── order_request.go
│ │ └── response/
│ │ ├── user_response.go
│ │ └── common.go # 统一响应结构
│ └── errors/ # 错误定义
│ └── errors.go
├── pkg/ # 可复用包
│ ├── auth/ # JWT 工具
│ │ └── jwt.go
│ ├── cache/ # 缓存抽象
│ │ └── redis.go
│ ├── logger/ # 日志工具
│ │ └── logger.go
│ ├── validator/ # 校验器
│ │ └── validator.go
│ └── utils/
│ └── crypto.go
├── config/ # 配置
│ ├── config.go
│ └── config.yaml
├── migrations/ # 数据库迁移
└── go.mod
Java 典型结构(Spring Boot)
project/
├── src/main/java/com/example/
│ ├── Application.java # 程序入口
│ ├── controller/ # 控制器
│ │ ├── UserController.java
│ │ └── OrderController.java
│ ├── service/ # 服务接口
│ │ ├── UserService.java
│ │ └── OrderService.java
│ ├── service/impl/ # 服务实现
│ │ ├── UserServiceImpl.java
│ │ └── OrderServiceImpl.java
│ ├── mapper/ # MyBatis Mapper
│ │ ├── UserMapper.java
│ │ └── OrderMapper.java
│ ├── entity/ # 实体类
│ │ ├── User.java
│ │ └── Order.java
│ ├── dto/ # 传输对象
│ │ ├── request/
│ │ │ ├── CreateUserRequest.java
│ │ │ └── UpdateUserRequest.java
│ │ └── response/
│ │ ├── UserResponse.java
│ │ └── CommonResponse.java
│ ├── config/ # 配置类
│ │ ├── SecurityConfig.java # 安全配置
│ │ ├── RedisConfig.java # Redis 配置
│ │ └── WebMvcConfig.java # MVC 配置
│ ├── filter/ # 过滤器
│ │ └── JwtAuthenticationFilter.java
│ ├── interceptor/ # 拦截器
│ │ ├── AuthInterceptor.java
│ │ └── PermissionInterceptor.java
│ ├── aspect/ # 切面
│ │ ├── LogAspect.java
│ │ └── PermissionAspect.java
│ ├── exception/ # 异常处理
│ │ ├── BusinessException.java
│ │ └── GlobalExceptionHandler.java
│ ├── enums/ # 枚举
│ │ └── ErrorCode.java
│ └── util/ # 工具类
│ ├── JwtUtil.java
│ └── RedisUtil.java
├── src/main/resources/
│ ├── mapper/ # MyBatis XML
│ │ └── UserMapper.xml
│ ├── application.yml
│ └── application-dev.yml
└── pom.xml
三、核心架构流程图
3.1 请求处理流程图
graph TB
subgraph "Go 请求处理流程"
A1[🌐 HTTP Request]
A1 --> B1[Recovery Middleware<br/>异常恢复]
B1 --> C1[Logger Middleware<br/>请求日志]
C1 --> D1[CORS Middleware<br/>跨域处理]
D1 --> E1[RateLimit Middleware<br/>限流控制]
E1 --> F1[Auth Middleware<br/>JWT 认证]
F1 --> G1[Permission Middleware<br/>权限校验]
G1 --> H1[Handler<br/>参数绑定与校验]
H1 --> I1[Service<br/>业务逻辑处理]
I1 --> J1{Cache Check<br/>缓存检查}
J1 -->|Hit| K1[返回缓存数据]
J1 -->|Miss| L1[Repository<br/>数据访问]
L1 --> M1[(Database)]
L1 --> N1[更新缓存]
N1 --> O1[Response<br/>统一响应]
K1 --> O1
H1 -.->|DTO| P1[Request DTO]
L1 -.->|Entity| Q1[Entity Model]
O1 -.->|DTO| R1[Response DTO]
end
graph TB
subgraph "Java 请求处理流程"
A2[🌐 HTTP Request]
A2 --> B2[Filter Chain<br/>过滤器链]
B2 --> C2[CorsFilter<br/>跨域处理]
C2 --> D2[RequestLoggingFilter<br/>请求日志]
D2 --> E2[JwtAuthenticationFilter<br/>JWT 认证]
E2 --> F2[RateLimitInterceptor<br/>限流拦截器]
F2 --> G2[Controller<br/>@Valid 参数校验]
G2 --> H2[PermissionAspect<br/>权限切面]
H2 --> I2[LogAspect<br/>日志切面]
I2 --> J2[Service<br/>业务逻辑]
J2 --> K2{@Cacheable<br/>缓存检查}
K2 -->|Hit| L2[返回缓存数据]
K2 -->|Miss| M2[Mapper/Repository<br/>数据访问]
M2 --> N2[(Database)]
M2 --> O2[更新缓存]
O2 --> P2[Response<br/>统一响应]
L2 --> P2
G2 -.->|DTO| Q2[Request DTO]
M2 -.->|Entity| R2[Entity/PO]
P2 -.->|VO| S2[Response VO]
end
3.2 完整请求时序图
Go 请求时序图
sequenceDiagram
autonumber
participant Client as 客户端
participant MW as 中间件链
participant Handler as Handler
participant Validator as Validator
participant Service as Service
participant Cache as Redis Cache
participant Repo as Repository
participant DB as Database
Client->>+MW: HTTP Request
Note over MW: Recovery Middleware
Note over MW: Logger Middleware
Note over MW: RateLimit Check
MW->>MW: 检查限流
alt 超出限流
MW-->>Client: 429 Too Many Requests
end
Note over MW: Auth Middleware
MW->>MW: 解析 JWT Token
alt Token 无效
MW-->>Client: 401 Unauthorized
end
Note over MW: Permission Middleware
MW->>Cache: 获取角色权限
Cache-->>MW: 权限列表
MW->>MW: 校验权限
alt 权限不足
MW-->>Client: 403 Forbidden
end
MW->>+Handler: 转发请求
Handler->>+Validator: 参数校验
Validator-->>-Handler: 校验结果
alt 校验失败
Handler-->>Client: 400 Bad Request
end
Handler->>+Service: 调用业务方法
Service->>+Cache: 查询缓存
alt 缓存命中
Cache-->>Service: 缓存数据
Service-->>Handler: 返回结果
else 缓存未命中
Cache-->>-Service: nil
Service->>+Repo: 查询数据
Repo->>+DB: SQL Query
DB-->>-Repo: 查询结果
Repo-->>-Service: Entity
Service->>Cache: 异步写入缓存
Service-->>Handler: 返回结果
end
Handler->>Handler: 转换为 Response DTO
Handler-->>-MW: Response
Note over MW: Logger 记录响应
MW-->>-Client: HTTP Response
Java 请求时序图
sequenceDiagram
autonumber
participant Client as 客户端
participant Filter as Filter Chain
participant Interceptor as Interceptor
participant Controller as Controller
participant AOP as AOP Aspects
participant Service as Service
participant Cache as Redis/@Cacheable
participant Mapper as Mapper/Repository
participant DB as Database
Client->>+Filter: HTTP Request
Note over Filter: CorsFilter
Note over Filter: RequestLoggingFilter
Filter->>Filter: 记录请求开始
Note over Filter: JwtAuthenticationFilter
Filter->>Filter: 解析 JWT
alt Token 无效
Filter-->>Client: 401 Unauthorized
end
Filter->>Filter: 设置 SecurityContext
Filter->>+Interceptor: 转发请求
Note over Interceptor: RateLimitInterceptor
Interceptor->>Cache: 检查限流
alt 超出限流
Interceptor-->>Client: 429 Too Many Requests
end
Interceptor->>+Controller: 转发请求
Note over Controller: @Valid 参数校验
Controller->>Controller: Bean Validation
alt 校验失败
Controller-->>Client: 400 Bad Request
end
Controller->>+AOP: 调用 Service 方法
Note over AOP: @Before PermissionAspect
AOP->>Cache: 获取用户权限
AOP->>AOP: 校验 @RequirePermission
alt 权限不足
AOP-->>Client: 403 Forbidden
end
Note over AOP: @Around LogAspect
AOP->>AOP: 记录方法调用
AOP->>+Service: 执行业务方法
Note over Service: @Transactional
Service->>Service: 开启事务
Service->>+Cache: @Cacheable 检查缓存
alt 缓存命中
Cache-->>Service: 缓存数据
else 缓存未命中
Cache-->>-Service: null
Service->>+Mapper: 查询数据
Mapper->>+DB: SQL Query
DB-->>-Mapper: ResultSet
Mapper-->>-Service: Entity
Note over Service: @CachePut 更新缓存
Service->>Cache: 写入缓存
end
Service->>Service: 提交事务
Service-->>-AOP: 返回结果
Note over AOP: @After LogAspect
AOP->>AOP: 记录返回值
AOP-->>-Controller: 返回结果
Controller->>Controller: 封装 CommonResponse
Controller-->>-Interceptor: Response
Interceptor-->>-Filter: Response
Note over Filter: 记录响应日志
Filter-->>-Client: HTTP Response
3.3 认证授权流程图
graph TB
subgraph "登录认证流程"
A[用户提交登录请求] --> B{参数校验}
B -->|失败| C[返回参数错误]
B -->|成功| D[查询用户信息]
D --> E{用户存在?}
E -->|否| F[返回用户不存在]
E -->|是| G{密码正确?}
G -->|否| H[返回密码错误]
G -->|是| I{账号状态正常?}
I -->|否| J[返回账号已禁用]
I -->|是| K[生成 JWT Token]
K --> L[生成 Refresh Token]
L --> M[更新最后登录时间]
M --> N[返回 Token 信息]
end
subgraph "Token 刷新流程"
O[提交 Refresh Token] --> P{Token 有效?}
P -->|否| Q[返回 Token 无效]
P -->|是| R{Token 在黑名单?}
R -->|是| S[返回 Token 已失效]
R -->|否| T[生成新 Access Token]
T --> U[返回新 Token]
end
subgraph "登出流程"
V[用户请求登出] --> W[将 Token 加入黑名单]
W --> X[设置黑名单过期时间]
X --> Y[返回登出成功]
end
3.4 权限校验流程图
graph TB
subgraph "RBAC 权限校验"
A[收到请求] --> B[从 Token 获取用户信息]
B --> C[获取用户角色 ID]
C --> D{角色权限在缓存中?}
D -->|是| E[从缓存获取权限列表]
D -->|否| F[从数据库查询权限]
F --> G[写入缓存]
G --> E
E --> H{检查所需权限}
H --> I{是否有通配符权限 *?}
I -->|是| J[权限校验通过]
I -->|否| K{是否有资源通配符?<br/>如 user:*}
K -->|是| L{请求权限匹配前缀?}
L -->|是| J
L -->|否| M{是否精确匹配?}
K -->|否| M
M -->|是| J
M -->|否| N{是否还有其他权限需检查?}
N -->|是| H
N -->|否| O[权限校验失败]
O --> P[返回 403 Forbidden]
J --> Q[继续处理请求]
end
3.5 缓存处理流程图
graph TB
subgraph "缓存读取策略 - Cache Aside"
A[业务请求] --> B{查询缓存}
B -->|命中| C[返回缓存数据]
B -->|未命中| D{布隆过滤器检查<br/>可选}
D -->|不存在| E[返回空/默认值]
D -->|可能存在| F{获取分布式锁}
F -->|获取失败| G[等待后重试]
G --> B
F -->|获取成功| H[再次检查缓存<br/>双重检查]
H -->|命中| I[释放锁]
I --> C
H -->|未命中| J[查询数据库]
J --> K{数据存在?}
K -->|是| L[写入缓存]
K -->|否| M[写入空值缓存<br/>防止穿透]
L --> I
M --> I
end
subgraph "缓存更新策略"
N[数据变更] --> O[更新数据库]
O --> P{更新成功?}
P -->|否| Q[返回错误]
P -->|是| R[删除缓存]
R --> S{删除成功?}
S -->|否| T[记录日志<br/>异步重试]
S -->|是| U[返回成功]
T --> U
end
3.6 异常处理流程图
graph TB
subgraph "Go 异常处理流程"
A1[Handler 接收请求] --> B1{参数绑定}
B1 -->|失败| C1[返回 400 + 校验错误]
B1 -->|成功| D1[调用 Service]
D1 --> E1{Service 返回 error?}
E1 -->|是 AppError| F1[提取错误码和消息]
E1 -->|是 其他 error| G1[包装为 500 错误]
E1 -->|无错误| H1[转换响应 DTO]
F1 --> I1[根据错误码映射 HTTP 状态]
G1 --> I1
I1 --> J1[返回统一响应结构]
H1 --> K1[返回成功响应]
L1[Panic 发生] --> M1[Recovery 中间件捕获]
M1 --> N1[记录堆栈日志]
N1 --> O1[返回 500 错误]
end
subgraph "Java 异常处理流程"
A2[Controller 接收请求] --> B2{@Valid 校验}
B2 -->|失败| C2[抛出 MethodArgumentNotValidException]
B2 -->|成功| D2[调用 Service]
D2 --> E2{抛出异常?}
E2 -->|BusinessException| F2[GlobalExceptionHandler 捕获]
E2 -->|其他 Exception| G2[GlobalExceptionHandler 捕获]
E2 -->|无异常| H2[返回 CommonResponse]
F2 --> I2[提取 ErrorCode]
G2 --> J2[记录异常日志]
J2 --> K2[返回通用 500 错误]
I2 --> L2[映射 HTTP 状态码]
L2 --> M2[返回 CommonResponse]
C2 --> N2[GlobalExceptionHandler 捕获]
N2 --> O2[提取校验错误信息]
O2 --> P2[返回 400 + 错误详情]
end
3.7 事务处理流程图
graph TB
subgraph "Go 事务处理"
A1[Service 方法开始] --> B1[txManager.WithTransaction]
B1 --> C1[开启事务 BeginTx]
C1 --> D1[将 tx 放入 Context]
D1 --> E1[执行业务逻辑]
E1 --> F1[Repository 从 Context 获取 tx]
F1 --> G1[执行 SQL]
G1 --> H1{发生错误?}
H1 -->|是| I1[Rollback]
H1 -->|否| J1{还有其他操作?}
J1 -->|是| E1
J1 -->|否| K1[Commit]
I1 --> L1[返回错误]
K1 --> M1[返回成功]
N1[发生 Panic] --> O1[defer 中 Rollback]
O1 --> P1[重新 Panic]
end
subgraph "Java 事务处理"
A2[@Transactional 方法调用] --> B2[TransactionInterceptor 拦截]
B2 --> C2[获取 TransactionManager]
C2 --> D2[根据传播行为处理]
D2 --> E2{REQUIRED?}
E2 -->|是| F2{已有事务?}
F2 -->|是| G2[加入现有事务]
F2 -->|否| H2[创建新事务]
E2 -->|REQUIRES_NEW| I2[挂起现有事务<br/>创建新事务]
G2 --> J2[执行业务方法]
H2 --> J2
I2 --> J2
J2 --> K2{抛出异常?}
K2 -->|是| L2{匹配 rollbackFor?}
L2 -->|是| M2[Rollback]
L2 -->|否| N2[Commit]
K2 -->|否| N2
M2 --> O2[抛出异常]
N2 --> P2[返回结果]
end
3.8 完整系统架构图
graph TB
subgraph "客户端层"
Web[Web 浏览器]
Mobile[移动 App]
API[第三方 API]
end
subgraph "网关层"
LB[负载均衡<br/>Nginx/Cloud LB]
Gateway[API Gateway<br/>限流/认证/路由]
end
subgraph "应用层"
subgraph "Go 服务"
G_MW[中间件链]
G_Handler[Handler]
G_Service[Service]
G_Repo[Repository]
end
subgraph "Java 服务"
J_Filter[Filter Chain]
J_Controller[Controller]
J_Service[Service]
J_Mapper[Mapper/Repository]
end
end
subgraph "缓存层"
Redis[(Redis Cluster)]
LocalCache[本地缓存<br/>Caffeine/BigCache]
end
subgraph "数据层"
MySQL[(MySQL 主从)]
ES[(Elasticsearch)]
MongoDB[(MongoDB)]
end
subgraph "消息队列"
Kafka[Kafka/RabbitMQ]
end
subgraph "基础设施"
Config[配置中心<br/>Nacos/Consul]
Registry[服务注册<br/>Nacos/Etcd]
Trace[链路追踪<br/>Jaeger/Zipkin]
Monitor[监控告警<br/>Prometheus/Grafana]
Log[日志中心<br/>ELK/Loki]
end
Web --> LB
Mobile --> LB
API --> LB
LB --> Gateway
Gateway --> G_MW
Gateway --> J_Filter
G_MW --> G_Handler --> G_Service --> G_Repo
J_Filter --> J_Controller --> J_Service --> J_Mapper
G_Service --> Redis
G_Service --> LocalCache
J_Service --> Redis
J_Service --> LocalCache
G_Repo --> MySQL
G_Repo --> ES
J_Mapper --> MySQL
J_Mapper --> MongoDB
G_Service --> Kafka
J_Service --> Kafka
G_MW -.-> Trace
J_Filter -.-> Trace
G_Service -.-> Monitor
J_Service -.-> Monitor
G_Handler -.-> Log
J_Controller -.-> Log
3.9 数据流转图
graph LR
subgraph "Go 数据流转"
A1[HTTP Request<br/>JSON] -->|Bind| B1[Request DTO<br/>struct]
B1 -->|Validate| C1[校验后 DTO]
C1 -->|Transform| D1[Entity/Model]
D1 -->|GORM| E1[SQL 执行]
E1 -->|Scan| F1[Entity 结果]
F1 -->|Convert| G1[Response DTO]
G1 -->|Marshal| H1[HTTP Response<br/>JSON]
end
subgraph "Java 数据流转"
A2[HTTP Request<br/>JSON] -->|Jackson| B2[Request DTO]
B2 -->|@Valid| C2[校验后 DTO]
C2 -->|BeanUtils/MapStruct| D2[Entity/PO]
D2 -->|MyBatis/JPA| E2[SQL 执行]
E2 -->|ResultMap/@Entity| F2[Entity 结果]
F2 -->|Converter| G2[Response VO]
G2 -->|Jackson| H2[HTTP Response<br/>JSON]
end
3.10 并发处理模型对比
graph TB
subgraph "Go 并发模型"
A1[HTTP 请求] --> B1[Goroutine 处理]
B1 --> C1{需要并发操作?}
C1 -->|是| D1[启动多个 Goroutine]
D1 --> E1[通过 Channel 通信]
E1 --> F1[WaitGroup 等待完成]
F1 --> G1[合并结果]
C1 -->|否| G1
G1 --> H1[返回响应]
I1[Context 传递] -.-> B1
I1 -.-> D1
J1[超时控制<br/>context.WithTimeout] -.-> I1
end
subgraph "Java 并发模型"
A2[HTTP 请求] --> B2[线程池处理<br/>Tomcat Thread]
B2 --> C2{需要并发操作?}
C2 -->|是| D2[CompletableFuture]
D2 --> E2[异步任务编排]
E2 --> F2[allOf/join 等待]
F2 --> G2[合并结果]
C2 -->|否| G2
G2 --> H2[返回响应]
I2[@Async 异步方法] -.-> D2
J2[线程池配置<br/>ThreadPoolExecutor] -.-> I2
end
四、参数校验
Go 实现
// pkg/validator/validator.go
package validator
import (
"regexp"
"github.com/go-playground/validator/v10"
)
var validate *validator.Validate
func init() {
validate = validator.New()
// 注册自定义校验器
validate.RegisterValidation("mobile", validateMobile)
validate.RegisterValidation("idcard", validateIDCard)
}
func validateMobile(fl validator.FieldLevel) bool {
mobile := fl.Field().String()
matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, mobile)
return matched
}
func validateIDCard(fl validator.FieldLevel) bool {
idcard := fl.Field().String()
matched, _ := regexp.MatchString(`^\d{17}[\dXx]$`, idcard)
return matched
}
func Validate(s interface{}) error {
return validate.Struct(s)
}
// 获取第一个校验错误的友好提示
func GetValidationError(err error) string {
if errs, ok := err.(validator.ValidationErrors); ok {
for _, e := range errs {
return formatFieldError(e)
}
}
return err.Error()
}
func formatFieldError(e validator.FieldError) string {
fieldMessages := map[string]map[string]string{
"Username": {
"required": "用户名不能为空",
"min": "用户名长度不能少于3个字符",
"max": "用户名长度不能超过32个字符",
},
"Mobile": {
"required": "手机号不能为空",
"mobile": "手机号格式不正确",
},
"Email": {
"required": "邮箱不能为空",
"email": "邮箱格式不正确",
},
}
if messages, ok := fieldMessages[e.Field()]; ok {
if msg, ok := messages[e.Tag()]; ok {
return msg
}
}
return e.Error()
}
// internal/dto/request/user_request.go
package request
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=32"`
Password string `json:"password" binding:"required,min=8,max=64"`
Email string `json:"email" binding:"required,email"`
Mobile string `json:"mobile" binding:"required,mobile"`
IDCard string `json:"id_card" binding:"omitempty,idcard"`
Age int `json:"age" binding:"omitempty,gte=0,lte=150"`
RoleID int64 `json:"role_id" binding:"required,gt=0"`
}
type UpdateUserRequest struct {
Username *string `json:"username" binding:"omitempty,min=3,max=32"`
Email *string `json:"email" binding:"omitempty,email"`
Mobile *string `json:"mobile" binding:"omitempty,mobile"`
Status *int `json:"status" binding:"omitempty,oneof=0 1 2"`
}
type ListUserRequest struct {
Page int `form:"page" binding:"omitempty,gte=1"`
PageSize int `form:"page_size" binding:"omitempty,gte=1,lte=100"`
Keyword string `form:"keyword" binding:"omitempty,max=64"`
Status *int `form:"status" binding:"omitempty,oneof=0 1 2"`
OrderBy string `form:"order_by" binding:"omitempty,oneof=created_at updated_at"`
Order string `form:"order" binding:"omitempty,oneof=asc desc"`
}
func (r *ListUserRequest) SetDefaults() {
if r.Page <= 0 {
r.Page = 1
}
if r.PageSize <= 0 {
r.PageSize = 20
}
if r.OrderBy == "" {
r.OrderBy = "created_at"
}
if r.Order == "" {
r.Order = "desc"
}
}
// internal/handler/user_handler.go
func (h *UserHandler) Create(c *gin.Context) {
var req request.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, validator.GetValidationError(err))
return
}
// 业务层额外校验(如唯一性)
user, err := h.userService.Create(c.Request.Context(), &req)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, dto.ToUserResponse(user))
}
Java 实现
// 自定义校验注解
// validator/Mobile.java
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// validator/MobileValidator.java
public class MobileValidator implements ConstraintValidator<Mobile, String> {
private static final Pattern MOBILE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true; // 由 @NotBlank 处理空值
}
return MOBILE_PATTERN.matcher(value).matches();
}
}
// dto/request/CreateUserRequest.java
@Data
public class CreateUserRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 32, message = "用户名长度需在3-32个字符之间")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 8, max = 64, message = "密码长度需在8-64个字符之间")
private String password;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "手机号不能为空")
@Mobile
private String mobile;
@Pattern(regexp = "^\\d{17}[\\dXx]$", message = "身份证号格式不正确")
private String idCard;
@Min(value = 0, message = "年龄不能小于0")
@Max(value = 150, message = "年龄不能大于150")
private Integer age;
@NotNull(message = "角色ID不能为空")
@Positive(message = "角色ID必须为正数")
private Long roleId;
}
// dto/request/UpdateUserRequest.java
@Data
public class UpdateUserRequest {
@Size(min = 3, max = 32, message = "用户名长度需在3-32个字符之间")
private String username;
@Email(message = "邮箱格式不正确")
private String email;
@Mobile
private String mobile;
@ValidStatus // 自定义校验:值必须在 0, 1, 2 中
private Integer status;
}
// dto/request/ListUserRequest.java
@Data
public class ListUserRequest {
@Min(value = 1, message = "页码最小为1")
private Integer page = 1;
@Min(value = 1, message = "每页条数最小为1")
@Max(value = 100, message = "每页条数最大为100")
private Integer pageSize = 20;
@Size(max = 64, message = "关键词最大64个字符")
private String keyword;
private Integer status;
@Pattern(regexp = "^(created_at|updated_at)$", message = "排序字段不合法")
private String orderBy = "created_at";
@Pattern(regexp = "^(asc|desc)$", message = "排序方向不合法")
private String order = "desc";
}
// controller/UserController.java
@RestController
@RequestMapping("/api/v1/users")
@Validated
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public CommonResponse<UserResponse> create(
@Valid @RequestBody CreateUserRequest request) {
User user = userService.create(request);
return CommonResponse.success(UserResponse.from(user));
}
@GetMapping
public CommonResponse<PageResult<UserResponse>> list(
@Valid ListUserRequest request) {
return CommonResponse.success(userService.list(request));
}
}
校验对比表
| 方面 |
Go |
Java |
| 校验库 |
go-playground/validator |
Hibernate Validator (JSR-380) |
| 校验位置 |
Handler 层显式调用 |
@Valid 注解自动触发 |
| 自定义校验 |
RegisterValidation 函数 |
@Constraint 注解 + Validator 类 |
| 错误消息 |
手动映射 |
message 属性声明 |
| 分组校验 |
通过标签组合 |
groups 分组 |
五、身份认证
Go 实现(JWT)
// pkg/auth/jwt.go
package auth
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
var (
ErrTokenExpired = errors.New("token已过期")
ErrTokenInvalid = errors.New("token无效")
ErrTokenMalformed = errors.New("token格式错误")
)
type Claims struct {
UserID int64 `json:"user_id"`
Username string `json:"username"`
RoleID int64 `json:"role_id"`
jwt.RegisteredClaims
}
type JWTManager struct {
secretKey []byte
accessExpiry time.Duration
refreshExpiry time.Duration
issuer string
}
func NewJWTManager(secret string, accessExpiry, refreshExpiry time.Duration) *JWTManager {
return &JWTManager{
secretKey: []byte(secret),
accessExpiry: accessExpiry,
refreshExpiry: refreshExpiry,
issuer: "myapp",
}
}
type TokenPair struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
}
func (m *JWTManager) GenerateTokenPair(userID int64, username string, roleID int64) (*TokenPair, error) {
now := time.Now()
// Access Token
accessClaims := Claims{
UserID: userID,
Username: username,
RoleID: roleID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(m.accessExpiry)),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
Issuer: m.issuer,
Subject: "access",
},
}
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessTokenString, err := accessToken.SignedString(m.secretKey)
if err != nil {
return nil, err
}
// Refresh Token
refreshClaims := Claims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(m.refreshExpiry)),
IssuedAt: jwt.NewNumericDate(now),
Subject: "refresh",
},
}
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
refreshTokenString, err := refreshToken.SignedString(m.secretKey)
if err != nil {
return nil, err
}
return &TokenPair{
AccessToken: accessTokenString,
RefreshToken: refreshTokenString,
ExpiresIn: int64(m.accessExpiry.Seconds()),
}, nil
}
func (m *JWTManager) ParseToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return m.secretKey, nil
})
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, ErrTokenExpired
}
if errors.Is(err, jwt.ErrTokenMalformed) {
return nil, ErrTokenMalformed
}
return nil, ErrTokenInvalid
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, ErrTokenInvalid
}
// internal/handler/middleware/auth.go
package middleware
import (
"strings"
"github.com/gin-gonic/gin"
)
type contextKey string
const (
UserIDKey contextKey = "user_id"
UsernameKey contextKey = "username"
RoleIDKey contextKey = "role_id"
)
func AuthMiddleware(jwtManager *auth.JWTManager, cache cache.TokenCache) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
response.Unauthorized(c, "缺少认证信息")
c.Abort()
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
response.Unauthorized(c, "认证格式错误")
c.Abort()
return
}
tokenString := parts[1]
// 检查 Token 是否在黑名单中(用于登出)
if cache.IsBlacklisted(c.Request.Context(), tokenString) {
response.Unauthorized(c, "Token已失效")
c.Abort()
return
}
claims, err := jwtManager.ParseToken(tokenString)
if err != nil {
switch err {
case auth.ErrTokenExpired:
response.Unauthorized(c, "Token已过期")
default:
response.Unauthorized(c, "Token无效")
}
c.Abort()
return
}
// 存入上下文
c.Set(string(UserIDKey), claims.UserID)
c.Set(string(UsernameKey), claims.Username)
c.Set(string(RoleIDKey), claims.RoleID)
c.Next()
}
}
// 获取当前用户ID
func GetCurrentUserID(c *gin.Context) int64 {
if v, exists := c.Get(string(UserIDKey)); exists {
return v.(int64)
}
return 0
}
// 获取当前用户角色
func GetCurrentRoleID(c *gin.Context) int64 {
if v, exists := c.Get(string(RoleIDKey)); exists {
return v.(int64)
}
return 0
}
Java 实现(Spring Security + JWT)
// util/JwtUtil.java
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access-expiry:7200}")
private long accessExpiry; // 秒
@Value("${jwt.refresh-expiry:604800}")
private long refreshExpiry; // 秒
@Data
@AllArgsConstructor
public static class TokenPair {
private String accessToken;
private String refreshToken;
private Long expiresIn;
}
public TokenPair generateTokenPair(Long userId, String username, Long roleId) {
Date now = new Date();
String accessToken = Jwts.builder()
.setSubject(userId.toString())
.claim("username", username)
.claim("role_id", roleId)
.claim("type", "access")
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + accessExpiry * 1000))
.signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)
.compact();
String refreshToken = Jwts.builder()
.setSubject(userId.toString())
.claim("type", "refresh")
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + refreshExpiry * 1000))
.signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)
.compact();
return new TokenPair(accessToken, refreshToken, accessExpiry);
}
public Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
.build()
.parseClaimsJws(token)
.getBody();
}
public Long getUserIdFromToken(String token) {
return Long.parseLong(parseToken(token).getSubject());
}
public boolean isTokenExpired(String token) {
try {
return parseToken(token).getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
return true;
}
}
}
// filter/JwtAuthenticationFilter.java
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
private final RedisTemplate<String, Object> redisTemplate;
private static final String TOKEN_BLACKLIST_PREFIX = "token:blacklist:";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
try {
// 检查黑名单
if (Boolean.TRUE.equals(redisTemplate.hasKey(TOKEN_BLACKLIST_PREFIX + token))) {
sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "Token已失效");
return;
}
Long userId = jwtUtil.getUserIdFromToken(token);
if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userId.toString());
if (!jwtUtil.isTokenExpired(token)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (ExpiredJwtException e) {
sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "Token已过期");
return;
} catch (JwtException e) {
sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "Token无效");
return;
}
filterChain.doFilter(request, response);
}
private void sendErrorResponse(HttpServletResponse response, HttpStatus status, String message)
throws IOException {
response.setStatus(status.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
String.format("{\"code\":%d,\"message\":\"%s\"}", status.value(), message));
}
}
// config/SecurityConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/api/v1/public/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated())
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
认证对比表
| 方面 |
Go |
Java |
| JWT 库 |
golang-jwt/jwt |
jjwt / spring-security-jwt |
| 认证实现 |
中间件(Middleware) |
Filter + Spring Security |
| Token 存储 |
Context(gin.Context) |
SecurityContext |
| 黑名单 |
Redis 手动实现 |
Redis 手动实现 |
| 密码加密 |
bcrypt 手动调用 |
PasswordEncoder Bean |
六、权限控制
Go 实现(RBAC)
// internal/service/permission_service.go
package service
type PermissionService interface {
GetRolePermissions(ctx context.Context, roleID int64) ([]string, error)
HasPermission(ctx context.Context, roleID int64, permCode string) (bool, error)
CheckPermissions(ctx context.Context, roleID int64, permCodes ...string) (bool, error)
}
type permissionService struct {
roleRepo repository.RoleRepository
permissionRepo repository.PermissionRepository
cache cache.Cache
}
func NewPermissionService(
roleRepo repository.RoleRepository,
permRepo repository.PermissionRepository,
cache cache.Cache,
) PermissionService {
return &permissionService{
roleRepo: roleRepo,
permissionRepo: permRepo,
cache: cache,
}
}
func (s *permissionService) GetRolePermissions(ctx context.Context, roleID int64) ([]string, error) {
// 先查缓存
cacheKey := fmt.Sprintf("role:permissions:%d", roleID)
var permissions []string
if err := s.cache.Get(ctx, cacheKey, &permissions); err == nil {
return permissions, nil
}
// 查数据库
permissions, err := s.permissionRepo.FindCodesByRoleID(ctx, roleID)
if err != nil {
return nil, err
}
// 写入缓存,过期时间 30 分钟
_ = s.cache.Set(ctx, cacheKey, permissions, 30*time.Minute)
return permissions, nil
}
func (s *permissionService) HasPermission(ctx context.Context, roleID int64, permCode string) (bool, error) {
permissions, err := s.GetRolePermissions(ctx, roleID)
if err != nil {
return false, err
}
for _, p := range permissions {
if p == permCode || p == "*" { // * 表示超级权限
return true, nil
}
// 支持通配符匹配:user:* 匹配 user:create, user:read 等
if strings.HasSuffix(p, ":*") {
prefix := strings.TrimSuffix(p, "*")
if strings.HasPrefix(permCode, prefix) {
return true, nil
}
}
}
return false, nil
}
func (s *permissionService) CheckPermissions(ctx context.Context, roleID int64, permCodes ...string) (bool, error) {
for _, code := range permCodes {
has, err := s.HasPermission(ctx, roleID, code)
if err != nil {
return false, err
}
if !has {
return false, nil
}
}
return true, nil
}
// internal/handler/middleware/permission.go
package middleware
import (
"github.com/gin-gonic/gin"
)
// RequirePermission 创建权限校验中间件
func RequirePermission(permService service.PermissionService, permissions ...string) gin.HandlerFunc {
return func(c *gin.Context) {
roleID := GetCurrentRoleID(c)
if roleID == 0 {
response.Forbidden(c, "未获取到角色信息")
c.Abort()
return
}
has, err := permService.CheckPermissions(c.Request.Context(), roleID, permissions...)
if err != nil {
response.InternalError(c, "权限校验失败")
c.Abort()
return
}
if !has {
response.Forbidden(c, "权限不足")
c.Abort()
return
}
c.Next()
}
}
// RequireAnyPermission 满足任一权限即可
func RequireAnyPermission(permService service.PermissionService, permissions ...string) gin.HandlerFunc {
return func(c *gin.Context) {
roleID := GetCurrentRoleID(c)
if roleID == 0 {
response.Forbidden(c, "未获取到角色信息")
c.Abort()
return
}
for _, perm := range permissions {
has, err := permService.HasPermission(c.Request.Context(), roleID, perm)
if err != nil {
continue
}
if has {
c.Next()
return
}
}
response.Forbidden(c, "权限不足")
c.Abort()
}
}
// RequireRole 角色校验
func RequireRole(roles ...string) gin.HandlerFunc {
roleSet := make(map[string]struct{})
for _, r := range roles {
roleSet[r] = struct{}{}
}
return func(c *gin.Context) {
roleCode := GetCurrentRoleCode(c)
if _, ok := roleSet[roleCode]; !ok {
response.Forbidden(c, "角色权限不足")
c.Abort()
return
}
c.Next()
}
}
// 路由注册示例
// cmd/server/router.go
func SetupRouter(
userHandler *handler.UserHandler,
orderHandler *handler.OrderHandler,
permService service.PermissionService,
jwtManager *auth.JWTManager,
tokenCache cache.TokenCache,
) *gin.Engine {
r := gin.New()
// 全局中间件
r.Use(middleware.Recovery())
r.Use(middleware.Logger())
r.Use(middleware.RateLimit(100, time.Minute))
r.Use(middleware.CORS())
// 公开路由
public := r.Group("/api/v1")
{
public.POST("/auth/login", userHandler.Login)
public.POST("/auth/register", userHandler.Register)
public.POST("/auth/refresh", userHandler.RefreshToken)
}
// 需要认证的路由
authorized := r.Group("/api/v1")
authorized.Use(middleware.AuthMiddleware(jwtManager, tokenCache))
{
// 用户模块
users := authorized.Group("/users")
{
users.GET("",
middleware.RequirePermission(permService, "user:list"),
userHandler.List)
users.GET("/:id",
middleware.RequirePermission(permService, "user:read"),
userHandler.GetByID)
users.POST("",
middleware.RequirePermission(permService, "user:create"),
userHandler.Create)
users.PUT("/:id",
middleware.RequirePermission(permService, "user:update"),
userHandler.Update)
users.DELETE("/:id",
middleware.RequirePermission(permService, "user:delete"),
userHandler.Delete)
}
// 订单模块
orders := authorized.Group("/orders")
{
orders.GET("",
middleware.RequireAnyPermission(permService, "order:list", "order:*"),
orderHandler.List)
orders.POST("/:id/cancel",
middleware.RequirePermission(permService, "order:cancel"),
orderHandler.Cancel)
}
// 管理员专用
admin := authorized.Group("/admin")
admin.Use(middleware.RequireRole("admin", "super_admin"))
{
admin.GET("/dashboard", userHandler.Dashboard)
admin.POST("/users/:id/ban", userHandler.BanUser)
}
}
return r
}
Java 实现(Spring Security + 自定义注解)
// annotation/RequirePermission.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String[] value(); // 所需权限码
Logical logical() default Logical.AND; // 权限间逻辑关系
}
// annotation/RequireRole.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
String[] value();
}
// enums/Logical.java
public enum Logical {
AND, // 所有权限都需要
OR // 满足任一即可
}
// service/PermissionService.java
public interface PermissionService {
List<String> getRolePermissions(Long roleId);
boolean hasPermission(Long roleId, String permCode);
boolean checkPermissions(Long roleId, List<String> permCodes, Logical logical);
}
// service/impl/PermissionServiceImpl.java
@Service
@RequiredArgsConstructor
public class PermissionServiceImpl implements PermissionService {
private final RoleMapper roleMapper;
private final PermissionMapper permissionMapper;
private final RedisTemplate<String, Object> redisTemplate;
private static final String ROLE_PERM_KEY = "role:permissions:";
private static final long CACHE_EXPIRE = 30; // 分钟
@Override
@SuppressWarnings("unchecked")
public List<String> getRolePermissions(Long roleId) {
String cacheKey = ROLE_PERM_KEY + roleId;
// 查缓存
Object cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return (List<String>) cached;
}
// 查数据库
List<String> permissions = permissionMapper.selectCodesByRoleId(roleId);
// 写缓存
redisTemplate.opsForValue().set(cacheKey, permissions, CACHE_EXPIRE, TimeUnit.MINUTES);
return permissions;
}
@Override
public boolean hasPermission(Long roleId, String permCode) {
List<String> permissions = getRolePermissions(roleId);
for (String p : permissions) {
if (p.equals(permCode) || p.equals("*")) {
return true;
}
// 通配符支持
if (p.endsWith(":*")) {
String prefix = p.substring(0, p.length() - 1);
if (permCode.startsWith(prefix)) {
return true;
}
}
}
return false;
}
@Override
public boolean checkPermissions(Long roleId, List<String> permCodes, Logical logical) {
if (logical == Logical.AND) {
return permCodes.stream().allMatch(code -> hasPermission(roleId, code));
} else {
return permCodes.stream().anyMatch(code -> hasPermission(roleId, code));
}
}
}
// aspect/PermissionAspect.java
@Aspect
@Component
@RequiredArgsConstructor
@Order(2) // 在认证之后执行
public class PermissionAspect {
private final PermissionService permissionService;
@Before("@annotation(requirePermission)")
public void checkPermission(JoinPoint joinPoint, RequirePermission requirePermission) {
Long roleId = getCurrentRoleId();
if (roleId == null) {
throw new ForbiddenException("未获取到角色信息");
}
List<String> required = Arrays.asList(requirePermission.value());
boolean hasPermission = permissionService.checkPermissions(
roleId, required, requirePermission.logical());
if (!hasPermission) {
throw new ForbiddenException("权限不足");
}
}
@Before("@annotation(requireRole)")
public void checkRole(JoinPoint joinPoint, RequireRole requireRole) {
String currentRole = getCurrentRoleCode();
if (currentRole == null) {
throw new ForbiddenException("未获取到角色信息");
}
Set<String> allowedRoles = Set.of(requireRole.value());
if (!allowedRoles.contains(currentRole)) {
throw new ForbiddenException("角色权限不足");
}
}
private Long getCurrentRoleId() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof CustomUserDetails) {
return ((CustomUserDetails) auth.getPrincipal()).getRoleId();
}
return null;
}
private String getCurrentRoleCode() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof CustomUserDetails) {
return ((CustomUserDetails) auth.getPrincipal()).getRoleCode();
}
return null;
}
}
// controller/UserController.java
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
@RequirePermission("user:list")
public CommonResponse<PageResult<UserResponse>> list(@Valid ListUserRequest request) {
return CommonResponse.success(userService.list(request));
}
@GetMapping("/{id}")
@RequirePermission("user:read")
public CommonResponse<UserResponse> getById(@PathVariable Long id) {
return CommonResponse.success(userService.getById(id));
}
@PostMapping
@RequirePermission("user:create")
public CommonResponse<UserResponse> create(@Valid @RequestBody CreateUserRequest request) {
return CommonResponse.success(userService.create(request));
}
@PutMapping("/{id}")
@RequirePermission("user:update")
public CommonResponse<Void> update(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
userService.update(id, request);
return CommonResponse.success();
}
@DeleteMapping("/{id}")
@RequirePermission("user:delete")
public CommonResponse<Void> delete(@PathVariable Long id) {
userService.delete(id);
return CommonResponse.success();
}
// 需要多个权限(AND 逻辑)
@PostMapping("/{id}/reset-password")
@RequirePermission(value = {"user:update", "user:sensitive"}, logical = Logical.AND)
public CommonResponse<Void> resetPassword(@PathVariable Long id) {
userService.resetPassword(id);
return CommonResponse.success();
}
// 满足任一权限即可(OR 逻辑)
@GetMapping("/{id}/detail")
@RequirePermission(value = {"user:read", "user:admin"}, logical = Logical.OR)
public CommonResponse<UserDetailResponse> getDetail(@PathVariable Long id) {
return CommonResponse.success(userService.getDetail(id));
}
}
// controller/AdminController.java
@RestController
@RequestMapping("/api/v1/admin")
@RequireRole({"admin", "super_admin"}) // 类级别角色限制
public class AdminController {
@GetMapping("/dashboard")
public CommonResponse<DashboardVO> dashboard() {
// ...
}
@PostMapping("/users/{id}/ban")
@RequirePermission("user:ban") // 方法级别额外权限
public CommonResponse<Void> banUser(@PathVariable Long id) {
// ...
}
}
权限对比表
| 方面 |
Go |
Java |
| 权限检查 |
中间件函数 |
AOP 切面 + 注解 |
| 权限声明 |
路由注册时指定 |
方法/类注解 |
| 权限缓存 |
手动实现 |
手动实现 |
| 通配符 |
手动解析 |
手动解析 |
| 逻辑组合 |
不同中间件函数 |
注解属性 |
七、缓存策略
Go 实现
// pkg/cache/cache.go
package cache
import (
"context"
"encoding/json"
"time"
"github.com/redis/go-redis/v9"
)
type Cache interface {
Get(ctx context.Context, key string, dest interface{}) error
Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error
Delete(ctx context.Context, keys ...string) error
Exists(ctx context.Context, key string) (bool, error)
SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) (bool, error)
Incr(ctx context.Context, key string) (int64, error)
Expire(ctx context.Context, key string, expiration time.Duration) error
}
type redisCache struct {
client *redis.Client
}
func NewRedisCache(client *redis.Client) Cache {
return &redisCache{client: client}
}
func (c *redisCache) Get(ctx context.Context, key string, dest interface{}) error {
data, err := c.client.Get(ctx, key).Bytes()
if err != nil {
if err == redis.Nil {
return ErrCacheMiss
}
return err
}
return json.Unmarshal(data, dest)
}
func (c *redisCache) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
return c.client.Set(ctx, key, data, expiration).Err()
}
func (c *redisCache) Delete(ctx context.Context, keys ...string) error {
return c.client.Del(ctx, keys...).Err()
}
var ErrCacheMiss = errors.New("cache miss")
// internal/repository/cache/user_cache.go
package cache
import (
"context"
"fmt"
"time"
)
type UserCache interface {
GetUser(ctx context.Context, id int64) (*entity.User, error)
SetUser(ctx context.Context, user *entity.User) error
DeleteUser(ctx context.Context, id int64) error
GetUserByEmail(ctx context.Context, email string) (*entity.User, error)
SetUserByEmail(ctx context.Context, user *entity.User) error
}
type userCache struct {
cache cache.Cache
expiration time.Duration
}
func NewUserCache(c cache.Cache) UserCache {
return &userCache{
cache: c,
expiration: 30 * time.Minute,
}
}
func (c *userCache) userKey(id int64) string {
return fmt.Sprintf("user:id:%d", id)
}
func (c *userCache) userEmailKey(email string) string {
return fmt.Sprintf("user:email:%s", email)
}
func (c *userCache) GetUser(ctx context.Context, id int64) (*entity.User, error) {
var user entity.User
err := c.cache.Get(ctx, c.userKey(id), &user)
if err != nil {
return nil, err
}
return &user, nil
}
func (c *userCache) SetUser(ctx context.Context, user *entity.User) error {
return c.cache.Set(ctx, c.userKey(user.ID), user, c.expiration)
}
func (c *userCache) DeleteUser(ctx context.Context, id int64) error {
return c.cache.Delete(ctx, c.userKey(id))
}
// internal/service/user_service.go - 带缓存的服务层
package service
type userService struct {
userRepo repository.UserRepository
userCache cache.UserCache
logger *zap.Logger
}
func (s *userService) GetByID(ctx context.Context, id int64) (*entity.User, error) {
// 1. 查缓存
user, err := s.userCache.GetUser(ctx, id)
if err == nil {
s.logger.Debug("cache hit", zap.Int64("user_id", id))
return user, nil
}
if err != cache.ErrCacheMiss {
s.logger.Warn("cache error", zap.Error(err))
}
// 2. 查数据库
user, err = s.userRepo.FindByID(ctx, id)
if err != nil {
return nil, err
}
if user == nil {
return nil, ErrUserNotFound
}
// 3. 写入缓存(异步)
go func() {
if err := s.userCache.SetUser(context.Background(), user); err != nil {
s.logger.Warn("failed to set cache", zap.Error(err))
}
}()
return user, nil
}
func (s *userService) Update(ctx context.Context, id int64, req *dto.UpdateUserRequest) error {
// 1. 更新数据库
if err := s.userRepo.Update(ctx, id, req); err != nil {
return err
}
// 2. 删除缓存(保证一致性)
if err := s.userCache.DeleteUser(ctx, id); err != nil {
s.logger.Warn("failed to delete cache", zap.Error(err))
}
return nil
}
func (s *userService) Delete(ctx context.Context, id int64) error {
// 1. 删除数据库
if err := s.userRepo.Delete(ctx, id); err != nil {
return err
}
// 2. 删除缓存
if err := s.userCache.DeleteUser(ctx, id); err != nil {
s.logger.Warn("failed to delete cache", zap.Error(err))
}
return nil
}
// 防缓存穿透 - 布隆过滤器 + 空值缓存
// internal/service/user_service.go
func (s *userService) GetByIDWithProtection(ctx context.Context, id int64) (*entity.User, error) {
// 1. 布隆过滤器检查(可选)
if s.bloomFilter != nil && !s.bloomFilter.Test([]byte(fmt.Sprintf("user:%d", id))) {
return nil, ErrUserNotFound
}
// 2. 查缓存
user, err := s.userCache.GetUser(ctx, id)
if err == nil {
// 检查是否为空值缓存
if user.ID == 0 {
return nil, ErrUserNotFound
}
return user, nil
}
// 3. 分布式锁防止缓存击穿
lockKey := fmt.Sprintf("lock:user:%d", id)
locked, err := s.cache.SetNX(ctx, lockKey, "1", 5*time.Second)
if err != nil {
return nil, err
}
if !locked {
// 未获取到锁,等待后重试
time.Sleep(100 * time.Millisecond)
return s.GetByIDWithProtection(ctx, id)
}
defer s.cache.Delete(ctx, lockKey)
// 4. 再次检查缓存(双重检查)
user, err = s.userCache.GetUser(ctx, id)
if err == nil {
return user, nil
}
// 5. 查数据库
user, err = s.userRepo.FindByID(ctx, id)
if err != nil {
return nil, err
}
if user == nil {
// 缓存空值,防止缓存穿透
_ = s.userCache.SetUser(ctx, &entity.User{})
return nil, ErrUserNotFound
}
// 6. 写入缓存
_ = s.userCache.SetUser(ctx, user)
return user, nil
}
Java 实现(Spring Cache + Redis)
// config/RedisConfig.java
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key 序列化
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// Value 序列化
Jackson2JsonRedisSerializer<Object> jsonSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
jsonSerializer.setObjectMapper(om);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
// 不同缓存空间配置不同过期时间
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put("user", config.entryTtl(Duration.ofMinutes(30)));
configMap.put("order", config.entryTtl(Duration.ofMinutes(10)));
configMap.put("permission", config.entryTtl(Duration.ofHours(1)));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(configMap)
.build();
}
}
// service/impl/UserServiceImpl.java
@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final RedisTemplate<String, Object> redisTemplate;
private static final String USER_CACHE_KEY = "user:id:";
private static final long CACHE_EXPIRE = 30; // 分钟
@Override
@Cacheable(value = "user", key = "#id", unless = "#result == null")
public User getById(Long id) {
log.debug("Cache miss, querying database for user: {}", id);
return userMapper.selectById(id);
}
@Override
@CachePut(value = "user", key = "#result.id")
@Transactional
public User create(CreateUserRequest request) {
User user = new User();
BeanUtils.copyProperties(request, user);
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setCreatedAt(LocalDateTime.now());
userMapper.insert(user);
return user;
}
@Override
@CacheEvict(value = "user", key = "#id")
@Transactional
public void update(Long id, UpdateUserRequest request) {
User user = userMapper.selectById(id);
if (user == null) {
throw new NotFoundException("用户不存在");
}
if (request.getUsername() != null) {
user.setUsername(request.getUsername());
}
if (request.getEmail() != null) {
user.setEmail(request.getEmail());
}
user.setUpdatedAt(LocalDateTime.now());
userMapper.updateById(user);
}
@Override
@CacheEvict(value = "user", key = "#id")
@Transactional
public void delete(Long id) {
userMapper.deleteById(id);
}
// 批量删除缓存
@Override
@CacheEvict(value = "user", allEntries = true)
@Transactional
public void batchDelete(List<Long> ids) {
userMapper.deleteBatchIds(ids);
}
// 手动缓存控制 - 带穿透保护
@Override
public User getByIdWithProtection(Long id) {
String cacheKey = USER_CACHE_KEY + id;
// 1. 查缓存
Object cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
if (cached instanceof NullValue) {
return null; // 空值缓存
}
return (User) cached;
}
// 2. 分布式锁
String lockKey = "lock:user:" + id;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(5));
if (Boolean.FALSE.equals(locked)) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getByIdWithProtection(id);
}
try {
// 3. 双重检查
cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached instanceof NullValue ? null : (User) cached;
}
// 4. 查数据库
User user = userMapper.selectById(id);
// 5. 写缓存
if (user != null) {
redisTemplate.opsForValue().set(cacheKey, user,
CACHE_EXPIRE, TimeUnit.MINUTES);
} else {
// 缓存空值,防止穿透
redisTemplate.opsForValue().set(cacheKey, NullValue.INSTANCE,
5, TimeUnit.MINUTES);
}
return user;
} finally {
redisTemplate.delete(lockKey);
}
}
// 空值标记
private static class NullValue implements Serializable {
static final NullValue INSTANCE = new NullValue();
private static final long serialVersionUID = 1L;
}
}
// 自定义缓存注解 - 支持更复杂的缓存策略
// annotation/CacheableWithLock.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheableWithLock {
String cacheName();
String key();
long expire() default 30; // 分钟
long lockTimeout() default 5; // 秒
boolean cacheNull() default true;
}
// aspect/CacheAspect.java
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class CacheAspect {
private final RedisTemplate<String, Object> redisTemplate;
private final SpelExpressionParser parser = new SpelExpressionParser();
@Around("@annotation(cacheableWithLock)")
public Object around(ProceedingJoinPoint pjp, CacheableWithLock cacheableWithLock)
throws Throwable {
String cacheKey = buildCacheKey(pjp, cacheableWithLock);
// 1. 查缓存
Object cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
if (cached instanceof NullValue) {
return null;
}
log.debug("Cache hit: {}", cacheKey);
return cached;
}
// 2. 加锁
String lockKey = "lock:" + cacheKey;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1",
Duration.ofSeconds(cacheableWithLock.lockTimeout()));
if (Boolean.FALSE.equals(locked)) {
Thread.sleep(100);
return around(pjp, cacheableWithLock); // 重试
}
try {
// 3. 双重检查
cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached instanceof NullValue ? null : cached;
}
// 4. 执行方法
Object result = pjp.proceed();
// 5. 写缓存
if (result != null) {
redisTemplate.opsForValue().set(cacheKey, result,
Duration.ofMinutes(cacheableWithLock.expire()));
} else if (cacheableWithLock.cacheNull()) {
redisTemplate.opsForValue().set(cacheKey, NullValue.INSTANCE,
Duration.ofMinutes(5));
}
return result;
} finally {
redisTemplate.delete(lockKey);
}
}
private String buildCacheKey(ProceedingJoinPoint pjp, CacheableWithLock annotation) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
EvaluationContext context = new StandardEvaluationContext();
String[] paramNames = signature.getParameterNames();
Object[] args = pjp.getArgs();
for (int i = 0; i < paramNames.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
String keyExpression = annotation.key();
String key = parser.parseExpression(keyExpression)
.getValue(context, String.class);
return annotation.cacheName() + ":" + key;
}
private static class NullValue implements Serializable {
static final NullValue INSTANCE = new NullValue();
}
}
缓存对比表
| 方面 |
Go |
Java |
| 缓存框架 |
go-redis + 手动封装 |
Spring Cache + Redis |
| 缓存注解 |
无(手动实现) |
@Cacheable/@CachePut/@CacheEvict |
| 序列化 |
json.Marshal |
Jackson/GenericJackson2Json |
| 穿透保护 |
手动实现空值缓存 + 分布式锁 |
手动实现或自定义切面 |
| 缓存预热 |
启动时手动加载 |
@PostConstruct / ApplicationRunner |
| 多级缓存 |
手动实现本地 + Redis |
Caffeine + Redis 组合 |
八、日志系统
Go 实现(Zap)
// pkg/logger/logger.go
package logger
import (
"os"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
var Logger *zap.Logger
type Config struct {
Level string
Filename string
MaxSize int // MB
MaxBackups int
MaxAge int // days
Compress bool
Console bool
}
func Init(cfg *Config) error {
level := parseLevel(cfg.Level)
// 编码器配置
encoderConfig := zapcore.EncoderConfig{
TimeKey: "timestamp",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,
MessageKey: "message",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
var cores []zapcore.Core
// 文件输出
if cfg.Filename != "" {
fileWriter := &lumberjack.Logger{
Filename: cfg.Filename,
MaxSize: cfg.MaxSize,
MaxBackups: cfg.MaxBackups,
MaxAge: cfg.MaxAge,
Compress: cfg.Compress,
}
fileCore := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
zapcore.AddSync(fileWriter),
level,
)
cores = append(cores, fileCore)
}
// 控制台输出
if cfg.Console {
consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig)
consoleCore := zapcore.NewCore(
consoleEncoder,
zapcore.AddSync(os.Stdout),
level,
)
cores = append(cores, consoleCore)
}
core := zapcore.NewTee(cores...)
Logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
return nil
}
func parseLevel(level string) zapcore.Level {
switch level {
case "debug":
return zapcore.DebugLevel
case "info":
return zapcore.InfoLevel
case "warn":
return zapcore.WarnLevel
case "error":
return zapcore.ErrorLevel
default:
return zapcore.InfoLevel
}
}
// 便捷方法
func Debug(msg string, fields ...zap.Field) { Logger.Debug(msg, fields...) }
func Info(msg string, fields ...zap.Field) { Logger.Info(msg, fields...) }
func Warn(msg string, fields ...zap.Field) { Logger.Warn(msg, fields...) }
func Error(msg string, fields ...zap.Field) { Logger.Error(msg, fields...) }
func With(fields ...zap.Field) *zap.Logger {
return Logger.With(fields...)
}
// internal/handler/middleware/logger.go
package middleware
import (
"bytes"
"io"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type responseWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (w *responseWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
func Logger(log *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
// 读取请求体
var requestBody []byte
if c.Request.Body != nil {
requestBody, _ = io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
}
// 包装 ResponseWriter 以捕获响应
blw := &responseWriter{
ResponseWriter: c.Writer,
body: bytes.NewBuffer(nil),
}
c.Writer = blw
// 生成请求 ID
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = generateRequestID()
}
c.Set("request_id", requestID)
c.Header("X-Request-ID", requestID)
// 执行请求
c.Next()
// 计算耗时
latency := time.Since(start)
// 构建日志字段
fields := []zap.Field{
zap.String("request_id", requestID),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.Int("status", c.Writer.Status()),
zap.Duration("latency", latency),
zap.String("client_ip", c.ClientIP()),
zap.String("user_agent", c.Request.UserAgent()),
}
// 添加用户信息(如果已认证)
if userID, exists := c.Get("user_id"); exists {
fields = append(fields, zap.Any("user_id", userID))
}
// 记录请求体(敏感信息脱敏)
if len(requestBody) > 0 && len(requestBody) < 10240 {
sanitized := sanitizeBody(requestBody)
fields = append(fields, zap.String("request_body", sanitized))
}
// 错误请求记录响应体
if c.Writer.Status() >= 400 {
fields = append(fields, zap.String("response_body", blw.body.String()))
}
// 根据状态码选择日志级别
switch {
case c.Writer.Status() >= 500:
log.Error("Server error", fields...)
case c.Writer.Status() >= 400:
log.Warn("Client error", fields...)
default:
log.Info("Request completed", fields...)
}
}
}
// 敏感字段脱敏
func sanitizeBody(body []byte) string {
var data map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil {
return "[non-json body]"
}
sensitiveFields := []string{"password", "token", "secret", "card_number"}
for _, field := range sensitiveFields {
if _, ok := data[field]; ok {
data[field] = "***"
}
}
sanitized, _ := json.Marshal(data)
return string(sanitized)
}
func generateRequestID() string {
return fmt.Sprintf("%d-%s", time.Now().UnixNano(), randomString(8))
}
// 操作审计日志
// internal/service/audit_service.go
package service
type AuditLog struct {
ID int64
UserID int64
Username string
Action string // create, update, delete, login, logout
Resource string // user, order, product
ResourceID string
OldValue string // JSON
NewValue string // JSON
IP string
UserAgent string
RequestID string
CreatedAt time.Time
}
type AuditService interface {
Log(ctx context.Context, log *AuditLog) error
Query(ctx context.Context, query *AuditQuery) ([]*AuditLog, int64, error)
}
type auditService struct {
repo repository.AuditRepository
logger *zap.Logger
}
func (s *auditService) Log(ctx context.Context, log *AuditLog) error {
log.CreatedAt = time.Now()
// 异步写入数据库
go func() {
if err := s.repo.Create(context.Background(), log); err != nil {
s.logger.Error("failed to save audit log",
zap.Error(err),
zap.Any("audit", log))
}
}()
// 同步写入日志文件(确保不丢失)
s.logger.Info("audit",
zap.Int64("user_id", log.UserID),
zap.String("action", log.Action),
zap.String("resource", log.Resource),
zap.String("resource_id", log.ResourceID),
zap.String("request_id", log.RequestID))
return nil
}
Java 实现(Logback + AOP)
// resources/logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- JSON 格式文件输出(用于 ELK) -->
<appender name="FILE_JSON" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/${APP_NAME}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/${APP_NAME}.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>request_id</includeMdcKeyName>
<includeMdcKeyName>user_id</includeMdcKeyName>
</encoder>
</appender>
<!-- 审计日志单独文件 -->
<appender name="AUDIT_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/audit.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/audit.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>90</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
<!-- 审计日志 Logger -->
<logger name="AUDIT" level="INFO" additivity="false">
<appender-ref ref="AUDIT_FILE"/>
</logger>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE_JSON"/>
</root>
</configuration>
// filter/RequestLoggingFilter.java
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class RequestLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
// 生成 Request ID
String requestId = request.getHeader("X-Request-ID");
if (requestId == null || requestId.isEmpty()) {
requestId = UUID.randomUUID().toString();
}
// 放入 MDC(日志上下文)
MDC.put("request_id", requestId);
response.setHeader("X-Request-ID", requestId);
// 包装请求以支持多次读取
ContentCachingRequestWrapper wrappedRequest =
new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse =
new ContentCachingResponseWrapper(response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
} finally {
long duration = System.currentTimeMillis() - startTime;
logRequest(wrappedRequest, wrappedResponse, duration);
wrappedResponse.copyBodyToResponse();
MDC.clear();
}
}
private void logRequest(ContentCachingRequestWrapper request,
ContentCachingResponseWrapper response,
long duration) {
int status = response.getStatus();
String method = request.getMethod();
String uri = request.getRequestURI();
String query = request.getQueryString();
String clientIp = getClientIp(request);
// 构建日志消息
Map<String, Object> logData = new LinkedHashMap<>();
logData.put("method", method);
logData.put("uri", uri);
logData.put("query", query);
logData.put("status", status);
logData.put("duration_ms", duration);
logData.put("client_ip", clientIp);
logData.put("user_agent", request.getHeader("User-Agent"));
// 请求体(敏感信息脱敏)
String requestBody = getRequestBody(request);
if (requestBody != null && !requestBody.isEmpty()) {
logData.put("request_body", sanitize(requestBody));
}
// 错误响应记录响应体
if (status >= 400) {
String responseBody = getResponseBody(response);
logData.put("response_body", responseBody);
}
if (status >= 500) {
log.error("Request completed: {}", logData);
} else if (status >= 400) {
log.warn("Request completed: {}", logData);
} else {
log.info("Request completed: {}", logData);
}
}
private String sanitize(String body) {
try {
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> data = mapper.readValue(body, Map.class);
List<String> sensitiveFields = Arrays.asList(
"password", "token", "secret", "card_number", "cvv");
for (String field : sensitiveFields) {
if (data.containsKey(field)) {
data.put(field, "***");
}
}
return mapper.writeValueAsString(data);
} catch (Exception e) {
return body;
}
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip.split(",")[0].trim();
}
}
// aspect/AuditLogAspect.java
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class AuditLogAspect {
private final AuditLogService auditLogService;
private final ObjectMapper objectMapper;
private static final Logger AUDIT_LOGGER = LoggerFactory.getLogger("AUDIT");
@Around("@annotation(auditLog)")
public Object around(ProceedingJoinPoint pjp, AuditLog auditLog) throws Throwable {
// 获取旧值(更新/删除操作)
String oldValue = null;
if (auditLog.action() == AuditAction.UPDATE ||
auditLog.action() == AuditAction.DELETE) {
oldValue = getOldValue(pjp, auditLog);
}
// 执行方法
Object result = pjp.proceed();
// 记录审计日志
try {
AuditLogEntity entity = buildAuditLog(pjp, auditLog, oldValue, result);
// 异步保存到数据库
auditLogService.saveAsync(entity);
// 同步写入日志文件
AUDIT_LOGGER.info(objectMapper.writeValueAsString(entity));
} catch (Exception e) {
log.error("Failed to save audit log", e);
}
return result;
}
private AuditLogEntity buildAuditLog(ProceedingJoinPoint pjp,
AuditLog annotation,
String oldValue,
Object result) {
HttpServletRequest request = getCurrentRequest();
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
AuditLogEntity entity = new AuditLogEntity();
entity.setAction(annotation.action().name());
entity.setResource(annotation.resource());
entity.setResourceId(extractResourceId(pjp, annotation));
entity.setOldValue(oldValue);
entity.setNewValue(result != null ? toJson(result) : null);
entity.setRequestId(MDC.get("request_id"));
entity.setCreatedAt(LocalDateTime.now());
if (auth != null && auth.getPrincipal() instanceof CustomUserDetails) {
CustomUserDetails user = (CustomUserDetails) auth.getPrincipal();
entity.setUserId(user.getId());
entity.setUsername(user.getUsername());
}
if (request != null) {
entity.setIp(getClientIp(request));
entity.setUserAgent(request.getHeader("User-Agent"));
}
return entity;
}
}
// annotation/AuditLog.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
AuditAction action();
String resource();
String resourceIdExpr() default ""; // SpEL 表达式
}
// enums/AuditAction.java
public enum AuditAction {
CREATE, UPDATE, DELETE, LOGIN, LOGOUT, EXPORT, IMPORT
}
// 使用示例
@Service
public class UserServiceImpl implements UserService {
@Override
@AuditLog(action = AuditAction.CREATE, resource = "user")
@Transactional
public User create(CreateUserRequest request) {
// ...
}
@Override
@AuditLog(action = AuditAction.UPDATE, resource = "user", resourceIdExpr = "#id")
@Transactional
public void update(Long id, UpdateUserRequest request) {
// ...
}
@Override
@AuditLog(action = AuditAction.DELETE, resource = "user", resourceIdExpr = "#id")
@Transactional
public void delete(Long id) {
// ...
}
}
日志对比表
| 方面 |
Go |
Java |
| 日志库 |
Zap / Zerolog |
Logback / Log4j2 |
| 结构化日志 |
原生支持 |
Logstash Encoder |
| 日志上下文 |
Context 传递 |
MDC(Mapped Diagnostic Context) |
| 请求日志 |
中间件实现 |
Filter / Interceptor |
| 审计日志 |
手动调用服务 |
AOP + 注解 |
| 日志轮转 |
lumberjack |
Logback RollingPolicy |
| 敏感信息 |
手动脱敏 |
手动脱敏 |
九、异常处理与统一响应
Go 实现
// internal/errors/errors.go
package errors
import (
"fmt"
"net/http"
)
type ErrorCode int
const (
CodeSuccess ErrorCode = 0
CodeBadRequest ErrorCode = 400
CodeUnauthorized ErrorCode = 401
CodeForbidden ErrorCode = 403
CodeNotFound ErrorCode = 404
CodeConflict ErrorCode = 409
CodeTooManyRequests ErrorCode = 429
CodeInternalError ErrorCode = 500
CodeServiceUnavailable ErrorCode = 503
// 业务错误码 (10000+)
CodeUserNotFound ErrorCode = 10001
CodeUserExists ErrorCode = 10002
CodePasswordWrong ErrorCode = 10003
CodeTokenExpired ErrorCode = 10004
CodeTokenInvalid ErrorCode = 10005
CodePermissionDenied ErrorCode = 10006
CodeOrderNotFound ErrorCode = 20001
CodeOrderCancelled ErrorCode = 20002
CodeInsufficientStock ErrorCode = 20003
)
var codeMessages = map[ErrorCode]string{
CodeSuccess: "成功",
CodeBadRequest: "请求参数错误",
CodeUnauthorized: "未授权",
CodeForbidden: "禁止访问",
CodeNotFound: "资源不存在",
CodeConflict: "资源冲突",
CodeTooManyRequests: "请求过于频繁",
CodeInternalError: "服务器内部错误",
CodeServiceUnavailable: "服务暂不可用",
CodeUserNotFound: "用户不存在",
CodeUserExists: "用户已存在",
CodePasswordWrong: "密码错误",
CodeTokenExpired: "Token已过期",
CodeTokenInvalid: "Token无效",
CodePermissionDenied: "权限不足",
CodeOrderNotFound: "订单不存在",
CodeOrderCancelled: "订单已取消",
CodeInsufficientStock: "库存不足",
}
type AppError struct {
Code ErrorCode
Message string
Err error // 原始错误(不对外暴露)
Data interface{}
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Err
}
func New(code ErrorCode) *AppError {
return &AppError{
Code: code,
Message: codeMessages[code],
}
}
func NewWithMessage(code ErrorCode, message string) *AppError {
return &AppError{
Code: code,
Message: message,
}
}
func Wrap(code ErrorCode, err error) *AppError {
return &AppError{
Code: code,
Message: codeMessages[code],
Err: err,
}
}
func WrapWithMessage(code ErrorCode, message string, err error) *AppError {
return &AppError{
Code: code,
Message: message,
Err: err,
}
}
// 判断错误类型
func IsAppError(err error) bool {
_, ok := err.(*AppError)
return ok
}
func GetCode(err error) ErrorCode {
if appErr, ok := err.(*AppError); ok {
return appErr.Code
}
return CodeInternalError
}
func GetHTTPStatus(code ErrorCode) int {
switch {
case code == CodeSuccess:
return http.StatusOK
case code == CodeBadRequest:
return http.StatusBadRequest
case code == CodeUnauthorized, code == CodeTokenExpired, code == CodeTokenInvalid:
return http.StatusUnauthorized
case code == CodeForbidden, code == CodePermissionDenied:
return http.StatusForbidden
case code == CodeNotFound, code == CodeUserNotFound, code == CodeOrderNotFound:
return http.StatusNotFound
case code == CodeConflict, code == CodeUserExists:
return http.StatusConflict
case code == CodeTooManyRequests:
return http.StatusTooManyRequests
case code >= 10000:
return http.StatusBadRequest // 业务错误返回 400
default:
return http.StatusInternalServerError
}
}
// 预定义错误
var (
ErrUserNotFound = New(CodeUserNotFound)
ErrUserExists = New(CodeUserExists)
ErrPasswordWrong = New(CodePasswordWrong)
ErrTokenExpired = New(CodeTokenExpired)
ErrTokenInvalid = New(CodeTokenInvalid)
ErrPermissionDenied = New(CodePermissionDenied)
ErrOrderNotFound = New(CodeOrderNotFound)
ErrInsufficientStock = New(CodeInsufficientStock)
)
// internal/dto/response/response.go
package response
type PageData struct {
List interface{} `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
Pages int `json:"pages"`
}
func NewPageData(list interface{}, total int64, page, pageSize int) *PageData {
pages := int(total) / pageSize
if int(total)%pageSize > 0 {
pages++
}
return &PageData{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
Pages: pages,
}
}
func Success(c *gin.Context, data interface{}) {
requestID, _ := c.Get("request_id")
c.JSON(200, Response{
Code: 0,
Message: "success",
Data: data,
RequestID: requestID.(string),
})
}
func SuccessWithMessage(c *gin.Context, message string, data interface{}) {
requestID, _ := c.Get("request_id")
c.JSON(200, Response{
Code: 0,
Message: message,
Data: data,
RequestID: requestID.(string),
})
}
func Error(c *gin.Context, err error) {
requestID, _ := c.Get("request_id")
var appErr *errors.AppError
if e, ok := err.(*errors.AppError); ok {
appErr = e
} else {
appErr = errors.Wrap(errors.CodeInternalError, err)
}
httpStatus := errors.GetHTTPStatus(appErr.Code)
c.JSON(httpStatus, Response{
Code: int(appErr.Code),
Message: appErr.Message,
RequestID: requestID.(string),
})
}
func BadRequest(c *gin.Context, message string) {
requestID, _ := c.Get("request_id")
c.JSON(400, Response{
Code: int(errors.CodeBadRequest),
Message: message,
RequestID: requestID.(string),
})
}
func Unauthorized(c *gin.Context, message string) {
requestID, _ := c.Get("request_id")
c.JSON(401, Response{
Code: int(errors.CodeUnauthorized),
Message: message,
RequestID: requestID.(string),
})
}
func Forbidden(c *gin.Context, message string) {
requestID, _ := c.Get("request_id")
c.JSON(403, Response{
Code: int(errors.CodeForbidden),
Message: message,
RequestID: requestID.(string),
})
}
func NotFound(c *gin.Context, message string) {
requestID, _ := c.Get("request_id")
c.JSON(404, Response{
Code: int(errors.CodeNotFound),
Message: message,
RequestID: requestID.(string),
})
}
func InternalError(c *gin.Context, message string) {
requestID, _ := c.Get("request_id")
c.JSON(500, Response{
Code: int(errors.CodeInternalError),
Message: message,
RequestID: requestID.(string),
})
}
// internal/handler/middleware/recovery.go
package middleware
import (
"fmt"
"runtime/debug"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
func Recovery(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
// 记录堆栈
stack := string(debug.Stack())
requestID, _ := c.Get("request_id")
logger.Error("Panic recovered",
zap.Any("error", r),
zap.String("stack", stack),
zap.String("request_id", requestID.(string)),
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
)
response.InternalError(c, "服务器内部错误")
c.Abort()
}
}()
c.Next()
}
}
// 使用示例 - internal/service/user_service.go
func (s *userService) GetByID(ctx context.Context, id int64) (*entity.User, error) {
user, err := s.userRepo.FindByID(ctx, id)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
if user == nil {
return nil, errors.ErrUserNotFound
}
return user, nil
}
func (s *userService) Create(ctx context.Context, req *dto.CreateUserRequest) (*entity.User, error) {
// 检查用户是否存在
existing, err := s.userRepo.FindByEmail(ctx, req.Email)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
if existing != nil {
return nil, errors.ErrUserExists
}
// 业务规则校验
if req.Age < 18 {
return nil, errors.NewWithMessage(errors.CodeBadRequest, "用户年龄不能小于18岁")
}
user := &entity.User{
Username: req.Username,
Email: req.Email,
Password: hashPassword(req.Password),
}
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
return user, nil
}
// internal/handler/user_handler.go
func (h *UserHandler) GetByID(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "无效的用户ID")
return
}
user, err := h.userService.GetByID(c.Request.Context(), id)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, dto.ToUserResponse(user))
}
Java 实现
// enums/ErrorCode.java
@Getter
@AllArgsConstructor
public enum ErrorCode {
// 通用错误
SUCCESS(0, "成功"),
BAD_REQUEST(400, "请求参数错误"),
UNAUTHORIZED(401, "未授权"),
FORBIDDEN(403, "禁止访问"),
NOT_FOUND(404, "资源不存在"),
CONFLICT(409, "资源冲突"),
TOO_MANY_REQUESTS(429, "请求过于频繁"),
INTERNAL_ERROR(500, "服务器内部错误"),
SERVICE_UNAVAILABLE(503, "服务暂不可用"),
// 用户模块 (10000+)
USER_NOT_FOUND(10001, "用户不存在"),
USER_EXISTS(10002, "用户已存在"),
PASSWORD_WRONG(10003, "密码错误"),
TOKEN_EXPIRED(10004, "Token已过期"),
TOKEN_INVALID(10005, "Token无效"),
PERMISSION_DENIED(10006, "权限不足"),
// 订单模块 (20000+)
ORDER_NOT_FOUND(20001, "订单不存在"),
ORDER_CANCELLED(20002, "订单已取消"),
INSUFFICIENT_STOCK(20003, "库存不足"),
ORDER_STATUS_ERROR(20004, "订单状态不允许此操作");
private final int code;
private final String message;
public HttpStatus toHttpStatus() {
if (code == 0) return HttpStatus.OK;
if (code == 400 || code >= 10000) return HttpStatus.BAD_REQUEST;
if (code == 401) return HttpStatus.UNAUTHORIZED;
if (code == 403) return HttpStatus.FORBIDDEN;
if (code == 404) return HttpStatus.NOT_FOUND;
if (code == 409) return HttpStatus.CONFLICT;
if (code == 429) return HttpStatus.TOO_MANY_REQUESTS;
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
// exception/BusinessException.java
@Getter
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
private final Object data;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
this.data = null;
}
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.data = null;
}
public BusinessException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.errorCode = errorCode;
this.data = null;
}
public BusinessException(ErrorCode errorCode, String message, Object data) {
super(message);
this.errorCode = errorCode;
this.data = data;
}
}
// exception/NotFoundException.java
public class NotFoundException extends BusinessException {
public NotFoundException(String message) {
super(ErrorCode.NOT_FOUND, message);
}
}
// exception/ForbiddenException.java
public class ForbiddenException extends BusinessException {
public ForbiddenException(String message) {
super(ErrorCode.FORBIDDEN, message);
}
}
// dto/response/CommonResponse.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonResponse<T> {
private int code;
private String message;
private T data;
private String requestId;
private long timestamp;
public CommonResponse(int code, String message) {
this.code = code;
this.message = message;
this.timestamp = System.currentTimeMillis();
this.requestId = MDC.get("request_id");
}
public CommonResponse(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
this.timestamp = System.currentTimeMillis();
this.requestId = MDC.get("request_id");
}
public static <T> CommonResponse<T> success() {
return new CommonResponse<>(0, "success", null);
}
public static <T> CommonResponse<T> success(T data) {
return new CommonResponse<>(0, "success", data);
}
public static <T> CommonResponse<T> success(String message, T data) {
return new CommonResponse<>(0, message, data);
}
public static <T> CommonResponse<T> error(ErrorCode errorCode) {
return new CommonResponse<>(errorCode.getCode(), errorCode.getMessage());
}
public static <T> CommonResponse<T> error(ErrorCode errorCode, String message) {
return new CommonResponse<>(errorCode.getCode(), message);
}
public static <T> CommonResponse<T> error(int code, String message) {
return new CommonResponse<>(code, message);
}
}
// dto/response/PageResult.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResult<T> {
private List<T> list;
private long total;
private int page;
private int pageSize;
private int pages;
public static <T> PageResult<T> of(List<T> list, long total, int page, int pageSize) {
PageResult<T> result = new PageResult<>();
result.setList(list);
result.setTotal(total);
result.setPage(page);
result.setPageSize(pageSize);
result.setPages((int) Math.ceil((double) total / pageSize));
return result;
}
public static <T> PageResult<T> of(IPage<T> page) {
return of(page.getRecords(), page.getTotal(),
(int) page.getCurrent(), (int) page.getSize());
}
}
// exception/GlobalExceptionHandler.java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 业务异常
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<CommonResponse<Object>> handleBusinessException(
BusinessException e, HttpServletRequest request) {
log.warn("Business exception: {} - {}", e.getErrorCode(), e.getMessage());
CommonResponse<Object> response = CommonResponse.error(
e.getErrorCode().getCode(), e.getMessage());
if (e.getData() != null) {
response.setData(e.getData());
}
return ResponseEntity
.status(e.getErrorCode().toHttpStatus())
.body(response);
}
/**
* 参数校验异常 - @Valid 校验失败
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<CommonResponse<Map<String, String>>> handleValidationException(
MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();
e.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
// 取第一个错误作为主要消息
String message = e.getBindingResult().getFieldErrors().stream()
.findFirst()
.map(FieldError::getDefaultMessage)
.orElse("参数校验失败");
log.warn("Validation failed: {}", errors);
CommonResponse<Map<String, String>> response =
CommonResponse.error(ErrorCode.BAD_REQUEST, message);
response.setData(errors);
return ResponseEntity.badRequest().body(response);
}
/**
* 参数校验异常 - @Validated 校验失败(方法参数)
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<CommonResponse<Void>> handleConstraintViolation(
ConstraintViolationException e) {
String message = e.getConstraintViolations().stream()
.findFirst()
.map(ConstraintViolation::getMessage)
.orElse("参数校验失败");
return ResponseEntity.badRequest()
.body(CommonResponse.error(ErrorCode.BAD_REQUEST, message));
}
/**
* 参数绑定异常
*/
@ExceptionHandler(BindException.class)
public ResponseEntity<CommonResponse<Void>> handleBindException(BindException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.findFirst()
.map(FieldError::getDefaultMessage)
.orElse("参数绑定失败");
return ResponseEntity.badRequest()
.body(CommonResponse.error(ErrorCode.BAD_REQUEST, message));
}
/**
* 请求方法不支持
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<CommonResponse<Void>> handleMethodNotSupported(
HttpRequestMethodNotSupportedException e) {
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(CommonResponse.error(405, "请求方法不支持: " + e.getMethod()));
}
/**
* 请求体缺失
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<CommonResponse<Void>> handleMessageNotReadable(
HttpMessageNotReadableException e) {
return ResponseEntity.badRequest()
.body(CommonResponse.error(ErrorCode.BAD_REQUEST, "请求体格式错误"));
}
/**
* JWT 相关异常
*/
@ExceptionHandler(ExpiredJwtException.class)
public ResponseEntity<CommonResponse<Void>> handleExpiredJwt(ExpiredJwtException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(CommonResponse.error(ErrorCode.TOKEN_EXPIRED));
}
@ExceptionHandler(JwtException.class)
public ResponseEntity<CommonResponse<Void>> handleJwtException(JwtException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(CommonResponse.error(ErrorCode.TOKEN_INVALID));
}
/**
* 访问拒绝
*/
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<CommonResponse<Void>> handleAccessDenied(AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(CommonResponse.error(ErrorCode.FORBIDDEN));
}
/**
* 限流异常
*/
@ExceptionHandler(RateLimitExceededException.class)
public ResponseEntity<CommonResponse<Void>> handleRateLimit(RateLimitExceededException e) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.body(CommonResponse.error(ErrorCode.TOO_MANY_REQUESTS));
}
/**
* 兜底异常处理
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<CommonResponse<Void>> handleException(
Exception e, HttpServletRequest request) {
log.error("Unhandled exception: {} - {}",
request.getRequestURI(), e.getMessage(), e);
// 生产环境不暴露具体错误信息
String message = "服务器内部错误";
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(CommonResponse.error(ErrorCode.INTERNAL_ERROR, message));
}
}
// 使用示例 - service/impl/UserServiceImpl.java
@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
@Override
public User getById(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
return user;
}
@Override
@Transactional
public User create(CreateUserRequest request) {
// 检查用户是否存在
User existing = userMapper.selectByEmail(request.getEmail());
if (existing != null) {
throw new BusinessException(ErrorCode.USER_EXISTS);
}
// 业务规则校验
if (request.getAge() != null && request.getAge() < 18) {
throw new BusinessException(ErrorCode.BAD_REQUEST, "用户年龄不能小于18岁");
}
User user = new User();
BeanUtils.copyProperties(request, user);
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setCreatedAt(LocalDateTime.now());
userMapper.insert(user);
return user;
}
@Override
@Transactional
public void updateStatus(Long id, Integer status) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
// 状态流转校验
if (user.getStatus().equals(2) && status.equals(1)) {
throw new BusinessException(ErrorCode.BAD_REQUEST, "已禁用的用户不能直接激活");
}
user.setStatus(status);
user.setUpdatedAt(LocalDateTime.now());
userMapper.updateById(user);
}
}
// controller/UserController.java
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public CommonResponse<UserResponse> getById(@PathVariable Long id) {
User user = userService.getById(id);
return CommonResponse.success(UserResponse.from(user));
}
@PostMapping
public CommonResponse<UserResponse> create(
@Valid @RequestBody CreateUserRequest request) {
User user = userService.create(request);
return CommonResponse.success(UserResponse.from(user));
}
@GetMapping
public CommonResponse<PageResult<UserResponse>> list(@Valid ListUserRequest request) {
PageResult<UserResponse> result = userService.list(request);
return CommonResponse.success(result);
}
}
异常处理对比表
| 方面 |
Go |
Java |
| 错误类型 |
自定义 error 结构体 |
自定义 Exception 类 |
| 错误传播 |
返回值传递 |
throw 抛出 |
| 全局处理 |
Recovery 中间件 |
@RestControllerAdvice |
| 错误码 |
常量定义 |
枚举定义 |
| 堆栈跟踪 |
debug.Stack() |
Exception.getStackTrace() |
| 错误包装 |
errors.Wrap |
new Exception(msg, cause) |
十、事务管理
Go 实现
// pkg/database/transaction.go
package database
import (
"context"
"database/sql"
)
type TxKey struct{}
// 从 Context 获取事务
func GetTx(ctx context.Context) *sql.Tx {
if tx, ok := ctx.Value(TxKey{}).(*sql.Tx); ok {
return tx
}
return nil
}
// 事务管理器
type TxManager interface {
WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error
}
type txManager struct {
db *sql.DB
}
func NewTxManager(db *sql.DB) TxManager {
return &txManager{db: db}
}
func (m *txManager) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
// 检查是否已在事务中
if GetTx(ctx) != nil {
return fn(ctx)
}
tx, err := m.db.BeginTx(ctx, nil)
if err != nil {
return err
}
// 将事务放入 Context
txCtx := context.WithValue(ctx, TxKey{}, tx)
defer func() {
if r := recover(); r != nil {
_ = tx.Rollback()
panic(r)
}
}()
if err := fn(txCtx); err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
return fmt.Errorf("rollback error: %v (original: %v)", rbErr, err)
}
return err
}
return tx.Commit()
}
// GORM 版本
type GormTxManager struct {
db *gorm.DB
}
func NewGormTxManager(db *gorm.DB) *GormTxManager {
return &GormTxManager{db: db}
}
func (m *GormTxManager) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
return m.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
txCtx := context.WithValue(ctx, TxKey{}, tx)
return fn(txCtx)
})
}
// internal/repository/user_repository.go
type userRepository struct {
db *gorm.DB
}
// 自动获取事务或普通连接
func (r *userRepository) getDB(ctx context.Context) *gorm.DB {
if tx, ok := ctx.Value(database.TxKey{}).(*gorm.DB); ok {
return tx
}
return r.db.WithContext(ctx)
}
func (r *userRepository) Create(ctx context.Context, user *entity.User) error {
return r.getDB(ctx).Create(user).Error
}
func (r *userRepository) Update(ctx context.Context, user *entity.User) error {
return r.getDB(ctx).Save(user).Error
}
func (r *userRepository) Delete(ctx context.Context, id int64) error {
return r.getDB(ctx).Delete(&entity.User{}, id).Error
}
// internal/service/order_service.go
type orderService struct {
txManager database.TxManager
orderRepo repository.OrderRepository
productRepo repository.ProductRepository
userRepo repository.UserRepository
logger *zap.Logger
}
func (s *orderService) CreateOrder(ctx context.Context, req *dto.CreateOrderRequest) (*entity.Order, error) {
var order *entity.Order
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
// 1. 检查用户
user, err := s.userRepo.FindByID(txCtx, req.UserID)
if err != nil {
return err
}
if user == nil {
return errors.ErrUserNotFound
}
// 2. 检查库存并扣减
product, err := s.productRepo.FindByIDForUpdate(txCtx, req.ProductID)
if err != nil {
return err
}
if product == nil {
return errors.New(errors.CodeNotFound, "商品不存在")
}
if product.Stock < req.Quantity {
return errors.ErrInsufficientStock
}
product.Stock -= req.Quantity
if err := s.productRepo.Update(txCtx, product); err != nil {
return err
}
// 3. 创建订单
order = &entity.Order{
UserID: req.UserID,
ProductID: req.ProductID,
Quantity: req.Quantity,
TotalPrice: product.Price * float64(req.Quantity),
Status: entity.OrderStatusPending,
CreatedAt: time.Now(),
}
if err := s.orderRepo.Create(txCtx, order); err != nil {
return err
}
// 4. 创建订单明细
detail := &entity.OrderDetail{
OrderID: order.ID,
ProductID: req.ProductID,
Quantity: req.Quantity,
Price: product.Price,
}
if err := s.orderRepo.CreateDetail(txCtx, detail); err != nil {
return err
}
return nil
})
if err != nil {
s.logger.Error("create order failed",
zap.Error(err),
zap.Int64("user_id", req.UserID))
return nil, err
}
return order, nil
}
// 带 Saga 补偿的分布式事务示例
func (s *orderService) CreateOrderWithSaga(ctx context.Context, req *dto.CreateOrderRequest) (*entity.Order, error) {
var order *entity.Order
var compensations []func() error
// 定义补偿函数
defer func() {
if r := recover(); r != nil {
s.executeCompensations(compensations)
panic(r)
}
}()
// Step 1: 扣减库存
if err := s.productRepo.DeductStock(ctx, req.ProductID, req.Quantity); err != nil {
return nil, err
}
compensations = append(compensations, func() error {
return s.productRepo.AddStock(ctx, req.ProductID, req.Quantity)
})
// Step 2: 创建订单
order = &entity.Order{
UserID: req.UserID,
ProductID: req.ProductID,
Quantity: req.Quantity,
Status: entity.OrderStatusPending,
}
if err := s.orderRepo.Create(ctx, order); err != nil {
s.executeCompensations(compensations)
return nil, err
}
compensations = append(compensations, func() error {
return s.orderRepo.Delete(ctx, order.ID)
})
// Step 3: 调用支付服务(外部服务)
if err := s.paymentClient.CreatePayment(ctx, order.ID, order.TotalPrice); err != nil {
s.executeCompensations(compensations)
return nil, err
}
return order, nil
}
func (s *orderService) executeCompensations(compensations []func() error) {
// 逆序执行补偿
for i := len(compensations) - 1; i >= 0; i-- {
if err := compensations[i](); err != nil {
s.logger.Error("compensation failed", zap.Error(err))
}
}
}
Java 实现
// service/impl/OrderServiceImpl.java
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderServiceImpl implements OrderService {
private final OrderMapper orderMapper;
private final ProductMapper productMapper;
private final UserMapper userMapper;
private final OrderDetailMapper orderDetailMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Order createOrder(CreateOrderRequest request) {
// 1. 检查用户
User user = userMapper.selectById(request.getUserId());
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
// 2. 检查库存并扣减(悲观锁)
Product product = productMapper.selectByIdForUpdate(request.getProductId());
if (product == null) {
throw new BusinessException(ErrorCode.NOT_FOUND, "商品不存在");
}
if (product.getStock() < request.getQuantity()) {
throw new BusinessException(ErrorCode.INSUFFICIENT_STOCK);
}
product.setStock(product.getStock() - request.getQuantity());
product.setUpdatedAt(LocalDateTime.now());
productMapper.updateById(product);
// 3. 创建订单
Order order = new Order();
order.setUserId(request.getUserId());
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setTotalPrice(product.getPrice().multiply(BigDecimal.valueOf(request.getQuantity())));
order.setStatus(OrderStatus.PENDING.getCode());
order.setCreatedAt(LocalDateTime.now());
orderMapper.insert(order);
// 4. 创建订单明细
OrderDetail detail = new OrderDetail();
detail.setOrderId(order.getId());
detail.setProductId(request.getProductId());
detail.setQuantity(request.getQuantity());
detail.setPrice(product.getPrice());
orderDetailMapper.insert(detail);
return order;
}
/**
* 带传播行为的嵌套事务
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Order createOrderWithNotification(CreateOrderRequest request) {
// 创建订单(主事务)
Order order = createOrder(request);
// 发送通知(独立事务,失败不影响主事务)
try {
notificationService.sendOrderCreatedNotification(order);
} catch (Exception e) {
log.warn("Failed to send notification for order: {}", order.getId(), e);
}
return order;
}
/**
* 取消订单(状态校验 + 库存回滚)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void cancelOrder(Long orderId, String reason) {
Order order = orderMapper.selectByIdForUpdate(orderId);
if (order == null) {
throw new BusinessException(ErrorCode.ORDER_NOT_FOUND);
}
// 状态校验
if (!OrderStatus.canCancel(order.getStatus())) {
throw new BusinessException(ErrorCode.ORDER_STATUS_ERROR,
"当前订单状态不允许取消");
}
// 更新订单状态
order.setStatus(OrderStatus.CANCELLED.getCode());
order.setCancelReason(reason);
order.setCancelledAt(LocalDateTime.now());
order.setUpdatedAt(LocalDateTime.now());
orderMapper.updateById(order);
// 恢复库存
productMapper.addStock(order.getProductId(), order.getQuantity());
log.info("Order cancelled: orderId={}, reason={}", orderId, reason);
}
}
// 通知服务 - 使用 REQUIRES_NEW 传播行为
@Service
@RequiredArgsConstructor
@Slf4j
public class NotificationServiceImpl implements NotificationService {
private final NotificationMapper notificationMapper;
private final EmailClient emailClient;
/**
* 独立事务:即使失败也不影响主事务
*/
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void sendOrderCreatedNotification(Order order) {
// 记录通知
Notification notification = new Notification();
notification.setUserId(order.getUserId());
notification.setType("ORDER_CREATED");
notification.setContent("您的订单 " + order.getId() + " 已创建成功");
notification.setCreatedAt(LocalDateTime.now());
notificationMapper.insert(notification);
// 发送邮件(可能失败)
emailClient.sendOrderConfirmation(order);
}
/**
* 异步发送,不影响主流程
*/
@Override
@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendOrderCreatedNotificationAsync(Order order) {
sendOrderCreatedNotification(order);
}
}
// 编程式事务管理
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderServiceWithProgrammaticTx implements OrderService {
private final TransactionTemplate transactionTemplate;
private final PlatformTransactionManager transactionManager;
private final OrderMapper orderMapper;
private final ProductMapper productMapper;
/**
* 使用 TransactionTemplate
*/
public Order createOrderWithTemplate(CreateOrderRequest request) {
return transactionTemplate.execute(status -> {
try {
// 业务逻辑
Product product = productMapper.selectByIdForUpdate(request.getProductId());
if (product.getStock() < request.getQuantity()) {
throw new BusinessException(ErrorCode.INSUFFICIENT_STOCK);
}
product.setStock(product.getStock() - request.getQuantity());
productMapper.updateById(product);
Order order = new Order();
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setStatus(OrderStatus.PENDING.getCode());
orderMapper.insert(order);
return order;
} catch (Exception e) {
status.setRollbackOnly();
throw e;
}
});
}
/**
* 手动控制事务
*/
public Order createOrderManual(CreateOrderRequest request) {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
def.setTimeout(30);
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 业务逻辑
Product product = productMapper.selectByIdForUpdate(request.getProductId());
if (product.getStock() < request.getQuantity()) {
throw new BusinessException(ErrorCode.INSUFFICIENT_STOCK);
}
product.setStock(product.getStock() - request.getQuantity());
productMapper.updateById(product);
Order order = new Order();
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
orderMapper.insert(order);
transactionManager.commit(status);
return order;
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
/**
* 分段提交(大批量处理)
*/
@Override
public void batchUpdateOrderStatus(List<Long> orderIds, Integer newStatus) {
int batchSize = 100;
for (int i = 0; i < orderIds.size(); i += batchSize) {
int end = Math.min(i + batchSize, orderIds.size());
List<Long> batch = orderIds.subList(i, end);
transactionTemplate.execute(status -> {
orderMapper.batchUpdateStatus(batch, newStatus, LocalDateTime.now());
return null;
});
log.info("Batch updated orders: {}-{}", i, end);
}
}
}
// 分布式事务 - Seata AT 模式示例
@Service
@RequiredArgsConstructor
@Slf4j
public class DistributedOrderService {
private final OrderMapper orderMapper;
private final ProductFeignClient productClient;
private final PaymentFeignClient paymentClient;
/**
* Seata 分布式事务
*/
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public Order createDistributedOrder(CreateOrderRequest request) {
// 1. 扣减库存(库存服务)
productClient.deductStock(request.getProductId(), request.getQuantity());
// 2. 创建订单(订单服务 - 本地)
Order order = new Order();
order.setUserId(request.getUserId());
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setStatus(OrderStatus.PENDING.getCode());
orderMapper.insert(order);
// 3. 创建支付单(支付服务)
paymentClient.createPayment(order.getId(), order.getTotalPrice());
return order;
}
/**
* TCC 模式示例
*/
@GlobalTransactional
public Order createOrderWithTcc(CreateOrderRequest request) {
// Try: 预留资源
productClient.tryDeductStock(request.getProductId(), request.getQuantity());
Order order = new Order();
order.setStatus(OrderStatus.TRYING.getCode());
orderMapper.insert(order);
paymentClient.tryCreatePayment(order.getId(), order.getTotalPrice());
// Confirm 和 Cancel 由 Seata 框架自动调用
return order;
}
}
// Saga 模式 - 手动补偿
@Service
@RequiredArgsConstructor
@Slf4j
public class SagaOrderService {
private final OrderMapper orderMapper;
private final ProductClient productClient;
private final PaymentClient paymentClient;
public Order createOrderWithSaga(CreateOrderRequest request) {
List<Runnable> compensations = new ArrayList<>();
try {
// Step 1: 扣减库存
productClient.deductStock(request.getProductId(), request.getQuantity());
compensations.add(() ->
productClient.addStock(request.getProductId(), request.getQuantity()));
// Step 2: 创建订单
Order order = new Order();
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setStatus(OrderStatus.PENDING.getCode());
orderMapper.insert(order);
compensations.add(() -> orderMapper.deleteById(order.getId()));
// Step 3: 创建支付
paymentClient.createPayment(order.getId(), order.getTotalPrice());
compensations.add(() -> paymentClient.cancelPayment(order.getId()));
return order;
} catch (Exception e) {
// 执行补偿(逆序)
log.error("Saga failed, executing compensations", e);
Collections.reverse(compensations);
for (Runnable compensation : compensations) {
try {
compensation.run();
} catch (Exception compEx) {
log.error("Compensation failed", compEx);
// 记录补偿失败,人工处理
}
}
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "订单创建失败");
}
}
}
事务对比表
| 方面 |
Go |
Java |
| 事务声明 |
手动 Begin/Commit/Rollback |
@Transactional 注解 |
| 事务传播 |
Context 传递手动处理 |
propagation 属性 |
| 隔离级别 |
BeginTx 参数 |
isolation 属性 |
| 只读事务 |
需手动实现 |
readOnly = true |
| 超时控制 |
Context WithTimeout |
timeout 属性 |
| 嵌套事务 |
需手动保存点 |
NESTED 传播行为 |
| 分布式事务 |
手动 Saga/消息队列 |
Seata/手动 Saga |
十一、限流与熔断
Go 实现
// pkg/ratelimit/ratelimit.go
package ratelimit
import (
"context"
"time"
"github.com/redis/go-redis/v9"
"golang.org/x/time/rate"
)
// 本地限流器(单机)
type LocalRateLimiter struct {
limiter *rate.Limiter
}
func NewLocalRateLimiter(r rate.Limit, b int) *LocalRateLimiter {
return &LocalRateLimiter{
limiter: rate.NewLimiter(r, b),
}
}
func (l *LocalRateLimiter) Allow() bool {
return l.limiter.Allow()
}
func (l *LocalRateLimiter) Wait(ctx context.Context) error {
return l.limiter.Wait(ctx)
}
// 分布式限流器(基于 Redis)
type RedisRateLimiter struct {
client *redis.Client
keyPrefix string
limit int
window time.Duration
}
func NewRedisRateLimiter(client *redis.Client, keyPrefix string, limit int, window time.Duration) *RedisRateLimiter {
return &RedisRateLimiter{
client: client,
keyPrefix: keyPrefix,
limit: limit,
window: window,
}
}
// 滑动窗口限流
func (l *RedisRateLimiter) Allow(ctx context.Context, key string) (bool, error) {
now := time.Now().UnixMilli()
windowStart := now - l.window.Milliseconds()
fullKey := l.keyPrefix + key
pipe := l.client.Pipeline()
// 移除窗口外的请求
pipe.ZRemRangeByScore(ctx, fullKey, "0", fmt.Sprintf("%d", windowStart))
// 获取当前窗口内的请求数
countCmd := pipe.ZCard(ctx, fullKey)
// 添加当前请求
pipe.ZAdd(ctx, fullKey, redis.Z{Score: float64(now), Member: now})
// 设置过期时间
pipe.Expire(ctx, fullKey, l.window)
_, err := pipe.Exec(ctx)
if err != nil {
return false, err
}
count := countCmd.Val()
return count < int64(l.limit), nil
}
// 令牌桶限流(Redis 实现)
const tokenBucketScript = `
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local bucket = redis.call('HMGET', key, 'tokens', 'last_time')
local tokens = tonumber(bucket[1]) or capacity
local last_time = tonumber(bucket[2]) or now
local elapsed = now - last_time
local new_tokens = math.min(capacity, tokens + elapsed * rate / 1000)
if new_tokens >= requested then
new_tokens = new_tokens - requested
redis.call('HMSET', key, 'tokens', new_tokens, 'last_time', now)
redis.call('EXPIRE', key, math.ceil(capacity / rate) + 1)
return 1
else
return 0
end
`
type TokenBucketLimiter struct {
client *redis.Client
script *redis.Script
capacity int
rate int // tokens per second
}
func NewTokenBucketLimiter(client *redis.Client, capacity, rate int) *TokenBucketLimiter {
return &TokenBucketLimiter{
client: client,
script: redis.NewScript(tokenBucketScript),
capacity: capacity,
rate: rate,
}
}
func (l *TokenBucketLimiter) Allow(ctx context.Context, key string) (bool, error) {
result, err := l.script.Run(ctx, l.client, []string{key},
l.capacity, l.rate, time.Now().UnixMilli(), 1).Int()
if err != nil {
return false, err
}
return result == 1, nil
}
// internal/handler/middleware/ratelimit.go
package middleware
import (
"github.com/gin-gonic/gin"
)
// IP 限流中间件
func RateLimitByIP(limiter *ratelimit.RedisRateLimiter) gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
allowed, err := limiter.Allow(c.Request.Context(), "ip:"+ip)
if err != nil {
// 限流器故障时放行(降级策略)
c.Next()
return
}
if !allowed {
response.TooManyRequests(c, "请求过于频繁,请稍后再试")
c.Abort()
return
}
c.Next()
}
}
// 用户限流中间件
func RateLimitByUser(limiter *ratelimit.RedisRateLimiter) gin.HandlerFunc {
return func(c *gin.Context) {
userID := GetCurrentUserID(c)
if userID == 0 {
c.Next()
return
}
allowed, err := limiter.Allow(c.Request.Context(), fmt.Sprintf("user:%d", userID))
if err != nil {
c.Next()
return
}
if !allowed {
response.TooManyRequests(c, "请求过于频繁,请稍后再试")
c.Abort()
return
}
c.Next()
}
}
// API 限流中间件(按接口)
func RateLimitByAPI(limiter *ratelimit.TokenBucketLimiter) gin.HandlerFunc {
return func(c *gin.Context) {
key := fmt.Sprintf("api:%s:%s", c.Request.Method, c.FullPath())
allowed, err := limiter.Allow(c.Request.Context(), key)
if err != nil {
c.Next()
return
}
if !allowed {
c.Header("Retry-After", "1")
response.TooManyRequests(c, "接口请求过于频繁")
c.Abort()
return
}
c.Next()
}
}
// pkg/circuitbreaker/circuitbreaker.go
package circuitbreaker
import (
"context"
"errors"
"sync"
"time"
)
var (
ErrCircuitOpen = errors.New("circuit breaker is open")
ErrTooManyRequests = errors.New("too many requests")
)
type State int
const (
StateClosed State = iota
StateOpen
StateHalfOpen
)
type CircuitBreaker struct {
name string
maxFailures int
timeout time.Duration
halfOpenMax int
mu sync.RWMutex
state State
failures int
successes int
lastFailure time.Time
halfOpenCount int
}
type Config struct {
Name string
MaxFailures int // 触发熔断的失败次数
Timeout time.Duration // 熔断持续时间
HalfOpenMax int // 半开状态允许的请求数
}
func New(cfg Config) *CircuitBreaker {
return &CircuitBreaker{
name: cfg.Name,
maxFailures: cfg.MaxFailures,
timeout: cfg.Timeout,
halfOpenMax: cfg.HalfOpenMax,
state: StateClosed,
}
}
func (cb *CircuitBreaker) Execute(ctx context.Context, fn func() error) error {
if err := cb.beforeRequest(); err != nil {
return err
}
err := fn()
cb.afterRequest(err)
return err
}
func (cb *CircuitBreaker) beforeRequest() error {
cb.mu.Lock()
defer cb.mu.Unlock()
switch cb.state {
case StateClosed:
return nil
case StateOpen:
if time.Since(cb.lastFailure) > cb.timeout {
cb.state = StateHalfOpen
cb.halfOpenCount = 0
return nil
}
return ErrCircuitOpen
case StateHalfOpen:
if cb.halfOpenCount >= cb.halfOpenMax {
return ErrTooManyRequests
}
cb.halfOpenCount++
return nil
}
return nil
}
func (cb *CircuitBreaker) afterRequest(err error) {
cb.mu.Lock()
defer cb.mu.Unlock()
if err != nil {
cb.onFailure()
} else {
cb.onSuccess()
}
}
func (cb *CircuitBreaker) onFailure() {
cb.failures++
cb.lastFailure = time.Now()
switch cb.state {
case StateClosed:
if cb.failures >= cb.maxFailures {
cb.state = StateOpen
}
case StateHalfOpen:
cb.state = StateOpen
}
}
func (cb *CircuitBreaker) onSuccess() {
switch cb.state {
case StateClosed:
cb.failures = 0
case StateHalfOpen:
cb.successes++
if cb.successes >= cb.halfOpenMax {
cb.state = StateClosed
cb.failures = 0
cb.successes = 0
}
}
}
func (cb *CircuitBreaker) State() State {
cb.mu.RLock()
defer cb.mu.RUnlock()
return cb.state
}
// 使用示例
type PaymentClient struct {
cb *CircuitBreaker
httpCli *http.Client
}
func (c *PaymentClient) CreatePayment(ctx context.Context, orderID int64, amount float64) error {
return c.cb.Execute(ctx, func() error {
resp, err := c.httpCli.Post(...)
if err != nil {
return err
}
if resp.StatusCode >= 500 {
return errors.New("payment service error")
}
return nil
})
}
Java 实现(Resilience4j)
// config/Resilience4jConfig.java
@Configuration
public class Resilience4jConfig {
@Bean
public RateLimiterConfig rateLimiterConfig() {
return RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofSeconds(1))
.limitForPeriod(100) // 每秒 100 个请求
.timeoutDuration(Duration.ofMillis(500))
.build();
}
@Bean
public RateLimiterRegistry rateLimiterRegistry(RateLimiterConfig config) {
return RateLimiterRegistry.of(config);
}
@Bean
public CircuitBreakerConfig circuitBreakerConfig() {
return CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 失败率阈值 50%
.waitDurationInOpenState(Duration.ofSeconds(30)) // 熔断持续时间
.permittedNumberOfCallsInHalfOpenState(10) // 半开状态请求数
.slidingWindowSize(100) // 滑动窗口大小
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.build();
}
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry(CircuitBreakerConfig config) {
return CircuitBreakerRegistry.of(config);
}
@Bean
public RetryConfig retryConfig() {
return RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(500))
.retryExceptions(IOException.class, TimeoutException.class)
.ignoreExceptions(BusinessException.class)
.build();
}
@Bean
public RetryRegistry retryRegistry(RetryConfig config) {
return RetryRegistry.of(config);
}
}
// application.yml 配置方式
/*
resilience4j:
ratelimiter:
instances:
default:
limitForPeriod: 100
limitRefreshPeriod: 1s
timeoutDuration: 500ms
userApi:
limitForPeriod: 50
limitRefreshPeriod: 1s
circuitbreaker:
instances:
paymentService:
failureRateThreshold: 50
waitDurationInOpenState: 30s
permittedNumberOfCallsInHalfOpenState: 10
slidingWindowSize: 100
retry:
instances:
paymentService:
maxAttempts: 3
waitDuration: 500ms
*/
// service/impl/PaymentServiceImpl.java
@Service
@RequiredArgsConstructor
@Slf4j
public class PaymentServiceImpl implements PaymentService {
private final PaymentClient paymentClient;
private final CircuitBreakerRegistry circuitBreakerRegistry;
private final RetryRegistry retryRegistry;
private final RateLimiterRegistry rateLimiterRegistry;
@Override
@CircuitBreaker(name = "paymentService", fallbackMethod = "createPaymentFallback")
@Retry(name = "paymentService")
@RateLimiter(name = "paymentService")
public PaymentResult createPayment(Long orderId, BigDecimal amount) {
log.info("Creating payment for order: {}", orderId);
return paymentClient.create(orderId, amount);
}
// 熔断降级方法
public PaymentResult createPaymentFallback(Long orderId, BigDecimal amount, Throwable t) {
log.warn("Payment service fallback triggered for order: {}, error: {}",
orderId, t.getMessage());
// 返回降级响应
PaymentResult result = new PaymentResult();
result.setSuccess(false);
result.setMessage("支付服务暂时不可用,请稍后重试");
result.setPending(true);
return result;
}
// 编程式使用
public PaymentResult createPaymentProgrammatic(Long orderId, BigDecimal amount) {
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("paymentService");
Retry retry = retryRegistry.retry("paymentService");
RateLimiter rateLimiter = rateLimiterRegistry.rateLimiter("paymentService");
Supplier<PaymentResult> supplier = () -> paymentClient.create(orderId, amount);
Supplier<PaymentResult> decoratedSupplier = Decorators.ofSupplier(supplier)
.withCircuitBreaker(circuitBreaker)
.withRetry(retry)
.withRateLimiter(rateLimiter)
.withFallback(List.of(CallNotPermittedException.class, RequestNotPermitted.class),
t -> createPaymentFallback(orderId, amount, t))
.decorate();
return decoratedSupplier.get();
}
}
// 自定义 Redis 分布式限流
// ratelimit/RedisRateLimiter.java
@Component
@RequiredArgsConstructor
public class RedisRateLimiter {
private final StringRedisTemplate redisTemplate;
private static final String SLIDING_WINDOW_SCRIPT = """
local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now .. '-' .. math.random())
redis.call('EXPIRE', key, math.ceil(window / 1000))
return 1
else
return 0
end
""";
private final DefaultRedisScript<Long> slidingWindowScript;
@PostConstruct
public void init() {
slidingWindowScript = new DefaultRedisScript<>();
slidingWindowScript.setScriptText(SLIDING_WINDOW_SCRIPT);
slidingWindowScript.setResultType(Long.class);
}
/**
* 滑动窗口限流
* @param key 限流 key
* @param windowMs 窗口大小(毫秒)
* @param limit 限制次数
* @return 是否允许
*/
public boolean isAllowed(String key, long windowMs, int limit) {
Long result = redisTemplate.execute(
slidingWindowScript,
List.of(key),
String.valueOf(windowMs),
String.valueOf(limit),
String.valueOf(System.currentTimeMillis())
);
return result != null && result == 1L;
}
/**
* IP 限流
*/
public boolean isAllowedByIp(String ip, int limit, Duration window) {
return isAllowed("ratelimit:ip:" + ip, window.toMillis(), limit);
}
/**
* 用户限流
*/
public boolean isAllowedByUser(Long userId, int limit, Duration window) {
return isAllowed("ratelimit:user:" + userId, window.toMillis(), limit);
}
/**
* 接口限流
*/
public boolean isAllowedByApi(String method, String path, int limit, Duration window) {
return isAllowed("ratelimit:api:" + method + ":" + path, window.toMillis(), limit);
}
}
// interceptor/RateLimitInterceptor.java
@Component
@RequiredArgsConstructor
public class RateLimitInterceptor implements HandlerInterceptor {
private final RedisRateLimiter rateLimiter;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
String ip = getClientIp(request);
// IP 限流:每分钟 100 次
if (!rateLimiter.isAllowedByIp(ip, 100, Duration.ofMinutes(1))) {
sendRateLimitResponse(response);
return false;
}
// 用户限流(如果已登录)
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof CustomUserDetails) {
Long userId = ((CustomUserDetails) auth.getPrincipal()).getId();
if (!rateLimiter.isAllowedByUser(userId, 200, Duration.ofMinutes(1))) {
sendRateLimitResponse(response);
return false;
}
}
return true;
}
private void sendRateLimitResponse(HttpServletResponse response) throws IOException {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.setContentType("application/json;charset=UTF-8");
response.setHeader("Retry-After", "60");
response.getWriter().write(
"{\"code\":429,\"message\":\"请求过于频繁,请稍后再试\"}");
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip.split(",")[0].trim();
}
}
限流熔断对比表
| 方面 |
Go |
Java |
| 本地限流 |
golang.org/x/time/rate |
Guava RateLimiter / Bucket4j |
| 分布式限流 |
Redis Lua 脚本手动实现 |
Redis Lua / Redisson |
| 熔断器 |
手动实现 / sony/gobreaker |
Resilience4j / Sentinel |
| 配置方式 |
代码配置 |
注解 + YAML 配置 |
| 降级处理 |
回调函数 |
fallbackMethod |
| 监控指标 |
Prometheus 手动埋点 |
Actuator 自动暴露 |
十二、完整分层代码对比
12.1 Handler / Controller 层
Go 实现
// internal/handler/user_handler.go
package handler
import (
"strconv"
"github.com/gin-gonic/gin"
"myapp/internal/dto/request"
"myapp/internal/dto/response"
"myapp/internal/service"
"myapp/pkg/validator"
)
type UserHandler struct {
userService service.UserService
}
func NewUserHandler(userService service.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
// Create 创建用户
// @Summary 创建用户
// @Tags 用户管理
// @Accept json
// @Produce json
// @Param request body request.CreateUserRequest true "创建用户请求"
// @Success 200 {object} response.Response{data=response.UserResponse}
// @Failure 400 {object} response.Response
// @Router /api/v1/users [post]
func (h *UserHandler) Create(c *gin.Context) {
var req request.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, validator.GetValidationError(err))
return
}
user, err := h.userService.Create(c.Request.Context(), &req)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, response.ToUserResponse(user))
}
// GetByID 根据ID获取用户
func (h *UserHandler) GetByID(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "无效的用户ID")
return
}
user, err := h.userService.GetByID(c.Request.Context(), id)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, response.ToUserResponse(user))
}
// List 获取用户列表
func (h *UserHandler) List(c *gin.Context) {
var req request.ListUserRequest
if err := c.ShouldBindQuery(&req); err != nil {
response.BadRequest(c, validator.GetValidationError(err))
return
}
req.SetDefaults()
users, total, err := h.userService.List(c.Request.Context(), &req)
if err != nil {
response.Error(c, err)
return
}
list := make([]*response.UserResponse, len(users))
for i, u := range users {
list[i] = response.ToUserResponse(u)
}
response.Success(c, response.NewPageData(list, total, req.Page, req.PageSize))
}
// Update 更新用户
func (h *UserHandler) Update(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "无效的用户ID")
return
}
var req request.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, validator.GetValidationError(err))
return
}
if err := h.userService.Update(c.Request.Context(), id, &req); err != nil {
response.Error(c, err)
return
}
response.SuccessWithMessage(c, "更新成功", nil)
}
// Delete 删除用户
func (h *UserHandler) Delete(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "无效的用户ID")
return
}
if err := h.userService.Delete(c.Request.Context(), id); err != nil {
response.Error(c, err)
return
}
response.SuccessWithMessage(c, "删除成功", nil)
}
// Login 用户登录
func (h *UserHandler) Login(c *gin.Context) {
var req request.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, validator.GetValidationError(err))
return
}
tokenPair, err := h.userService.Login(c.Request.Context(), &req)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, tokenPair)
}
// GetCurrentUser 获取当前登录用户
func (h *UserHandler) GetCurrentUser(c *gin.Context) {
userID := middleware.GetCurrentUserID(c)
if userID == 0 {
response.Unauthorized(c, "未登录")
return
}
user, err := h.userService.GetByID(c.Request.Context(), userID)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, response.ToUserDetailResponse(user))
}
Java 实现
// controller/UserController.java
package com.example.controller;
import com.example.dto.request.*;
import com.example.dto.response.*;
import com.example.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
@Tag(name = "用户管理")
@Validated
public class UserController {
private final UserService userService;
@PostMapping
@Operation(summary = "创建用户")
@RequirePermission("user:create")
public CommonResponse<UserResponse> create(
@Valid @RequestBody CreateUserRequest request) {
return CommonResponse.success(userService.create(request));
}
@GetMapping("/{id}")
@Operation(summary = "根据ID获取用户")
@RequirePermission("user:read")
public CommonResponse<UserResponse> getById(@PathVariable Long id) {
return CommonResponse.success(userService.getById(id));
}
@GetMapping
@Operation(summary = "获取用户列表")
@RequirePermission("user:list")
public CommonResponse<PageResult<UserResponse>> list(@Valid ListUserRequest request) {
return CommonResponse.success(userService.list(request));
}
@PutMapping("/{id}")
@Operation(summary = "更新用户")
@RequirePermission("user:update")
public CommonResponse<Void> update(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
userService.update(id, request);
return CommonResponse.success("更新成功");
}
@DeleteMapping("/{id}")
@Operation(summary = "删除用户")
@RequirePermission("user:delete")
public CommonResponse<Void> delete(@PathVariable Long id) {
userService.delete(id);
return CommonResponse.success("删除成功");
}
@PostMapping("/login")
@Operation(summary = "用户登录")
public CommonResponse<TokenPairResponse> login(
@Valid @RequestBody LoginRequest request) {
return CommonResponse.success(userService.login(request));
}
@GetMapping("/me")
@Operation(summary = "获取当前登录用户")
public CommonResponse<UserDetailResponse> getCurrentUser() {
Long userId = SecurityUtils.getCurrentUserId();
return CommonResponse.success(userService.getDetail(userId));
}
@PutMapping("/me/password")
@Operation(summary = "修改密码")
public CommonResponse<Void> changePassword(
@Valid @RequestBody ChangePasswordRequest request) {
Long userId = SecurityUtils.getCurrentUserId();
userService.changePassword(userId, request);
return CommonResponse.success("密码修改成功");
}
@PostMapping("/{id}/disable")
@Operation(summary = "禁用用户")
@RequirePermission("user:disable")
@AuditLog(action = AuditAction.UPDATE, resource = "user", resourceIdExpr = "#id")
public CommonResponse<Void> disable(@PathVariable Long id) {
userService.updateStatus(id, UserStatus.DISABLED.getCode());
return CommonResponse.success("用户已禁用");
}
}
Handler/Controller 对比表
| 方面 |
Go |
Java |
| 路由绑定 |
构造函数注入 + 手动注册 |
@RequestMapping 注解 |
| 参数校验 |
ShouldBindJSON + 手动调用 |
@Valid 自动触发 |
| 路径参数 |
c.Param("id") + 手动转换 |
@PathVariable 自动转换 |
| 查询参数 |
c.ShouldBindQuery |
自动绑定到对象 |
| 响应封装 |
调用 response.Success/Error |
返回 CommonResponse |
| API 文档 |
swaggo 注释生成 |
SpringDoc 注解 |
| 权限控制 |
中间件链 |
@RequirePermission 注解 |
12.2 Service 层
Go 实现
// internal/service/user_service.go
package service
import (
"context"
"time"
"myapp/internal/dto/request"
"myapp/internal/entity"
"myapp/internal/errors"
"myapp/internal/repository"
"myapp/internal/repository/cache"
"myapp/pkg/auth"
"myapp/pkg/database"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
)
type UserService interface {
Create(ctx context.Context, req *request.CreateUserRequest) (*entity.User, error)
GetByID(ctx context.Context, id int64) (*entity.User, error)
List(ctx context.Context, req *request.ListUserRequest) ([]*entity.User, int64, error)
Update(ctx context.Context, id int64, req *request.UpdateUserRequest) error
Delete(ctx context.Context, id int64) error
Login(ctx context.Context, req *request.LoginRequest) (*auth.TokenPair, error)
ChangePassword(ctx context.Context, userID int64, req *request.ChangePasswordRequest) error
}
type userService struct {
userRepo repository.UserRepository
userCache cache.UserCache
txManager database.TxManager
jwtManager *auth.JWTManager
logger *zap.Logger
}
func NewUserService(
userRepo repository.UserRepository,
userCache cache.UserCache,
txManager database.TxManager,
jwtManager *auth.JWTManager,
logger *zap.Logger,
) UserService {
return &userService{
userRepo: userRepo,
userCache: userCache,
txManager: txManager,
jwtManager: jwtManager,
logger: logger,
}
}
func (s *userService) Create(ctx context.Context, req *request.CreateUserRequest) (*entity.User, error) {
// 检查邮箱是否已存在
existing, err := s.userRepo.FindByEmail(ctx, req.Email)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
if existing != nil {
return nil, errors.ErrUserExists
}
// 检查用户名是否已存在
existing, err = s.userRepo.FindByUsername(ctx, req.Username)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
if existing != nil {
return nil, errors.NewWithMessage(errors.CodeConflict, "用户名已存在")
}
// 密码加密
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
user := &entity.User{
Username: req.Username,
Email: req.Email,
Mobile: req.Mobile,
Password: string(hashedPassword),
RoleID: req.RoleID,
Status: entity.UserStatusActive,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
s.logger.Info("user created",
zap.Int64("user_id", user.ID),
zap.String("username", user.Username))
return user, nil
}
func (s *userService) GetByID(ctx context.Context, id int64) (*entity.User, error) {
// 先查缓存
user, err := s.userCache.GetUser(ctx, id)
if err == nil {
return user, nil
}
// 查数据库
user, err = s.userRepo.FindByID(ctx, id)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
if user == nil {
return nil, errors.ErrUserNotFound
}
// 异步写入缓存
go func() {
if err := s.userCache.SetUser(context.Background(), user); err != nil {
s.logger.Warn("failed to cache user", zap.Error(err))
}
}()
return user, nil
}
func (s *userService) List(ctx context.Context, req *request.ListUserRequest) ([]*entity.User, int64, error) {
users, total, err := s.userRepo.FindByCondition(ctx, &repository.UserQuery{
Keyword: req.Keyword,
Status: req.Status,
Page: req.Page,
PageSize: req.PageSize,
OrderBy: req.OrderBy,
Order: req.Order,
})
if err != nil {
return nil, 0, errors.Wrap(errors.CodeInternalError, err)
}
return users, total, nil
}
func (s *userService) Update(ctx context.Context, id int64, req *request.UpdateUserRequest) error {
user, err := s.userRepo.FindByID(ctx, id)
if err != nil {
return errors.Wrap(errors.CodeInternalError, err)
}
if user == nil {
return errors.ErrUserNotFound
}
// 检查用户名唯一性
if req.Username != nil && *req.Username != user.Username {
existing, err := s.userRepo.FindByUsername(ctx, *req.Username)
if err != nil {
return errors.Wrap(errors.CodeInternalError, err)
}
if existing != nil {
return errors.NewWithMessage(errors.CodeConflict, "用户名已存在")
}
user.Username = *req.Username
}
if req.Email != nil {
user.Email = *req.Email
}
if req.Mobile != nil {
user.Mobile = *req.Mobile
}
if req.Status != nil {
user.Status = *req.Status
}
user.UpdatedAt = time.Now()
if err := s.userRepo.Update(ctx, user); err != nil {
return errors.Wrap(errors.CodeInternalError, err)
}
// 删除缓存
_ = s.userCache.DeleteUser(ctx, id)
return nil
}
func (s *userService) Delete(ctx context.Context, id int64) error {
user, err := s.userRepo.FindByID(ctx, id)
if err != nil {
return errors.Wrap(errors.CodeInternalError, err)
}
if user == nil {
return errors.ErrUserNotFound
}
// 软删除
if err := s.userRepo.Delete(ctx, id); err != nil {
return errors.Wrap(errors.CodeInternalError, err)
}
// 删除缓存
_ = s.userCache.DeleteUser(ctx, id)
s.logger.Info("user deleted", zap.Int64("user_id", id))
return nil
}
func (s *userService) Login(ctx context.Context, req *request.LoginRequest) (*auth.TokenPair, error) {
user, err := s.userRepo.FindByUsername(ctx, req.Username)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
if user == nil {
return nil, errors.ErrUserNotFound
}
// 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
return nil, errors.ErrPasswordWrong
}
// 检查用户状态
if user.Status != entity.UserStatusActive {
return nil, errors.NewWithMessage(errors.CodeForbidden, "用户已被禁用")
}
// 生成 Token
tokenPair, err := s.jwtManager.GenerateTokenPair(user.ID, user.Username, user.RoleID)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
// 更新最后登录时间
go func() {
_ = s.userRepo.UpdateLastLogin(context.Background(), user.ID, time.Now())
}()
s.logger.Info("user logged in",
zap.Int64("user_id", user.ID),
zap.String("username", user.Username))
return tokenPair, nil
}
func (s *userService) ChangePassword(ctx context.Context, userID int64, req *request.ChangePasswordRequest) error {
user, err := s.userRepo.FindByID(ctx, userID)
if err != nil {
return errors.Wrap(errors.CodeInternalError, err)
}
if user == nil {
return errors.ErrUserNotFound
}
// 验证旧密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword)); err != nil {
return errors.NewWithMessage(errors.CodeBadRequest, "原密码错误")
}
// 加密新密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
return errors.Wrap(errors.CodeInternalError, err)
}
user.Password = string(hashedPassword)
user.UpdatedAt = time.Now()
if err := s.userRepo.Update(ctx, user); err != nil {
return errors.Wrap(errors.CodeInternalError, err)
}
return nil
}
Java 实现
// service/UserService.java
package com.example.service;
import com.example.dto.request.*;
import com.example.dto.response.*;
import com.example.entity.User;
public interface UserService {
UserResponse create(CreateUserRequest request);
UserResponse getById(Long id);
UserDetailResponse getDetail(Long id);
PageResult<UserResponse> list(ListUserRequest request);
void update(Long id, UpdateUserRequest request);
void delete(Long id);
TokenPairResponse login(LoginRequest request);
void changePassword(Long userId, ChangePasswordRequest request);
void updateStatus(Long id, Integer status);
}
// service/impl/UserServiceImpl.java
package com.example.service.impl;
import com.example.dto.request.*;
import com.example.dto.response.*;
import com.example.entity.User;
import com.example.enums.ErrorCode;
import com.example.enums.UserStatus;
import com.example.exception.BusinessException;
import com.example.mapper.UserMapper;
import com.example.service.UserService;
import com.example.util.JwtUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.*;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
@Override
@Transactional(rollbackFor = Exception.class)
public UserResponse create(CreateUserRequest request) {
// 检查邮箱是否已存在
if (userMapper.selectByEmail(request.getEmail()) != null) {
throw new BusinessException(ErrorCode.USER_EXISTS, "邮箱已被使用");
}
// 检查用户名是否已存在
if (userMapper.selectByUsername(request.getUsername()) != null) {
throw new BusinessException(ErrorCode.CONFLICT, "用户名已存在");
}
User user = new User();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setMobile(request.getMobile());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setRoleId(request.getRoleId());
user.setStatus(UserStatus.ACTIVE.getCode());
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
userMapper.insert(user);
log.info("User created: userId={}, username={}", user.getId(), user.getUsername());
return UserResponse.from(user);
}
@Override
@Cacheable(value = "user", key = "#id", unless = "#result == null")
public UserResponse getById(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
return UserResponse.from(user);
}
@Override
@Cacheable(value = "user:detail", key = "#id", unless = "#result == null")
public UserDetailResponse getDetail(Long id) {
User user = userMapper.selectWithRoleById(id);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
return UserDetailResponse.from(user);
}
@Override
public PageResult<UserResponse> list(ListUserRequest request) {
Page<User> page = new Page<>(request.getPage(), request.getPageSize());
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 关键词搜索
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w
.like(User::getUsername, request.getKeyword())
.or()
.like(User::getEmail, request.getKeyword())
.or()
.like(User::getMobile, request.getKeyword())
);
}
// 状态过滤
if (request.getStatus() != null) {
wrapper.eq(User::getStatus, request.getStatus());
}
// 排序
if ("created_at".equals(request.getOrderBy())) {
wrapper.orderBy(true, "asc".equals(request.getOrder()), User::getCreatedAt);
} else if ("updated_at".equals(request.getOrderBy())) {
wrapper.orderBy(true, "asc".equals(request.getOrder()), User::getUpdatedAt);
}
Page<User> result = userMapper.selectPage(page, wrapper);
return PageResult.of(
result.getRecords().stream().map(UserResponse::from).toList(),
result.getTotal(),
request.getPage(),
request.getPageSize()
);
}
@Override
@Transactional(rollbackFor = Exception.class)
@CacheEvict(value = {"user", "user:detail"}, key = "#id")
public void update(Long id, UpdateUserRequest request) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
// 检查用户名唯一性
if (StringUtils.hasText(request.getUsername())
&& !request.getUsername().equals(user.getUsername())) {
if (userMapper.selectByUsername(request.getUsername()) != null) {
throw new BusinessException(ErrorCode.CONFLICT, "用户名已存在");
}
user.setUsername(request.getUsername());
}
if (StringUtils.hasText(request.getEmail())) {
user.setEmail(request.getEmail());
}
if (StringUtils.hasText(request.getMobile())) {
user.setMobile(request.getMobile());
}
if (request.getStatus() != null) {
user.setStatus(request.getStatus());
}
user.setUpdatedAt(LocalDateTime.now());
userMapper.updateById(user);
log.info("User updated: userId={}", id);
}
@Override
@Transactional(rollbackFor = Exception.class)
@Caching(evict = {
@CacheEvict(value = "user", key = "#id"),
@CacheEvict(value = "user:detail", key = "#id")
})
public void delete(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
// 软删除
userMapper.deleteById(id);
log.info("User deleted: userId={}", id);
}
@Override
public TokenPairResponse login(LoginRequest request) {
User user = userMapper.selectByUsername(request.getUsername());
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
// 验证密码
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new BusinessException(ErrorCode.PASSWORD_WRONG);
}
// 检查用户状态
if (!UserStatus.ACTIVE.getCode().equals(user.getStatus())) {
throw new BusinessException(ErrorCode.FORBIDDEN, "用户已被禁用");
}
// 生成 Token
JwtUtil.TokenPair tokenPair = jwtUtil.generateTokenPair(
user.getId(), user.getUsername(), user.getRoleId());
// 异步更新最后登录时间
updateLastLoginAsync(user.getId());
log.info("User logged in: userId={}, username={}", user.getId(), user.getUsername());
return TokenPairResponse.from(tokenPair);
}
@Async
protected void updateLastLoginAsync(Long userId) {
try {
userMapper.updateLastLogin(userId, LocalDateTime.now());
} catch (Exception e) {
log.warn("Failed to update last login time: userId={}", userId, e);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void changePassword(Long userId, ChangePasswordRequest request) {
User user = userMapper.selectById(userId);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
// 验证旧密码
if (!passwordEncoder.matches(request.getOldPassword(), user.getPassword())) {
throw new BusinessException(ErrorCode.BAD_REQUEST, "原密码错误");
}
// 更新密码
user.setPassword(passwordEncoder.encode(request.getNewPassword()));
user.setUpdatedAt(LocalDateTime.now());
userMapper.updateById(user);
log.info("User password changed: userId={}", userId);
}
@Override
@Transactional(rollbackFor = Exception.class)
@CacheEvict(value = {"user", "user:detail"}, key = "#id")
public void updateStatus(Long id, Integer status) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
// 状态流转校验
if (UserStatus.DELETED.getCode().equals(user.getStatus())) {
throw new BusinessException(ErrorCode.BAD_REQUEST, "已删除的用户不能修改状态");
}
user.setStatus(status);
user.setUpdatedAt(LocalDateTime.now());
userMapper.updateById(user);
log.info("User status updated: userId={}, status={}", id, status);
}
}
Service 层对比表
| 方面 |
Go |
Java |
| 接口定义 |
interface 类型 |
interface 关键字 |
| 依赖注入 |
构造函数手动注入 |
@Autowired / @RequiredArgsConstructor |
| 事务管理 |
txManager.WithTransaction |
@Transactional 注解 |
| 缓存操作 |
手动调用 cache 方法 |
@Cacheable/@CacheEvict 注解 |
| 密码加密 |
bcrypt 包手动调用 |
PasswordEncoder Bean |
| 日志记录 |
zap.Logger 字段日志 |
@Slf4j + log.info() |
| 异步处理 |
go func() 协程 |
@Async 注解 |
| 错误返回 |
return nil, error |
throw Exception |
12.3 Repository / Mapper 层
Go 实现
// internal/repository/user_repository.go
package repository
import (
"context"
"time"
"myapp/internal/entity"
"gorm.io/gorm"
)
type UserQuery struct {
Keyword string
Status *int
RoleID *int64
Page int
PageSize int
OrderBy string
Order string
}
type UserRepository interface {
Create(ctx context.Context, user *entity.User) error
Update(ctx context.Context, user *entity.User) error
Delete(ctx context.Context, id int64) error
FindByID(ctx context.Context, id int64) (*entity.User, error)
FindByEmail(ctx context.Context, email string) (*entity.User, error)
FindByUsername(ctx context.Context, username string) (*entity.User, error)
FindByCondition(ctx context.Context, query *UserQuery) ([]*entity.User, int64, error)
UpdateLastLogin(ctx context.Context, id int64, loginTime time.Time) error
BatchUpdateStatus(ctx context.Context, ids []int64, status int) error
}
type userRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db}
}
// getDB 获取数据库连接(支持事务)
func (r *userRepository) getDB(ctx context.Context) *gorm.DB {
if tx, ok := ctx.Value(database.TxKey{}).(*gorm.DB); ok {
return tx
}
return r.db.WithContext(ctx)
}
func (r *userRepository) Create(ctx context.Context, user *entity.User) error {
return r.getDB(ctx).Create(user).Error
}
func (r *userRepository) Update(ctx context.Context, user *entity.User) error {
return r.getDB(ctx).Save(user).Error
}
func (r *userRepository) Delete(ctx context.Context, id int64) error {
// 软删除
return r.getDB(ctx).Delete(&entity.User{}, id).Error
}
func (r *userRepository) FindByID(ctx context.Context, id int64) (*entity.User, error) {
var user entity.User
err := r.getDB(ctx).First(&user, id).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByEmail(ctx context.Context, email string) (*entity.User, error) {
var user entity.User
err := r.getDB(ctx).Where("email = ?", email).First(&user).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByUsername(ctx context.Context, username string) (*entity.User, error) {
var user entity.User
err := r.getDB(ctx).Where("username = ?", username).First(&user).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByCondition(ctx context.Context, query *UserQuery) ([]*entity.User, int64, error) {
db := r.getDB(ctx).Model(&entity.User{})
// 关键词搜索
if query.Keyword != "" {
keyword := "%" + query.Keyword + "%"
db = db.Where("username LIKE ? OR email LIKE ? OR mobile LIKE ?",
keyword, keyword, keyword)
}
// 状态过滤
if query.Status != nil {
db = db.Where("status = ?", *query.Status)
}
// 角色过滤
if query.RoleID != nil {
db = db.Where("role_id = ?", *query.RoleID)
}
// 统计总数
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
// 排序
orderClause := "created_at DESC"
if query.OrderBy != "" {
order := "DESC"
if query.Order == "asc" {
order = "ASC"
}
orderClause = query.OrderBy + " " + order
}
db = db.Order(orderClause)
// 分页
offset := (query.Page - 1) * query.PageSize
db = db.Offset(offset).Limit(query.PageSize)
var users []*entity.User
if err := db.Find(&users).Error; err != nil {
return nil, 0, err
}
return users, total, nil
}
func (r *userRepository) UpdateLastLogin(ctx context.Context, id int64, loginTime time.Time) error {
return r.getDB(ctx).Model(&entity.User{}).
Where("id = ?", id).
Update("last_login_at", loginTime).Error
}
func (r *userRepository) BatchUpdateStatus(ctx context.Context, ids []int64, status int) error {
return r.getDB(ctx).Model(&entity.User{}).
Where("id IN ?", ids).
Updates(map[string]interface{}{
"status": status,
"updated_at": time.Now(),
}).Error
}
// 使用原生 SQL 的复杂查询示例
func (r *userRepository) FindWithRoleByID(ctx context.Context, id int64) (*entity.UserWithRole, error) {
var result entity.UserWithRole
err := r.getDB(ctx).Raw(`
SELECT u.*, r.name as role_name, r.code as role_code
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.id = ? AND u.deleted_at IS NULL
`, id).Scan(&result).Error
if err != nil {
return nil, err
}
if result.ID == 0 {
return nil, nil
}
return &result, nil
}
// 悲观锁查询
func (r *userRepository) FindByIDForUpdate(ctx context.Context, id int64) (*entity.User, error) {
var user entity.User
err := r.getDB(ctx).
Clauses(clause.Locking{Strength: "UPDATE"}).
First(&user, id).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &user, nil
}
Java 实现(MyBatis-Plus)
// mapper/UserMapper.java
package com.example.mapper;
import com.example.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.*;
import java.time.LocalDateTime;
import java.util.List;
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("SELECT * FROM users WHERE email = #{email} AND deleted_at IS NULL")
User selectByEmail(@Param("email") String email);
@Select("SELECT * FROM users WHERE username = #{username} AND deleted_at IS NULL")
User selectByUsername(@Param("username") String username);
@Update("UPDATE users SET last_login_at = #{loginTime} WHERE id = #{id}")
void updateLastLogin(@Param("id") Long id, @Param("loginTime") LocalDateTime loginTime);
@Update("<script>" +
"UPDATE users SET status = #{status}, updated_at = NOW() " +
"WHERE id IN " +
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>" +
"#{id}" +
"</foreach>" +
"</script>")
void batchUpdateStatus(@Param("ids") List<Long> ids, @Param("status") Integer status);
// 关联查询
@Select("SELECT u.*, r.name as role_name, r.code as role_code " +
"FROM users u " +
"LEFT JOIN roles r ON u.role_id = r.id " +
"WHERE u.id = #{id} AND u.deleted_at IS NULL")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "username", column = "username"),
@Result(property = "email", column = "email"),
@Result(property = "role.name", column = "role_name"),
@Result(property = "role.code", column = "role_code")
})
User selectWithRoleById(@Param("id") Long id);
// 悲观锁查询
@Select("SELECT * FROM users WHERE id = #{id} FOR UPDATE")
User selectByIdForUpdate(@Param("id") Long id);
// 复杂统计查询
@Select("SELECT DATE(created_at) as date, COUNT(*) as count " +
"FROM users " +
"WHERE created_at >= #{startDate} AND created_at < #{endDate} " +
"GROUP BY DATE(created_at) " +
"ORDER BY date")
List<UserDailyStats> selectDailyStats(@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate);
}
// mapper/UserMapper.xml(复杂查询用 XML)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
<resultMap id="UserWithRoleMap" type="com.example.entity.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="email" column="email"/>
<result property="mobile" column="mobile"/>
<result property="status" column="status"/>
<result property="createdAt" column="created_at"/>
<association property="role" javaType="com.example.entity.Role">
<id property="id" column="role_id"/>
<result property="name" column="role_name"/>
<result property="code" column="role_code"/>
</association>
</resultMap>
<select id="selectUsersWithRole" resultMap="UserWithRoleMap">
SELECT
u.id, u.username, u.email, u.mobile, u.status, u.created_at,
r.id as role_id, r.name as role_name, r.code as role_code
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.deleted_at IS NULL
<if test="query.keyword != null and query.keyword != ''">
AND (u.username LIKE CONCAT('%', #{query.keyword}, '%')
OR u.email LIKE CONCAT('%', #{query.keyword}, '%')
OR u.mobile LIKE CONCAT('%', #{query.keyword}, '%'))
</if>
<if test="query.status != null">
AND u.status = #{query.status}
</if>
<if test="query.roleId != null">
AND u.role_id = #{query.roleId}
</if>
ORDER BY
<choose>
<when test="query.orderBy == 'created_at'">
u.created_at
</when>
<when test="query.orderBy == 'updated_at'">
u.updated_at
</when>
<otherwise>
u.created_at
</otherwise>
</choose>
<if test="query.order == 'asc'">ASC</if>
<if test="query.order != 'asc'">DESC</if>
LIMIT #{query.pageSize} OFFSET #{query.offset}
</select>
<select id="countUsersWithCondition" resultType="long">
SELECT COUNT(*)
FROM users u
WHERE u.deleted_at IS NULL
<if test="query.keyword != null and query.keyword != ''">
AND (u.username LIKE CONCAT('%', #{query.keyword}, '%')
OR u.email LIKE CONCAT('%', #{query.keyword}, '%'))
</if>
<if test="query.status != null">
AND u.status = #{query.status}
</if>
</select>
</mapper>
// Java JPA Repository 实现(对比)
// repository/UserRepository.java
package com.example.repository;
import com.example.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import javax.persistence.LockModeType;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
Optional<User> findByEmail(String email);
Optional<User> findByUsername(String username);
@Modifying
@Query("UPDATE User u SET u.lastLoginAt = :loginTime WHERE u.id = :id")
void updateLastLogin(@Param("id") Long id, @Param("loginTime") LocalDateTime loginTime);
@Modifying
@Query("UPDATE User u SET u.status = :status, u.updatedAt = CURRENT_TIMESTAMP WHERE u.id IN :ids")
void batchUpdateStatus(@Param("ids") List<Long> ids, @Param("status") Integer status);
// 悲观锁
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT u FROM User u WHERE u.id = :id")
Optional<User> findByIdForUpdate(@Param("id") Long id);
// 自定义查询
@Query("SELECT u FROM User u LEFT JOIN FETCH u.role WHERE u.id = :id")
Optional<User> findWithRoleById(@Param("id") Long id);
// 统计查询
@Query(value = "SELECT DATE(created_at) as date, COUNT(*) as count " +
"FROM users WHERE created_at >= :start AND created_at < :end " +
"GROUP BY DATE(created_at)", nativeQuery = true)
List<Object[]> findDailyStats(@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
// 存在性检查
boolean existsByEmail(String email);
boolean existsByUsername(String username);
// 软删除
@Modifying
@Query("UPDATE User u SET u.deletedAt = CURRENT_TIMESTAMP WHERE u.id = :id")
void softDelete(@Param("id") Long id);
}
Repository/Mapper 层对比表
| 方面 |
Go (GORM) |
Java (MyBatis) |
Java (JPA) |
| 定义方式 |
interface + struct 实现 |
interface + 注解/XML |
interface 继承 JpaRepository |
| 简单查询 |
db.First/Find |
@Select 注解 |
方法名派生查询 |
| 复杂查询 |
链式调用 / Raw SQL |
XML 动态 SQL |
@Query / Specification |
| 分页 |
Offset/Limit |
PageHelper / 手动 |
Pageable 参数 |
| 事务支持 |
Context 传递 tx |
自动参与事务 |
自动参与事务 |
| 悲观锁 |
clause.Locking |
FOR UPDATE |
@Lock 注解 |
| 批量操作 |
Where IN + Updates |
foreach 标签 |
@Modifying + @Query |
| 关联查询 |
Preload / Joins / Raw |
resultMap / 嵌套查询 |
FETCH JOIN |
12.4 Entity / Model 层
Go 实现
// internal/entity/user.go
package entity
import (
"time"
"gorm.io/gorm"
)
// 用户状态
const (
UserStatusActive = 1
UserStatusDisabled = 2
UserStatusDeleted = 3
)
type User struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Username string `gorm:"size:32;uniqueIndex;not null" json:"username"`
Email string `gorm:"size:128;uniqueIndex;not null" json:"email"`
Mobile string `gorm:"size:20;index" json:"mobile"`
Password string `gorm:"size:128;not null" json:"-"` // json:"-" 不序列化
RoleID int64 `gorm:"index;not null" json:"role_id"`
Status int `gorm:"default:1;not null" json:"status"`
Avatar string `gorm:"size:256" json:"avatar"`
LastLoginAt *time.Time `json:"last_login_at"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 软删除
// 关联(不映射到数据库)
Role *Role `gorm:"foreignKey:RoleID" json:"role,omitempty"`
}
func (User) TableName() string {
return "users"
}
// IsActive 判断用户是否激活
func (u *User) IsActive() bool {
return u.Status == UserStatusActive
}
// 带角色信息的用户(用于关联查询结果)
type UserWithRole struct {
User
RoleName string `json:"role_name"`
RoleCode string `json:"role_code"`
}
// internal/entity/role.go
type Role struct {
ID int64 `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:32;not null" json:"name"`
Code string `gorm:"size:32;uniqueIndex;not null" json:"code"`
Description string `gorm:"size:256" json:"description"`
Status int `gorm:"default:1" json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Permissions []Permission `gorm:"many2many:role_permissions" json:"permissions,omitempty"`
}
// internal/entity/permission.go
type Permission struct {
ID int64 `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:64;not null" json:"name"`
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
Resource string `gorm:"size:32" json:"resource"`
Action string `gorm:"size:32" json:"action"`
CreatedAt time.Time `json:"created_at"`
}
// internal/entity/order.go
type Order struct {
ID int64 `gorm:"primaryKey" json:"id"`
OrderNo string `gorm:"size:32;uniqueIndex;not null" json:"order_no"`
UserID int64 `gorm:"index;not null" json:"user_id"`
ProductID int64 `gorm:"index;not null" json:"product_id"`
Quantity int `gorm:"not null" json:"quantity"`
TotalPrice float64 `gorm:"type:decimal(10,2);not null" json:"total_price"`
Status int `gorm:"default:1;not null" json:"status"`
PaymentTime *time.Time `json:"payment_time"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
Product *Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
}
// 订单状态
const (
OrderStatusPending = 1 // 待支付
OrderStatusPaid = 2 // 已支付
OrderStatusShipped = 3 // 已发货
OrderStatusCompleted = 4 // 已完成
OrderStatusCancelled = 5 // 已取消
)
// CanCancel 判断订单是否可取消
func (o *Order) CanCancel() bool {
return o.Status == OrderStatusPending || o.Status == OrderStatusPaid
}
Java 实现
// entity/User.java
package com.example.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("users")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String email;
private String mobile;
private String password;
private Long roleId;
private Integer status;
private String avatar;
private LocalDateTime lastLoginAt;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
@TableLogic // 逻辑删除
private LocalDateTime deletedAt;
// 非数据库字段
@TableField(exist = false)
private Role role;
public boolean isActive() {
return UserStatus.ACTIVE.getCode().equals(this.status);
}
}
// entity/User.java(JPA 版本)
package com.example.entity;
import lombok.Data;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;
import javax.persistence.*;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "users", indexes = {
@Index(name = "idx_email", columnList = "email"),
@Index(name = "idx_username", columnList = "username"),
@Index(name = "idx_role_id", columnList = "role_id")
})
@SQLDelete(sql = "UPDATE users SET deleted_at = NOW() WHERE id = ?")
@Where(clause = "deleted_at IS NULL")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 32, nullable = false, unique = true)
private String username;
@Column(length = 128, nullable = false, unique = true)
private String email;
@Column(length = 20)
private String mobile;
@Column(length = 128, nullable = false)
private String password;
@Column(name = "role_id", nullable = false)
private Long roleId;
@Column(nullable = false)
private Integer status = 1;
@Column(length = 256)
private String avatar;
@Column(name = "last_login_at")
private LocalDateTime lastLoginAt;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "deleted_at")
private LocalDateTime deletedAt;
// 关联关系
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "role_id", insertable = false, updatable = false)
private Role role;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
public boolean isActive() {
return UserStatus.ACTIVE.getCode().equals(this.status);
}
}
// enums/UserStatus.java
package com.example.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum UserStatus {
ACTIVE(1, "正常"),
DISABLED(2, "禁用"),
DELETED(3, "删除");
private final Integer code;
private final String description;
public static UserStatus fromCode(Integer code) {
for (UserStatus status : values()) {
if (status.code.equals(code)) {
return status;
}
}
return null;
}
}
// entity/Role.java
@Data
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 32, nullable = false)
private String name;
@Column(length = 32, nullable = false, unique = true)
private String code;
@Column(length = 256)
private String description;
private Integer status = 1;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
private List<Permission> permissions;
}
// entity/Order.java
@Data
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_no", length = 32, nullable = false, unique = true)
private String orderNo;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "product_id", nullable = false)
private Long productId;
@Column(nullable = false)
private Integer quantity;
@Column(name = "total_price", precision = 10, scale = 2, nullable = false)
private BigDecimal totalPrice;
@Column(nullable = false)
private Integer status = 1;
@Column(name = "payment_time")
private LocalDateTime paymentTime;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", insertable = false, updatable = false)
private Product product;
public boolean canCancel() {
return OrderStatus.PENDING.getCode().equals(status)
|| OrderStatus.PAID.getCode().equals(status);
}
}
Entity 层对比表
| 方面 |
Go (GORM) |
Java (MyBatis-Plus) |
Java (JPA) |
| 主键定义 |
gorm:"primaryKey" |
@TableId |
@Id + @GeneratedValue |
| 自增 |
autoIncrement |
IdType.AUTO |
GenerationType.IDENTITY |
| 表名 |
TableName() 方法 |
@TableName |
@Table(name=) |
| 字段映射 |
自动驼峰转下划线 |
自动转换 |
@Column(name=) |
| 唯一索引 |
uniqueIndex |
数据库层定义 |
unique = true |
| 软删除 |
gorm.DeletedAt |
@TableLogic |
@SQLDelete + @Where |
| 自动时间 |
autoCreateTime |
@TableField(fill=) |
@PrePersist |
| 关联关系 |
gorm:"foreignKey" |
@TableField(exist=false) |
@ManyToOne 等 |
| 忽略字段 |
gorm:"-" |
@TableField(exist=false) |
@Transient |
| JSON 忽略 |
json:"-" |
@JsonIgnore |
@JsonIgnore |
12.5 DTO 层
Go 实现
// internal/dto/request/user_request.go
package request
// CreateUserRequest 创建用户请求
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=32"`
Email string `json:"email" binding:"required,email"`
Mobile string `json:"mobile" binding:"required,mobile"`
Password string `json:"password" binding:"required,min=8,max=64"`
RoleID int64 `json:"role_id" binding:"required,gt=0"`
}
// UpdateUserRequest 更新用户请求
type UpdateUserRequest struct {
Username *string `json:"username" binding:"omitempty,min=3,max=32"`
Email *string `json:"email" binding:"omitempty,email"`
Mobile *string `json:"mobile" binding:"omitempty,mobile"`
Status *int `json:"status" binding:"omitempty,oneof=1 2"`
Avatar *string `json:"avatar" binding:"omitempty,url"`
}
// ListUserRequest 用户列表请求
type ListUserRequest struct {
Page int `form:"page" binding:"omitempty,gte=1"`
PageSize int `form:"page_size" binding:"omitempty,gte=1,lte=100"`
Keyword string `form:"keyword" binding:"omitempty,max=64"`
Status *int `form:"status" binding:"omitempty,oneof=1 2 3"`
RoleID *int64 `form:"role_id" binding:"omitempty,gt=0"`
OrderBy string `form:"order_by" binding:"omitempty,oneof=created_at updated_at"`
Order string `form:"order" binding:"omitempty,oneof=asc desc"`
}
func (r *ListUserRequest) SetDefaults() {
if r.Page <= 0 {
r.Page = 1
}
if r.PageSize <= 0 {
r.PageSize = 20
}
if r.OrderBy == "" {
r.OrderBy = "created_at"
}
if r.Order == "" {
r.Order = "desc"
}
}
func (r *ListUserRequest) Offset() int {
return (r.Page - 1) * r.PageSize
}
// LoginRequest 登录请求
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// ChangePasswordRequest 修改密码请求
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=8,max=64,nefield=OldPassword"`
}
// internal/dto/response/user_response.go
package response
import (
"myapp/internal/entity"
"time"
)
// UserResponse 用户响应
type UserResponse struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Mobile string `json:"mobile"`
RoleID int64 `json:"role_id"`
Status int `json:"status"`
Avatar string `json:"avatar"`
CreatedAt time.Time `json:"created_at"`
}
// ToUserResponse 转换实体到响应
func ToUserResponse(user *entity.User) *UserResponse {
if user == nil {
return nil
}
return &UserResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Mobile: maskMobile(user.Mobile),
RoleID: user.RoleID,
Status: user.Status,
Avatar: user.Avatar,
CreatedAt: user.CreatedAt,
}
}
// UserDetailResponse 用户详情响应
type UserDetailResponse struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Mobile string `json:"mobile"`
RoleID int64 `json:"role_id"`
RoleName string `json:"role_name"`
Status int `json:"status"`
StatusText string `json:"status_text"`
Avatar string `json:"avatar"`
LastLoginAt *time.Time `json:"last_login_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func ToUserDetailResponse(user *entity.User) *UserDetailResponse {
if user == nil {
return nil
}
resp := &UserDetailResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Mobile: maskMobile(user.Mobile),
RoleID: user.RoleID,
Status: user.Status,
StatusText: getStatusText(user.Status),
Avatar: user.Avatar,
LastLoginAt: user.LastLoginAt,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
if user.Role != nil {
resp.RoleName = user.Role.Name
}
return resp
}
// 手机号脱敏
func maskMobile(mobile string) string {
if len(mobile) != 11 {
return mobile
}
return mobile[:3] + "****" + mobile[7:]
}
func getStatusText(status int) string {
switch status {
case 1:
return "正常"
case 2:
return "禁用"
case 3:
return "删除"
default:
return "未知"
}
}
// TokenPairResponse Token 响应
type TokenPairResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
TokenType string `json:"token_type"`
}
func ToTokenPairResponse(pair *auth.TokenPair) *TokenPairResponse {
return &TokenPairResponse{
AccessToken: pair.AccessToken,
RefreshToken: pair.RefreshToken,
ExpiresIn: pair.ExpiresIn,
TokenType: "Bearer",
}
}
// internal/dto/response/common.go
package response
// Response 统一响应结构
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
RequestID string `json:"request_id,omitempty"`
Timestamp int64 `json:"timestamp"`
}
// PageData 分页数据
type PageData struct {
List interface{} `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
Pages int `json:"pages"`
}
func NewPageData(list interface{}, total int64, page, pageSize int) *PageData {
pages := int(total) / pageSize
if int(total)%pageSize > 0 {
pages++
}
return &PageData{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
Pages: pages,
}
}
Java 实现
// dto/request/CreateUserRequest.java
package com.example.dto.request;
import com.example.validator.Mobile;
import lombok.Data;
import javax.validation.constraints.*;
@Data
public class CreateUserRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 32, message = "用户名长度需在3-32个字符之间")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "手机号不能为空")
@Mobile
private String mobile;
@NotBlank(message = "密码不能为空")
@Size(min = 8, max = 64, message = "密码长度需在8-64个字符之间")
private String password;
@NotNull(message = "角色ID不能为空")
@Positive(message = "角色ID必须为正数")
private Long roleId;
}
// dto/request/UpdateUserRequest.java
@Data
public class UpdateUserRequest {
@Size(min = 3, max = 32, message = "用户名长度需在3-32个字符之间")
private String username;
@Email(message = "邮箱格式不正确")
private String email;
@Mobile
private String mobile;
@Min(value = 1)
@Max(value = 2)
private Integer status;
@Pattern(regexp = "^https?://.*", message = "头像必须是有效的URL")
private String avatar;
}
// dto/request/ListUserRequest.java
@Data
public class ListUserRequest {
@Min(value = 1, message = "页码最小为1")
private Integer page = 1;
@Min(value = 1, message = "每页条数最小为1")
@Max(value = 100, message = "每页条数最大为100")
private Integer pageSize = 20;
@Size(max = 64, message = "关键词最大64个字符")
private String keyword;
private Integer status;
private Long roleId;
@Pattern(regexp = "^(created_at|updated_at)$", message = "排序字段不合法")
private String orderBy = "created_at";
@Pattern(regexp = "^(asc|desc)$", message = "排序方向不合法")
private String order = "desc";
public int getOffset() {
return (page - 1) * pageSize;
}
}
// dto/request/LoginRequest.java
@Data
public class LoginRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
// dto/request/ChangePasswordRequest.java
@Data
public class ChangePasswordRequest {
@NotBlank(message = "原密码不能为空")
private String oldPassword;
@NotBlank(message = "新密码不能为空")
@Size(min = 8, max = 64, message = "密码长度需在8-64个字符之间")
private String newPassword;
@AssertTrue(message = "新密码不能与原密码相同")
private boolean isPasswordDifferent() {
return newPassword == null || !newPassword.equals(oldPassword);
}
}
// dto/response/UserResponse.java
package com.example.dto.response;
import com.example.entity.User;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class UserResponse {
private Long id;
private String username;
private String email;
private String mobile;
private Long roleId;
private Integer status;
private String avatar;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
public static UserResponse from(User user) {
if (user == null) {
return null;
}
UserResponse response = new UserResponse();
response.setId(user.getId());
response.setUsername(user.getUsername());
response.setEmail(user.getEmail());
response.setMobile(maskMobile(user.getMobile()));
response.setRoleId(user.getRoleId());
response.setStatus(user.getStatus());
response.setAvatar(user.getAvatar());
response.setCreatedAt(user.getCreatedAt());
return response;
}
private static String maskMobile(String mobile) {
if (mobile == null || mobile.length() != 11) {
return mobile;
}
return mobile.substring(0, 3) + "****" + mobile.substring(7);
}
}
// dto/response/UserDetailResponse.java
@Data
public class UserDetailResponse {
private Long id;
private String username;
private String email;
private String mobile;
private Long roleId;
private String roleName;
private Integer status;
private String statusText;
private String avatar;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime lastLoginAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatedAt;
public static UserDetailResponse from(User user) {
if (user == null) {
return null;
}
UserDetailResponse response = new UserDetailResponse();
response.setId(user.getId());
response.setUsername(user.getUsername());
response.setEmail(user.getEmail());
response.setMobile(maskMobile(user.getMobile()));
response.setRoleId(user.getRoleId());
response.setStatus(user.getStatus());
response.setStatusText(UserStatus.fromCode(user.getStatus()).getDescription());
response.setAvatar(user.getAvatar());
response.setLastLoginAt(user.getLastLoginAt());
response.setCreatedAt(user.getCreatedAt());
response.setUpdatedAt(user.getUpdatedAt());
if (user.getRole() != null) {
response.setRoleName(user.getRole().getName());
}
return response;
}
}
// dto/response/TokenPairResponse.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TokenPairResponse {
private String accessToken;
private String refreshToken;
private Long expiresIn;
private String tokenType = "Bearer";
public static TokenPairResponse from(JwtUtil.TokenPair pair) {
return new TokenPairResponse(
pair.getAccessToken(),
pair.getRefreshToken(),
pair.getExpiresIn(),
"Bearer"
);
}
}
// dto/response/CommonResponse.java
@Data
@NoArgsConstructor
public class CommonResponse<T> {
private int code;
private String message;
private T data;
private String requestId;
private long timestamp;
public CommonResponse(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
this.timestamp = System.currentTimeMillis();
this.requestId = MDC.get("request_id");
}
public static <T> CommonResponse<T> success() {
return new CommonResponse<>(0, "success", null);
}
public static <T> CommonResponse<T> success(T data) {
return new CommonResponse<>(0, "success", data);
}
public static <T> CommonResponse<T> success(String message) {
return new CommonResponse<>(0, message, null);
}
public static <T> CommonResponse<T> error(ErrorCode errorCode) {
return new CommonResponse<>(errorCode.getCode(), errorCode.getMessage(), null);
}
public static <T> CommonResponse<T> error(ErrorCode errorCode, String message) {
return new CommonResponse<>(errorCode.getCode(), message, null);
}
}
// dto/response/PageResult.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResult<T> {
private List<T> list;
private long total;
private int page;
private int pageSize;
private int pages;
public static <T> PageResult<T> of(List<T> list, long total, int page, int pageSize) {
PageResult<T> result = new PageResult<>();
result.setList(list);
result.setTotal(total);
result.setPage(page);
result.setPageSize(pageSize);
result.setPages((int) Math.ceil((double) total / pageSize));
return result;
}
public static <T, E> PageResult<T> of(IPage<E> page, Function<E, T> converter) {
List<T> list = page.getRecords().stream()
.map(converter)
.collect(Collectors.toList());
return of(list, page.getTotal(), (int) page.getCurrent(), (int) page.getSize());
}
}
DTO 层对比表
| 方面 |
Go |
Java |
| 请求绑定 |
struct tags (binding) |
@Valid + JSR-380 注解 |
| 可选字段 |
指针类型 (*string) |
包装类型 (String) |
| 默认值 |
SetDefaults() 方法 |
字段初始化 |
| 响应转换 |
ToXxxResponse 函数 |
static from() 方法 |
| 时间格式 |
json tag 或序列化器 |
@JsonFormat |
| 数据脱敏 |
手动处理 |
手动处理 |
| 分页封装 |
PageData 结构体 |
PageResult 泛型类 |
| 统一响应 |
Response 结构体 |
CommonResponse 泛型类 |
十三、关键特性总结对比
13.1 核心差异总表
graph LR
subgraph "Go 技术栈"
G1[Gin/Echo] --> G2[手动 DI]
G2 --> G3[GORM/sqlx]
G3 --> G4[go-redis]
G4 --> G5[Zap Logger]
end
subgraph "Java 技术栈"
J1[Spring MVC] --> J2[Spring IoC]
J2 --> J3[MyBatis/JPA]
J3 --> J4[Spring Cache]
J4 --> J5[Logback/Log4j2]
end
subgraph "共同关注点"
C1[分层架构]
C2[参数校验]
C3[认证鉴权]
C4[异常处理]
C5[日志审计]
end
13.2 设计理念对比
| 方面 |
Go |
Java |
| 哲学 |
显式优于隐式 |
约定优于配置 |
| 依赖注入 |
构造函数手动组装 |
IoC 容器自动注入 |
| AOP 实现 |
中间件 + 装饰器模式 |
动态代理 + 注解 |
| 错误处理 |
返回值 (value, error) |
异常抛出 try-catch |
| 并发模型 |
Goroutine + Channel |
Thread + CompletableFuture |
| 配置管理 |
Viper + 结构体 |
@Value + Properties |
| 代码生成 |
较少依赖 |
Lombok / MapStruct |
| 框架侵入 |
低侵入 |
较高侵入 |
13.3 性能与资源对比
| 指标 |
Go |
Java (Spring Boot) |
| 启动时间 |
~100ms |
~3-10s |
| 内存占用 |
~10-30MB |
~200-500MB |
| 编译产物 |
单二进制文件 |
JAR + JRE |
| 冷启动 |
快 |
慢(适合预热) |
| GC 停顿 |
低延迟 |
G1/ZGC 可调优 |
| 并发处理 |
Goroutine 轻量 |
线程池管理 |
13.4 开发效率对比
| 方面 |
Go |
Java |
| 学习曲线 |
语法简单,生态需学习 |
语法复杂,生态成熟 |
| 代码量 |
相对多(显式代码) |
相对少(注解魔法) |
| IDE 支持 |
GoLand / VSCode |
IntelliJ IDEA |
| 调试体验 |
Delve 调试器 |
成熟的 Debug 工具 |
| 重构支持 |
一般 |
强大 |
| 文档生成 |
swaggo |
SpringDoc/Swagger |
13.5 适用场景建议
graph TB
subgraph "Go 更适合"
GA[高性能微服务]
GB[云原生应用]
GC[DevOps 工具]
GD[网络密集型服务]
GE[容器化部署]
end
subgraph "Java 更适合"
JA[大型企业应用]
JB[复杂业务系统]
JC[遗留系统集成]
JD[团队规模大]
JE[生态依赖多]
end
subgraph "均可胜任"
CA[REST API 服务]
CB[CRUD 应用]
CC[消息队列消费者]
CD[定时任务服务]
end
十四、附录:完整项目结构参考
Go 项目结构
myapp/
├── cmd/
│ └── server/
│ └── main.go # 程序入口
├── internal/
│ ├── handler/ # HTTP 处理器
│ │ ├── user_handler.go
│ │ ├── order_handler.go
│ │ └── middleware/
│ │ ├── auth.go
│ │ ├── permission.go
│ │ ├── logger.go
│ │ ├── ratelimit.go
│ │ ├── recovery.go
│ │ └── cors.go
│ ├── service/ # 业务逻辑
│ │ ├── user_service.go
│ │ ├── order_service.go
│ │ └── permission_service.go
│ ├── repository/ # 数据访问
│ │ ├── user_repository.go
│ │ ├── order_repository.go
│ │ └── cache/
│ │ └── user_cache.go
│ ├── entity/ # 实体定义
│ │ ├── user.go
│ │ ├── order.go
│ │ └── role.go
│ ├── dto/
│ │ ├── request/
│ │ │ └── user_request.go
│ │ └── response/
│ │ ├── user_response.go
│ │ └── common.go
│ └── errors/
│ └── errors.go
├── pkg/
│ ├── auth/
│ │ └── jwt.go
│ ├── cache/
│ │ └── redis.go
│ ├── logger/
│ │ └── logger.go
│ ├── validator/
│ │ └── validator.go
│ ├── database/
│ │ └── transaction.go
│ └── ratelimit/
│ └── ratelimit.go
├── config/
│ ├── config.go
│ └── config.yaml
├── docs/ # Swagger 文档
├── migrations/ # 数据库迁移
├── scripts/ # 构建脚本
├── Dockerfile
├── docker-compose.yaml
├── Makefile
└── go.mod
Java 项目结构
myapp/
├── src/main/java/com/example/
│ ├── Application.java
│ ├── controller/
│ │ ├── UserController.java
│ │ └── OrderController.java
│ ├── service/
│ │ ├── UserService.java
│ │ ├── OrderService.java
│ │ └── impl/
│ │ ├── UserServiceImpl.java
│ │ └── OrderServiceImpl.java
│ ├── mapper/ # MyBatis Mapper
│ │ ├── UserMapper.java
│ │ └── OrderMapper.java
│ ├── entity/
│ │ ├── User.java
│ │ ├── Order.java
│ │ └── Role.java
│ ├── dto/
│ │ ├── request/
│ │ │ ├── CreateUserRequest.java
│ │ │ └── ListUserRequest.java
│ │ └── response/
│ │ ├── UserResponse.java
│ │ ├── CommonResponse.java
│ │ └── PageResult.java
│ ├── config/
│ │ ├── SecurityConfig.java
│ │ ├── RedisConfig.java
│ │ ├── WebMvcConfig.java
│ │ └── Resilience4jConfig.java
│ ├── filter/
│ │ └── JwtAuthenticationFilter.java
│ ├── interceptor/
│ │ ├── AuthInterceptor.java
│ │ └── RateLimitInterceptor.java
│ ├── aspect/
│ │ ├── LogAspect.java
│ │ ├── PermissionAspect.java
│ │ └── AuditLogAspect.java
│ ├── exception/
│ │ ├── BusinessException.java
│ │ └── GlobalExceptionHandler.java
│ ├── enums/
│ │ ├── ErrorCode.java
│ │ └── UserStatus.java
│ ├── annotation/
│ │ ├── RequirePermission.java
│ │ └── AuditLog.java
│ ├── validator/
│ │ ├── Mobile.java
│ │ └── MobileValidator.java
│ └── util/
│ ├── JwtUtil.java
│ └── SecurityUtils.java
├── src/main/resources/
│ ├── mapper/
│ │ └── UserMapper.xml
│ ├── application.yml
│ ├── application-dev.yml
│ ├── application-prod.yml
│ └── logback-spring.xml
├── src/test/
├── Dockerfile
├── docker-compose.yaml
└── pom.xml
结语
本文档从实际企业项目出发,系统对比了 Go 与 Java 在分层架构各层的实现差异。两种语言/框架各有优势:
- Go:简洁、高性能、低资源占用,适合云原生和高并发场景
- Java:生态丰富、工具链成熟、企业级特性完善,适合大型复杂业务系统
选择时应综合考虑团队技术栈、项目规模、性能需求和长期维护成本。核心的架构思想(分层、解耦、抽象)是通用的,具体实现可根据语言特性灵活调整。