phase 5 part 4: java-spring awareness, k8s basics with validated manifests, run_examples supports single-file Java
This commit is contained in:
1 parent
075872e31e
commit
f695bbd7cc
18 files changed
+1192
-23
No files matched your search
@@ -0,0 +1,110 @@
|
||||
---
|
||||
id: backend.java-spring.awareness
|
||||
title: Java / Spring Boot(了解级):读懂项目结构、IoC 与依赖注入、分层、事务注解和与 Go / Python 的对照
|
||||
type: lesson
|
||||
domain: backend
|
||||
stack: [java-spring]
|
||||
tags: [Java, Spring Boot, IoC, 依赖注入, 注解, 事务, JPA, MyBatis, 虚拟线程]
|
||||
level: basic
|
||||
prerequisites: [backend.go.index]
|
||||
est_hours: 2
|
||||
related: [backend.java-spring.scenario.call-java-service, backend.java-spring.interview]
|
||||
week: [14]
|
||||
order: 1
|
||||
---
|
||||
|
||||
# Java / Spring Boot(了解级):读懂项目结构、IoC 与依赖注入、分层、事务注解和与 Go / Python 的对照
|
||||
|
||||
> 定位:**能读懂、能对接、面试能聊概念**,不作为主线。BOSS直聘 2026-10-23 的调研中,有 AI 应用岗位写了“熟悉 Java / Go / Python 其中之一”,或者团队已有 Java 业务系统需要 Agent 去调用。示例只用 JDK,在 JDK 26(JDK 21+ 即可)上实际运行通过;Spring 代码为示意,未编译。以官方文档为准:https://docs.spring.io/spring-boot/ 。
|
||||
|
||||
## 解释说明
|
||||
|
||||
### 与你熟悉的技术对照
|
||||
|
||||
| 概念 | Java / Spring Boot | Go | Python / FastAPI |
|
||||
| --- | --- | --- | --- |
|
||||
| 数据类 | `record` | struct | `dataclass` / Pydantic 模型 |
|
||||
| 路由 | `@RestController` + `@GetMapping` | Gin 路由 | `@app.get` |
|
||||
| 依赖注入 | IoC 容器 + 构造器注入 | 手动在 main 中组装 | `Depends` |
|
||||
| 参数校验 | `@Valid` + Bean Validation | binding 标签 | Pydantic |
|
||||
| 数据访问 | Spring Data JPA / MyBatis | database/sql、sqlc、GORM | SQLAlchemy |
|
||||
| 事务 | `@Transactional` | 手动 `tx.Begin/Commit` | `session.begin()` |
|
||||
| 并发 | 线程池、虚拟线程(JDK 21) | goroutine | asyncio |
|
||||
| 构建 | Maven / Gradle | go build | uv / pip |
|
||||
|
||||
### Spring 的核心概念
|
||||
|
||||
- **IoC 容器**:对象(Bean)由容器创建和管理,你声明依赖,容器注入。推荐**构造器注入**(依赖不可变、便于测试)。
|
||||
- **分层**:Controller(接收请求)→ Service(业务逻辑、事务)→ Repository(数据访问)。
|
||||
- **自动配置**:Spring Boot 根据依赖和配置自动创建常用组件(数据源、Web 服务器),配置在 `application.yml`。
|
||||
- **`@Transactional` 的坑**:基于代理实现,同一个类内部方法调用不经过代理,事务不生效;默认只对运行时异常回滚。
|
||||
- **AI 相关**:Spring AI、LangChain4j 等框架让 Java 项目也能接入大模型(版本变化快,以官方文档为准)。
|
||||
|
||||
### 怎么快速读懂一个 Spring Boot 项目
|
||||
|
||||
1. 看 `pom.xml` / `build.gradle`:依赖了什么。
|
||||
2. 找带 `@SpringBootApplication` 的启动类。
|
||||
3. 看 `application.yml`:数据库、端口、外部服务地址。
|
||||
4. 从 Controller 顺着调用链读到 Service 和 Repository。
|
||||
5. 看全局异常处理(`@RestControllerAdvice`)和拦截器 / 过滤器(鉴权)。
|
||||
|
||||
## 案例
|
||||
|
||||
你负责的 Python Agent 需要调用公司的 Java 订单服务:读懂它的接口定义(Controller 和 DTO)、错误码、鉴权方式、分页规则,把它封装成 Agent 的工具。
|
||||
|
||||
## 代码示例
|
||||
|
||||
只用 JDK 的对照示例 [examples/java-basics/OrderDemo.java](examples/java-basics/OrderDemo.java)(`java OrderDemo.java` 运行通过):record、接口 + 构造器注入、Optional、Stream 分组汇总、BigDecimal 金额、虚拟线程。
|
||||
|
||||
```text
|
||||
跨租户查询被拒绝: 订单不存在: B001
|
||||
t1 按状态汇总: {paid=20.00, refunded=5.00}
|
||||
用 double 计算金额的问题: 0.1 + 0.2 = 0.30000000000000004
|
||||
1000 个虚拟线程计数结果: 1000
|
||||
```
|
||||
|
||||
对应的 Spring Boot 写法(示意,未编译):
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/orders")
|
||||
class OrderController {
|
||||
private final OrderService service;
|
||||
OrderController(OrderService service) { this.service = service; } // 构造器注入,无需 @Autowired
|
||||
|
||||
@GetMapping("/{id}")
|
||||
OrderDto get(@PathVariable String id, @RequestHeader("X-Tenant-Id") String tenant) {
|
||||
return OrderDto.from(service.get(tenant, id));
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
class OrderService {
|
||||
private final OrderRepository repo;
|
||||
OrderService(OrderRepository repo) { this.repo = repo; }
|
||||
|
||||
@Transactional
|
||||
public void refund(String tenant, String id) { /* 修改订单 + 写退款记录,在一个事务中 */ }
|
||||
}
|
||||
|
||||
@RestControllerAdvice
|
||||
class GlobalErrors {
|
||||
@ExceptionHandler(NotFoundException.class)
|
||||
ResponseEntity<Map<String, String>> notFound(NotFoundException e) {
|
||||
return ResponseEntity.status(404).body(Map.of("code", "NOT_FOUND", "message", e.getMessage()));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 易错点
|
||||
|
||||
- 金额用 `double`。
|
||||
- 同类内部调用带 `@Transactional` 的方法,以为事务生效。
|
||||
- 受检异常被捕获后吞掉,事务没有回滚。
|
||||
- 在面试中把“了解”说成“精通”:如实说明能读懂并对接,主语言是 Go / Python。
|
||||
|
||||
## 面试常问
|
||||
|
||||
- 什么是 IoC 和依赖注入?
|
||||
- `@Transactional` 什么情况下会失效?
|
||||
- 你不会 Java,如何和 Java 团队协作?
|
||||
@@ -0,0 +1,87 @@
|
||||
// Java 基础对照示例(不依赖 Spring,只用 JDK):record、接口 + 构造器注入、Optional、Stream、异常、并发
|
||||
// 运行(JDK 21+):java OrderDemo.java —— 单文件源码直接运行,不需要先 javac
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class OrderDemo {
|
||||
// record:不可变数据类,自动生成构造器、getter、equals/hashCode/toString(类似 Go struct / Python dataclass(frozen))
|
||||
record Order(String id, String tenant, String status, BigDecimal amount) {}
|
||||
|
||||
// 接口 + 实现:Spring 中会用 @Repository 标注,由容器创建并注入
|
||||
interface OrderRepository {
|
||||
Optional<Order> findById(String id);
|
||||
List<Order> findByTenant(String tenant);
|
||||
}
|
||||
|
||||
static class InMemoryOrderRepository implements OrderRepository {
|
||||
private final Map<String, Order> data = new ConcurrentHashMap<>();
|
||||
InMemoryOrderRepository(List<Order> seed) { seed.forEach(o -> data.put(o.id(), o)); }
|
||||
public Optional<Order> findById(String id) { return Optional.ofNullable(data.get(id)); }
|
||||
public List<Order> findByTenant(String tenant) {
|
||||
return data.values().stream().filter(o -> o.tenant().equals(tenant)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
static class NotFoundException extends RuntimeException {
|
||||
NotFoundException(String msg) { super(msg); }
|
||||
}
|
||||
|
||||
// 构造器注入:Spring 推荐的方式(@Service),依赖是 final 的,测试时可以直接传入假实现
|
||||
static class OrderService {
|
||||
private final OrderRepository repo;
|
||||
OrderService(OrderRepository repo) { this.repo = repo; }
|
||||
|
||||
Order get(String tenant, String id) {
|
||||
return repo.findById(id)
|
||||
.filter(o -> o.tenant().equals(tenant)) // 租户隔离:别的租户的订单当作不存在
|
||||
.orElseThrow(() -> new NotFoundException("订单不存在: " + id));
|
||||
}
|
||||
|
||||
Map<String, BigDecimal> sumByStatus(String tenant) { // Stream 分组汇总;金额用 BigDecimal,不用 double
|
||||
return repo.findByTenant(tenant).stream()
|
||||
.collect(Collectors.groupingBy(Order::status,
|
||||
Collectors.reducing(BigDecimal.ZERO, Order::amount, BigDecimal::add)));
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
var repo = new InMemoryOrderRepository(List.of(
|
||||
new Order("A001", "t1", "paid", new BigDecimal("19.90")),
|
||||
new Order("A002", "t1", "paid", new BigDecimal("0.10")),
|
||||
new Order("A003", "t1", "refunded", new BigDecimal("5.00")),
|
||||
new Order("B001", "t2", "paid", new BigDecimal("99.00"))));
|
||||
var service = new OrderService(repo);
|
||||
|
||||
check(service.get("t1", "A001").amount().equals(new BigDecimal("19.90")), "查询订单");
|
||||
try {
|
||||
service.get("t1", "B001");
|
||||
check(false, "跨租户应当查不到");
|
||||
} catch (NotFoundException e) {
|
||||
System.out.println("跨租户查询被拒绝: " + e.getMessage());
|
||||
}
|
||||
var sums = service.sumByStatus("t1");
|
||||
check(sums.get("paid").equals(new BigDecimal("20.00")), "BigDecimal 精确求和");
|
||||
System.out.println("t1 按状态汇总: " + new java.util.TreeMap<>(sums));
|
||||
System.out.println("用 double 计算金额的问题: 0.1 + 0.2 = " + (0.1 + 0.2));
|
||||
|
||||
// 并发:虚拟线程(JDK 21 正式特性)+ 原子计数,类比 Go 的 goroutine
|
||||
var counter = new AtomicInteger();
|
||||
try (ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) {
|
||||
for (int i = 0; i < 1000; i++) pool.submit(counter::incrementAndGet);
|
||||
} // try-with-resources:等待全部任务完成
|
||||
check(counter.get() == 1000, "虚拟线程并发计数");
|
||||
System.out.println("1000 个虚拟线程计数结果: " + counter.get());
|
||||
System.out.println("全部检查通过");
|
||||
}
|
||||
|
||||
static void check(boolean ok, String name) {
|
||||
if (!ok) throw new AssertionError("检查失败: " + name);
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,32 @@ title: Java / Spring Boot(了解级)
|
||||
type: index
|
||||
domain: backend
|
||||
stack: [java-spring]
|
||||
tags: []
|
||||
tags: [Java, Spring Boot, 了解级]
|
||||
week: [14]
|
||||
---
|
||||
|
||||
# Java / Spring Boot(了解级)
|
||||
|
||||
了解级:全栈岗位常见要求,能读懂代码、知道分层和常用注解。
|
||||
能读懂、能对接、面试能聊概念。BOSS直聘 2026-10-23 的调研中有岗位要求“Java / Go / Python 其中之一”或需要对接已有 Java 系统,不作为主线投入。
|
||||
|
||||
> 本专题建设中:课程、企业场景、面试题和示例代码会陆续补充。
|
||||
## 学习路线
|
||||
|
||||
| 顺序 | 课程 | 级别 | 预计 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | [Java / Spring Boot(了解级):读懂项目结构、IoC 与依赖注入、分层、事务注解和与 Go / Python 的对照](01-java-spring-awareness.md) | 基础 | 2 小时 |
|
||||
|
||||
## 企业场景
|
||||
|
||||
- [Python Agent 调用公司已有的 Java 业务系统:读接口、封装工具、错误映射和联调](scenarios/call-java-service.md)
|
||||
|
||||
## 面试题
|
||||
|
||||
- [Java / Spring Boot(了解级)面试题](interview.md)
|
||||
|
||||
## 示例代码
|
||||
|
||||
- [examples/java-basics](examples/java-basics/):只用 JDK 的对照示例(`java OrderDemo.java`,JDK 21+;已运行通过)。Spring 代码为示意,未编译。
|
||||
|
||||
## 在 16 周计划中
|
||||
|
||||
- [第 14 周:后端扩展与部署](../../../plan/week14-backend-extend-deploy.md)
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
id: backend.java-spring.interview
|
||||
title: Java / Spring Boot(了解级)面试题
|
||||
type: interview
|
||||
domain: backend
|
||||
stack: [java-spring]
|
||||
tags: [Java, Spring Boot, IoC, 事务, 面试]
|
||||
level: basic
|
||||
related: [backend.java-spring.awareness]
|
||||
week: [14, 16]
|
||||
---
|
||||
|
||||
# Java / Spring Boot(了解级)面试题
|
||||
|
||||
## 基础
|
||||
|
||||
### 【基础】什么是 IoC 和依赖注入?
|
||||
|
||||
**参考答案**:控制反转:对象的创建和依赖组装由容器负责,而不是对象自己 `new`;依赖注入是实现方式,通过构造器、setter 或字段注入。好处是解耦、便于替换实现和测试。
|
||||
|
||||
**追问**:为什么推荐构造器注入?→ 依赖可以声明为 final、不会出现未初始化的状态、单元测试时直接传入假实现。
|
||||
|
||||
### 【基础】Spring Boot 项目通常怎么分层?
|
||||
|
||||
**参考答案**:Controller 处理 HTTP 和参数校验,Service 写业务逻辑和事务边界,Repository 负责数据访问;DTO 与实体分开,全局异常处理统一返回格式。
|
||||
|
||||
**追问**:和你写的 Go 项目怎么对应?→ handler / service / repository 三层,思路一致,只是 Go 中依赖在 main 中手动组装。
|
||||
|
||||
### 【基础】Java 中金额用什么类型?
|
||||
|
||||
**参考答案**:`BigDecimal`(或以分为单位的 `long`),不能用 `double`,二进制浮点数无法精确表示 0.1 等小数。
|
||||
|
||||
**追问**:`BigDecimal` 比较相等要注意什么?→ `equals` 会比较精度(`2.0` 和 `2.00` 不相等),数值比较用 `compareTo`。
|
||||
|
||||
## 进阶
|
||||
|
||||
### 【进阶】`@Transactional` 在什么情况下会失效?
|
||||
|
||||
**参考答案**:同类内部方法调用(不经过代理)、方法不是 public(取决于代理方式和版本)、异常被捕获没有抛出、抛出受检异常而未配置 `rollbackFor`、数据库引擎不支持事务。
|
||||
|
||||
**追问**:Go 中怎么做类似的事?→ 显式开启事务并把 `tx` 传入各个仓储方法,或者封装一个 `WithTx(func(tx) error)` 帮助函数。
|
||||
|
||||
### 【进阶】虚拟线程是什么?和 goroutine 有什么相似之处?
|
||||
|
||||
**参考答案**:JDK 21 正式提供的轻量级线程,由 JVM 调度到少量平台线程上,阻塞 I/O 时让出载体线程,可以创建大量并发任务;与 goroutine 一样让“同步写法 + 高并发”成为可能。
|
||||
|
||||
**追问**:有什么需要注意?→ 不适合 CPU 密集任务;某些同步块或本地调用会“钉住”载体线程(不同 JDK 版本有改进,以官方说明为准);不要池化虚拟线程。
|
||||
|
||||
### 【进阶】Java 团队说接口“已经幂等”,你怎么验证?
|
||||
|
||||
**参考答案**:看实现:是否有唯一约束或幂等键表、状态机是否只允许合法转换;做测试:同一请求并发和重复发送,检查数据只产生一条、返回结果一致。
|
||||
|
||||
**追问**:幂等键应该谁生成?→ 调用方生成并在重试时复用;服务端存储键和结果,设定过期时间。
|
||||
|
||||
## 场景题
|
||||
|
||||
### 【场景题】你的 Python Agent 需要调用公司的 Java 订单系统,怎么做?
|
||||
|
||||
**参考答案**:读 Controller / DTO 明确契约;Python 侧封装工具,透传用户身份;错误码映射为可重试、告知用户、转人工;非幂等接口推动增加幂等键;契约测试和 request id 联调。详见[调用 Java 服务场景](scenarios/call-java-service.md)。
|
||||
|
||||
**追问**:对方接口很慢怎么办?→ 设置超时,查询类接口加缓存,长操作改为异步(提交后轮询或回调),Agent 告诉用户“已提交,稍后通知”。
|
||||
|
||||
### 【场景题】面试官问“你会 Java 吗”,你怎么回答?
|
||||
|
||||
**参考答案**:如实说明:主语言是 Go / Python,能读懂 Spring Boot 项目的结构和代码,理解 IoC、分层、事务等核心概念,做过(或能做)与 Java 服务的对接;如果岗位需要,可以快速上手。不夸大。
|
||||
|
||||
**追问**:举个例子说明你读懂过 Java 代码?→ 讲清一次具体的对接:从哪个 Controller 读出了什么契约、发现了什么问题(例如非幂等)、怎么解决。
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
id: backend.java-spring.scenario.call-java-service
|
||||
title: Python Agent 调用公司已有的 Java 业务系统:读接口、封装工具、错误映射和联调
|
||||
type: scenario
|
||||
domain: backend
|
||||
stack: [java-spring, function-calling, fastapi]
|
||||
tags: [系统对接, 工具封装, 错误码映射, OpenAPI, 联调, 幂等]
|
||||
level: intermediate
|
||||
prerequisites: [backend.java-spring.awareness, ai.function-calling.tool-loop]
|
||||
est_hours: 2
|
||||
related: [backend.java-spring.interview, ai.function-calling.scenario.safe-business-tools]
|
||||
week: [14]
|
||||
---
|
||||
|
||||
# Python Agent 调用公司已有的 Java 业务系统:读接口、封装工具、错误映射和联调
|
||||
|
||||
## 业务背景与问题
|
||||
|
||||
公司的订单、售后系统是多年的 Spring Boot 服务。新做的 AI 客服 Agent(Python)需要“查订单、查物流、发起退款申请”。问题:
|
||||
|
||||
1. 接口文档过期,字段含义要读代码才知道;
|
||||
2. Java 服务的错误码体系复杂,Agent 不知道哪些错误可以重试、哪些要告诉用户;
|
||||
3. 退款接口不是幂等的,Agent 重试导致重复申请;
|
||||
4. 联调时双方互相指责。
|
||||
|
||||
## 约束
|
||||
|
||||
- 不能大改 Java 服务,只能小范围加接口或参数。
|
||||
- Agent 不能拥有超过用户本人的权限。
|
||||
|
||||
## 方案设计
|
||||
|
||||
1. **读接口**:从 Controller 和 DTO 读出真实的字段、校验规则、错误码;如果服务集成了 OpenAPI(springdoc 一类工具),导出规范生成 Python 客户端;否则自己写 Pydantic 模型。
|
||||
2. **封装工具**:每个业务能力一个工具,入参严格校验;工具层负责鉴权透传(用户令牌或服务账号 + 用户 id)、超时、重试(只对查询类和幂等接口)。
|
||||
3. **错误映射**:把 Java 错误码分为三类——可重试(超时、限流)、需要告诉用户(订单不存在、超过退款期限)、需要转人工(系统异常);返回给模型的是结构化的可读信息,而不是堆栈。
|
||||
4. **幂等**:和 Java 团队约定退款接口增加 `Idempotency-Key` 请求头(服务端用唯一约束实现),Agent 在一次会话中对同一操作使用同一个 key。
|
||||
5. **联调**:先写契约测试(请求 / 响应样例),双方各自对着样例开发;用 request id 串起两边日志。
|
||||
|
||||
## 关键代码
|
||||
|
||||
```python
|
||||
class JavaErrorKind(str, Enum):
|
||||
RETRYABLE = "retryable"
|
||||
USER_FACING = "user_facing"
|
||||
HANDOFF = "handoff"
|
||||
|
||||
ERROR_MAP = {
|
||||
"ORDER_NOT_FOUND": (JavaErrorKind.USER_FACING, "没有找到这个订单,请核对订单号"),
|
||||
"REFUND_EXPIRED": (JavaErrorKind.USER_FACING, "已超过可退款期限"),
|
||||
"RATE_LIMITED": (JavaErrorKind.RETRYABLE, "系统繁忙"),
|
||||
}
|
||||
|
||||
def call_refund(client: httpx.Client, user: User, order_id: str, reason: str, idem_key: str) -> dict:
|
||||
r = client.post("/api/refunds", json={"orderId": order_id, "reason": reason},
|
||||
headers={"Authorization": f"Bearer {user.token}", "Idempotency-Key": idem_key}, timeout=5)
|
||||
if r.is_success:
|
||||
return {"ok": True, "refund_id": r.json()["data"]["refundId"]}
|
||||
code = r.json().get("code", "UNKNOWN")
|
||||
kind, msg = ERROR_MAP.get(code, (JavaErrorKind.HANDOFF, "系统异常,已为你转人工"))
|
||||
return {"ok": False, "kind": kind, "message": msg} # 给模型的是结构化结果,不是堆栈
|
||||
```
|
||||
|
||||
工具调用安全的完整讨论见[安全的业务工具](../../../ai/function-calling/scenarios/safe-business-tools.md)。
|
||||
|
||||
## 取舍
|
||||
|
||||
| 选择 | 优点 | 缺点 |
|
||||
| --- | --- | --- |
|
||||
| **Python 工具层直接调用 Java 接口(本方案)** | 不改动 Java 服务主体 | 需要维护错误映射和模型定义 |
|
||||
| Java 团队提供专门给 Agent 的聚合接口 | 语义更清晰 | 依赖对方排期 |
|
||||
| 用 Spring AI 在 Java 侧实现 Agent | 技术栈统一 | 团队 AI 生态以 Python 为主 |
|
||||
|
||||
## 易错点
|
||||
|
||||
- Agent 使用高权限服务账号,绕过了用户级权限。
|
||||
- 对非幂等接口自动重试。
|
||||
- 把 Java 的异常堆栈直接交给模型,既泄露内部信息又让模型困惑。
|
||||
- 金额字段在 JSON 中是字符串还是数字没有约定,精度丢失。
|
||||
|
||||
## 面试怎么讲
|
||||
|
||||
> “我不是 Java 开发,但能读懂 Spring Boot 项目:从 Controller 和 DTO 读出真实的接口契约。对接时我在 Python 侧封装工具,鉴权透传用户身份;把 Java 的错误码分成可重试、告知用户、转人工三类,给模型结构化结果;和 Java 团队约定退款接口支持幂等键。联调前先写契约样例,用 request id 串联日志。”
|
||||
Reference in new issue
Block a user