软件工程实践二:Spring Boot 知识回顾

文章目录

      • [一、创建项目(Spring Boot 向导)](#一、创建项目(Spring Boot 向导))
      • 二、项目最小代码示例
      • 三、运行与验证
      • 四、标准目录结构与说明
      • [五、Maven 依赖最小示例(仅供参考)](#五、Maven 依赖最小示例(仅供参考))
      • [六、常用配置(application.yml 示例)](#六、常用配置(application.yml 示例))
      • [七、返回 JSON 与统一异常](#七、返回 JSON 与统一异常)
      • [八、@Value 配置读取示例](#八、@Value 配置读取示例)
      • [九、日志引入与配置(SLF4J + Logback)](#九、日志引入与配置(SLF4J + Logback))
      • [十、@ConfigurationProperties 批量绑定与校验](#十、@ConfigurationProperties 批量绑定与校验)
      • [十一、GET 参数处理与示例](#十一、GET 参数处理与示例)
      • [十二、POST 参数上传与示例](#十二、POST 参数上传与示例)
      • [十三、过滤器 Filter 与案例](#十三、过滤器 Filter 与案例)
      • [十四、拦截器 HandlerInterceptor 与案例](#十四、拦截器 HandlerInterceptor 与案例)
      • [十五、PUT 参数上传与示例](#十五、PUT 参数上传与示例)
      • [十六、20 个 GET/POST 接口样例](#十六、20 个 GET/POST 接口样例)

原文链接:https://blog.ybyq.wang/archives/1099.html


一、创建项目(Spring Boot 向导)

步骤说明:

  • 打开 IDEA → New Project → 选择 Spring Boot;服务地址保持默认。
  • Project Metadata:
    • Group:如 com.example
    • Artifact/Name:如 demo
    • Packaging:jar(默认)
    • Java:17(或本机已安装的 LTS 版本)
  • Build system:Maven(不要选 Gradle)
  • Dependencies:搜索并仅勾选 Spring Web
  • Finish → 等待 Maven 下载依赖与索引完成。

提示:若看不到 Spring Boot(Initializr),确保安装了 IntelliJ 的 Spring 插件,或使用 File > New > Project... 再选择。

二、项目最小代码示例

  1. 主启动类(IDE 已生成,检查包名与类名)
java 复制代码
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }
}
  1. Hello 控制器(新增)
java 复制代码
package com.example.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.Map;

@RestController
@RequestMapping("/api")
public class HelloController {
  @GetMapping("/hello")
  public Map<String, Object> hello() {
    return Map.of(
      "msg", "hello",
      "time", new Date()
    );
  }
}
  1. 应用配置(可选)
yaml 复制代码
# src/main/resources/application.yml
server:
  # 如需修改端口,取消注释并调整值
  # port: 8080

三、运行与验证

  • DemoApplication 类左侧点击绿色运行箭头,或使用菜单 Run
  • 控制台看到 Started DemoApplication ... 表示启动成功。
  • 浏览器/工具访问:http://localhost:8080/api/hello
    • 期望响应示例:
json 复制代码
{"msg":"hello","time":"2025-01-01T00:00:00Z"}

常见问题:

  • 端口占用:在 application.yml 配置 server.port: 8081
  • 依赖未下载:检查网络代理或尝试 Reload All Maven Projects
  • JDK 不匹配:Project Structure > Project/Modules 指定与向导一致的 Java 版本。

四、标准目录结构与说明

text 复制代码
 demo/                      ← 工程根目录
 ├─ pom.xml  ← 构建脚本(Maven)
 ├─ src
 │  ├─ main
 │  │  ├─ java
 │  │  │  └─ com
 │  │  │     └─ example
 │  │  │        └─ demo
 │  │  │           ├─ DemoApplication.java      ← 启动类
 │  │  │           └─ controller
 │  │  │              └─ HelloController.java   ← Web 控制器
 │  │  └─ resources
 │  │     ├─ application.yml                    ← 应用配置
 │  │     └─ static/ templates/                 ← 静态资源/模板(可选)
 │  └─ test
 │     └─ java
 │        └─ com.example.demo
 │           └─ DemoApplicationTests.java       ← 测试类
  • DemoApplication:应用入口,@SpringBootApplication 聚合配置并触发自动装配。
  • controller:放置 @RestController@Controller 等 Web 层类。
  • resources:
    • application.yml|yaml:端口、数据源、日志级别等配置。
    • static/:静态文件(css/js/img)。
    • templates/:模板引擎(如 Thymeleaf)页面,需相应依赖时使用。
  • test:单元/集成测试。

五、Maven 依赖最小示例(仅供参考)

Maven pom.xml 关键片段:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.4</version>
    <relativePath/>
  </parent>

  <groupId>com.example</groupId>
  <artifactId>demo</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>demo</name>
  <description>Demo project for Spring Boot</description>

  <properties>
    <java.version>17</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

六、常用配置(application.yml 示例)

yaml 复制代码
spring:
  application:
    name: demo
  jackson:
    time-zone: Asia/Shanghai
    date-format: yyyy-MM-dd HH:mm:ss

server:
  port: 8080
  # servlet:
  #   context-path: /api

logging:
  level:
    root: INFO
    com.example.demo: DEBUG

# 多环境(profiles),开发/生产差异化配置(任选其一:此处或独立 profile 文件)
# spring:
#   profiles:
#     active: dev
  • 多环境配置(Profiles)示例:
yaml 复制代码
# application-dev.yml(开发环境)
logging:
  level:
    com.example.demo: DEBUG
yaml 复制代码
# application-prod.yml(生产环境)
logging:
  level:
    root: INFO
  • 运行时指定环境:

    • application.yml 写:

      yaml 复制代码
      spring:
        profiles:
          active: dev
    • 或启动参数:--spring.profiles.active=prod

  • 全局 CORS(跨域)配置(如需前端跨域访问):

java 复制代码
package com.example.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebCorsConfig implements WebMvcConfigurer {
  @Override
  public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**")
        .allowedOrigins("http://localhost:3000")
        .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
        .allowCredentials(true);
  }
}
  • 全局异常处理(将错误转换为统一 JSON 响应):
java 复制代码
package com.example.demo.common;

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {
  @ExceptionHandler(Exception.class)
  public Map<String, Object> handle(Exception ex) {
    return Map.of(
      "success", false,
      "error", ex.getClass().getSimpleName(),
      "message", ex.getMessage()
    );
  }
}

七、返回 JSON 与统一异常

  • 案例 1:路径参数 + 查询参数 + JSON 返回
java 复制代码
package com.example.demo.controller;

import org.springframework.web.bind.annotation.*;

import java.util.Map;

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

  @GetMapping("/{id}")
  public Map<String, Object> getUser(@PathVariable Long id,
                                     @RequestParam(defaultValue = "false") boolean detail) {
    return Map.of(
      "id", id,
      "name", "Alice",
      "detail", detail
    );
  }

  public record CreateUserRequest(String name, Integer age) {}

  @PostMapping
  public Map<String, Object> create(@RequestBody CreateUserRequest req) {
    long generatedId = System.currentTimeMillis();
    return Map.of(
      "id", generatedId,
      "name", req.name(),
      "age", req.age()
    );
  }
}
  • 案例 2:触发异常并由全局异常处理返回统一结构

将下述方法加入 UserController 以体验统一异常响应:

java 复制代码
@GetMapping("/error")
public Map<String, Object> error() {
  throw new IllegalStateException("示例异常");
}
  • 测试命令示例:
bash 复制代码
# 获取用户(携带查询参数 detail)
curl "http://localhost:8080/api/users/1?detail=true"

# 创建用户(POST JSON)
curl -X POST "http://localhost:8080/api/users" \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"Bob\",\"age\":20}"

# 触发错误,查看统一异常返回
curl "http://localhost:8080/api/users/error"

八、@Value 配置读取示例

  1. application.yml 定义配置:
yaml 复制代码
app:
  name: demo-app
  timeout: 5s            # 可自动转换为 Duration
  white-list:
    - 127.0.0.1
    - 10.0.0.1
  db:
    host: 127.0.0.1
    port: 3306
  1. 使用 @Value 读取(支持默认值与基础类型转换):
java 复制代码
package com.example.demo.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.time.Duration;

@Component
public class AppValueExample {

  @Value("${app.name}")
  private String appName;

  // 默认值示例:若未配置则为 3s
  @Value("${app.timeout:3s}")
  private Duration timeout;

  // 读取列表元素(带默认值)
  @Value("${app.white-list[0]:127.0.0.1}")
  private String firstWhiteIp;

  // 读取嵌套对象字段(带默认值)
  @Value("${app.db.host:localhost}")
  private String dbHost;

  @Value("${app.db.port:3306}")
  private Integer dbPort;

  public String getAppName() { return appName; }
  public Duration getTimeout() { return timeout; }
  public String getFirstWhiteIp() { return firstWhiteIp; }
  public String getDbHost() { return dbHost; }
  public Integer getDbPort() { return dbPort; }
}

说明:

  • 默认值语法:${key:defaultValue},当 key 不存在时使用 defaultValue
  • 类型转换:Duration/int/boolean 等常见类型由 Spring 自动转换(如 5s -> Duration.ofSeconds(5))。
  • 列表索引:${list[0]} 访问第一个元素。
  • 建议:若配置项较多/需要分组,优先使用 @ConfigurationProperties 进行批量绑定与校验。

九、日志引入与配置(SLF4J + Logback)

  • 默认:spring-boot-starter-logging 已内置,提供 SLF4J API + Logback 实现。
  • 使用:在类中注入并使用 SLF4J Logger。
java 复制代码
package com.example.demo.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.Map;

@RestController
@RequestMapping("/api")
public class HelloControllerWithLog {
  private static final Logger log = LoggerFactory.getLogger(HelloControllerWithLog.class);

  @GetMapping("/hello2")
  public Map<String, Object> hello() {
    log.info("hello endpoint called at {}", new Date());
    log.debug("debug details...");
    return Map.of("msg", "hello", "time", new Date());
  }
}
  • 最小 application.yml 配置:
yaml 复制代码
logging:
  level:
    root: INFO
    com.example.demo: DEBUG
  file:
    name: logs/app.log           # 输出到文件(会自动创建文件夹)
  logback:
    rollingpolicy:               # 基于 Logback 的滚动策略(无需 xml)
      max-file-size: 10MB
      max-history: 7
      total-size-cap: 1GB
  • 常用说明:

    • logging.level.<包名>:设置指定包日志级别。
    • logging.file.name:直接指定日志文件;或使用 logging.file.path 指定目录(文件名默认为 spring.log)。
    • logging.logback.rollingpolicy.*:文件按大小/历史保留自动滚动。
    • 自定义输出格式可用:logging.pattern.console / logging.pattern.file
  • 进阶(可选):使用 logback-spring.xml 进行更细粒度控制,或切换 Log4j2:

    • 切换 Log4j2:在 pom.xml 引入 spring-boot-starter-log4j2 并排除默认 logging 依赖。

十、@ConfigurationProperties 批量绑定与校验

  • 配置(支持嵌套对象、列表、Map、时长等类型):
yaml 复制代码
app:
  name: demo-app
  timeout: 5s
  white-list:
    - 127.0.0.1
    - 10.0.0.1
  db:
    host: 127.0.0.1
    port: 3306
  • 属性类(开启校验,演示嵌套绑定与默认值):
java 复制代码
package com.example.demo.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.List;

@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private String name;

    @DurationUnit(ChronoUnit.SECONDS)
    private Duration timeout = Duration.ofSeconds(3);

    private List<String> whiteList;

    private Database db = new Database();

    public static class Database {
        private String host;
        private int port = 3306;

        public String getHost() { return host; }
        public void setHost(String host) { this.host = host; }
        public int getPort() { return port; }
        public void setPort(int port) { this.port = port; }
    }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Duration getTimeout() { return timeout; }
    public void setTimeout(Duration timeout) { this.timeout = timeout; }
    public List<String> getWhiteList() { return whiteList; }
    public void setWhiteList(List<String> whiteList) { this.whiteList = whiteList; }
    public Database getDb() { return db; }
    public void setDb(Database db) { this.db = db; }
}
  • 启用方式(三选一):

    • 在启动类上开启扫描(推荐):
    java 复制代码
    import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.SpringApplication;
    
    @SpringBootApplication
    @ConfigurationPropertiesScan // 扫描同包及子包的 @ConfigurationProperties 类
    public class DemoApplication {
      public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
      }
    }
    • 或在配置类上显式启用:
    java 复制代码
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    
    @EnableConfigurationProperties(AppProperties.class)
    public class PropertiesConfig {}
    • 或直接把属性类声明为 Bean:
    java 复制代码
    import org.springframework.context.annotation.Bean;
    
    @org.springframework.context.annotation.Configuration
    public class PropertiesConfig2 {
      @Bean
      public AppProperties appProperties() { return new AppProperties(); }
    }

    也可在属性类上加 @Component 直接让其被扫描成 Bean(需保证包路径可被扫描)。

  • 校验依赖(若使用 @Validated/约束注解,请确保已引入):

xml 复制代码
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

十一、GET 参数处理与示例

GET 适用于通过查询参数与路径变量传递轻量数据;GET 不能上传文件(multipart/form-data)。

java 复制代码
package com.example.demo.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/get")
public class GetParamController {
  private static final Logger log = LoggerFactory.getLogger(GetParamController.class);

  // 1) 简单类型 + 默认值
  @GetMapping("/simple")
  public Map<String, Object> simple(
      @RequestParam String name,
      @RequestParam(defaultValue = "18") Integer age,
      @RequestParam(defaultValue = "false") boolean active) {
    log.info("GET simple: name={}, age={}, active={}", name, age, active);
    return Map.of("name", name, "age", age, "active", active);
  }

  // 2) 列表参数(重复 key:?tags=a&tags=b)
  @GetMapping("/list")
  public Map<String, Object> list(@RequestParam(required = false) List<String> tags) {
    log.info("GET list: tags={}", tags);
    return Map.of("tags", tags);
  }

  // 3) 所有查询参数(平铺)
  @GetMapping("/all")
  public Map<String, Object> all(@RequestParam Map<String, String> params) {
    log.info("GET all params: {}", params);
    return Map.of("params", params);
  }

  // 4) 所有查询参数(允许一个 key 多个值)
  @GetMapping("/all-multi")
  public Map<String, Object> allMulti(@RequestParam MultiValueMap<String, String> params) {
    log.info("GET all multi params: {}", params);
    return Map.of("params", params);
  }

  // 5) DTO 绑定(GET 默认使用 @ModelAttribute 绑定查询参数)
  public static class QueryDto {
    private String keyword;
    private Integer page;
    private Boolean highlight;

    public String getKeyword() { return keyword; }
    public void setKeyword(String keyword) { this.keyword = keyword; }
    public Integer getPage() { return page; }
    public void setPage(Integer page) { this.page = page; }
    public Boolean getHighlight() { return highlight; }
    public void setHighlight(Boolean highlight) { this.highlight = highlight; }
  }

  @GetMapping("/dto")
  public Map<String, Object> dto(@ModelAttribute QueryDto q) {
    log.info("GET dto: keyword={}, page={}, highlight={}", q.getKeyword(), q.getPage(), q.getHighlight());
    return Map.of("q", Map.of(
        "keyword", q.getKeyword(),
        "page", q.getPage(),
        "highlight", q.getHighlight()
    ));
  }

  // 6) 路径参数 + 查询参数
  @GetMapping("/users/{id}")
  public Map<String, Object> user(@PathVariable Long id, @RequestParam(defaultValue = "false") boolean verbose) {
    log.info("GET user: id={}, verbose={}", id, verbose);
    return Map.of("id", id, "verbose", verbose);
  }

  // 7) 日期/时间/Duration(注意:Duration 推荐使用 ISO-8601,如 PT30S)
  @GetMapping("/time")
  public Map<String, Object> time(
      @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate day,
      @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime at,
      @RequestParam(required = false) Duration timeout) {
    log.info("GET time: day={}, at={}, timeout={}", day, at, timeout);
    return Map.of(
        "day", day,
        "at", at,
        "timeoutSeconds", timeout != null ? timeout.getSeconds() : null
    );
  }
}

提示:

  • GET 不支持文件上传;文件请使用 POST multipart/form-data
  • List<String> 通过重复 key 传递最稳妥(如 ?tags=a&tags=b)。
  • Duration 在查询参数中推荐使用 ISO-8601 表达(如 PT30SPT5M)。

十二、POST 参数上传与示例

常见 POST 载荷类型:application/jsonapplication/x-www-form-urlencodedmultipart/form-datatext/plainapplication/octet-stream

java 复制代码
package com.example.demo.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/post")
public class PostParamController {
  private static final Logger log = LoggerFactory.getLogger(PostParamController.class);

  // 1) application/json:JSON 体绑定到 DTO
  public static class CreateUserRequest {
    private String name;
    private Integer age;
    private List<String> tags;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
    public List<String> getTags() { return tags; }
    public void setTags(List<String> tags) { this.tags = tags; }
  }

  @PostMapping(value = "/json", consumes = "application/json")
  public Map<String, Object> json(@RequestBody CreateUserRequest req) {
    log.info("POST json: name={}, age={}, tags={}", req.getName(), req.getAge(), req.getTags());
    return Map.of(
        "ok", true,
        "data", Map.of(
            "name", req.getName(),
            "age", req.getAge(),
            "tags", req.getTags()
        )
    );
  }

  // 2) application/x-www-form-urlencoded:表单键值
  @PostMapping(value = "/form", consumes = "application/x-www-form-urlencoded")
  public Map<String, Object> form(@RequestParam String name,
                                  @RequestParam Integer age,
                                  @RequestParam(required = false) List<String> tags) {
    log.info("POST form: name={}, age={}, tags={}", name, age, tags);
    return Map.of("name", name, "age", age, "tags", tags);
  }

  // 3) multipart/form-data:单文件 + 其他字段
  @PostMapping(value = "/file", consumes = "multipart/form-data")
  public Map<String, Object> uploadFile(@RequestParam("file") MultipartFile file,
                                        @RequestParam(required = false) String desc) throws IOException {
    log.info("POST file: name={}, size={}, desc={}", file.getOriginalFilename(), file.getSize(), desc);
    return Map.of(
        "filename", file.getOriginalFilename(),
        "size", file.getSize(),
        "desc", desc
    );
  }

  // 4) multipart/form-data:多文件
  @PostMapping(value = "/files", consumes = "multipart/form-data")
  public Map<String, Object> uploadFiles(@RequestParam("files") List<MultipartFile> files) {
    var names = files.stream().map(MultipartFile::getOriginalFilename).toList();
    var sizes = files.stream().map(MultipartFile::getSize).toList();
    log.info("POST files: count={}, names={}", files.size(), names);
    return Map.of(
        "count", files.size(),
        "names", names,
        "sizes", sizes
    );
  }

  // 5) multipart/form-data:混合 JSON + 文件(JSON 放在 part 中)
  public static class Meta {
    private String title;
    private Integer count;

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public Integer getCount() { return count; }
    public void setCount(Integer count) { this.count = count; }
  }

  @PostMapping(value = "/mixed", consumes = "multipart/form-data")
  public Map<String, Object> mixed(@RequestPart("meta") Meta meta,
                                   @RequestPart("file") MultipartFile file) {
    log.info("POST mixed: meta.title={}, meta.count={}, file={}", meta.getTitle(), meta.getCount(), file.getOriginalFilename());
    return Map.of(
        "meta", Map.of("title", meta.getTitle(), "count", meta.getCount()),
        "file", Map.of("name", file.getOriginalFilename(), "size", file.getSize())
    );
  }

  // 6) text/plain:纯文本
  @PostMapping(value = "/text", consumes = "text/plain")
  public Map<String, Object> text(@RequestBody String body) {
    log.info("POST text: len={}", body != null ? body.length() : 0);
    return Map.of("length", body != null ? body.length() : 0, "body", body);
  }

  // 7) application/octet-stream:二进制流
  @PostMapping(value = "/binary", consumes = "application/octet-stream")
  public Map<String, Object> binary(@RequestBody byte[] data) {
    log.info("POST binary: size={}", data != null ? data.length : 0);
    return Map.of("size", data != null ? data.length : 0);
  }
}

示例请求(curl):

bash 复制代码
# 1) JSON
curl -X POST "http://localhost:8080/api/post/json" \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","age":20,"tags":["java","spring"]}'

# 2) x-www-form-urlencoded(数组用重复 key)
curl -X POST "http://localhost:8080/api/post/form" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "name=Bob&age=22&tags=java&tags=spring"

# 3) 单文件上传(multipart)
curl -X POST "http://localhost:8080/api/post/file" \
  -F "file=@/path/to/a.png" \
  -F "desc=头像"

# 4) 多文件上传(同名字段)
curl -X POST "http://localhost:8080/api/post/files" \
  -F "files=@/path/to/a.png" \
  -F "files=@/path/to/b.jpg"

# 5) 混合 JSON + 文件(给 JSON part 设置类型)
curl -X POST "http://localhost:8080/api/post/mixed" \
  -F 'meta={"title":"doc","count":2};type=application/json' \
  -F "file=@/path/to/a.pdf"

# 6) 纯文本
curl -X POST "http://localhost:8080/api/post/text" \
  -H "Content-Type: text/plain" \
  --data-binary "hello world"

# 7) 二进制流
curl -X POST "http://localhost:8080/api/post/binary" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @/path/to/file.bin

提示:

  • 表单数组使用重复 key:tags=a&tags=b;或后端声明 String[]/List<String> 显式绑定。
  • 混合 JSON + 文件时,JSON part 的 Content-Type 必须是 application/json 才能被 @RequestPart 解析。
  • 大文件上传请调整 spring.servlet.multipart.max-file-sizemax-request-size

十三、过滤器 Filter 与案例

过滤器位于最前层(Servlet 容器级),可用于日志、鉴权、跨域预处理等。执行顺序:Filter → Servlet/DispatcherServlet → Interceptor → Controller。

java 复制代码
package com.example.demo.filter;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.time.Duration;
import java.time.Instant;

/**
 * 基础日志过滤器:记录请求方法、URI、耗时与状态码
 */
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 越小越先执行
public class RequestLoggingFilter implements Filter {
    private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        Instant start = Instant.now();
        String method = req.getMethod();
        String uri = req.getRequestURI();

        try {
            chain.doFilter(request, response);
        } finally {
            long ms = Duration.between(start, Instant.now()).toMillis();
            log.info("[REQ] {} {} -> status={}, {}ms", method, uri, resp.getStatus(), ms);
        }
    }
}
  • 测试(与前文 GET/POST 控制器配合):
bash 复制代码
# 不带 token 访问受保护的 API(期望 401)
curl -i "http://localhost:8080/api/post/json" \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","age":20}'

# 携带错误 token(期望 403)
curl -i "http://localhost:8080/api/post/json" \
  -H "X-Auth-Token: wrong" \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","age":20}'

# 携带正确 token(期望 200)
curl -i "http://localhost:8080/api/post/json" \
  -H "X-Auth-Token: demo-token" \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","age":20}'

# 被排除的健康检查路径(无需 token,期望 200)
curl -i "http://localhost:8080/health"

提示:

  • 全局跨域建议使用 Spring WebMvcConfigurer 的 CORS 配置(见前文)。Filter 中处理 CORS 需注意 OPTIONS 预检放行。
  • 引入 Spring Security 时,其过滤器链可能优先于或穿插业务 Filter;鉴权逻辑推荐迁移到 Security 中。

十四、拦截器 HandlerInterceptor 与案例

拦截器运行在 Spring MVC 层(DispatcherServlet 之后、Controller 之前/之后),适合做登录鉴权、上下文注入、审计日志等。顺序:Filter → Interceptor → Controller → 异常处理(Advice)。

java 复制代码
package com.example.demo.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.Nullable;
import org.springframework.web.servlet.HandlerInterceptor;

import java.time.Duration;
import java.time.Instant;

/**
 * 日志拦截器:记录方法、URI、耗时
 */
public class LoggingInterceptor implements HandlerInterceptor {
    private static final Logger log = LoggerFactory.getLogger(LoggingInterceptor.class);
    private static final String ATTR_START = "__start_time";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        request.setAttribute(ATTR_START, Instant.now());
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) {
        Object startObj = request.getAttribute(ATTR_START);
        long ms = 0;
        if (startObj instanceof Instant start) {
            ms = Duration.between(start, Instant.now()).toMillis();
        }
        String method = request.getMethod();
        String uri = request.getRequestURI();
        int status = response.getStatus();
        if (ex == null) {
            log.info("[INTCP] {} {} -> status={}, {}ms", method, uri, status, ms);
        } else {
            log.warn("[INTCP] {} {} -> status={}, {}ms, ex={}", method, uri, status, ms, ex.toString());
        }
    }
}
  • 鉴权拦截器(读取 Header 并写入请求属性):
java 复制代码
package com.example.demo.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;

/**
 * 简易 Token 校验:要求 Header: X-Auth-Token=demo-token
 * 校验通过后写入 request attribute: userId
 */
public class AuthInterceptor implements HandlerInterceptor {
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String token = request.getHeader("X-Auth-Token");
    if (token == null || token.isBlank()) {
      response.setStatus(401);
      response.setContentType("application/json;charset=UTF-8");
      response.getWriter().write("{\"success\":false,\"message\":\"Missing X-Auth-Token\"}");
      return false;
    }
    if (!"demo-token".equals(token)) {
      response.setStatus(403);
      response.setContentType("application/json;charset=UTF-8");
      response.getWriter().write("{\"success\":false,\"message\":\"Invalid token\"}");
      return false;
    }
    // 通过:写入请求属性,供 Controller 使用
    request.setAttribute("userId", "demo-user");
    return true;
  }
}
  • 注册拦截器(路径包含/排除、顺序):
java 复制代码
package com.example.demo.config;

import com.example.demo.interceptor.AuthInterceptor;
import com.example.demo.interceptor.LoggingInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    // 日志拦截器:对所有请求生效,最先执行
    registry.addInterceptor(new LoggingInterceptor())
        .addPathPatterns("/**")
        .order(1);

    // 鉴权拦截器:仅拦截 /api/**,排除无需登录的接口
    registry.addInterceptor(new AuthInterceptor())
        .addPathPatterns("/api/**")
        .excludePathPatterns(
            "/health",
            "/actuator/**",
            "/public/**",
            "/api/get/**"
        )
        .order(2);
  }
}
  • 在控制器中读取拦截器写入的属性:
java 复制代码
package com.example.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/api/me")
public class MeController {
  @GetMapping
  public Map<String, Object> me(@RequestAttribute(value = "userId", required = false) String userId) {
    return Map.of(
        "authenticated", userId != null,
        "userId", userId
    );
  }
}
  • 测试命令(与前文 POST 接口一起验证鉴权):
bash 复制代码
# 1) 未带 token 访问受保护接口(期望 401)
curl -i "http://localhost:8080/api/post/json" \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","age":20}'

# 2) 错误 token(期望 403)
curl -i "http://localhost:8080/api/post/json" \
  -H "X-Auth-Token: wrong" \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","age":20}'

# 3) 正确 token(期望 200)
curl -i "http://localhost:8080/api/post/json" \
  -H "X-Auth-Token: demo-token" \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","age":20}'

# 4) 访问 /api/me(展示拦截器写入的 userId)
curl -i "http://localhost:8080/api/me" -H "X-Auth-Token: demo-token"

# 5) 被排除的路径(无需 token)
curl -i "http://localhost:8080/health"

提示:

  • Interceptor 可访问 handler(方法/控制器信息),适合做基于注解的鉴权、审计。
  • 异常被抛出后将交由 @RestControllerAdvice 处理;如在 preHandle 已写响应并返回 false,后续链路不会继续。
  • 若引入 Spring Security,建议把登录鉴权放入 Security 过滤器链与授权机制中,拦截器更多用于业务级横切逻辑。

十五、PUT 参数上传与示例

PUT 常用于"更新"语义,通常与资源标识(如路径变量 id)配合;相较于 POST,PUT 更强调幂等性(同样的请求重复提交结果一致)。以下示例涵盖 JSON、x-www-form-urlencoded、multipart、text/plain 与二进制流。

java 复制代码
package com.example.demo.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/put")
public class PutParamController {
  private static final Logger log = LoggerFactory.getLogger(PutParamController.class);

  // 1) application/json:JSON 体绑定到 DTO
  public static class UpdateUserRequest {
    private String name;
    private Integer age;
    private List<String> tags;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
    public List<String> getTags() { return tags; }
    public void setTags(List<String> tags) { this.tags = tags; }
  }

  @PutMapping(value = "/json", consumes = "application/json")
  public Map<String, Object> json(@RequestBody UpdateUserRequest req) {
    log.info("PUT json: name={}, age={}, tags={}", req.getName(), req.getAge(), req.getTags());
    return Map.of(
        "ok", true,
        "data", Map.of(
            "name", req.getName(),
            "age", req.getAge(),
            "tags", req.getTags()
        )
    );
  }

  // 2) application/x-www-form-urlencoded:表单键值
  @PutMapping(value = "/form", consumes = "application/x-www-form-urlencoded")
  public Map<String, Object> form(@RequestParam String name,
                                  @RequestParam Integer age,
                                  @RequestParam(required = false) List<String> tags) {
    log.info("PUT form: name={}, age={}, tags={}", name, age, tags);
    return Map.of("name", name, "age", age, "tags", tags);
  }

  // 3) multipart/form-data:单文件 + 其他字段
  @PutMapping(value = "/file", consumes = "multipart/form-data")
  public Map<String, Object> uploadFile(@RequestParam("file") MultipartFile file,
                                        @RequestParam(required = false) String desc) throws IOException {
    log.info("PUT file: name={}, size={}, desc={}", file.getOriginalFilename(), file.getSize(), desc);
    return Map.of(
        "filename", file.getOriginalFilename(),
        "size", file.getSize(),
        "desc", desc
    );
  }

  // 4) multipart/form-data:混合 JSON + 文件(JSON 放在 part 中)
  public static class Meta {
    private String title;
    private Integer count;

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public Integer getCount() { return count; }
    public void setCount(Integer count) { this.count = count; }
  }

  @PutMapping(value = "/mixed", consumes = "multipart/form-data")
  public Map<String, Object> mixed(@RequestPart("meta") Meta meta,
                                   @RequestPart("file") MultipartFile file) {
    log.info("PUT mixed: meta.title={}, meta.count={}, file={}", meta.getTitle(), meta.getCount(), file.getOriginalFilename());
    return Map.of(
        "meta", Map.of("title", meta.getTitle(), "count", meta.getCount()),
        "file", Map.of("name", file.getOriginalFilename(), "size", file.getSize())
    );
  }

  // 5) text/plain:纯文本
  @PutMapping(value = "/text", consumes = "text/plain")
  public Map<String, Object> text(@RequestBody String body) {
    log.info("PUT text: len={}", body != null ? body.length() : 0);
    return Map.of("length", body != null ? body.length() : 0, "body", body);
  }

  // 6) application/octet-stream:二进制流
  @PutMapping(value = "/binary", consumes = "application/octet-stream")
  public Map<String, Object> binary(@RequestBody byte[] data) {
    log.info("PUT binary: size={}", data != null ? data.length : 0);
    return Map.of("size", data != null ? data.length : 0);
  }

  // 7) 路径变量 + JSON:典型"更新某个资源"
  @PutMapping(value = "/users/{id}", consumes = "application/json")
  public Map<String, Object> updateUser(@PathVariable Long id, @RequestBody UpdateUserRequest req) {
    log.info("PUT users/{}: name={}, age={}, tags={}", id, req.getName(), req.getAge(), req.getTags());
    return Map.of(
        "id", id,
        "updated", true,
        "data", Map.of("name", req.getName(), "age", req.getAge(), "tags", req.getTags())
    );
  }
}

示例请求(curl):

bash 复制代码
# 1) JSON
curl -X PUT "http://localhost:8080/api/put/json" \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","age":21,"tags":["java","spring"]}'

# 2) x-www-form-urlencoded(数组用重复 key)
curl -X PUT "http://localhost:8080/api/put/form" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "name=Bob&age=22&tags=java&tags=spring"

# 3) 单文件上传(multipart)
curl -X PUT "http://localhost:8080/api/put/file" \
  -F "file=@/path/to/a.png" \
  -F "desc=头像"

# 4) 混合 JSON + 文件(给 JSON part 设置类型)
curl -X PUT "http://localhost:8080/api/put/mixed" \
  -F 'meta={"title":"doc","count":2};type=application/json' \
  -F "file=@/path/to/a.pdf"

# 5) 纯文本
curl -X PUT "http://localhost:8080/api/put/text" \
  -H "Content-Type: text/plain" \
  --data-binary "hello world"

# 6) 二进制流
curl -X PUT "http://localhost:8080/api/put/binary" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @/path/to/file.bin

# 7) 路径变量 + JSON(更新指定 id)
curl -X PUT "http://localhost:8080/api/put/users/100" \
  -H "Content-Type: application/json" \
  -d '{"name":"Carol","age":23,"tags":["k8s"]}'

注意:

  • 幂等性:PUT 语义倾向幂等,更新相同资源应返回相同结果;可配合 If-Match/ETag 做并发控制。
  • 鉴权:若启用了拦截器/过滤器鉴权,PUT 同样需要携带头 X-Auth-Token: demo-token 才能通过。
  • 表单/文件:multipartx-www-form-urlencoded 对 PUT 同样适用;部分代理/网关可能对 PUT 有限制,需在网关放行并正确转发请求体。
  • 安全:开启 Spring Security 时,PUT 默认受 CSRF 保护;如为纯 API 服务可关闭 CSRF 或在请求中附带 CSRF token。

十六、20 个 GET/POST 接口样例

覆盖探活、版本、分页、详情、搜索、请求头/IP、速率限制、时间时区、登录、下单/支付、上传、表单、文本、数值计算等多样场景。每个接口提供功能说明、示例请求与"结果样式"。

  • 约定:若启用拦截器/安全,请携带 X-Auth-Token: demo-token
  1. GET /api/sample/ping --- 健康探活
  • 功能:返回服务运行状态与时间戳
  • 示例请求:
bash 复制代码
curl "http://localhost:8080/api/sample/ping"
  • 结果样式:
json 复制代码
{"ok":true,"ts":1735700000000}
  1. GET /api/sample/version --- 版本信息
  • 功能:返回应用名称、版本与构建
bash 复制代码
curl "http://localhost:8080/api/sample/version"
json 复制代码
{"app":"demo","version":"1.0.0","build":"local"}
  1. GET /api/sample/users --- 用户分页
  • 参数:page(默认1), size(默认10), keyword(可选)
bash 复制代码
curl "http://localhost:8080/api/sample/users?page=1&size=2&keyword=ali"
json 复制代码
{"page":1,"size":2,"keyword":"ali","items":[{"id":1,"name":"Alice","age":20},{"id":2,"name":"Bob","age":22}]}
  1. GET /api/sample/users/{id} --- 用户详情
bash 复制代码
curl "http://localhost:8080/api/sample/users/1001"
json 复制代码
{"id":1001,"name":"User-1001","age":19}
  1. GET /api/sample/search --- 关键字+标签搜索
  • 参数:qtags(多值)
bash 复制代码
curl "http://localhost:8080/api/sample/search?q=phone&tags=android&tags=5g"
json 复制代码
{"q":"phone","tags":["android","5g"],"results":["r1","r2"]}
  1. GET /api/sample/headers --- 请求头读取
  • 功能:读取 X-Trace-IdUser-Agent
bash 复制代码
curl -H "X-Trace-Id: abc-123" -H "User-Agent: curl/8.0" "http://localhost:8080/api/sample/headers"
json 复制代码
{"traceId":"abc-123","userAgent":"curl/8.0"}
  1. GET /api/sample/ip --- 客户端 IP/UA
bash 复制代码
curl -H "User-Agent: demo" "http://localhost:8080/api/sample/ip"
json 复制代码
{"ip":"127.0.0.1","userAgent":"demo"}
  1. GET /api/sample/rate-limit --- 速率限制信息
  • 功能:返回窗口、配额与剩余;同时在响应头设置 X-RateLimit-*
bash 复制代码
curl "http://localhost:8080/api/sample/rate-limit"
json 复制代码
{"window":"1m","limit":100,"remaining":40}
  1. GET /api/sample/echo --- 回显 message
bash 复制代码
curl "http://localhost:8080/api/sample/echo?message=hello"
json 复制代码
{"message":"hello","ts":1735700000000}
  1. GET /api/sample/time --- 当前时间(可选时区)
  • 参数:tz(如 Asia/Shanghai),非法时区自动回退系统默认
bash 复制代码
curl "http://localhost:8080/api/sample/time?tz=Asia/Shanghai"
json 复制代码
{"now":"2025-01-01T08:00:00+08:00[Asia/Shanghai]","zone":"Asia/Shanghai","epochMs":1735700000000}
  1. POST /api/sample/users --- 创建用户(JSON)
bash 复制代码
curl -X POST "http://localhost:8080/api/sample/users" \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","age":20,"tags":["java","spring"]}'
json 复制代码
{"id":1700000000000,"name":"Alice","age":20,"tags":["java","spring"]}
  1. POST /api/sample/users/batch --- 批量创建(JSON)
bash 复制代码
curl -X POST "http://localhost:8080/api/sample/users/batch" \
  -H "Content-Type: application/json" \
  -d '{"users":[{"name":"A","age":18},{"name":"B","age":19}]}'
json 复制代码
{"created":2}
  1. POST /api/sample/login --- 登录(JSON)
bash 复制代码
curl -X POST "http://localhost:8080/api/sample/login" \
  -H "Content-Type: application/json" \
  -d '{"username":"u","password":"p"}'
json 复制代码
{"ok":true,"token":"demo-token"}
  1. POST /api/sample/orders --- 创建订单(JSON)
bash 复制代码
curl -X POST "http://localhost:8080/api/sample/orders" \
  -H "Content-Type: application/json" \
  -d '{"userId":"U1","items":[{"sku":"S1","quantity":2,"price":10.5},{"sku":"S2","quantity":1,"price":20}]}'
json 复制代码
{"orderId":"ORD-1700000000000","userId":"U1","total":41.0}
  1. POST /api/sample/orders/{id}/pay --- 支付订单(JSON)
bash 复制代码
curl -X POST "http://localhost:8080/api/sample/orders/ORD-1/pay" \
  -H "Content-Type: application/json" \
  -d '{"method":"alipay","channel":"app"}'
json 复制代码
{"orderId":"ORD-1","paid":true,"method":"alipay","channel":"app"}
  1. POST /api/sample/upload --- 单文件上传(multipart)
bash 复制代码
curl -X POST "http://localhost:8080/api/sample/upload" \
  -F "file=@/path/to/a.png" -F "desc=头像"
json 复制代码
{"name":"a.png","size":12345,"desc":"头像"}
  1. POST /api/sample/uploads --- 多文件上传(multipart)
bash 复制代码
curl -X POST "http://localhost:8080/api/sample/uploads" \
  -F "files=@/path/to/a.png" \
  -F "files=@/path/to/b.jpg"
json 复制代码
{"count":2,"names":["a.png","b.jpg"],"sizes":[12345,23456]}
  1. POST /api/sample/feedback --- 文本反馈(text/plain)
bash 复制代码
curl -X POST "http://localhost:8080/api/sample/feedback" \
  -H "Content-Type: text/plain" --data-binary "很好用!"
json 复制代码
{"received":12}
  1. POST /api/sample/submit --- 表单提交(x-www-form-urlencoded)
bash 复制代码
curl -X POST "http://localhost:8080/api/sample/submit" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "title=建议&content=内容很多&tags=java&tags=spring"
json 复制代码
{"title":"建议","contentLen":12,"tags":["java","spring"]}
  1. POST /api/sample/compute --- 数值计算(JSON)
  • 功能:avg=true 返回平均值,否则返回总和
bash 复制代码
curl -X POST "http://localhost:8080/api/sample/compute" \
  -H "Content-Type: application/json" \
  -d '{"numbers":[1,2,3.5],"avg":true}'
json 复制代码
{"avg":2.1666666667,"count":3}
java 复制代码
package com.example.demo.controller;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/sample")
public class SampleApiController {
  private static final Logger log = LoggerFactory.getLogger(SampleApiController.class);

  // ---------- GET ----------

  @GetMapping("/ping")
  public Map<String, Object> ping() {
    return Map.of("ok", true, "ts", System.currentTimeMillis());
  }

  @GetMapping("/version")
  public Map<String, Object> version() {
    return Map.of("app", "demo", "version", "1.0.0", "build", "local");
  }

  @GetMapping("/users")
  public Map<String, Object> listUsers(@RequestParam(defaultValue = "1") int page,
                                       @RequestParam(defaultValue = "10") int size,
                                       @RequestParam(required = false) String keyword) {
    return Map.of(
        "page", page,
        "size", size,
        "keyword", keyword,
        "items", List.of(
            Map.of("id", 1, "name", "Alice", "age", 20),
            Map.of("id", 2, "name", "Bob", "age", 22)
        )
    );
  }

  @GetMapping("/users/{id}")
  public Map<String, Object> userDetail(@PathVariable long id) {
    return Map.of("id", id, "name", "User-" + id, "age", 18 + (id % 10));
  }

  @GetMapping("/search")
  public Map<String, Object> search(@RequestParam(name = "q") String keyword,
                                    @RequestParam(required = false) List<String> tags) {
    return Map.of("q", keyword, "tags", tags, "results", List.of("r1", "r2"));
  }

  @GetMapping("/headers")
  public Map<String, Object> headers(@RequestHeader(value = "X-Trace-Id", required = false) String traceId,
                                     @RequestHeader(value = "User-Agent", required = false) String userAgent) {
    return Map.of("traceId", traceId, "userAgent", userAgent);
  }

  @GetMapping("/ip")
  public Map<String, Object> ip(HttpServletRequest request,
                                @RequestHeader(value = "User-Agent", required = false) String userAgent) {
    String xff = request.getHeader("X-Forwarded-For");
    String ip = xff != null && !xff.isBlank() ? xff.split(",")[0].trim() : request.getRemoteAddr();
    return Map.of("ip", ip, "userAgent", userAgent);
  }

  @GetMapping("/rate-limit")
  public Map<String, Object> rateLimit(HttpServletResponse response) {
    int limit = 100, remaining = 42; // 演示值
    response.setHeader("X-RateLimit-Limit", String.valueOf(limit));
    response.setHeader("X-RateLimit-Remaining", String.valueOf(remaining));
    response.setHeader("X-RateLimit-Window", "1m");
    return Map.of("window", "1m", "limit", limit, "remaining", remaining);
  }

  @GetMapping("/echo")
  public Map<String, Object> echo(@RequestParam String message) {
    return Map.of("message", message, "ts", System.currentTimeMillis());
  }

  @GetMapping("/time")
  public Map<String, Object> time(@RequestParam(required = false) String tz) {
    ZoneId zone;
    try {
      zone = tz != null && !tz.isBlank() ? ZoneId.of(tz) : ZoneId.systemDefault();
    } catch (Exception e) {
      zone = ZoneId.systemDefault();
    }
    ZonedDateTime now = ZonedDateTime.now(zone);
    return Map.of("now", now.toString(), "zone", zone.getId(), "epochMs", System.currentTimeMillis());
  }

  // ---------- POST ----------

  public static class CreateUserReq {
    private String name;
    private Integer age;
    private List<String> tags;
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
    public List<String> getTags() { return tags; }
    public void setTags(List<String> tags) { this.tags = tags; }
  }

  public static class BatchCreateUserReq {
    private List<CreateUserReq> users;
    public List<CreateUserReq> getUsers() { return users; }
    public void setUsers(List<CreateUserReq> users) { this.users = users; }
  }

  @PostMapping(value = "/users", consumes = "application/json")
  public Map<String, Object> createUser(@RequestBody CreateUserReq req) {
    long id = System.currentTimeMillis();
    log.info("create user: {} {} {}", req.getName(), req.getAge(), req.getTags());
    return Map.of("id", id, "name", req.getName(), "age", req.getAge(), "tags", req.getTags());
  }

  @PostMapping(value = "/users/batch", consumes = "application/json")
  public Map<String, Object> batchCreate(@RequestBody BatchCreateUserReq req) {
    int count = req.getUsers() != null ? req.getUsers().size() : 0;
    return Map.of("created", count);
  }

  public static class LoginReq {
    private String username;
    private String password;
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
  }

  @PostMapping(value = "/login", consumes = "application/json")
  public Map<String, Object> login(@RequestBody LoginReq req) {
    boolean ok = req.getUsername() != null && !req.getUsername().isBlank();
    return Map.of("ok", ok, "token", ok ? "demo-token" : null);
  }

  public static class OrderItem {
    private String sku;
    private Integer quantity;
    private Double price;
    public String getSku() { return sku; }
    public void setSku(String sku) { this.sku = sku; }
    public Integer getQuantity() { return quantity; }
    public void setQuantity(Integer quantity) { this.quantity = quantity; }
    public Double getPrice() { return price; }
    public void setPrice(Double price) { this.price = price; }
  }

  public static class CreateOrderReq {
    private String userId;
    private List<OrderItem> items;
    public String getUserId() { return userId; }
    public void setUserId(String userId) { this.userId = userId; }
    public List<OrderItem> getItems() { return items; }
    public void setItems(List<OrderItem> items) { this.items = items; }
  }

  @PostMapping(value = "/orders", consumes = "application/json")
  public Map<String, Object> createOrder(@RequestBody CreateOrderReq req) {
    double total = 0.0;
    if (req.getItems() != null) {
      for (OrderItem it : req.getItems()) {
        if (it.getPrice() != null && it.getQuantity() != null) {
          total += it.getPrice() * it.getQuantity();
        }
      }
    }
    String orderId = "ORD-" + System.currentTimeMillis();
    return Map.of("orderId", orderId, "userId", req.getUserId(), "total", total);
  }

  public static class PayReq {
    private String method; // e.g., wechat, alipay, card
    private String channel; // e.g., app, web
    public String getMethod() { return method; }
    public void setMethod(String method) { this.method = method; }
    public String getChannel() { return channel; }
    public void setChannel(String channel) { this.channel = channel; }
  }

  @PostMapping(value = "/orders/{id}/pay", consumes = "application/json")
  public Map<String, Object> pay(@PathVariable String id, @RequestBody PayReq req) {
    return Map.of("orderId", id, "paid", true, "method", req.getMethod(), "channel", req.getChannel());
  }

  @PostMapping(value = "/upload", consumes = "multipart/form-data")
  public Map<String, Object> upload(@RequestParam("file") MultipartFile file,
                                    @RequestParam(required = false) String desc) {
    return Map.of("name", file.getOriginalFilename(), "size", file.getSize(), "desc", desc);
  }

  @PostMapping(value = "/uploads", consumes = "multipart/form-data")
  public Map<String, Object> uploads(@RequestParam("files") List<MultipartFile> files) {
    var names = files.stream().map(MultipartFile::getOriginalFilename).toList();
    var sizes = files.stream().map(MultipartFile::getSize).toList();
    return Map.of("count", files.size(), "names", names, "sizes", sizes);
  }

  @PostMapping(value = "/feedback", consumes = "text/plain")
  public Map<String, Object> feedback(@RequestBody String body) {
    return Map.of("received", body != null ? body.length() : 0);
  }

  @PostMapping(value = "/submit", consumes = "application/x-www-form-urlencoded")
  public Map<String, Object> submit(@RequestParam String title,
                                    @RequestParam String content,
                                    @RequestParam(required = false) List<String> tags) {
    return Map.of("title", title, "contentLen", content.length(), "tags", tags);
  }

  public static class ComputeReq {
    private List<Double> numbers;
    private Boolean avg; // true: 返回平均值;false/null:返回总和
    public List<Double> getNumbers() { return numbers; }
    public void setNumbers(List<Double> numbers) { this.numbers = numbers; }
    public Boolean getAvg() { return avg; }
    public void setAvg(Boolean avg) { this.avg = avg; }
  }

  @PostMapping(value = "/compute", consumes = "application/json")
  public Map<String, Object> compute(@RequestBody ComputeReq req) {
    double sum = 0.0;
    int n = 0;
    if (req.getNumbers() != null) {
      for (Double d : req.getNumbers()) {
        if (d != null) { sum += d; n++; }
      }
    }
    boolean avg = Boolean.TRUE.equals(req.getAvg());
    return avg ? Map.of("avg", n > 0 ? sum / n : 0.0, "count", n)
               : Map.of("sum", sum, "count", n);
  }
}

提示:如启用了拦截器或安全配置,上述接口同样需要按规则携带必要的认证信息(如 X-Auth-Token)才能访问。


作者:xuan

个人博客:https://blog.ybyq.wang

欢迎访问我的博客,获取更多技术文章和教程。

相关推荐
老赵的博客2 小时前
c++ unqiue指针
java·jvm·c++
o0o_-_2 小时前
【go/gopls/mcp】官方gopls内置mcp server使用
开发语言·后端·golang
wuxuanok2 小时前
SpringBoot -原理篇
java·spring boot·spring
柿蒂2 小时前
从if-else和switch,聊聊“八股“的作用
android·java·kotlin
苏三说技术2 小时前
为什么不建议在 Docker 中跑 MySQL?
后端
云动雨颤2 小时前
Spring Boot配置优化:Tomcat+数据库+缓存+日志,全场景教程
数据库·spring boot·tomcat
二饭2 小时前
Spring Boot 项目启动报错:MongoSocketOpenException 连接被拒绝排查日记
java·spring boot·后端
懒虫虫~3 小时前
通过内存去重替换SQL中distinct,优化SQL查询效率
java·sql·慢sql治理
鼠鼠我捏,要死了捏3 小时前
基于Redisson的分布式锁原理深度解析与性能优化实践指南
java·高并发·redisson