Old-School Backend / 2026 ・ No.05

古法后端入门

第5讲 回到 Spring Boot ── DI、三层,以及“Bean 只有一个”

发布

终端画面。执行两次 ./race.sh bad,200 件中申请人被调换的分别是 91 件和 99 件(例:EXP-2026-2219 是 arai 提交的却变成 furui)。./race.sh good 为 0 件
对把申请人放在字段里的负责人(Service)版本,arai 和 furui 同时提交 200 件申请,有 91 件和 99 件的申请人被调换。用参数接收的版本是 0 件(笔者于 2026 年 10 月 31 日在云端 Linux 上截图。窗口顶部的标题栏是为了说明而后加的)

第 3 讲徒手处理了 HTTP,第 4 讲徒手处理了数据库。这次回到 Spring Boot。徒手做错的事,Spring Boot 真的会接手吗?使用 Spring Boot 时,又有什么新的要注意的地方?
在第 1 讲的应用里加上实验用的窗口和负责人,实测 DI(依赖注入)、窗口・负责人・仓库管理员三层、Bean 只有一个、代理人与 @Transactional。

一

从徒手回来

新井终于是 Spring Boot 了。这下全都交给它了吧。

古井一半是。另一半,恰恰是因为用了 Spring Boot 才会出的事故。

新井变方便了,事故反而变多?

古井因为看不见的地方运行的东西变多了。今天就从数这些看不见的东西开始。

全 15 讲路线图(当前:第 5 讲)

1 全景图 2 Java 准备 3 裸写 HTTP 4 裸写 JDBC 5 Spring Boot 6 输入检查 7 异常与回复 8 数据库访问 9 事务 10 状态流转 11 认证与授权 12 文件 13 日志与监控 14 测试 15 运行起来 当前位置 基础 徒手 入口 数据 业务与防护 运维
  • 名册 ── 启动后有 196 个 Bean,自己写的是 8 个(实测)
  • DI ── 零件不自己造,用构造函数接收。不用 Spring 也能在 0.23 秒内运行(实测)
  • Bean 只有一个 ── 把申请人放在负责人的字段里,200 件中有 87~102 件申请人被调换(实测)
  • Jackson 与 Tomcat ── 第 3・4 讲的失败一个都没有发生(实测)
  • 代理人 ── 在同一个类里调用,@Transactional 不生效,留下了只有一半的申请(实测)

舞台和第 1 讲一样是三台机器。应用是在第 1 讲的 Spring Boot 4.1.2(Tomcat 11、Jackson 3、JDK 25)上,加了实验用的窗口(DemoController)和三个负责人。数据库是第 4 讲加了申请人列的 PostgreSQL 16。

二

Spring Boot 里的名册

Spring Boot 应用启动时,Spring 会造出需要的零件、把它们连起来,放进一个容器(ApplicationContext)里。这些零件叫作 Bean。启动后,让应用自己数一数 Bean。

终端画面。java -jar app.jar --demo.inspect=true 的输出。Started ExpensesApplication in 3.787 seconds,Bean の数 196(うち自分で書いたもの 8),expenseService = ExpenseService$$SpringCGLIB$$0 ← [expenseRepository] 等名册,DataSource 是 HikariDataSource,JsonMapper 是 tools.jackson.databind.json.JsonMapper
让启动后的应用数一数自己里面的 Bean。196 个里自己写的是 8 个。负责人和仓库管理员的名字后面带着 $$SpringCGLIB$$0,交出去的不是本体,而是代理人(笔者于 2026 年 10 月 31 日在云端 Linux 上截图。窗口顶部的标题栏是为了说明而后加的)
# 启动之后,Spring 创建的东西(Bean)的名册(← 是交给这个 Bean 的东西)
Started ExpensesApplication in 4.12 seconds (process running for 4.83)
Bean の数 196(うち自分で書いたもの 8)
expenseController = ExpenseController ← [expenseService]
demoController = DemoController ← [expenseService, badExpenseService, tripService]
expenseService = ExpenseService$$SpringCGLIB$$0 ← [expenseRepository]
badExpenseService = BadExpenseService ← [expenseRepository]
tripService = TripService$$SpringCGLIB$$0 ← [expenseRepository]
expenseRepository = ExpenseRepository$$SpringCGLIB$$0 ← [jdbcClient]
DataSource : dataSource = com.zaxxer.hikari.HikariDataSource
JdbcClient : jdbcClient = org.springframework.jdbc.core.simple.DefaultJdbcClient
PlatformTransactionManager : transactionManager = org.springframework.jdbc.support.JdbcTransactionManager
JsonMapper : jacksonJsonMapper = tools.jackson.databind.json.JsonMapper
DispatcherServlet : dispatcherServlet = org.springframework.web.servlet.DispatcherServlet
ServletWebServerFactory : tomcatServletWebServerFactory = org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory

一件申请经过的 Bean,以及配角们

Tomcat 接收 HTTP DispatcherServlet 选择窗口 ExpenseController 窗口(自己写的) ExpenseService 负责人(自己写的) ExpenseRepository 仓库管理员(自己写的) JdbcClient 发送 SQL JsonMapper Jackson TransactionManager @Transactional DataSource HikariCP Bean 有 196 个。自己写的是 8 个,其余是 Spring Boot 准备的 红色箭头=Spring 交给构造函数的东西。虚线是配角

Bean 有 196 个。自己写的是 8 个(应用本体、2 个窗口、3 个负责人、仓库管理员、名册员),其余 188 个由 Spring Boot 准备。第 3 讲徒手做的接收 HTTP 由 Tomcat 负责,选择窗口是 DispatcherServlet,JSON 是 Jackson 3 的 JsonMapper。第 4 讲的连接池是 HikariCP,事务是 JdbcTransactionManager。还有,负责人和仓库管理员的名字后面带着一个陌生的记号 $$SpringCGLIB$$0。谜底在第八章揭晓。

三

DI:不自己造,而是让别人交给你

再看一次负责人的代码。仓库管理员(ExpenseRepository)不是用 new 造的,而是通过构造函数的参数接收。

@Service
public class ExpenseService {
    private static final Logger log = LoggerFactory.getLogger(ExpenseService.class);
    private final ExpenseRepository repo;

    public ExpenseService(ExpenseRepository repo) {      // 仓库管理员不自己造,而是让别人交给我
        this.repo = repo;
    }

    @Transactional
    public String submit(ExpenseRequest req) {
        return submitAs("新井", req);
    }

    @Transactional
    public String submitAs(String applicant, ExpenseRequest req) {   // 申请人用参数接收
        String id = "EXP-2026-%04d".formatted(repo.nextNumber());
        log.info("担当者:受付番号を決めた {} {}", id, applicant);
        repo.insert(id, applicant, req);
        return id;
    }

“要用的零件不自己造,而是从外面交进来”。这就是 DI(Dependency Injection,依赖注入)。名册里的箭头(←),就是 Spring 把什么交给了谁。把 Spring 启动时做的事用手写出来,是这样的。

# 把 Spring 启动时做的事,用手写出来
JdbcClient jdbc = …;                                    // 来自 DataSource(HikariCP)
ExpenseRepository repo = new ExpenseRepository(jdbc);
ExpenseService service = new ExpenseService(repo);
TripService trips = new TripService(repo);             // 仓库管理员共用同一个
ExpenseController controller = new ExpenseController(service);

普通得让人泄气的 Java。写成“让别人交进来”的形式,就能替换交进来的东西。不用 Spring 也不用数据库,交一个假的仓库管理员,只让负责人运行。

# 不用 Spring 也不用数据库,只让负责人运行(FakeRun.java 的要点)
List<String> written = new ArrayList<>();
ExpenseRepository fake = new ExpenseRepository(null) {          // 假的仓库管理员(不去数据库)
    long n = 12;
    @Override long nextNumber() { return n++; }
    @Override void insert(String id, String applicant, ExpenseRequest r) { written.add(id + " " + applicant + " " + r.item()); }
};
ExpenseService service = new ExpenseService(fake);                 // 手工做 DI=只是交给构造函数
String id = service.submitAs("古井", new ExpenseRequest(LocalDate.of(2026, 10, 31), "交通費", new BigDecimal("1280")));
# 运行(3 次)
返ってきた受付番号 EXP-2026-0012、偽の倉庫番に書かれたもの [EXP-2026-0012 古井 交通費](163.5 ms)  JVM の起動から終わりまで 0.23 s
返ってきた受付番号 EXP-2026-0012、偽の倉庫番に書かれたもの [EXP-2026-0012 古井 交通費](201.7 ms)  JVM の起動から終わりまで 0.25 s
返ってきた受付番号 EXP-2026-0012、偽の倉庫番に書かれたもの [EXP-2026-0012 古井 交通費](184.4 ms)  JVM の起動から終わりまで 0.23 s

从 JVM 启动到结束 0.23~0.25 秒。启动 Spring Boot,光是这一步就要 3.6~4.1 秒。如果负责人自己 new 仓库管理员,没有数据库就无法测试负责人。DI 也是“容易测试的形式”,第 14 讲的测试以这种形式为前提。

POINT

零件用构造函数接收,放进 final 字段。构造函数只有一个时,连 @Autowired 都不需要。古老代码里常见的直接在字段上加 @Autowired 的写法,没有 Spring 时就没有交零件的入口,测试时会很麻烦。

四

窗口・负责人・仓库管理员

从第 1 讲用到现在的三个角色,在 Spring 的世界里被称为“三层”。

角色Spring 的标记做什么不做什么
窗口@RestController在 HTTP 的形式(URL、方法、JSON、状态码)与 Java 的值之间转换业务规则、SQL
负责人@Service业务规则(受理编号、金额计算、审批顺序)。事务的范围HTTP 的事、SQL 语句
仓库管理员@Repository写 SQL,和数据库对话。把数据库的异常翻译成 Spring 的异常业务判断

之所以分开,是因为变化的理由不同。画面需要改 JSON 的形式,负责人不变;数据库的表拆开,窗口不变。能各自单独测试,也多亏了 DI 和三层的组合。想在窗口里写 SQL 的时候,就是负责人或仓库管理员的工作迷路了的信号。

五

Bean 只有一个:别放在桌上

Spring 的 Bean,不特别指定的话整个应用只有一个(单例)。名册里的 expenseRepository,交给三个负责人的都是同一个。负责人也只有一个,同时到来的请求,由不同的线程同时使用同一个负责人。于是我们做了一个把申请人放在字段(桌上)的负责人。用 X-User 首部代替登录来发送申请人(真正的登录在第 11 讲)。

# 不好的负责人:把申请人放在字段里
@Service
public class BadExpenseService {
    private final ExpenseRepository repo;
    private String applicant;                             // ← Bean 只有一个。所有人用同一张桌子

    public BadExpenseService(ExpenseRepository repo) {
        this.repo = repo;
    }

    public String submit(String who, ExpenseRequest req) {
        this.applicant = who;
        String id = "EXP-2026-%04d".formatted(repo.nextNumber());   // 在和数据库往返一次的时候……
        repo.insert(id, this.applicant, req);
        return id;
    }
}
# 同时 200 件(每次 50 个),arai 和 furui 交替申请。与数据库里的申请人比较(3 次)
放在字段里的版本:200 件中、申請者が入れ替わったもの 87 件(例:EXP-2026-1002 は furui が出したのに arai)
放在字段里的版本:200 件中、申請者が入れ替わったもの 102 件(例:EXP-2026-1209 は arai が出したのに furui)
放在字段里的版本:200 件中、申請者が入れ替わったもの 94 件(例:EXP-2026-1402 は furui が出したのに arai)
用参数接收的版本:200 件中、申請者が入れ替わったもの 0 件
用参数接收的版本:200 件中、申請者が入れ替わったもの 0 件
用参数接收的版本:200 件中、申請者が入れ替わったもの 0 件

Bean 只有一个。同时到来的两个人用同一张桌子(实测)

Bean 只有一个。同时到来的两个人用同一张桌子(字段) arai 的请求 桌子(applicant) furui 的请求 桌上写 arai 与数据库往返 读桌子并保存 arai furui furui 桌上写 furui 时间 → arai 等数据库回复的时候,furui 改写了同一张桌子 arai 的申请,以 furui 的名字保存了 实测:200 件中有 87~102 件被调换。用参数接收的版本 0 件

200 件中有 87~102 件,几乎一半的申请人被调换了。因为 arai 在桌上写下名字、去数据库要受理编号的时候,furui 改写了同一张桌子。用测试一件一件试的时候,绝对不会发生。用参数接收申请人的负责人,3 次都是 0 件。

新井我的申请,变成古井前辈的名字了……

古井还好不是反过来。差点寿司费就成了你的名字。

POINT

Bean 的字段里只能放交进来的零件(final)和不变的设置。每个请求都不同的值(谁、什么、多少钱),用参数和局部变量来传。和第 2 讲的 SimpleDateFormat 一样,“一个东西大家一起用”的,就不要让它带状态。

六

JSON 交给 Jackson

把第 3 讲被正则表达式弄错的申请,原样发给 Spring Boot。

# 把第 3 讲那些有点刁钻的申请发给 Spring Boot
## a ふつう
{"status":"submitted","id":"EXP-2026-2201"}
  -> 201
## b 品目に引用符
{"status":"submitted","id":"EXP-2026-2202"}
  -> 201
## c json.dumps(\u)
{"status":"submitted","id":"EXP-2026-2203"}
  -> 201
## d 内訳つき(入れ子の amount)
{"status":"submitted","id":"EXP-2026-2204"}
  -> 201
## e 閉じかっこがない
{"timestamp":"2026-10-30T15:25:47.433Z","status":400,"error":"Bad Request","path":"/api/expenses"}
  -> 400
## f 金額が文字
{"timestamp":"2026-10-30T15:25:47.462Z","status":400,"error":"Bad Request","path":"/api/expenses"}
  -> 400
# 存进数据库的内容
EXP-2026-2201|交通費|1280.00
EXP-2026-2202|会議費 "A社"|3300.00
EXP-2026-2203|交通費|1280.00
EXP-2026-2204|交通費|1280.00
# 列表的回复(GET)里,带引号的项目是这样写的
{"id":"EXP-2026-2202","date":"2026-10-31","item":"会議費 \"A社\"","amount":3300.00,"status":"submitted"}

带引号的项目按 会議費 "A社" 原样存入,回复里正确地写成了 \"。交通費 变回了“交通費”,没有被明细里的 amount 迷惑,金额是 1280 日元。缺右括号的 JSON,以及金额写成“千二百八十円”的申请,都以 400 被拒绝。

不过有两点让人在意。一是记录(ExpenseRequest)里没有的 receipts,被 Jackson 默默丢掉了。画面那边以为发了明细,却哪里都没留下。二是 400 的回复只有“Bad Request”,看不出哪里不对。前者在第 6 讲的输入检查里,后者在第 7 讲的“异常与回复”里讨论。

七

Tomcat 接手的工作

# 坏掉的请求、不存在的方法、不存在的地方
## HELLO
HTTP/1.1 400 
Content-Type: text/html;charset=utf-8
Content-Language: en
Content-Length: 435
Date: Fri, 30 Oct 2026 15:25:47 GMT
Connection: close

<!doctype html><html lang="en"><head><title>HTTP Status 400 – Bad Request</title><style>…</style></head><body><h1>HTTP Status 400 – Bad Request</h1></body></html>
## PUT
{"timestamp":"2026-10-30T15:25:47.802Z","status":405,"error":"Method Not Allowed","path":"/api/expenses"}
  -> 405
## ない場所
{"timestamp":"2026-10-30T15:25:47.828Z","status":404,"error":"Not Found","path":"/api/nothing"}
  -> 404
# 列表回复的首部(不事先数长度,分块发送)
HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 30 Oct 2026 15:25:47 GMT

毫无意义的 HELLO 由 Tomcat 以 400(HTML 页面)拒绝,PUT 和不存在的地方,则由 Spring MVC 用 JSON 分别返回 405 和 404。列表的回复里没有 Content-Length,取而代之的是 Transfer-Encoding: chunked。这是一种不事先数长度、每一块都写上“从这里开始几个字节”再发送的方法,第 3 讲那种“用字符数写长度”的 bug 根本无从发生。

# 沉默的客人
黙っている客:60.1 秒後にサーバーが切った(受け取ったバイト 0)
黙っている客がいる間の申請 -> 201 (0.033746 s)

沉默的客人在 60.1 秒时被服务器切断。Tomcat 默认的等待时间(connectionTimeout)是 60 秒,在 Spring Boot 中可以用 server.tomcat.connection-timeout 修改。这段时间里,其他申请 0.034 秒就拿到了 201。Tomcat 最多准备 200 个工人(线程),把来的客人分配下去。

八

代理人与 @Transactional

揭晓名册里 ExpenseService$$SpringCGLIB$$0 的谜底。对于带有 @Transactional 方法的 Bean,Spring 会造一个包着本体的代理人(代理,proxy)交出去,代替本体。代理人被调用时先开始事务,再调用本体,成功就 commit,出异常就 rollback。第 4 讲手写的 setAutoCommit(false)、commit、rollback,由代理人代劳了。仓库管理员的代理人,负责把数据库的异常翻译成 Spring 的异常(不是第 4 讲的 SQLState,而是 DataIntegrityViolationException 等)。

把第 4 讲出差的申请(酒店费 1,000,000 日元放不进明细列),用三种调用方式发送。

# 出差负责人(TripService 的要点)
@Transactional
public String submit(TripRequest t) {
    log.info("出張:トランザクションの中? {}", TransactionSynchronizationManager.isActualTransactionActive());
    String id = "EXP-2026-%04d".formatted(repo.nextNumber());
    repo.insert(id, "新井", new ExpenseRequest(t.date(), t.item(), t.total()));
    int no = 1;
    for (TripRequest.Line l : t.lines()) repo.insertLine(id, no++, l.item(), l.amount());
    return id;
}

public String submitFromInside(TripRequest t) {     // 在同一个类里调用 submit
    return submit(t);
}

@Transactional
public String submitChecked(TripRequest t) throws ReceiptMissingException {
    String id = submit(t);
    throw new ReceiptMissingException("領収書の画像がない " + id);   // 受检异常
}
终端画面。上方是应用日志:Creating new transaction、出張:トランザクションの中? true、Rolling back JDBC transaction、PSQLException: numeric field overflow,之后是 出張:トランザクションの中? false。下方 ./trips.sh tx 留下抬头 0 行・明细 0 行,./trips.sh inside 留下抬头 1 行・明细 1 行
同一个 submit,经过代理人调用(tx)时在事务里运行、全部回滚;在同一个类里调用(inside)时没有事务,留下了只有一半的申请(笔者于 2026 年 10 月 31 日在云端 Linux 上截图。窗口顶部的标题栏是为了说明而后加的)
# 新干线 12,860 日元+酒店 1,000,000 日元(放不进明细列)。checked 的明细正确,最后抛出受检异常
## mode=tx
  -> 500
  残ったもの:expense 0 / expense_line 0
## mode=inside
  -> 500
  残ったもの:expense 1 / expense_line 1
## mode=checked(明細は正しい。最後にチェック例外)
  -> 500
  残ったもの:expense 1 / expense_line 2
# 应用日志(节选)
15:26:47.982 Creating new transaction with name [example.expenses.TripService.submit]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
15:26:47.983 出張:トランザクションの中? true
15:26:47.987 倉庫番:1行書いた EXP-2026-2206
15:26:47.992 倉庫番:明細を書いた EXP-2026-2206 1
15:26:48.004 Rolling back JDBC transaction on Connection [HikariProxyConnection@1350144235 wrapping org.postgresql.jdbc.PgConnection@59b65dce]
15:26:48.005 Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springf
org.postgresql.util.PSQLException: ERROR: numeric field overflow
15:26:48.170 出張:トランザクションの中? false
15:26:48.173 倉庫番:1行書いた EXP-2026-2207
15:26:48.174 倉庫番:明細を書いた EXP-2026-2207 1
15:26:48.177 Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springf
org.postgresql.util.PSQLException: ERROR: numeric field overflow
15:26:48.308 Creating new transaction with name [example.expenses.TripService.submitChecked]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
15:26:48.309 出張:トランザクションの中? true
15:26:48.310 倉庫番:1行書いた EXP-2026-2208
15:26:48.311 倉庫番:明細を書いた EXP-2026-2208 1
15:26:48.312 倉庫番:明細を書いた EXP-2026-2208 2
15:26:48.312 Committing JDBC transaction on Connection [HikariProxyConnection@1851115318 wrapping org.postgresql.jdbc.PgConnection@59b65dce]
15:26:48.315 Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: example.exp
example.expenses.ReceiptMissingException: 領収書の画像がない EXP-2026-2208

经过代理人就会回滚。不经过,就只是普通的方法(实测)

Controller 代理人 TripService$$SpringCGLIB$$0 ① 开始事务 ③ commit/rollback 本体 TripService submit() submitFromInside() this.submit() 在同一个类里调用,不经过代理人 mode=tx 经过代理人 → rollback,剩 0 行 mode=inside 不经过代理人 → 留下抬头 1・明细 1 mode=checked 受检异常 → commit,3 行全留下 DI 交给你的,有时是包着本体的代理人 用 this 调用不经过代理人。受检异常默认不回滚
  • tx ── 窗口经过代理人调用 submit。在事务里运行,失败后全部回滚,0 行
  • inside ── 窗口调用 submitFromInside,在里面调用 this.submit()。this 是本体自己,所以不经过代理人。日志是“トランザクションの中? false”,留下了和第 4 讲一样只有一半的申请
  • checked ── 全部写完后,抛出“没有收据”的受检异常(Exception 的子类)。Spring 默认对受检异常不回滚,而是 commit。3 行全都留下了

@Transactional 不是加上就生效的魔法,而是只有经过代理人调用时才生效的约定。想放进事务范围的处理,拆到别的 Bean 里,从外面调用。想在受检异常时也回滚,就写 rollbackFor,或者改成运行时异常。两个人同时修改同一件申请的话题,第 9 讲会再回到事务。

九

徒手的工作去了哪里

第 3・4 讲徒手做错的事,在 Spring Boot 上(实测)

试的内容 徒手(第 3・4 讲) Spring Boot(实测) 由谁 项目里有引号 × 会議費 \ 会議費 "A社" Jackson \u 转义 × 原样保存 交通費 Jackson 嵌套的 amount × 780 日元 1280 日元・丢掉明细 Jackson 坏 JSON・金额是文字 (没试过) 400 Spring MVC 一行 HELLO × 404 400 Tomcat PUT ○ 405(自己写) 405 Spring MVC 沉默的客人 × 全都卡住 60.1 秒切断 Tomcat 明细中途失败 × 留下一半 0 行(回滚) @Transactional 第 3・4 讲的失败,在 Spring Boot 上一个都没有发生 不过“默默丢掉不认识的项目(receipts)”是 Jackson 的默认行为。第 6 讲再想

第 3 讲和第 4 讲的失败,在 Spring Boot 上一个都没有发生。HTTP 是 Tomcat,选窗口是 Spring MVC,JSON 是 Jackson,连接是 HikariCP,回滚是代理人。正因为徒手做过,听到这些名字时才知道“是那份工作”。作为交换,多了 Bean 只有一个和代理人这两条 Spring 特有的约定。

十

在自己的电脑上验证

想试的事命令・写法
生成项目骨架(也可以用浏览器)https://start.spring.io
不打包直接运行./mvnw spring-boot:run
自动配置造了什么、没造什么java -jar app.jar --debug
交进来的是不是代理人log.info("{}", service.getClass());
现在是否在事务里TransactionSynchronizationManager.isActualTransactionActive()
切断沉默客人的时间server.tomcat.connection-timeout=20s

加上 --debug,启动时会输出“CONDITIONS EVALUATION REPORT”。像“因为有 JDBC 驱动所以造了 DataSource”这样的 Spring Boot 判断会列成清单,是查“为什么会有这个 Bean”的入口。

结 — 一页总结与下一讲

  1. Spring Boot 启动时造出 Bean 并连起来。这一讲的应用有 196 个,自己写的是 8 个。
  2. DI:零件用构造函数接收,放进 final。交一个假的,不用 Spring 也能测试。
  3. 窗口(HTTP 的形式)・负责人(业务规则与事务)・仓库管理员(SQL)。按变化的理由来分。
  4. Bean 只有一个。把每个请求的值放在字段里,申请人就被调换了(200 件中 87~102 件)。
  5. 第 3・4 讲的失败,由 Tomcat・Spring MVC・Jackson・HikariCP 接手。不过不认识的项目会被默默丢掉。
  6. @Transactional 只在经过代理人时生效。用 this 调用不生效,受检异常时会 commit。

新井明细被默默丢掉这件事,不起眼但让人在意。

古井眼光不错。下一讲是输入检查。空的项目、负数金额、全角数字、不认识的项目。在窗口之前,决定检查什么、检查到哪里。

参考

记录是笔者于 2026 年 10 月 30 日~31 日在云端 Linux(Ubuntu 24.04,内核 6.18)上,用 Spring Boot 4.1.2(Tomcat 11、Jackson 3)、OpenJDK 25.0.4、PostgreSQL 16.15 取得的。PC・申请服务器・数据库服务器,是在一台机器里建立的不同网络空间(network namespace)。申请人被调换的件数和启动时间,每次运行都会不同。表中的数字和输出,全部是从当时命令输出中读取的实际值。登场人物均为虚构。

古法后端入门 第5讲 — 回到 Spring Boot / 2026.10

← 返回文章列表