关于在SpringBoot3中使用HTTP接口
| 请求方法 | 描述 | 用途 |
|---|---|---|
| GET | 请求获取资源。 | 获取单个或多个资源,数据读取操作。 |
| POST | 提交数据给服务器,通常用于创建资源。 | 创建新资源(例如,提交表单数据或创建新记录)。 |
| PUT | 更新资源,通常用于替换资源的全部内容。 | 替换或更新资源的所有字段(幂等)。 |
| DELETE | 删除资源。 | 删除指定的资源(幂等)。 |
| PATCH | 部分更新资源。 | 更新资源的部分字段(通常用于只更新某些字段)。 |
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
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>
<!-- 项目坐标 -->
<groupId>local.ateng.java</groupId>
<artifactId>http-interface</artifactId>
<version>v1.0</version>
<name>http-interface</name>
<description>SpringBoot3 HTTP接口相关的模块</description>
<!-- 项目属性 -->
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>3.4.1</spring-boot.version>
<maven-compiler.version>3.12.1</maven-compiler.version>
<lombok.version>1.18.36</lombok.version>
</properties>
<!-- 项目依赖 -->
<dependencies>
<!-- Spring Boot Web Starter: 包含用于构建Web应用程序的Spring Boot依赖项 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- Web 容器使用 undertow 性能更强 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!-- Spring Boot Starter Test: 包含用于测试Spring Boot应用程序的依赖项 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Lombok: 简化Java代码编写的依赖项 -->
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<!-- Spring Boot 依赖管理 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 插件仓库配置 -->
<repositories>
<!-- Central Repository -->
<repository>
<id>central</id>
<name>阿里云中央仓库</name>
<url>https://maven.aliyun.com/repository/central</url>
<!--<name>Maven官方中央仓库</name>
<url>https://repo.maven.apache.org/maven2/</url>-->
</repository>
</repositories>
<!-- 构建配置 -->
<build>
<finalName>${project.name}-${project.version}</finalName>
<plugins>
<!-- Maven 编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<!-- 编译参数 -->
<compilerArgs>
<!-- 启用Java 8参数名称保留功能 -->
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<!-- Spring Boot Maven 插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<!-- 第一个资源配置块 -->
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
</resource>
<!-- 第二个资源配置块 -->
<resource>
<directory>src/main/resources</directory>
<includes>
<include>application*</include>
<include>bootstrap*.yml</include>
<include>common*</include>
<include>banner*</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>
</project>server:
port: 12009
servlet:
context-path: /
spring:
main:
web-application-type: servlet
application:
name: ${project.artifactId}
---package local.ateng.java.http.config;
import io.undertow.UndertowOptions;
import io.undertow.server.DefaultByteBufferPool;
import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Configuration;
/**
* Undertow 服务器配置
*
* @author 孔余
* @email 2385569970@qq.com
* @since 2025-02-10
*/
@Configuration
public class UndertowConfig implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
private final int core = Runtime.getRuntime().availableProcessors();
@Override
public void customize(UndertowServletWebServerFactory factory) {
// 定制 Undertow 服务器的构建器
factory.addBuilderCustomizers(builder -> {
// 设置IO线程数
builder.setIoThreads(core * 2);
// 设置工作线程数
builder.setWorkerThreads(core * 2 * 4);
// 设置缓冲区大小
builder.setBufferSize(1024);
// 设置是否直接使用Buffers
builder.setDirectBuffers(true);
// 启用HTTP/2
builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true);
// 设置最大HTTP POST请求大小
builder.setServerOption(UndertowOptions.MAX_ENTITY_SIZE, 10 * 1024 * 1024L); // 10MB
});
// 定制 WebSocket 的部署信息
factory.addDeploymentInfoCustomizers(deploymentInfo -> {
WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(true, 1024));
deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo);
});
}
}package local.ateng.java.http.entity;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
public class MyUser implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键id
*/
private Long id;
/**
* 名称
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 分数
*/
private BigDecimal score;
/**
* 生日
*/
private LocalDate birthday;
/**
* 所在省份
*/
private String province;
/**
* 创建时间
*/
private LocalDateTime createTime;
}GET方法用于请求获取指定资源。它是一个安全、幂等的请求方法,意味着它不会改变服务器的状态,并且可以多次调用而不产生副作用。常用于获取数据。
package local.ateng.java.http.controller;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/get")
public class GetController {
} // 获取单个用户,路径参数
@GetMapping("/user/{id}")
public String getUserByPath(@PathVariable Long id) {
return "User with ID: " + id;
}请求接口
curl -L -X GET http://localhost:12009/get/user/1
// 获取单个用户,请求参数
@GetMapping("/user")
public String getUserByParam(@RequestParam Long id) {
return "User with ID: " + id;
}请求接口
curl -L -X GET http://localhost:12009/get/user?id=1
// 获取单个用户,无参数
@GetMapping("/users")
public String getAllUsers() {
return "List of all users";
}请求接口
curl -L -X GET http://localhost:12009/get/users
POST方法用于提交数据给服务器,通常用于创建资源或提交表单数据。POST请求的请求体可以包含数据。
package local.ateng.java.http.controller;
import local.ateng.java.http.entity.MyUser;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/post")
public class PostController {
} // 创建新用户
@PostMapping("/user")
public String createUser(@RequestBody MyUser user) {
return "User " + user.getName() + " created with age " + user.getAge();
}请求接口
curl -Ss -X POST http://localhost:12009/post/user \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"age": 25,
"score": 88.5,
"birthday": "1999-05-15",
"province": "California",
"createTime": "2025-02-10T14:30:00"
}'
PUT方法用于更新指定资源,它是幂等的,这意味着无论请求多少次,结果都是相同的。常用于替换或更新现有资源。
package local.ateng.java.http.controller;
import local.ateng.java.http.entity.MyUser;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/put")
public class PutController {
} // 更新用户信息
@PutMapping("/user/{id}")
public String updateUser(@PathVariable Long id, @RequestBody MyUser user) {
return "User with ID: " + id + " updated to name: " + user.getName() + " and age: " + user.getAge();
}请求接口
curl -Ss -X PUT http://localhost:12009/put/user/1 \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"age": 25,
"score": 88.5,
"birthday": "1999-05-15",
"province": "California",
"createTime": "2025-02-10T14:30:00"
}'
DELETE方法用于删除指定资源。它同样是幂等的,调用多次不会产生不同的效果。常用于删除服务器上的资源。
package local.ateng.java.http.controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/delete")
public class DeleteController {
} // 删除用户
@DeleteMapping("/user/{id}")
public String deleteUser(@PathVariable Long id) {
return "User with ID: " + id + " deleted.";
}请求接口
curl -Ss -X DELETE http://localhost:12009/delete/user/1
PATCH方法用于部分更新资源,通常用于修改资源的部分字段。与PUT不同,PATCH不要求提交完整的资源数据,仅更新提供的字段。
package local.ateng.java.http.controller;
import local.ateng.java.http.entity.MyUser;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/patch")
public class PatchController {
} // 创建新用户
@PostMapping("/user")
public MyUser createUser(@RequestBody MyUser user) {
return user;
}请求接口
curl -Ss -X PATCH http://localhost:12009/patch/user/1 \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe"
}'
# 配置文件上传大小限制
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
resolve-lazily: true # 开启 multipart 懒加载package local.ateng.java.http.controller;
import local.ateng.java.http.entity.MyUser;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@RestController
@RequestMapping("/file")
public class FileController {
} // 上传单个文件
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file) {
// 获取文件名
String fileName = file.getOriginalFilename();
return "File uploaded successfully: " + fileName;
}请求接口
curl -Ss -X POST http://localhost:12009/file/upload \
-H "Content-Type: multipart/form-data" \
-F "file=@c:/tree.jpg"
上传文件和其他字段(例如描述)
// 上传文件和其他字段(例如描述)
@PostMapping("/uploadWithFields")
public String uploadFileWithFields(@RequestParam("file") MultipartFile file,
@RequestParam("description") String description) {
String fileName = file.getOriginalFilename();
return "File " + fileName + " uploaded with description: " + description;
}请求接口
curl -Ss -X POST http://localhost:12009/file/uploadWithFields \
-H "Content-Type: multipart/form-data" \
-F "file=@c:/tree.jpg" \
-F "description=this is my file"
// 上传文件和实体类
@PostMapping("/uploadWithJson")
public String uploadFileWithJson(@RequestPart("file") MultipartFile file,
@RequestPart("user") MyUser user) {
String fileName = file.getOriginalFilename();
return "File " + fileName + " uploaded with user: " + user;
}请求接口
-
参考:地址
-
需要将user的json内容写入到文件中,然后就可以使用curl命令上传
// 上传多个文件
@PostMapping("/uploadMultiple")
public String uploadMultipleFiles(@RequestParam("files") List<MultipartFile> files) {
StringBuilder fileNames = new StringBuilder();
// 遍历所有上传的文件
for (MultipartFile file : files) {
fileNames.append(file.getOriginalFilename()).append(", ");
}
return "Uploaded files: " + fileNames;
}调用接口
curl -Ss -X POST http://localhost:12009/file/uploadMultiple \
-H "Content-Type: multipart/form-data" \
-F "files=@c:/tree.jpg" \
-F "files=@d:/Temp/2025/202502/20250206/README.md"
/**
* 导出CSV数据接口
*
* 流式生成包含10万条记录的CSV文件,支持断点续传和客户端中断检测
*
* @param response HttpServletResponse对象,用于设置响应头和获取输出流
*/
@GetMapping("/export-csv")
public void exportCsvData(HttpServletResponse response) {
// 设置响应类型为CSV文件下载
response.setContentType("text/csv; charset=UTF-8");
response.setHeader("Content-Disposition",
"attachment; filename=csv_export_" + System.currentTimeMillis() + ".csv");
// 创建流式响应体
StreamingResponseBody stream = outputStream -> {
try {
// 写入CSV文件头
String header = "ID,名称,描述\n";
outputStream.write(header.getBytes(StandardCharsets.UTF_8));
// 分批生成数据并写入输出流
for (int i = 1; i <= 1000000; i++) {
String row = i + ",名称" + i + ",描述" + i + "\n";
try {
outputStream.write(row.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
handleIOException(e);
break; // 发生写入异常时终止循环
}
// 每1000条刷新一次缓冲区
if (i % 1000 == 0) {
outputStream.flush();
}
}
// 最终刷新确保所有数据写入
outputStream.flush();
} catch (IOException e) {
handleIOException(e);
}
};
try {
// 将流式响应写入响应输出流
OutputStream os = response.getOutputStream();
stream.writeTo(os);
} catch (Exception e) {
// 记录日志但不抛出异常,避免重复报错
System.err.println("CSV导出过程中发生异常:" + e.getMessage());
}
}
/**
* 统一处理IO异常
*
* @param e 发生的IO异常对象
*/
private void handleIOException(IOException e) {
if (isClientAbortException(e)) {
System.out.println("客户端已主动断开连接,终止数据导出");
} else {
System.err.println("CSV导出过程中发生IO异常:" + e.getMessage());
e.printStackTrace();
}
}
/**
* 判断是否是客户端主动断开连接异常
*
* @param e 需要判断的异常对象
* @return 如果是客户端断开异常返回true,否则返回false
*/
private boolean isClientAbortException(IOException e) {
Throwable cause = e;
while (cause != null) {
if (cause.getClass().getSimpleName().contains("ClientAbortException")) {
return true;
}
cause = cause.getCause();
}
return false;
}调用接口
curl -Ss -X GET http://localhost:12009/file/export-csv
package local.ateng.java.http.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Component
public class RequestTimeInterceptor implements HandlerInterceptor {
// 在请求处理之前调用,记录开始时间
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 记录请求开始时间
request.setAttribute("startTime", System.currentTimeMillis());
return true; // 继续执行下一个拦截器或者处理器
}
// 在请求处理之后调用,用于记录耗时
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// 计算请求耗时
long startTime = (Long) request.getAttribute("startTime");
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// 打印日志
System.out.println("Request URL: " + request.getRequestURL() + " | Duration: " + duration + " ms");
}
// 在请求处理完成之后调用
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
// 可以做一些资源清理等工作
request.removeAttribute("startTime");
}
}package local.ateng.java.http.config;
import local.ateng.java.http.interceptor.RequestTimeInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web 配置
*
* @author 孔余
* @email 2385569970@qq.com
* @since 2025-02-10
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final RequestTimeInterceptor requestTimeInterceptor;
@Autowired
public WebConfig(RequestTimeInterceptor requestTimeInterceptor) {
this.requestTimeInterceptor = requestTimeInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器,并配置拦截的路径
registry.addInterceptor(requestTimeInterceptor)
.addPathPatterns("/**"); // 拦截所有请求
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
}
/**
* 跨域配置
*/
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// 设置访问源地址
config.addAllowedOriginPattern("*");
// 设置访问源请求头
config.addAllowedHeader("*");
// 设置访问源请求方法
config.addAllowedMethod("*");
// 有效期 1800秒
config.setMaxAge(1800L);
// 添加映射路径,拦截一切请求
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
// 返回新的CorsFilter
return new CorsFilter(source);
}
}访问接口后就出现如下日志:
package local.ateng.java.http.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.ServletRequestHandledEvent;
/**
* 事件监听器:记录接口请求的处理耗时
*
* @author 孔余
* @email 2385569970@qq.com
* @since 2025-02-11
*/
@Component
@Slf4j
public class RequestTimingEventListener implements ApplicationListener<ServletRequestHandledEvent> {
@Override
public void onApplicationEvent(ServletRequestHandledEvent event) {
// 获取请求失败的原因
Throwable failureCause = event.getFailureCause();
String failureMessage = (failureCause == null) ? "" : failureCause.getMessage();
// 获取请求相关的信息
String clientAddress = event.getClientAddress();
String requestUrl = event.getRequestUrl();
String method = event.getMethod();
long processingTimeMillis = event.getProcessingTimeMillis();
// 日志输出请求处理信息
if (failureCause == null) {
log.info("客户端地址: {},请求路径: {},请求方法: {},处理耗时: {} 毫秒",
clientAddress, requestUrl, method, processingTimeMillis);
} else {
log.error("客户端地址: {},请求路径: {},请求方法: {},处理耗时: {} 毫秒,错误信息: {}",
clientAddress, requestUrl, method, processingTimeMillis, failureMessage);
}
}
}参考openssl创建证书,得到以下证书:
生成服务端证书时注意修改 dn 和 alt_names 模块的内容,alt_names中需要填写会和服务端有交互的域名和IP
- ateng-ca.crt
- ateng-server.key
- ateng-server.crt
将证书和私钥打包成 Java Keystore(PKCS12 格式)
在Spring Boot中,我们通常使用 PKCS12 格式的密钥库,而不是传统的Java keystore格式。接下来,你需要将生成的证书和私钥合并成一个密钥库文件(例如 ateng-server.p12),以便Spring Boot使用。
openssl pkcs12 -export -in ateng-server.crt -inkey ateng-server.key -out ateng-server.p12 -name ateng-server
你会被要求输入导出密码,记得记住这个密码,因为在配置 Spring Boot 时需要用到。
将证书 ateng-server.p12 放在 resources 目录下
---
# HTTPS 配置
server:
ssl:
key-store: classpath:ateng-server.p12
key-store-password: Admin@123
key-store-type: PKCS12
key-alias: ateng-servercurl -L --cacert ateng-ca.crt -X GET https://server:12009/get/user/1











