为什么同样是写接口,有人的API清晰优雅、一用就懂,有人的接口却混乱不堪、联调三天还在踩坑?RESTful不是简单的@GetMapping加名词路径,而是一套完整的架构设计哲学。本文结合10年Java后端开发经验,从URL命名到异常处理,从版本控制到幂等设计,系统性梳理12个工业级最佳实践,帮你写出让前端同事点赞的高质量API。

一、先搞懂:90%的人都理解错了RESTful

很多开发者以为RESTful就是"URL用名词+HTTP方法对应CRUD",这只是皮毛。REST(Representational State Transfer,表述性状态转移)是Roy Fielding在2000年提出的架构风格,核心是六大约束:

约束原则

核心含义

设计价值

资源导向

一切皆资源,用URI唯一标识

降低认知成本,接口语义自解释

无状态

服务端不保存会话状态,请求自带全部上下文

天然支持水平扩展,集群部署无压力

统一接口

HTTP方法+状态码+媒体类型标准化

降低学习成本,客户端通用化

可缓存

响应明确标记缓存性

提升性能,减轻服务端压力

客户端-服务端分离

前后端职责解耦,独立演化

前后端并行开发,技术栈灵活

分层系统

客户端无法感知中间层(网关、代理、负载均衡)

架构灵活扩展,安全策略统一

踩坑预警:很多项目号称RESTful,实则是"RPC风格套了层HTTP壳"——URL里塞满动词、所有请求都用POST、状态码永远200。这样的接口不仅失去了REST的优势,反而比传统RPC更难维护。

二、URL资源命名:5条黄金法则,告别动词式路径

URL是API的门面,命名直接决定了接口的可读性和专业度。

2.1 法则一:用名词复数,永远不用动词

核心原则:URL表示"资源是什么",HTTP方法表示"对资源做什么",二者各司其职。

正确示例

Plaintext
GET    /users          # 查询用户列表
GET    /users/123      # 查询单个用户
POST   /users          # 创建用户
PUT    /users/123      # 更新用户(全量)
PATCH  /users/123      # 更新用户(部分)
DELETE /users/123      # 删除用户

反面教材(90%的新手都会犯)

Plaintext
GET  /getUserList      # 动词+名词,冗余
GET  /queryUserById    # 查询动作写进URL
POST /deleteUser       # 用POST执行删除,语义混乱
POST /updateUserInfo   # 所有操作都用POST

2.2 法则二:层级关系清晰,嵌套不超过3层

资源之间存在从属关系时,用路径层级体现:

推荐写法

Plaintext
/users/123/orders              # 用户123的所有订单
/users/123/orders/456          # 用户123的456号订单
/users/123/orders/456/items    # 订单项

经验之谈:嵌套超过3层(如/a/1/b/2/c/3/d/4)会导致路径冗长、语义模糊。超过3层时,考虑将子资源提升为独立资源,通过查询参数关联。

2.3 法则三:小写字母+连字符,拒绝下划线和驼峰

URL路径遵循kebab-case(短横线命名),与域名风格保持一致:

GET /user-profiles/123
GET /userProfiles/123(驼峰,部分服务器大小写不敏感)
GET /user_profiles/123(下划线,可读性差且不利于SEO)

2.4 法则四:查询参数用于过滤、排序、分页

集合资源的筛选条件不要塞进路径,统一用查询参数:

Plaintext
GET /users?page=1&size=20&status=active&sort=created_at,desc

常见查询参数场景:

  • 分页pagesizeoffsetlimit
  • 过滤statustypekeyword
  • 排序sort=field,asc/desc
  • 字段裁剪fields=id,name,email

2.5 法则五:版本号前置,全局统一

API版本控制从第一天就要设计好,不要等线上跑起来再后悔:

推荐:URL路径版本化(最直观、最常用)

Plaintext
GET /api/v1/users
GET /api/v2/users

其他方案对比:

方案

示例

优点

缺点

路径版本

/api/v1/users

直观清晰,路由简单

URL不"纯净"

请求头版本

Accept: application/vnd.app.v1+json

URL保持REST风格

调试不便,工具支持弱

查询参数

/users?version=1

实现简单

语义混乱,缓存困难

工业级建议:中小企业直接用路径版本化,简单高效。追求REST纯度且有完善网关基础设施的团队,再考虑请求头方案。

三、HTTP方法:用对语义,别再让POST包打天下

HTTP方法不是摆设,每个方法都有明确的语义和特性,用错了就是埋坑。

3.1 五大核心方法对照表

方法

语义

幂等性

安全性

典型场景

GET

查询/读取资源

✅ 是

✅ 是

查询列表、查询详情

POST

创建资源

❌ 否

❌ 否

新增用户、提交表单

PUT

全量更新资源

✅ 是

❌ 否

完整更新用户信息

PATCH

部分更新资源

❌ 否

❌ 否

修改用户昵称、状态

DELETE

删除资源

✅ 是

❌ 否

删除用户

关键概念

  • 幂等性:多次执行与一次执行效果相同。重试机制、网络抖动场景下极其重要。
  • 安全性:不改变资源状态。GET、HEAD只能读,不能有副作用。

3.2 常见误区纠正

误区1:创建必须用POST,更新必须用PUT
→ 不一定。如果客户端可以指定资源ID(如UUID),创建也可以用PUT:PUT /users/abc-123

误区2:PATCH比PUT简单,随便用
→ PATCH看似灵活,实则坑多。请求体格式不统一(JSON Patch vs JSON Merge Patch)、部分更新的事务边界模糊。简单场景优先PUT,复杂局部更新再考虑PATCH。

误区3:复杂查询用POST传参更方便
→ 查询参数过长时,很多人图方便改用POST。重点纠正:X-HTTP-Method-Override 头不适用该场景。该请求头设计初衷是适配老旧浏览器Form表单,让POST请求隧道代理PUT/DELETE/PATCH方法,不是用来解决GET参数超长问题。
→ 工业级标准做法:参数超长(批量ID查询、复杂多条件组合查询)统一使用 POST /{resource}/search 专用查询接口,将查询条件放入请求Body,响应200状态码;该方案兼容性好、网关日志监控友好,不破坏REST设计思想。

3.3 Spring Boot代码示例

Java
@RestController
@RequestMapping("/api/v1/users")
public class UserController {

    @GetMapping
    public ResponseEntity<PageResult<UserVO>> listUsers(
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(required = false) String status) {
        PageResult<UserVO> result = userService.queryUsers(page, size, status);
        return ResponseEntity.ok(result);
    }

    @GetMapping("/{id}")
    public ResponseEntity<UserVO> getUserById(@PathVariable Long id) {
        UserVO user = userService.getUserById(id);
        return ResponseEntity.ok(user);
    }

    @PostMapping
    public ResponseEntity<UserVO> createUser(@Valid @RequestBody UserCreateDTO dto) {
        UserVO user = userService.createUser(dto);
        return ResponseEntity
                .status(HttpStatus.CREATED)
                .header("Location", "/api/v1/users/" + user.getId())
                .body(user);
    }

    @PutMapping("/{id}")
    public ResponseEntity<UserVO> updateUser(
            @PathVariable Long id,
            @Valid @RequestBody UserUpdateDTO dto) {
        UserVO user = userService.updateUser(id, dto);
        return ResponseEntity.ok(user);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.noContent().build();
    }
}

四、HTTP状态码:别再所有接口都返回200

状态码是HTTP协议的灵魂。最糟糕的API设计莫过于:状态码永远200,靠响应体里的code字段判断成败。

4.1 常用状态码分类及使用场景

分类

状态码

含义

API场景

2xx 成功

200 OK

请求成功

GET查询、PUT更新成功

201 Created

资源创建成功

POST创建成功,配合Location头

204 No Content

成功但无返回体

DELETE删除成功

3xx 重定向

301 Moved Permanently

永久重定向

资源路径迁移

304 Not Modified

资源未修改

缓存协商命中

4xx 客户端错误

400 Bad Request

请求参数错误

参数校验失败、格式错误

401 Unauthorized

未认证

未登录、Token无效

403 Forbidden

无权限

已登录但无权操作

404 Not Found

资源不存在

ID不存在、路径错误

405 Method Not Allowed

方法不允许

对资源用了不支持的HTTP方法

409 Conflict

资源冲突

唯一键重复、乐观锁冲突

429 Too Many Requests

请求过于频繁

限流触发

5xx 服务端错误

500 Internal Server Error

服务器内部错误

未捕获异常、程序Bug

502 Bad Gateway

网关错误

上游服务不可用

503 Service Unavailable

服务不可用

停机维护、过载保护

4.2 两大经典反模式

反模式一:永远200,错误码放body

JSON
{
  "code": 50001,
  "msg": "用户不存在",
  "data": null
}

危害:HTTP基础设施全部失效——缓存、监控、日志、网关、浏览器调试工具都无法识别错误。运维同学骂骂咧咧退出群聊。

反模式二:业务错误全部返回500
→ 参数错误、用户不存在、余额不足都是客户端问题,应该用4xx。5xx意味着"服务端出故障了",会触发告警、熔断、重试。乱用5xx会把运维搞疯。

最佳实践:HTTP状态码表示协议层面的结果,响应体code表示业务层面的细分错误码。二者各司其职,不重复、不冲突。400状态码下,body里可以有更细的业务错误码和字段级错误信息。

五、统一响应结构:前后端协作的基石

没有统一的响应格式,前端同学每个接口都要写一套解析逻辑,苦不堪言。

5.1 成功响应标准格式

JSON
{
  "code": 0,
  "message": "success",
  "data": {
    "id": 123,
    "username": "zhangsan",
    "email": "zhangsan@example.com"
  },
  "timestamp": 1719840000000
}

  • code:业务状态码,0表示成功,非0表示具体业务错误
  • message:人类可读的描述信息
  • data:响应数据,单个对象或列表
  • timestamp:服务端响应时间戳(可选,排查问题神器)

5.2 分页响应格式

JSON
{
  "code": 0,
  "message": "success",
  "data": {
    "list": [...],
    "total": 156,
    "page": 1,
    "size": 20,
    "pages": 8
  }
}

5.3 错误响应标准格式

JSON
{
  "code": 10001,
  "message": "参数校验失败",
  "details": [
    {
      "field": "email",
      "message": "邮箱格式不正确"
    },
    {
      "field": "password",
      "message": "密码长度不能少于8位"
    }
  ],
  "requestId": "req-abc123xyz",
  "timestamp": 1719840000000
}

关键设计点

  • details:字段级错误详情,前端可直接展示在表单对应位置
  • requestId:全局唯一请求ID,排查问题时一键定位日志
  • 错误码分层设计:1xxxx参数错误、2xxxx业务错误、3xxxx系统错误

5.4 Spring Boot全局统一返回封装

Java
@RestControllerAdvice
public class GlobalResponseHandler implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        // 排除swagger、文件下载等特殊接口
        return !returnType.getDeclaringClass().getName().contains("springdoc");
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                    MediaType selectedContentType, Class selectedConverterType,
                                    ServerHttpRequest request, ServerHttpResponse response) {
        // 已经是统一格式则直接返回
        if (body instanceof ApiResult) {
            return body;
        }
        // String类型特殊处理:强制设置JSON响应头,避免前端text/plain解析失败
        if (body instanceof String) {
            response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
            return JSON.toJSONString(ApiResult.success(body));
        }
        return ApiResult.success(body);
    }
}

六、全局异常处理:别让堆栈信息暴露给前端

没有异常处理的API,遇到问题前端拿到一堆堆栈,用户看到500白页,体验极差。

6.1 异常分类与处理策略

异常类型

HTTP状态码

处理方式

参数校验异常

400

捕获MethodArgumentNotValidException,提取字段错误

业务异常

400 / 409 等

自定义BusinessException,携带错误码和消息

认证异常

401

捕获AuthenticationException

权限异常

403

捕获AccessDeniedException

资源不存在

404

捕获ResourceNotFoundException

限流异常

429

捕获RateLimitException

未预期异常

500

兜底捕获,记录错误日志,返回友好提示

6.2 全局异常处理器代码

Java
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiResult<Void>> handleBusinessException(BusinessException e) {
        log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage());
        ApiResult<Void> result = ApiResult.error(e.getCode(), e.getMessage());
        return ResponseEntity.badRequest().body(result);
    }

    /**
     * 参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResult<Void>> handleValidationException(
            MethodArgumentNotValidException e) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        List<ErrorDetail> details = fieldErrors.stream()
                .map(error -> new ErrorDetail(error.getField(), error.getDefaultMessage()))
                .collect(Collectors.toList());
        
        ApiResult<Void> result = ApiResult.error(ErrorCode.PARAM_INVALID, details);
        return ResponseEntity.badRequest().body(result);
    }

    /**
     * 资源不存在异常
     */
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiResult<Void>> handleNotFoundException(ResourceNotFoundException e) {
        ApiResult<Void> result = ApiResult.error(ErrorCode.RESOURCE_NOT_FOUND, e.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(result);
    }

    /**
     * 兜底异常处理
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResult<Void>> handleException(Exception e) {
        String requestId = MDC.get("requestId");
        log.error("系统异常 requestId={}", requestId, e);
        
        ApiResult<Void> result = ApiResult.error(
                ErrorCode.SYSTEM_ERROR,
                "系统内部错误,请联系管理员。请求ID:" + requestId
        );
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
    }
}

安全提醒:生产环境绝对不能把异常堆栈返回给前端!既泄露了技术栈信息,又给攻击者提供了漏洞线索。返回友好提示+requestId,后端通过日志排查。

七、参数校验:把拦路虎挡在Controller层

参数校验不做好,业务层到处是if-else判空,脏数据还可能溜进数据库。

7.1 JSR-380 Bean Validation 常用注解

注解

作用

示例

@NotNull

不能为null

ID字段

@NotBlank

不能为null且不能是空字符串

用户名

@Size(min, max)

字符串/集合长度范围

密码8-20位

@Min / @Max

数值范围

年龄1-120

@Email

邮箱格式

邮箱字段

@Pattern

正则匹配

手机号格式

@Valid

级联校验

嵌套对象

7.2 DTO示例

Java
@Data
public class UserCreateDTO {

    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间")
    @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
    private String username;

    @NotBlank(message = "密码不能为空")
    @Size(min = 8, max = 20, message = "密码长度必须在8-20个字符之间")
    private String password;

    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;

    @NotNull(message = "年龄不能为空")
    @Min(value = 1, message = "年龄不能小于1岁")
    @Max(value = 120, message = "年龄不能大于120岁")
    private Integer age;
}

7.3 分组校验:同一个DTO应对不同场景

创建和更新的校验规则不一样,不要写两个DTO:

Java
// 定义分组接口
public interface CreateGroup {}
public interface UpdateGroup {}

// DTO中指定分组
@NotNull(groups = UpdateGroup.class, message = "ID不能为空")
private Long id;

@NotBlank(groups = CreateGroup.class, message = "密码不能为空")
private String password;

// Controller中启用分组
@PostMapping
public ResponseEntity<UserVO> createUser(
        @Validated(CreateGroup.class) @RequestBody UserCreateDTO dto) {
    // ...
}

八、幂等性设计:网络重试下的数据安全

分布式系统中,网络抖动、超时重试是常态。没有幂等保护,重复提交可能导致重复下单、重复扣款、数据错乱。

8.1 什么接口需要保证幂等?

  • 天然幂等:GET、PUT、DELETE(正确实现的前提下)
  • 需要特别处理:POST创建操作、有副作用的业务操作

8.2 四种幂等实现方案

方案

实现原理

适用场景

优缺点

唯一索引

数据库唯一键约束防重复

注册、订单号唯一的场景

简单可靠,但依赖DB,冲突抛异常

幂等Token

先申请Token,请求携带Token,用完即删

前端表单提交、防重复点击

通用性强,但需要一次额外交互

乐观锁

version版本号,更新时校验版本

更新操作防并发修改

性能好,适合更新场景

分布式锁

Redis/Redisson锁,同一业务ID串行执行

复杂业务、跨服务操作

功能强大,但实现成本高

8.3 基于Token的幂等方案实现

流程

  1. 进入提交页面前,前端调用接口获取幂等Token
  1. 后端生成Token存入Redis,设置过期时间
  1. 提交请求时Header中携带Idempotent-Token
  1. 后端先删除Token(DEL命令原子性),删除成功则执行业务
  1. 删除失败则说明重复请求,直接返回

Java
@Aspect

@Component

public class IdempotentAspect {

    @Autowired

    private StringRedisTemplate redisTemplate;

    // 原子执行Lua脚本:区分Token过期/无效 和 重复提交,修复原生逻辑边界BUG

    private static final String IDEMPOTENT_LUA_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    @Around("@annotation(idempotent)")

    public Object around(ProceedingJoinPoint point, Idempotent idempotent) throws Throwable {

        HttpServletRequest request = ((ServletRequestAttributes)

                RequestContextHolder.getRequestAttributes()).getRequest();

        String token = request.getHeader("Idempotent-Token");

        

        if (StringUtils.isBlank(token)) {

            throw new BusinessException(ErrorCode.IDEMPOTENT_TOKEN_MISSING);

        }

        

        String key = "idempotent:" + token;

        // Lua原子脚本:解决原生delete逻辑BUG,区分两类异常场景

        Long result = redisTemplate.execute(

                new DefaultRedisScript<>(IDEMPOTENT_LUA_SCRIPT, Long.class),

                Collections.singletonList(key),

                token

        );

        if (result == null || result == 0) {

            // 0=Token不存在(过期/伪造无效Token);拒绝原因为Token非法,而非重复请求

            throw new BusinessException(ErrorCode.TOKEN_INVALID_OR_EXPIRED, "幂等Token已过期或无效,请重新发起请求");

        }

        // 校验通过,执行业务逻辑

        return point.proceed();

    }

}

九、接口安全:从认证到防刷,构建多层防护

API暴露在外网,安全是底线。

9.1 认证授权:JWT + 权限控制

Java
// 登录接口放行,其他接口校验Token
@Configuration
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeHttpRequests()
                .requestMatchers("/api/v1/auth/login").permitAll()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtAuthenticationFilter,
                    UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}

9.2 接口限流:防止恶意请求打爆服务

基于Redis + 滑动窗口实现限流:

Java
// 注解式限流
@RateLimit(key = "user_login", limit = 5, period = 60,
            message = "登录过于频繁,请1分钟后再试")
@PostMapping("/login")
public ResponseEntity<LoginVO> login(@RequestBody LoginDTO dto) {
    // ...
}

限流粒度

  • IP级限流:防爬虫、防暴力破解
  • 用户级限流:防单用户高频调用
  • 接口级限流:保护核心接口可用性

9.3 其他安全措施

  • HTTPS:生产环境强制HTTPS,HTTP重定向
  • 敏感数据脱敏:手机号、身份证、邮箱返回时脱敏展示
  • SQL注入防护:使用MyBatis参数绑定,禁止字符串拼接SQL
  • XSS防护:统一过滤请求参数中的特殊字符
  • 请求签名:核心支付等接口用签名防篡改
  • CORS配置:严格控制跨域白名单,不要*放行

十、接口文档:Swagger不是全部,高质量文档这样写

代码即文档是理想,但实际协作中,清晰的文档能减少80%的沟通成本。

10.1 SpringDoc + OpenAPI 3.0 集成

Java
@Configuration
public class OpenApiConfig {

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("用户管理系统 API")
                        .version("1.0.0")
                        .description("RESTful API 接口文档")
                        .contact(new Contact().name("技术团队").email("dev@example.com")))
                .addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
                .components(new Components()
                        .addSecuritySchemes("bearerAuth",
                                new SecurityScheme()
                                        .type(SecurityScheme.Type.HTTP)
                                        .scheme("bearer")
                                        .bearerFormat("JWT")));
    }
}

10.2 高质量接口文档的标准

只靠注解自动生成还不够,高质量文档需要:

  1. 清晰的接口说明:每个接口是做什么的、适用场景
  1. 完整的参数说明:每个字段的含义、取值范围、是否必填
  1. 真实的请求示例:可直接复制调试的JSON示例
  1. 完整的响应示例:成功、失败各种场景的返回样例
  1. 错误码对照表:所有可能的业务错误码及含义
  1. 变更日志:版本迭代记录,哪些接口废弃了

10.3 Controller层注解示例

Java
@RestController
@RequestMapping("/api/v1/users")
@Tag(name = "用户管理", description = "用户CRUD、状态管理等接口")
public class UserController {

    @Operation(summary = "分页查询用户列表",
               description = "根据状态筛选,支持分页、排序")
    @Parameters({
            @Parameter(name = "page", description = "页码,从1开始", example = "1"),
            @Parameter(name = "size", description = "每页条数", example = "20"),
            @Parameter(name = "status", description = "用户状态:active-启用 disabled-禁用")
    })
    @ApiResponses({
            @ApiResponse(responseCode = "200", description = "查询成功"),
            @ApiResponse(responseCode = "401", description = "未登录")
    })
    @GetMapping
    public ResponseEntity<ApiResult<PageResult<UserVO>>> listUsers(...) {
        // ...
    }
}

十一、性能优化:让你的接口飞起来

设计得再规范,响应慢了用户照样骂娘。

11.1 缓存策略:能不查DB就不查

  • HTTP缓存:GET接口设置Cache-ControlETag,浏览器/CDN缓存
  • 服务端缓存:Redis缓存热点数据(用户信息、字典数据、配置项)
  • 本地缓存:Caffeine缓存极少变更的数据(枚举、常量配置)

11.2 数据库优化:慢查询是万恶之源

  • 查询只查需要的字段,禁止select *
  • 合理建索引,explain分析执行计划
  • 深分页优化:where id > lastId limit size 替代 offset limit
  • 复杂统计查询走从库或ES,不要打主库

11.3 异步化:非核心逻辑别阻塞主流程

创建用户后发欢迎邮件、记录操作日志、统计数据更新——这些都可以异步:

Java
@Async
public void sendWelcomeEmail(Long userId) {
    // 异步发送邮件
}

11.4 其他优化手段

  • 批量操作:循环插入改批量插入,减少DB交互次数
  • 避免大事务:事务范围尽量小,长事务拖垮连接池
  • 压缩传输:开启Gzip压缩,减少响应体体积
  • 连接池优化:HTTP连接池、数据库连接池参数调优

十二、可观测性:线上出问题,一秒定位

没有监控的接口,线上出了问题就是瞎子摸象。

12.1 全链路追踪:TraceId贯穿始终

Java
// 过滤器生成requestId,塞入MDC
public class TraceFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        String traceId = UUID.randomUUID().toString().replace("-", "");
        MDC.put("requestId", traceId);
        try {
            ((HttpServletResponse) response).setHeader("X-Trace-Id", traceId);
            chain.doFilter(request, response);
        } finally {
            MDC.remove("requestId");
        }
    }
}

12.2 日志规范:别打无用日志,关键信息不能少

日志打印原则

  • 入参出参必打(注意脱敏,密码、身份证不能打明文)
  • 异常日志打完整堆栈,只打message等于没打
  • 关键业务节点埋点日志(创建订单、支付成功、状态变更)
  • 禁止循环内打info日志,量大了拖垮性能

12.3 核心指标监控

接入Prometheus + Grafana,监控以下指标:

  • QPS:接口调用量趋势
  • 响应时间:平均耗时、P95、P99
  • 错误率:4xx、5xx占比
  • 限流次数:被限流拦截的请求数
  • 业务指标:下单量、注册量等业务维度

结语:RESTful不是银弹,但规范让协作更高效

写这篇文章的时候,我想起早年接手过的一个"祖传项目"——200多个接口,命名五花八门,有的用GET有的用POST,响应格式三四种,状态码永远200,出错了靠前端猜。联调一周,前端同学跑过来问我:"你们后端是不是每个人一套标准?"

那一刻我深刻意识到:API不是写给自己看的,是写给团队、写给前端、写给第三方调用者的。 规范的价值不在于"政治正确",而在于降低沟通成本、减少线上Bug、提升协作效率。

本文总结的12个最佳实践,覆盖了从URL命名到线上运维的全生命周期。不需要一次性全部落地,可以从最基础的URL规范、统一响应、全局异常处理开始,逐步优化。

最后送大家一句话:好的API设计,应该让调用者"猜都能猜对"——看到URL就知道是什么资源,看到方法就知道是什么操作,看到状态码就知道成功还是失败。达到这个境界,你的API设计就算入门了。

如果这篇文章对你有帮助,欢迎点赞收藏。有不同观点也欢迎评论区交流,一起进步。

附:RESTful API设计自查清单

✅ URL使用名词复数,无动词
✅ 嵌套层级不超过3层
✅ 路径小写+连字符
✅ HTTP方法语义正确
✅ 版本号统一管理
✅ 状态码使用准确,不全部200
✅ 统一响应结构
✅ 全局异常处理,堆栈不外露
✅ 参数校验完善
✅ 核心接口幂等保障
✅ 认证授权到位
✅ 接口文档完整
✅ TraceId全链路
✅ 核心指标监控

本文作者:10年Java后端老兵,专注架构设计与工程实践。关注我,持续输出干货技术文章。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐