故事开始

上个月,我接了个需求。

CTO说:”你先研究一下某二手回收公司的后端架构,然后改个接口。”

我说:”好。”

然后他甩给我一个Java后端项目,说:”后端也要改一下,加个失效日志。”

我看着屏幕上25个微服务、37000多个Java文件、LEFT JOIN、INNER JOIN、JOOQ、MQ、定时任务、Feign Client、Redis分布式锁……

每个词我都认识,连起来不知道在干嘛。


第一关:看不懂代码

我打开某二手回收公司的后端代码。

1
2
3
4
5
6
7
8
9
recycle/           ← 回收业务,15个微服务
── online-sapi/ ← 线上回收
├── offline-sapi/ ← 线下回收
├── order-api/ ← 订单中心
├── shanhs-ai-audit/ ← AI审核系统
└── ...(还有11个)

boss/ ← 后台管理,4个微服务
sales/ ← 销售业务,6个微服务

25个微服务,37000个Java文件。

我打开一个Service文件,看到的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BidServiceConfigService extends GenericServiceImpl<BidServiceConfig, Integer> {

@Autowired
BidServiceConfigLogService bidServiceConfigLogService;

@Autowired
DSLContext dslContext;

@Autowired
AliMQProducter mqProducter;

// ... 还有20个依赖
}

GenericServiceImpl是什么?DSLContext是什么?AliMQProducter是什么?

我打开了前端项目的Service文件做对比:

1
2
3
4
5
6
7
8
9
10
11
12
// 前端
export class BidService {
constructor(
private http: HttpClient,
private log: LogService,
private mq: MQService
) {}

getConfig(id: number) {
return this.http.get(`/api/config/${id}`);
}
}

这不是一样的东西吗?

前端:constructor(依赖)
后端:@Autowired 依赖

前端:this.http.get()
后端:dslContext.select().from().where()

只是语法不同,做的事情一样。


第二关:改了第一行代码

我的第一个任务是:给买家通用配置加一个失效日志。

老板说:”现在配置生效有日志,失效没有日志,你加一下。”

我看了代码,发现逻辑是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
定时任务(每小时扫描)


找到即将失效的配置


发出延迟MQ消息(到失效时间点)


MQ Consumer收到消息


更新状态 IS_VALID = false ← 这里!

我只需要在最后一步加上记日志的代码。

前端思维版

1
2
3
4
5
6
// 如果是前端,我会这么写
async function expireConfig(config: Config) {
config.isValid = false;
await saveLog(config.id, '配置失效'); // 加这一行
await updateConfig(config);
}

后端实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 我写了这个
public void updateStatusWithLog(BidServiceConfig config, Boolean isValid, String desc) {
// 状态变了才记日志
if (!Objects.equals(config.getIsValid(), isValid)) {
BidServiceConfigLog log = new BidServiceConfigLog();
log.setConfigId(config.getId());
log.setBeforeOperation("配置状态:" + getStatusName(config.getIsValid()));
log.setAfterOperation("配置状态:" + getStatusName(isValid) + "(" + desc + ")");
log.setChangeType("Status");
bidServiceConfigLogService.save(log); // ← 加的就是这行
}

// 更新状态
BidServiceConfig update = new BidServiceConfig();
update.setId(config.getId());
update.setIsValid(isValid);
update(update);
}

写完一跑,通过了。

我突然发现:后端开发不就是写函数吗?


第三关:JOIN的血泪教训

接下来,老板让我优化一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 旧版:LEFT JOIN + Java判断
public boolean hasRecycleAccountBuyerPaymentInProgress(Long prdDetailId) {
Integer payOrderStatus = dao.execute(e -> e
.select(ORD_PAYORDER.STATUS)
.from(BID_MAIN_ORDER)
.leftJoin(BID_MAIN_ORDER_OFFER).on(...) // ← LEFT JOIN
.leftJoin(ORD_PAYORDER).on(...) // ← LEFT JOIN
.where(...)
.fetchOneInto(Integer.class));

// 在Java里判断
return Objects.equals(payOrderStatus, ProductFinanceStatus.PAYING);
}

问题是:LEFT JOIN可能返回null,然后 Objects.equals(null, 2) 返回false。

虽然结果是对的,但逻辑不严谨——没有付款记录 ≠ 不在支付中

前端思维版

1
2
3
4
5
6
// 前端怎么做的?
// 1. 先查所有数据(LEFT JOIN)
const orders = await db.query("SELECT * FROM orders WHERE productId = 123");

// 2. 在JS里过滤
const isPaying = orders.some(o => o.payStatus === 'PAYING');

前端是”查出来再判断”,后端可以”让数据库直接告诉我有还是没有”。

优化后的版本

1
2
3
4
5
6
7
8
9
10
11
// 新版:JOIN + EXISTS
public boolean hasRecycleAccountBuyerPaymentInProgress(Long prdDetailId) {
return dao.execute(e -> e.fetchExists(
e.selectOne()
.from(BID_MAIN_ORDER)
.join(BID_MAIN_ORDER_OFFER).on(...) // ← 改成JOIN
.join(ORD_PAYORDER).on(...) // ← 改成JOIN
.where(...)
.and(ORD_PAYORDER.STATUS.eq(ProductFinanceStatus.PAYING)) // ← 直接写在SQL里
));
}

改动:

  • LEFT JOIN → JOIN(没有付款记录的直接过滤掉)
  • Java层判断 → SQL层过滤(STATUS=PAYING直接写在WHERE里)
  • 返回Integer再判断 → 直接返回true/false

为什么这么改?

1
2
3
4
5
LEFT JOIN:像Excel的VLOOKUP,找不到就返回空
JOIN:像SQLWHERE,找不到就不返回这行

你要回答的问题是"有没有",不是"是什么"。
既然都不需要,不如让数据库直接过滤掉。

第四关:SQL也不会写了

我写了十年前端,SELECT * FROM 都快忘了。

JOOQ是什么鬼?

1
2
3
4
5
dao.create()
.select(BID_SERVICE_CONFIG.fields())
.from(BID_SERVICE_CONFIG)
.where(BID_SERVICE_CONFIG.ID.eq(id))
.fetchOneInto(BidServiceConfig.class);

我看了半天,突然反应过来:

这不就是用Java写SQL吗?

1
SELECT * FROM bid_service_config WHERE id = ?

JOOQ = 类型安全的SQL构建器。

  • select(fields())SELECT *
  • from(TABLE)FROM 表
  • where(COLUMN.eq(value))WHERE 列 = ?
  • fetchOneInto(Class) → 返回一个对象

为什么用JOOQ不用MyBatis?

1
2
3
4
5
MyBatis:写XMLSQL和Java分开
SQL写错了,运行时才报错

JOOQ:写Java,SQL在代码里
SQL写错了,编译时就报错

前端类比:

1
2
3
4
5
// MyBatis = 写SQL字符串(像内联样式)
const sql = "SELECT * FROM users WHERE id = " + userId; // 容易SQL注入

// JOOQ = 用API构建查询(像CSS-in-JS)
db.select().from(users).where(users.id.eq(userId)); // 类型安全

第五关:其实已经会了

一个月后,我回头看自己改过的代码:

1
2
3
4
5
6
✅ 新建了定时任务 Job(每小时扫描过期配置)
✅ 改了 MQ Consumer(加日志记录)
✅ 新建了 updateStatusWithLog 方法(更新+记日志)
✅ 优化了 hasRecycleAccountBuyerPaymentInProgress(LEFT JOINJOIN
✅ 用SQL验证了业务逻辑(写了20+条查询)
✅ 写了前端→后端对照表(帮自己理解)

这些都是后端开发。

我突然发现:我不是”不会写后端”,我是”不熟后端语法”。


前端→后端对照表

这是我总结的对照表,帮自己理解:

前端(JS/TS) 后端(Java) 实际代码
list.filter(x => x.id > 0) stream().filter(x -> x.getId() > 0) PdProductRefundServiceImpl
list.map(x => x.name) stream().map(X::getName) OperationManagementService
obj?.name ?? '默认' StringUtils.defaultIfBlank(obj.getName(), "默认") MlGoodsSkuPriceConfigService
if (obj == null) if (Objects.isNull(obj)) BidServiceConfigConsumer
axios.post('/api', data) @PostMapping + @RequestBody ✅ 所有Controller
const { id, name } = obj obj.getId(), obj.getName() ✅ 到处都在用
try { } catch(e) {} try { } catch(Exception e) {} ✅ 各种Service
VLOOKUP找不到返回空 LEFT JOIN找不到返回null ✅ 我踩的坑
Array.filter().length > 0 stream().anyMatch() BidServiceConfigService

后端就是前端换了个语法。


我学到的几件事

1. 全栈不是”什么都会”,是”需要什么学什么”

我不需要学完Java所有语法,我只需要知道:

  • 怎么接收请求(Controller)
  • 怎么写业务逻辑(Service)
  • 怎么查数据库(JOOQ/MyBatis)
  • 怎么发异步消息(MQ)
  • 怎么写定时任务(Job)

其他的东西,遇到了再学。

2. 前端思维在后端也适用

1
2
3
4
5
6
前端:组件 → 状态 → 渲染
后端:请求 → 逻辑 → 数据库

"请求"当成"用户点击"
"逻辑"当成"函数调用"
"数据库"当成"后端API"

3. 最大的障碍不是技术,是心理

我看到37000个Java文件时,第一反应是:”我不会。”

但真正开始改代码后,发现:

  • 改一个方法,就改一个方法
  • 加一个日志,就加一行代码
  • 优化一个SQL,就改几个JOIN

都是小事,一件一件做,就做完了。

4. 给自己三个月

如果我现在问自己:”三个月前你会写Java后端吗?”

答案是:不会。

“现在呢?”

会一点。

会写定时任务、会改MQ Consumer、会优化SQL、会用JOOQ查数据库。

不是”精通”,是”能用”。

这就够了。


写在最后

这篇文章不是”前端转全栈教程”,是“一个前端工程师的真实经历”

我没有系统学过Java,没有看过《Java核心技术》,没有刷过LeetCode。

我就是接了个需求,被迫改代码,遇到问题查资料,改完了发现”其实我也会”。

如果你也是前端,也在考虑要不要搞后端,我的建议是:

别想”我能不能学会”,想”我需不需要用”。

需要用,就学。
用到了,就会了。
会了,就继续用。

三个月后,你也会说:”其实我也会写后端。”


本文基于作者2026年6-7月的真实项目经历,代码来自某二手回收公司后端项目(已脱敏)。

下一篇预告:《前端转全栈:我需要的不是学Java,是这张地图》——后端技术全景图,一张图看懂后端有哪些东西。