軟件工程實踐二:Spring Boot 知識回顧

文章目錄

      • 一、創建項目(Spring Boot 向導)
      • 二、項目最小代碼示例
      • 三、運行與驗證
      • 四、標準目錄結構與說明
      • 五、Maven 依賴最小示例(僅供參考)
      • 六、常用配置(application.yml 示例)
      • 七、返回 JSON 與統一異常
      • 八、@Value 配置讀取示例
      • 九、日志引入與配置(SLF4J + Logback)
      • 十、@ConfigurationProperties 批量綁定與校驗
      • 十一、GET 參數處理與示例
      • 十二、POST 參數上傳與示例
      • 十三、過濾器 Filter 與案例
      • 十四、攔截器 HandlerInterceptor 與案例
      • 十五、PUT 參數上傳與示例
      • 十六、20 個 GET/POST 接口樣例


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


一、創建項目(Spring Boot 向導)

2025-09-10T01:45:26.png

步驟說明:

  • 打開 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 已生成,檢查包名與類名)
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 控制器(新增)
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. 應用配置(可選)
# src/main/resources/application.yml
server:# 如需修改端口,取消注釋并調整值# port: 8080

三、運行與驗證

  • DemoApplication 類左側點擊綠色運行箭頭,或使用菜單 Run
  • 控制臺看到 Started DemoApplication ... 表示啟動成功。
  • 瀏覽器/工具訪問:http://localhost:8080/api/hello
    • 期望響應示例:
{"msg":"hello","time":"2025-01-01T00:00:00Z"}

常見問題:

  • 端口占用:在 application.yml 配置 server.port: 8081
  • 依賴未下載:檢查網絡代理或嘗試 Reload All Maven Projects
  • JDK 不匹配:Project Structure > Project/Modules 指定與向導一致的 Java 版本。

四、標準目錄結構與說明

 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 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 示例)

spring:application:name: demojackson:time-zone: Asia/Shanghaidate-format: yyyy-MM-dd HH:mm:ssserver:port: 8080# servlet:#   context-path: /apilogging:level:root: INFOcom.example.demo: DEBUG# 多環境(profiles),開發/生產差異化配置(任選其一:此處或獨立 profile 文件)
# spring:
#   profiles:
#     active: dev
  • 多環境配置(Profiles)示例:
# application-dev.yml(開發環境)
logging:level:com.example.demo: DEBUG
# application-prod.yml(生產環境)
logging:level:root: INFO
  • 運行時指定環境:

    • application.yml 寫:
      spring:profiles:active: dev
      
    • 或啟動參數:--spring.profiles.active=prod
  • 全局 CORS(跨域)配置(如需前端跨域訪問):

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 {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOrigins("http://localhost:3000").allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS").allowCredentials(true);}
}
  • 全局異常處理(將錯誤轉換為統一 JSON 響應):
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 返回
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) {}@PostMappingpublic Map<String, Object> create(@RequestBody CreateUserRequest req) {long generatedId = System.currentTimeMillis();return Map.of("id", generatedId,"name", req.name(),"age", req.age());}
}
  • 案例 2:觸發異常并由全局異常處理返回統一結構

將下述方法加入 UserController 以體驗統一異常響應:

@GetMapping("/error")
public Map<String, Object> error() {throw new IllegalStateException("示例異常");
}
  • 測試命令示例:
# 獲取用戶(攜帶查詢參數 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 定義配置:
app:name: demo-apptimeout: 5s            # 可自動轉換為 Durationwhite-list:- 127.0.0.1- 10.0.0.1db:host: 127.0.0.1port: 3306
  1. 使用 @Value 讀取(支持默認值與基礎類型轉換):
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。
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 配置:
logging:level:root: INFOcom.example.demo: DEBUGfile:name: logs/app.log           # 輸出到文件(會自動創建文件夾)logback:rollingpolicy:               # 基于 Logback 的滾動策略(無需 xml)max-file-size: 10MBmax-history: 7total-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、時長等類型):
app:name: demo-apptimeout: 5swhite-list:- 127.0.0.1- 10.0.0.1db:host: 127.0.0.1port: 3306
  • 屬性類(開啟校驗,演示嵌套綁定與默認值):
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; }
}
  • 啟用方式(三選一):

    • 在啟動類上開啟掃描(推薦):
    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);}
    }
    
    • 或在配置類上顯式啟用:
    import org.springframework.boot.context.properties.EnableConfigurationProperties;@EnableConfigurationProperties(AppProperties.class)
    public class PropertiesConfig {}
    
    • 或直接把屬性類聲明為 Bean:
    import org.springframework.context.annotation.Bean;@org.springframework.context.annotation.Configuration
    public class PropertiesConfig2 {@Beanpublic AppProperties appProperties() { return new AppProperties(); }
    }
    

    也可在屬性類上加 @Component 直接讓其被掃描成 Bean(需保證包路徑可被掃描)。

  • 校驗依賴(若使用 @Validated/約束注解,請確保已引入):

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>

十一、GET 參數處理與示例

GET 適用于通過查詢參數與路徑變量傳遞輕量數據;GET 不能上傳文件(multipart/form-data)。

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

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 體綁定到 DTOpublic 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):

# 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。

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);@Overridepublic 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 控制器配合):
# 不帶 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)。

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";@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {request.setAttribute(ATTR_START, Instant.now());return true;}@Overridepublic 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 并寫入請求屬性):
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 {@Overridepublic 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;}
}
  • 注冊攔截器(路徑包含/排除、順序):
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 {@Overridepublic 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);}
}
  • 在控制器中讀取攔截器寫入的屬性:
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 {@GetMappingpublic Map<String, Object> me(@RequestAttribute(value = "userId", required = false) String userId) {return Map.of("authenticated", userId != null,"userId", userId);}
}
  • 測試命令(與前文 POST 接口一起驗證鑒權):
# 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 與二進制流。

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 體綁定到 DTOpublic 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):

# 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 — 健康探活
  • 功能:返回服務運行狀態與時間戳
  • 示例請求:
curl "http://localhost:8080/api/sample/ping"
  • 結果樣式:
{"ok":true,"ts":1735700000000}
  1. GET /api/sample/version — 版本信息
  • 功能:返回應用名稱、版本與構建
curl "http://localhost:8080/api/sample/version"
{"app":"demo","version":"1.0.0","build":"local"}
  1. GET /api/sample/users — 用戶分頁
  • 參數:page(默認1), size(默認10), keyword(可選)
curl "http://localhost:8080/api/sample/users?page=1&size=2&keyword=ali"
{"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} — 用戶詳情
curl "http://localhost:8080/api/sample/users/1001"
{"id":1001,"name":"User-1001","age":19}
  1. GET /api/sample/search — 關鍵字+標簽搜索
  • 參數:qtags(多值)
curl "http://localhost:8080/api/sample/search?q=phone&tags=android&tags=5g"
{"q":"phone","tags":["android","5g"],"results":["r1","r2"]}
  1. GET /api/sample/headers — 請求頭讀取
  • 功能:讀取 X-Trace-IdUser-Agent
curl -H "X-Trace-Id: abc-123" -H "User-Agent: curl/8.0" "http://localhost:8080/api/sample/headers"
{"traceId":"abc-123","userAgent":"curl/8.0"}
  1. GET /api/sample/ip — 客戶端 IP/UA
curl -H "User-Agent: demo" "http://localhost:8080/api/sample/ip"
{"ip":"127.0.0.1","userAgent":"demo"}
  1. GET /api/sample/rate-limit — 速率限制信息
  • 功能:返回窗口、配額與剩余;同時在響應頭設置 X-RateLimit-*
curl "http://localhost:8080/api/sample/rate-limit"
{"window":"1m","limit":100,"remaining":40}
  1. GET /api/sample/echo — 回顯 message
curl "http://localhost:8080/api/sample/echo?message=hello"
{"message":"hello","ts":1735700000000}
  1. GET /api/sample/time — 當前時間(可選時區)
  • 參數:tz(如 Asia/Shanghai),非法時區自動回退系統默認
curl "http://localhost:8080/api/sample/time?tz=Asia/Shanghai"
{"now":"2025-01-01T08:00:00+08:00[Asia/Shanghai]","zone":"Asia/Shanghai","epochMs":1735700000000}
  1. POST /api/sample/users — 創建用戶(JSON)
curl -X POST "http://localhost:8080/api/sample/users" \-H "Content-Type: application/json" \-d '{"name":"Alice","age":20,"tags":["java","spring"]}'
{"id":1700000000000,"name":"Alice","age":20,"tags":["java","spring"]}
  1. POST /api/sample/users/batch — 批量創建(JSON)
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}]}'
{"created":2}
  1. POST /api/sample/login — 登錄(JSON)
curl -X POST "http://localhost:8080/api/sample/login" \-H "Content-Type: application/json" \-d '{"username":"u","password":"p"}'
{"ok":true,"token":"demo-token"}
  1. POST /api/sample/orders — 創建訂單(JSON)
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}]}'
{"orderId":"ORD-1700000000000","userId":"U1","total":41.0}
  1. POST /api/sample/orders/{id}/pay — 支付訂單(JSON)
curl -X POST "http://localhost:8080/api/sample/orders/ORD-1/pay" \-H "Content-Type: application/json" \-d '{"method":"alipay","channel":"app"}'
{"orderId":"ORD-1","paid":true,"method":"alipay","channel":"app"}
  1. POST /api/sample/upload — 單文件上傳(multipart)
curl -X POST "http://localhost:8080/api/sample/upload" \-F "file=@/path/to/a.png" -F "desc=頭像"
{"name":"a.png","size":12345,"desc":"頭像"}
  1. POST /api/sample/uploads — 多文件上傳(multipart)
curl -X POST "http://localhost:8080/api/sample/uploads" \-F "files=@/path/to/a.png" \-F "files=@/path/to/b.jpg"
{"count":2,"names":["a.png","b.jpg"],"sizes":[12345,23456]}
  1. POST /api/sample/feedback — 文本反饋(text/plain)
curl -X POST "http://localhost:8080/api/sample/feedback" \-H "Content-Type: text/plain" --data-binary "很好用!"
{"received":12}
  1. POST /api/sample/submit — 表單提交(x-www-form-urlencoded)
curl -X POST "http://localhost:8080/api/sample/submit" \-H "Content-Type: application/x-www-form-urlencoded" \-d "title=建議&content=內容很多&tags=java&tags=spring"
{"title":"建議","contentLen":12,"tags":["java","spring"]}
  1. POST /api/sample/compute — 數值計算(JSON)
  • 功能:avg=true 返回平均值,否則返回總和
curl -X POST "http://localhost:8080/api/sample/compute" \-H "Content-Type: application/json" \-d '{"numbers":[1,2,3.5],"avg":true}'
{"avg":2.1666666667,"count":3}
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, cardprivate String channel; // e.g., app, webpublic 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
歡迎訪問我的博客,獲取更多技術文章和教程。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/922427.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/922427.shtml
英文地址,請注明出處:http://en.pswp.cn/news/922427.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【系列文章】Linux中的并發與競爭[04]-信號量

【系列文章】Linux中的并發與競爭[04]-信號量 該文章為系列文章&#xff1a;Linux中的并發與競爭中的第4篇 該系列的導航頁連接&#xff1a; 【系列文章】Linux中的并發與競爭-導航頁 文章目錄【系列文章】Linux中的并發與競爭[04]-信號量一、信號量二、實驗程序的編寫2.1驅動…

Elasticsearch啟動失敗?5步修復權限問題

文章目錄&#x1f6a8; 為什么會出現這個問題&#xff1f;? 解決方案&#xff1a;修復數據目錄權限并確保配置生效步驟 1&#xff1a;確認數據目錄存在且權限正確步驟 2&#xff1a;確認 elasticsearch.yml 中的配置步驟 3&#xff1a;**刪除或清空 /usr/share/elasticsearch/…

Docker push 命令:鏡像發布與管理的藝術

Docker push 命令&#xff1a;鏡像發布與管理的藝術1. 命令概述2. 命令語法3. 核心參數解析4. 推送架構圖解5. 完整工作流程6. 實戰場景示例6.1 基礎推送操作6.2 企業級推送流程6.3 多架構鏡像推送7. 鏡像命名規范詳解8. 安全最佳實踐8.1 內容信任機制8.2 最小權限原則9. 性能優…

智能合約測試框架全解析

概述 智能合約測試庫是區塊鏈開發中至關重要的工具&#xff0c;用于確保智能合約的安全性、正確性和可靠性。以下是主流的智能合約測試庫及其詳細解析。 一、主流測試框架對比 測試框架開發語言主要特點適用場景Hardhat WaffleJavaScript/TypeScript強大的調試功能&#xf…

【大模型算法工程師面試題】大模型領域新興的主流庫有哪些?

文章目錄 大模型領域新興主流庫全解析:國產化適配+優劣對比+選型指南(附推薦指數) 引言 一、總覽:大模型工具鏈選型框架(含推薦指數) 二、分模塊詳解:優劣對比+推薦指數+選型建議 2.1:訓練框架(解決“千億模型怎么訓”) 2.2:推理優化(解決“模型跑起來慢”) 2.3:…

端口打開與服務可用

端口打開與服務可用“端口已打開但服務不可用” 并非矛盾&#xff0c;而是網絡訪問中常見的分層問題。要理解這一點&#xff0c;需要先明確 “端口打開” 和 “服務可用” 的本質區別&#xff1a;1. 什么是 “端口打開”&#xff1f;“端口打開” 通常指 操作系統的網絡層監聽該…

ByteDance_FrontEnd

約面了&#xff0c;放輕松&#xff0c;好好面 盲點 基礎知識 Function 和 Object 都是函數&#xff0c;而函數也是對象。 Object.prototype 是幾乎所有對象的原型鏈終點&#xff08;其 proto 是 null&#xff09;。 Function.prototype 是所有函數的原型&#xff08;包括 Obje…

go語言,彩色驗證碼生成,加減法驗證,

代碼結構相關代碼 captcha/internal/captcha/generator.go package captchaimport (_ "embed" // &#x1f448; 啟用 embed"image""image/color""image/draw""image/png""io""math/rand""golang.…

PuTTY軟件訪問ZYNQ板卡的Linux系統

PuTTY 是一款非常經典、輕量級、免費的 SSH、Telnet 和串行端口連接客戶端&#xff0c;主要運行于 Windows 平臺。它是在開源許可下開發的&#xff0c;因其小巧、簡單、可靠而成為系統管理員、網絡工程師和開發人員的必備工具。網上有非常多的下載資源。 我們使用PuTTY軟件對ZY…

做一個RBAC權限

在分布式應用場景下&#xff0c;我們可以利用網關對請求進行集中處理&#xff0c;實現了低耦合&#xff0c;高內聚的特性。 登陸權限驗證和鑒權的功能都可以在網關層面進行處理&#xff1a; 用戶登錄后簽署的jwt保存在header中&#xff0c;用戶信息則保存在redis中網關應該對不…

【算法】day1 雙指針

1、移動零&#xff08;同向分3區域&#xff09; 283. 移動零 - 力扣&#xff08;LeetCode&#xff09; 題目&#xff1a; 思路&#xff1a;注意原地操作。快排也是這個方法&#xff1a;左邊小于等于 tmp&#xff0c;右邊大于 tmp&#xff0c;最后 tmp 放到 dest。 代碼&#…

Linux 日志分析:用 ELK 搭建個人運維監控平臺

Linux 日志分析&#xff1a;用 ELK 搭建個人運維監控平臺 &#x1f31f; Hello&#xff0c;我是摘星&#xff01; &#x1f308; 在彩虹般絢爛的技術棧中&#xff0c;我是那個永不停歇的色彩收集者。 &#x1f98b; 每一個優化都是我培育的花朵&#xff0c;每一個特性都是我放飛…

Linux網絡:socket編程UDP

文章目錄前言一&#xff0c;socket二&#xff0c;服務端socket3-1 創建socket3-2 綁定地址和端口3-3 接收數據3-4 回復數據3-5關閉socket3-6 完整代碼三&#xff0c;客戶端socket3-1 為什么客戶端通常不需要手動定義 IP 和端口前言 學習 socket 編程的意義在于&#xff1a;它讓…

【從零到公網】本地電腦部署服務并實現公網訪問(IPv4/IPv6/DDNS 全攻略)

從零到公網&#xff1a;本地電腦部署服務并實現公網訪問&#xff08;IPv4/IPv6/DDNS 全攻略&#xff09; 適用場景&#xff1a;本地 API 服務、大模型推理服務、NAS、遠程桌面等需要公網訪問的場景 關鍵詞&#xff1a;公網 IP、端口映射、內網穿透、IPv6、Cloudflare DDNS 一、…

模塊二 落地微服務

11 | 服務發布和引用的實踐 服務發布和引用常見的三種方式&#xff1a;Restful API、XML配置以及IDL文件。今天我將以XML配置方式為例&#xff0c;給你講解服務發布和引用的具體實踐以及可能會遇到的問題。 XML配置方式的服務發布和引用流程 1. 服務提供者定義接口 服務提供者發…

C++程序員速通C#:從Hello World到數據類型

C程序員光速入門C#&#xff08;一&#xff09;&#xff1a;總覽、數據類型、運算符 一.Hello world&#xff01; 隨著.NET的深入人心,作為一個程序員&#xff0c;當然不能在新技術面前停而止步&#xff0c;面對著c在.net中的失敗,雖然有一絲遺憾&#xff0c;但是我們應該認識到…

Linux相關概念和易錯知識點(44)(IP地址、子網和公網、NAPT、代理)

目錄1.IP地址&#xff08;1&#xff09;局域網和公網①局域網a.網關地址b.局域網通信②運營商子網③公網&#xff08;2&#xff09;NAPT①NAPT過程②理解NAPT③理解源IP和目的IPa.目的IPb.源IP③最長前綴匹配④NAT技術缺陷2.代理服務&#xff08;1&#xff09;正向代理&#xf…

工業智能終端賦能自動化生產線建設數字化管理

在當今數字化浪潮的推動下&#xff0c;自動化生產線正逐漸成為各行各業提升效率和降低成本的重要選擇。隨著智能制造的深入發展&#xff0c;工業智能終端的引入不僅為生產線帶來了技術革新&#xff0c;也賦予了數字化管理新的動力。一、工業智能終端&#xff1a;一體化設計&…

【Vue2手錄06】計算屬性Computed

一、表單元素的v-model綁定&#xff08;核心場景&#xff09; v-model 是Vue實現“表單元素與數據雙向同步”的語法糖&#xff0c;不同表單元素的綁定規則存在差異&#xff0c;需根據元素類型選擇正確的綁定方式。 1.1 四大表單元素的綁定規則對比表單元素類型綁定數據類型核心…

FPGA入門-數碼管靜態顯示

19. 數碼管的靜態顯示 在許多項目設計中&#xff0c;我們通常需要一些顯示設備來顯示我們需要的信息&#xff0c;可以選擇的顯示設備有很多&#xff0c;而數碼管是使用最多&#xff0c;最簡單的顯示設備之一。數碼管是一種半導體發光器件&#xff0c;具有響應時間短、體積小、…