Spring完整项目:带版本的任务API · Spring任务API实现了哪些业务边界至Spring完整项目怎样复用同一份源码
本篇承接语言基础,围绕同一任务模型展开。示例未执行;命令与现象均为读者未来自行验证的步骤和预期,不能视为运行记录。
Spring完整项目:带版本的任务API
本篇承接语言基础,围绕同一任务模型展开。示例未执行;命令与现象均为读者未来自行验证的步骤和预期,不能视为运行记录。
Spring任务API实现了哪些业务边界
Spring任务API实现了哪些业务边界?项目实现任务列表、创建、详情、替换和删除。标题非空、至多120个UTF-16单元,done必填,更新和删除携带读取时的字符串版本;列表前100条。不存在为404,格式错误为400,旧版本为409,未知内部故障为500。没有账号、角色、公开部署、生产迁移或自动重试功能;任务为当前本机进程内共享数据。
完整文件在examples/java/p2-spring,可单独复制目录。请求→DTO→服务事务→Repository→实体→View形成可追踪链条。读者应先沿构造器依赖阅读,再看事务与异常处理,最后读测试;不需要先搭建外部数据库。本文所有源码与测试均为示例未执行,本轮没有Maven、JDK、HTTP或H2运行证据。
Spring项目的手写OpenAPI能证明接口运行吗
Spring项目的手写OpenAPI能证明接口运行吗?不能。OpenAPI文件是先于实现落盘的手写静态契约,只说明路径、方法、请求字段、响应和错误约定,既不是从运行服务生成,也不是已验证的服务清单。Java与Vue项目各自带有字节一致的openapi.json,方便单独复制和比较;维护时须同时修改并校对,不能依赖两个副本永远自动同步。
POST /api/tasks输入title/done返回201;GET列表和详情返回200;PUT输入title/done/version返回200;DELETE使用version查询参数返回204且无JSON体。JSON对象禁止未知字段,所有id/version均为字符串。契约描述Long上限和UTF-16限制,但OpenAPI字符串maxLength通常按Unicode码点解释;补充文字是本例Java长度规则的必要约束,客户端不能只凭schema推导字形长度。
Spring完整项目怎样复用同一份源码
Spring完整项目怎样复用同一份源码?下方直接包含仓库源码、配置、测试、契约和README,避免文章另抄一份而与项目漂移。README保存读者未来可自行执行的命令及预期结果;文件包含不会执行Java、启动服务器或连接数据库。src/test中的测试是可运行目标源码,当前未执行,也不构成编译成功的证明。
阅读时把pom.xml的Java/Boot基线、application.properties的内存库生命周期、TaskService的事务和Errors的HTTP映射对应起来。任何修改若影响字段或状态码,都应同时改手写契约、Vue调用和验收规格。生产迁移/鉴权应在明确需求后独立设计,不把配置示例误当部署指南。
openapi.json
完整文件:openapi.json 完整源码。保留原始内容,使用前请核对本文的版本与环境说明。
pom.xml
<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.7</version><relativePath/></parent>
<groupId>notes</groupId><artifactId>p2-spring-tasks</artifactId><version>1.0.0</version>
<properties><java.version>21</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-validation</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></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>README.md
# Spring MVC任务API(示例未执行)
Java21、Maven3.6.3+;Spring Boot固定3.5.7为已发布教学基线,非当前最新版承诺。依赖交由该parent管理。本轮未下载依赖、编译、运行或测试,未启动H2。
项目可单独复制。openapi.json为先于实现手写的3.0.3契约,不是运行结果或生成文档。src/main为完整源码,src/test为未执行测试。无账号、无认证,不承诺生产安全;只绑定127.0.0.1。H2内存库重启即丢失,create-drop只适用教学。列表固定id升序前100条,不提供分页。
读者日后自行运行:在此目录先 `mvn test`,再 `mvn spring-boot:run`。这会启动应用和内存数据库;本次编写未执行这些命令。健康检查不在契约范围,直接请求GET /api/tasks。
预期流程:POST /api/tasks,JSON为 {"title":"阅读事务","done":false},返回201与字符串id/version;GET /api/tasks/{id}得200;PUT同地址携带title、done、原version得200;旧version更新或删除得409,GET后用新version重试。DELETE地址附?version=当前版本得204,再GET得404。格式错误/超Long范围/空标题/缺done/未知字段得400。所有契约错误体为code/message;500仅通用消息,服务端记录异常。ID和版本在网络上始终是字符串,禁止转成JavaScript Number。
测试TaskApiTest覆盖空标题、字符串ID、改变字段后的旧版本冲突、删除、404和Long溢出;并发规格:两个独立事务读取相同版本并修改不同标题,恰好一个提交,另一个409;这项需读者另以并发集成测试验证,不能用顺序测试证明。无变化的PUT允许保留版本,不承诺每次请求版本递增。
边界:校验在控制器、事务在服务代理、JPA实体不直接输出;flush使数据库冲突在服务事务内出现,事务提交仍可能失败,响应只在服务返回后形成。生产迁移、权限、重试幂等键、分页、持久数据库和运维另设计。本例删除也检查版本。JPA的@Version是数据库写入条件,显式版本比较不能替代它。src/main/java/notes/tasks/ApiFailure.java
package notes.tasks;
final class ApiFailure extends RuntimeException {
final int status;
final String code;
ApiFailure(int status, String code, String message) { super(message); this.status = status; this.code = code; }
}src/main/java/notes/tasks/Errors.java
package notes.tasks;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.*;
@RestControllerAdvice
class Errors {
private static final Logger log = LoggerFactory.getLogger(Errors.class);
@ExceptionHandler(ApiFailure.class)
ResponseEntity<TaskDtos.Error> known(ApiFailure e) { return reply(e.status, e.code, e.getMessage()); }
@ExceptionHandler({MethodArgumentNotValidException.class, HttpMessageNotReadableException.class,
MissingServletRequestParameterException.class})
ResponseEntity<TaskDtos.Error> invalid(Exception e) { return reply(400, "invalid", "检查JSON、标题、done和版本"); }
@ExceptionHandler(OptimisticLockingFailureException.class)
ResponseEntity<TaskDtos.Error> conflict(Exception e) { return reply(409, "conflict", "任务已变化,请重新读取"); }
@ExceptionHandler(Exception.class)
ResponseEntity<TaskDtos.Error> unexpected(Exception e) {
if (e instanceof org.springframework.web.ErrorResponse frameworkError)
return ResponseEntity.status(frameworkError.getStatusCode()).headers(frameworkError.getHeaders())
.body(new TaskDtos.Error("http_error", "请求无法处理"));
log.error("Task request failed", e);
return reply(500, "internal", "服务器暂时无法完成请求");
}
private ResponseEntity<TaskDtos.Error> reply(int status, String code, String message) {
return ResponseEntity.status(status).body(new TaskDtos.Error(code, message));
}
}src/main/java/notes/tasks/JsonConfig.java
package notes.tasks;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.cfg.CoercionAction;
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
import com.fasterxml.jackson.databind.type.LogicalType;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class JsonConfig {
@Bean
Jackson2ObjectMapperBuilderCustomizer strictJson() {
return builder -> builder.featuresToDisable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
.postConfigurer(mapper -> {
for (var shape : new CoercionInputShape[] { CoercionInputShape.Integer,
CoercionInputShape.Float, CoercionInputShape.Boolean }) {
mapper.coercionConfigFor(LogicalType.Textual).setCoercion(shape, CoercionAction.Fail);
}
});
}
}src/main/java/notes/tasks/Task.java
package notes.tasks;
import jakarta.persistence.*;
@Entity
@Table(name = "tasks")
class Task {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@Version Long version;
@Column(nullable = false, length = 120) String title;
@Column(nullable = false) boolean done;
protected Task() {}
Task(String title, boolean done) { this.title = title; this.done = done; }
}src/main/java/notes/tasks/TaskController.java
package notes.tasks;
import java.util.List;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/tasks")
class TaskController {
private final TaskService service;
TaskController(TaskService service) { this.service = service; }
@GetMapping public List<TaskDtos.View> list() { return service.list(); }
@GetMapping("/{id}") public TaskDtos.View get(@PathVariable String id) { return service.get(id); }
@PostMapping @ResponseStatus(HttpStatus.CREATED)
public TaskDtos.View create(@Valid @RequestBody TaskDtos.Create input) { return service.create(input); }
@PutMapping("/{id}")
public TaskDtos.View update(@PathVariable String id, @Valid @RequestBody TaskDtos.Update input) { return service.update(id, input); }
@DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable String id, @RequestParam String version) { service.delete(id, version); }
}src/main/java/notes/tasks/TaskDtos.java
package notes.tasks;
import jakarta.validation.constraints.*;
final class TaskDtos {
private TaskDtos() {}
record Create(@NotBlank @Size(max = 120) String title, @NotNull Boolean done) {}
record Update(@NotBlank @Size(max = 120) String title, @NotNull Boolean done,
@NotNull @Pattern(regexp = "0|[1-9][0-9]{0,18}") String version) {}
record View(String id, String title, boolean done, String version) {
static View from(Task t) { return new View(t.id.toString(), t.title, t.done, t.version.toString()); }
}
record Error(String code, String message) {}
}src/main/java/notes/tasks/TaskRepository.java
package notes.tasks;
import org.springframework.data.jpa.repository.JpaRepository;
interface TaskRepository extends JpaRepository<Task, Long> {}src/main/java/notes/tasks/TasksApplication.java
package notes.tasks;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TasksApplication {
public static void main(String[] args) { SpringApplication.run(TasksApplication.class, args); }
}继续阅读
- 排错
- 知识专题
- 知识专题