软件工程实践二: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 向导)
步骤说明:
- 打开 IDEA → New Project → 选择 Spring Boot;服务地址保持默认。
- Project Metadata:
- Group:如
com.example
- Artifact/Name:如
demo
- Packaging:
jar
(默认) - Java:
17
(或本机已安装的 LTS 版本)
- Group:如
- Build system:
Maven
(不要选 Gradle) - Dependencies:搜索并仅勾选
Spring Web
。 - Finish → 等待 Maven 下载依赖与索引完成。
提示:若看不到 Spring Boot(Initializr),确保安装了 IntelliJ 的 Spring 插件,或使用 File > New > Project...
再选择。
二、项目最小代码示例
- 主启动类(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);}
}
- 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());}
}
- 应用配置(可选)
# 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 配置读取示例
- 在
application.yml
定义配置:
app:name: demo-apptimeout: 5s # 可自动转换为 Durationwhite-list:- 127.0.0.1- 10.0.0.1db:host: 127.0.0.1port: 3306
- 使用
@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 依赖。
- 切换 Log4j2:在
十、@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 表达(如PT30S
、PT5M
)。
十二、POST 参数上传与示例
常见 POST 载荷类型:
application/json
、application/x-www-form-urlencoded
、multipart/form-data
、text/plain
、application/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-size
与max-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
才能通过。 - 表单/文件:
multipart
与x-www-form-urlencoded
对 PUT 同样适用;部分代理/网关可能对 PUT 有限制,需在网关放行并正确转发请求体。 - 安全:开启 Spring Security 时,PUT 默认受 CSRF 保护;如为纯 API 服务可关闭 CSRF 或在请求中附带 CSRF token。
十六、20 个 GET/POST 接口样例
覆盖探活、版本、分页、详情、搜索、请求头/IP、速率限制、时间时区、登录、下单/支付、上传、表单、文本、数值计算等多样场景。每个接口提供功能说明、示例请求与“结果样式”。
- 约定:若启用拦截器/安全,请携带
X-Auth-Token: demo-token
。
- GET
/api/sample/ping
— 健康探活
- 功能:返回服务运行状态与时间戳
- 示例请求:
curl "http://localhost:8080/api/sample/ping"
- 结果样式:
{"ok":true,"ts":1735700000000}
- GET
/api/sample/version
— 版本信息
- 功能:返回应用名称、版本与构建
curl "http://localhost:8080/api/sample/version"
{"app":"demo","version":"1.0.0","build":"local"}
- 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}]}
- GET
/api/sample/users/{id}
— 用户详情
curl "http://localhost:8080/api/sample/users/1001"
{"id":1001,"name":"User-1001","age":19}
- GET
/api/sample/search
— 关键字+标签搜索
- 参数:
q
、tags
(多值)
curl "http://localhost:8080/api/sample/search?q=phone&tags=android&tags=5g"
{"q":"phone","tags":["android","5g"],"results":["r1","r2"]}
- GET
/api/sample/headers
— 请求头读取
- 功能:读取
X-Trace-Id
与User-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"}
- GET
/api/sample/ip
— 客户端 IP/UA
curl -H "User-Agent: demo" "http://localhost:8080/api/sample/ip"
{"ip":"127.0.0.1","userAgent":"demo"}
- GET
/api/sample/rate-limit
— 速率限制信息
- 功能:返回窗口、配额与剩余;同时在响应头设置
X-RateLimit-*
curl "http://localhost:8080/api/sample/rate-limit"
{"window":"1m","limit":100,"remaining":40}
- GET
/api/sample/echo
— 回显 message
curl "http://localhost:8080/api/sample/echo?message=hello"
{"message":"hello","ts":1735700000000}
- 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}
- 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"]}
- 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}
- 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"}
- 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}
- 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"}
- 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":"头像"}
- 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]}
- POST
/api/sample/feedback
— 文本反馈(text/plain)
curl -X POST "http://localhost:8080/api/sample/feedback" \-H "Content-Type: text/plain" --data-binary "很好用!"
{"received":12}
- 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"]}
- 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
欢迎访问我的博客,获取更多技术文章和教程。