一次 HTTP 请求在 Spring Boot 里从外到内穿过 5 层,每层职责单一:

下面用一个用户查询 + 新增用户的完整例子,包含「暴露接口给别人调」和「我调别人的接口」两件事。
com.example.demo
├── controller UserController.java 对外暴露 HTTP 接口
├── service UserService.java 业务逻辑
├── mapper UserMapper.java 数据库访问
├── entity User.java 数据表映射
├── dto UserQueryDTO / Result 入参 / 统一返回
└── client RemoteClient.java 调用别人的接口
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
// GET /api/users/1 → 路径参数
@GetMapping("/{id}")
public Result<User> getById(@PathVariable Long id) {
return Result.ok(userService.getById(id));
}
// GET /api/users?name=张&page=1 → 查询参数
@GetMapping
public Result<List<User>> list(@RequestParam(required = false) String name,
@RequestParam(defaultValue = "1") int page) {
return Result.ok(userService.findByName(name, page));
}
// POST /api/users body 传 JSON → 新增
@PostMapping
public Result<Long> create(@RequestBody @Valid UserCreateDTO dto) {
return Result.ok(userService.create(dto));
}
// PUT /api/users/1 → 修改
@PutMapping("/{id}")
public Result<Void> update(@PathVariable Long id, @RequestBody UserUpdateDTO dto) {
userService.update(id, dto);
return Result.ok(null);
}
// DELETE /api/users/1 → 删除
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
userService.delete(id);
return Result.ok(null);
}
}
注解速记
注解 作用
@RestController = @Controller + @ResponseBody,返回值自动转 JSON
@RequestMapping 类级别的公共路径前缀
@GetMapping / @PostMapping 限定 HTTP 方法,GET 查、POST 增、PUT 改、DELETE 删
@PathVariable 取 URL 里的值 /users/{id}
@RequestParam 取 ?key=value 里的值
@RequestBody 把请求体 JSON 反序列化成对象
@Valid 触发 DTO 上的校验(@NotBlank、@Size 等)
@Service
@RequiredArgsConstructor
public class UserService {
private final UserMapper userMapper;
public User getById(Long id) {
User u = userMapper.selectById(id);
if (u == null) throw new BizException("用户不存在");
return u;
}
@Transactional // 多条写操作必须加,保证同成功同失败
public Long create(UserCreateDTO dto) {
if (userMapper.existsByPhone(dto.phone())) throw new BizException("手机号已存在");
User user = new User(null, dto.name(), dto.phone(), LocalDateTime.now());
userMapper.insert(user);
return user.getId(); // MyBatis-Plus 插入后自动回填主键
}
}
@Mapper
public interface UserMapper extends BaseMapper<User> { // MyBatis-Plus 自带 CRUD
@Select("SELECT COUNT(1) FROM user WHERE phone = #{phone}")
int existsByPhone(String phone);
}
application.yml:
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo?serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发期打印 SQL
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"张三","phone":"13800000000"}'
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(3))
.setReadTimeout(Duration.ofSeconds(10))
.build();
}
}
@Service
@RequiredArgsConstructor
public class WeatherService {
private final RestTemplate restTemplate;
public String getWeather(String city) {
String url = "https://api.example.com/weather?city={city}";
// getForObject:GET 请求,返回值直接映射成对象
return restTemplate.getForObject(url, String.class, city);
}
public String postOrder(OrderDTO dto) {
// postForObject:POST 请求,自动把 dto 序列化成 JSON body
return restTemplate.postForObject("https://api.example.com/order", dto, String.class);
}
}
依赖:spring-cloud-starter-openfeign,启动类加 @EnableFeignClients。
@FeignClient(name = "remoteApi", url = "${remote.api.url}")
public interface RemoteClient {
@GetMapping("/users/{id}")
Result<User> getUser(@PathVariable Long id);
@PostMapping("/orders")
Result<Long> createOrder(@RequestBody OrderDTO dto);
}
// 直接注入使用,异常用 Fallback 兜底
@Service
@RequiredArgsConstructor
public class OrderService {
private final RemoteClient remoteClient;
public Result<Long> submit(OrderDTO dto) {
return remoteClient.createOrder(dto);
}
}
对比怎么选:偶尔调一两个外部接口 → RestTemplate;要调一堆同服务的接口(微服务内部)→ OpenFeign;高并发响应式场景 → WebClient。
下一篇
已是最新文章