文档概述
本文档系统对比 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 请求处理流程图
3.2 完整请求时序图
Go 请求时序图
Java 请求时序图
3.3 认证授权流程图
3.4 权限校验流程图
3.5 缓存处理流程图
3.6 异常处理流程图
3.7 事务处理流程图
3.8 完整系统架构图
3.9 数据流转图
3.10 并发处理模型对比
四、参数校验
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()
}
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"
}
}
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 实现
@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 {};
}
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;
}
return MOBILE_PATTERN.matcher(value).matches();
}
}
@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;
}
@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
private Integer status;
}
@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";
}
@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)
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()
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
}
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
}
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]
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()
}
}
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)
@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;
}
}
}
@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));
}
}
@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)
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
}
_ = 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
}
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
}
package middleware
import (
"github.com/gin-gonic/gin"
)
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()
}
}
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()
}
}
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()
}
}
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 + 自定义注解)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String[] value();
Logical logical() default Logical.AND;
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
String[] value();
}
public enum Logical {
AND,
OR
}
public interface PermissionService {
List<String> getRolePermissions(Long roleId);
boolean hasPermission(Long roleId, String permCode);
boolean checkPermissions(Long roleId, List<String> permCodes, Logical logical);
}
@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
@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;
}
}
@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();
}
@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();
}
@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));
}
}
@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 实现
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")
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))
}
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) {
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))
}
user, err = s.userRepo.FindByID(ctx, id)
if err != nil {
return nil, err
}
if user == nil {
return nil, ErrUserNotFound
}
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 {
if err := s.userRepo.Update(ctx, id, req); err != nil {
return err
}
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 {
if err := s.userRepo.Delete(ctx, id); err != nil {
return err
}
if err := s.userCache.DeleteUser(ctx, id); err != nil {
s.logger.Warn("failed to delete cache", zap.Error(err))
}
return nil
}
func (s *userService) GetByIDWithProtection(ctx context.Context, id int64) (*entity.User, error) {
if s.bloomFilter != nil && !s.bloomFilter.Test([]byte(fmt.Sprintf("user:%d", id))) {
return nil, ErrUserNotFound
}
user, err := s.userCache.GetUser(ctx, id)
if err == nil {
if user.ID == 0 {
return nil, ErrUserNotFound
}
return user, nil
}
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)
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, err
}
if user == nil {
_ = s.userCache.SetUser(ctx, &entity.User{})
return nil, ErrUserNotFound
}
_ = s.userCache.SetUser(ctx, user)
return user, nil
}
Java 实现(Spring Cache + Redis)
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
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
@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;
Object cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
if (cached instanceof NullValue) {
return null;
}
return (User) cached;
}
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 {
cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached instanceof NullValue ? null : (User) cached;
}
User user = userMapper.selectById(id);
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;
}
}
@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
@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);
Object cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
if (cached instanceof NullValue) {
return null;
}
log.debug("Cache hit: {}", cacheKey);
return cached;
}
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 {
cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached instanceof NullValue ? null : cached;
}
Object result = pjp.proceed();
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)
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
MaxBackups int
MaxAge int
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...)
}
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))
}
blw := &responseWriter{
ResponseWriter: c.Writer,
body: bytes.NewBuffer(nil),
}
c.Writer = blw
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))
}
package service
type AuditLog struct {
ID int64
UserID int64
Username string
Action string
Resource string
ResourceID string
OldValue string
NewValue string
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)
<?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>
@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();
String requestId = request.getHeader("X-Request-ID");
if (requestId == null || requestId.isEmpty()) {
requestId = UUID.randomUUID().toString();
}
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
@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;
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
AuditAction action();
String resource();
String resourceIdExpr() default "";
}
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 实现
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
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
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)
)
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),
})
}
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()
}
}
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
}
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 实现
@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, "服务暂不可用"),
USER_NOT_FOUND(10001, "用户不存在"),
USER_EXISTS(10002, "用户已存在"),
PASSWORD_WRONG(10003, "密码错误"),
TOKEN_EXPIRED(10004, "Token已过期"),
TOKEN_INVALID(10005, "Token无效"),
PERMISSION_DENIED(10006, "权限不足"),
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;
}
}
@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;
}
}
public class NotFoundException extends BusinessException {
public NotFoundException(String message) {
super(ErrorCode.NOT_FOUND, message);
}
}
public class ForbiddenException extends BusinessException {
public ForbiddenException(String message) {
super(ErrorCode.FORBIDDEN, message);
}
}
@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);
}
}
@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());
}
}
@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);
}
@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);
}
@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, "请求体格式错误"));
}
@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
@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);
}
}
@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 实现
package database
import (
"context"
"database/sql"
)
type TxKey struct{}
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
}
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()
}
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)
})
}
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
}
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 {
user, err := s.userRepo.FindByID(txCtx, req.UserID)
if err != nil {
return err
}
if user == nil {
return errors.ErrUserNotFound
}
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
}
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
}
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
}
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)
}
}()
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)
})
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)
})
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
@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) {
User user = userMapper.selectById(request.getUserId());
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
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);
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);
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);
}
}
@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;
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);
}
}
}
@Service
@RequiredArgsConstructor
@Slf4j
public class DistributedOrderService {
private final OrderMapper orderMapper;
private final ProductFeignClient productClient;
private final PaymentFeignClient paymentClient;
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public Order createDistributedOrder(CreateOrderRequest request) {
productClient.deductStock(request.getProductId(), request.getQuantity());
Order order = new Order();
order.setUserId(request.getUserId());
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setStatus(OrderStatus.PENDING.getCode());
orderMapper.insert(order);
paymentClient.createPayment(order.getId(), order.getTotalPrice());
return order;
}
@GlobalTransactional
public Order createOrderWithTcc(CreateOrderRequest request) {
productClient.tryDeductStock(request.getProductId(), request.getQuantity());
Order order = new Order();
order.setStatus(OrderStatus.TRYING.getCode());
orderMapper.insert(order);
paymentClient.tryCreatePayment(order.getId(), order.getTotalPrice());
return order;
}
}
@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 {
productClient.deductStock(request.getProductId(), request.getQuantity());
compensations.add(() ->
productClient.addStock(request.getProductId(), request.getQuantity()));
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()));
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 实现
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)
}
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
}
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
}
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
}
package middleware
import (
"github.com/gin-gonic/gin"
)
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()
}
}
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()
}
}
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)
@Configuration
public class Resilience4jConfig {
@Bean
public RateLimiterConfig rateLimiterConfig() {
return RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofSeconds(1))
.limitForPeriod(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)
.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);
}
}
@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();
}
}
@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);
}
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;
}
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);
}
}
@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);
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 实现
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}
}
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))
}
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))
}
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))
}
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)
}
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)
}
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)
}
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 实现
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 实现
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, "用户已被禁用")
}
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 实现
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);
}
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, "用户已被禁用");
}
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 实现
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}
}
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
}
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)
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);
}
<?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>
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 实现
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:"-"`
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"
}
func (u *User) IsActive() bool {
return u.Status == UserStatusActive
}
type UserWithRole struct {
User
RoleName string `json:"role_name"`
RoleCode string `json:"role_code"`
}
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"`
}
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"`
}
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
)
func (o *Order) CanCancel() bool {
return o.Status == OrderStatusPending || o.Status == OrderStatusPaid
}
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);
}
}
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);
}
}
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;
}
}
@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;
}
@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 实现
package request
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"`
}
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"`
}
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
}
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=8,max=64,nefield=OldPassword"`
}
package response
import (
"myapp/internal/entity"
"time"
)
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"`
}
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,
}
}
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 "未知"
}
}
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",
}
}
package 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"`
}
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 实现
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;
}
@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;
}
@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;
}
}
@Data
public class LoginRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
@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);
}
}
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);
}
}
@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;
}
}
@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"
);
}
}
@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);
}
}
@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 核心差异总表
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 适用场景建议
十四、附录:完整项目结构参考
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:生态丰富、工具链成熟、企业级特性完善,适合大型复杂业务系统
选择时应综合考虑团队技术栈、项目规模、性能需求和长期维护成本。核心的架构思想(分层、解耦、抽象)是通用的,具体实现可根据语言特性灵活调整。