diff --git a/pom.xml b/pom.xml
index 977b95e..4f7ec91 100644
--- a/pom.xml
+++ b/pom.xml
@@ -37,6 +37,12 @@
org.springframework.boot
spring-boot-starter-actuator
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
diff --git a/src/main/java/com/example/demo/common/ApiResponse.java b/src/main/java/com/example/demo/common/ApiResponse.java
new file mode 100644
index 0000000..a38e218
--- /dev/null
+++ b/src/main/java/com/example/demo/common/ApiResponse.java
@@ -0,0 +1,22 @@
+package com.example.demo.common;
+
+import java.time.Instant;
+
+public record ApiResponse(
+ int code,
+ String message,
+ T data,
+ Instant timestamp
+) {
+ public static ApiResponse ok(T data) {
+ return new ApiResponse<>(0, "success", data, Instant.now());
+ }
+
+ public static ApiResponse ok(String message, T data) {
+ return new ApiResponse<>(0, message, data, Instant.now());
+ }
+
+ public static ApiResponse fail(int code, String message) {
+ return new ApiResponse<>(code, message, null, Instant.now());
+ }
+}
diff --git a/src/main/java/com/example/demo/controller/AopEventController.java b/src/main/java/com/example/demo/controller/AopEventController.java
index ac2ec67..18232b6 100644
--- a/src/main/java/com/example/demo/controller/AopEventController.java
+++ b/src/main/java/com/example/demo/controller/AopEventController.java
@@ -6,7 +6,6 @@ import com.example.demo.aop.RateLimited;
import com.example.demo.event.UserEventPublisher;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
@@ -20,17 +19,20 @@ import java.util.Map;
@RequestMapping("/aop")
public class AopEventController {
- @Autowired
- private UserService userService;
+ private final UserService userService;
+ private final UserEventPublisher eventPublisher;
+ private final PerformanceAspect performanceAspect;
+ private final RateLimitAspect rateLimitAspect;
- @Autowired
- private UserEventPublisher eventPublisher;
-
- @Autowired
- private PerformanceAspect performanceAspect;
-
- @Autowired
- private RateLimitAspect rateLimitAspect;
+ public AopEventController(UserService userService,
+ UserEventPublisher eventPublisher,
+ PerformanceAspect performanceAspect,
+ RateLimitAspect rateLimitAspect) {
+ this.userService = userService;
+ this.eventPublisher = eventPublisher;
+ this.performanceAspect = performanceAspect;
+ this.rateLimitAspect = rateLimitAspect;
+ }
/**
* 学习首页
diff --git a/src/main/java/com/example/demo/controller/UserController.java b/src/main/java/com/example/demo/controller/UserController.java
index 758083d..e1e7461 100644
--- a/src/main/java/com/example/demo/controller/UserController.java
+++ b/src/main/java/com/example/demo/controller/UserController.java
@@ -1,64 +1,54 @@
package com.example.demo.controller;
+import com.example.demo.common.ApiResponse;
+import com.example.demo.dto.UserRequest;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
-import org.springframework.beans.factory.annotation.Autowired;
+import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.util.List;
-/**
- * 用户控制器 - RESTful API 示例
- *
- * 学习点:
- * - @RestController: 组合了 @Controller 和 @ResponseBody
- * - @RequestMapping: 路由映射
- * - @PathVariable: 路径变量
- * - @RequestParam: 查询参数
- * - @RequestBody: 请求体
- */
@RestController
@RequestMapping("/api/users")
public class UserController {
- @Autowired
- private UserService userService;
+ private final UserService userService;
+
+ public UserController(UserService userService) {
+ this.userService = userService;
+ }
- // GET /api/users - 获取所有用户
@GetMapping
- public List getAllUsers() {
- return userService.findAll();
+ public ApiResponse> getAllUsers() {
+ return ApiResponse.ok(userService.findAll());
}
- // GET /api/users/{id} - 获取单个用户
@GetMapping("/{id}")
- public User getUserById(@PathVariable Long id) {
- return userService.findById(id);
+ public ApiResponse getUserById(@PathVariable Long id) {
+ return ApiResponse.ok(userService.findById(id));
}
- // POST /api/users - 创建用户
@PostMapping
- public User createUser(@RequestBody User user) {
- return userService.save(user);
+ public ApiResponse createUser(@Valid @RequestBody UserRequest req) {
+ User user = new User(null, req.name(), req.email(), req.age());
+ return ApiResponse.ok("创建成功", userService.create(user));
}
- // PUT /api/users/{id} - 更新用户
@PutMapping("/{id}")
- public User updateUser(@PathVariable Long id, @RequestBody User user) {
- user.setId(id);
- return userService.save(user);
+ public ApiResponse updateUser(@PathVariable Long id, @Valid @RequestBody UserRequest req) {
+ User user = new User(id, req.name(), req.email(), req.age());
+ return ApiResponse.ok("更新成功", userService.update(id, user));
}
- // DELETE /api/users/{id} - 删除用户
@DeleteMapping("/{id}")
- public String deleteUser(@PathVariable Long id) {
+ public ApiResponse deleteUser(@PathVariable Long id) {
userService.delete(id);
- return "用户 " + id + " 已删除";
+ return ApiResponse.ok("删除成功", null);
}
- // GET /api/users/search?name=xxx - 搜索用户
@GetMapping("/search")
- public List searchUsers(@RequestParam String name) {
- return userService.findByName(name);
+ public ApiResponse> searchUsers(@RequestParam String name) {
+ return ApiResponse.ok(userService.findByName(name));
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/example/demo/dto/UserRequest.java b/src/main/java/com/example/demo/dto/UserRequest.java
new file mode 100644
index 0000000..1c3fdf8
--- /dev/null
+++ b/src/main/java/com/example/demo/dto/UserRequest.java
@@ -0,0 +1,19 @@
+package com.example.demo.dto;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+
+public record UserRequest(
+ @NotBlank(message = "姓名不能为空")
+ String name,
+
+ @NotBlank(message = "邮箱不能为空")
+ @Email(message = "邮箱格式不正确")
+ String email,
+
+ @Min(value = 1, message = "年龄最小为 1")
+ @Max(value = 120, message = "年龄最大为 120")
+ Integer age
+) {}
diff --git a/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java b/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..2e54f79
--- /dev/null
+++ b/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java
@@ -0,0 +1,38 @@
+package com.example.demo.exception;
+
+import com.example.demo.common.ApiResponse;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(ResourceNotFoundException.class)
+ public ResponseEntity> handleNotFound(ResourceNotFoundException e) {
+ return ResponseEntity.status(HttpStatus.NOT_FOUND)
+ .body(ApiResponse.fail(404, e.getMessage()));
+ }
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity>> handleValidation(MethodArgumentNotValidException e) {
+ Map errors = new HashMap<>();
+ for (FieldError error : e.getBindingResult().getFieldErrors()) {
+ errors.put(error.getField(), error.getDefaultMessage());
+ }
+ return ResponseEntity.badRequest()
+ .body(new ApiResponse<>(400, "参数校验失败", errors, java.time.Instant.now()));
+ }
+
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity> handleAny(Exception e) {
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+ .body(ApiResponse.fail(500, "服务器内部错误: " + e.getMessage()));
+ }
+}
diff --git a/src/main/java/com/example/demo/exception/ResourceNotFoundException.java b/src/main/java/com/example/demo/exception/ResourceNotFoundException.java
new file mode 100644
index 0000000..d3ba1b7
--- /dev/null
+++ b/src/main/java/com/example/demo/exception/ResourceNotFoundException.java
@@ -0,0 +1,7 @@
+package com.example.demo.exception;
+
+public class ResourceNotFoundException extends RuntimeException {
+ public ResourceNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/example/demo/service/UserService.java b/src/main/java/com/example/demo/service/UserService.java
index e68dbb5..7009f12 100644
--- a/src/main/java/com/example/demo/service/UserService.java
+++ b/src/main/java/com/example/demo/service/UserService.java
@@ -1,5 +1,6 @@
package com.example.demo.service;
+import com.example.demo.exception.ResourceNotFoundException;
import com.example.demo.model.User;
import org.springframework.stereotype.Service;
@@ -8,23 +9,13 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
-/**
- * 用户服务 - 业务逻辑层
- *
- * 学习点:
- * - @Service: 标记为服务层组件,自动注册为 Bean
- * - 依赖注入:Controller 通过 @Autowired 注入此服务
- * - 分层架构:Controller -> Service -> Repository
- */
@Service
public class UserService {
- // 内存存储(演示用,实际项目用数据库)
private final List users = new ArrayList<>();
private final AtomicLong idGenerator = new AtomicLong(1);
public UserService() {
- // 初始化一些测试数据
users.add(new User(idGenerator.getAndIncrement(), "张三", "zhangsan@example.com", 25));
users.add(new User(idGenerator.getAndIncrement(), "李四", "lisi@example.com", 30));
users.add(new User(idGenerator.getAndIncrement(), "王五", "wangwu@example.com", 28));
@@ -38,7 +29,7 @@ public class UserService {
return users.stream()
.filter(u -> u.getId().equals(id))
.findFirst()
- .orElse(null);
+ .orElseThrow(() -> new ResourceNotFoundException("用户不存在: id=" + id));
}
public List findByName(String name) {
@@ -47,23 +38,28 @@ public class UserService {
.collect(Collectors.toList());
}
- public User save(User user) {
- if (user.getId() == null) {
- user.setId(idGenerator.getAndIncrement());
- users.add(user);
- } else {
- // 更新
- for (int i = 0; i < users.size(); i++) {
- if (users.get(i).getId().equals(user.getId())) {
- users.set(i, user);
- break;
- }
- }
- }
+ public User create(User user) {
+ user.setId(idGenerator.getAndIncrement());
+ users.add(user);
return user;
}
- public void delete(Long id) {
- users.removeIf(u -> u.getId().equals(id));
+ public User update(Long id, User user) {
+ findById(id);
+ user.setId(id);
+ for (int i = 0; i < users.size(); i++) {
+ if (users.get(i).getId().equals(id)) {
+ users.set(i, user);
+ return user;
+ }
+ }
+ throw new ResourceNotFoundException("用户不存在: id=" + id);
}
-}
\ No newline at end of file
+
+ public void delete(Long id) {
+ boolean removed = users.removeIf(u -> u.getId().equals(id));
+ if (!removed) {
+ throw new ResourceNotFoundException("用户不存在: id=" + id);
+ }
+ }
+}
diff --git a/src/main/resources/static/users.html b/src/main/resources/static/users.html
index 56a30e5..8a2a40c 100644
--- a/src/main/resources/static/users.html
+++ b/src/main/resources/static/users.html
@@ -143,7 +143,8 @@ public class UserController {
// 加载用户列表
async function loadUsers() {
const res = await fetch('/api/users');
- const users = await res.json();
+ const payload = await res.json();
+ const users = payload.data || [];
const tbody = document.querySelector('#userTable tbody');
tbody.innerHTML = users.map(u => `
@@ -196,7 +197,7 @@ public class UserController {
await fetch(`/api/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ ...user, id: parseInt(id) })
+ body: JSON.stringify(user)
});
} else {
await fetch('/api/users', {
diff --git a/src/test/java/com/example/demo/controller/UserControllerTest.java b/src/test/java/com/example/demo/controller/UserControllerTest.java
new file mode 100644
index 0000000..fb41930
--- /dev/null
+++ b/src/test/java/com/example/demo/controller/UserControllerTest.java
@@ -0,0 +1,62 @@
+package com.example.demo.controller;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+class UserControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ void shouldListUsersWithApiResponseWrapper() throws Exception {
+ mockMvc.perform(get("/api/users"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data").isArray());
+ }
+
+ @Test
+ void shouldCreateUser() throws Exception {
+ String json = """
+ {
+ \"name\": \"测试用户\",
+ \"email\": \"test@example.com\",
+ \"age\": 22
+ }
+ """;
+
+ mockMvc.perform(post("/api/users")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(json))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data.name").value("测试用户"));
+ }
+
+ @Test
+ void shouldRejectInvalidUser() throws Exception {
+ String json = """
+ {
+ \"name\": \"\",
+ \"email\": \"bad-mail\",
+ \"age\": 222
+ }
+ """;
+
+ mockMvc.perform(post("/api/users")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(json))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400));
+ }
+}