🎯 课程目标

从 Java 零基础到能独立开发 Spring Boot 后端服务,具备企业级工程意识。

👥 适合谁

有 1 年以上 JS/TS 经验的前端工程师,想掌握 Java 后端开发。

⏱️ 学习时长

认真学完约需 5-7 周,每天 2 小时。完成所有练习题和项目任务是关键。

📍 课程地图(28 节)

阶段内容关键产出
总览(2节)课程地图、环境准备JDK + Maven 环境就绪
语言基础(7节)变量/字符串/控制流/集合/方法/异常/泛型能写 200 行 Java 程序
面向对象(5节)类/继承/封装/枚举/设计模式能写面向对象 Java 代码
工程能力(4节)Maven/项目结构/Stream/测试工程化开发能力达标
Spring Boot(6节)入门/Controller/Service/校验/异常/数据库能独立开发 REST API
实战(4节)接口规范/安全/项目路线/面试速查完整后端项目

🔑 前端工程师学 Java 的核心优势

已有优势:TypeScript 类型思维

你已经理解了静态类型的价值,Java 的强类型系统只是"更严格的 TypeScript"。

已有优势:HTTP 协议

你知道 GET/POST、状态码、请求头,学 Spring Boot 只需学"如何在服务端处理这些"。

已有优势:面向对象

ES6 class、继承、接口(TypeScript)你都用过,Java 的 OOP 只是更完整、更规范。

需要适应:工程化思维

Java 生态强调分层、约定、规范。从"随便建个文件"到"按约定组织代码"是最大的转变。

💡 学习建议:每学一个新概念,先问自己"这在 TypeScript 里对应什么?"。大部分都有直接对应。
返回总入口

🔄 Node.js vs Java 工具链对比

职责Node.jsJava
运行时nodejava(JVM)
版本管理nvmsdkman
包管理npm / pnpmMaven / Gradle
依赖文件package.jsonpom.xml
编译不需要javac(编译为字节码)
运行脚本npm run devmvn spring-boot:run
IDEVS CodeIntelliJ IDEA(强烈推荐)

🚀 安装步骤(macOS)

# 安装 sdkman(Java 版本管理,类似 nvm) curl -s "https://get.sdkman.io" | bash source "$HOME/.sdkman/bin/sdkman-init.sh" # 安装 JDK 21(LTS 版本) sdk install java 21.0.3-tem sdk default java 21.0.3-tem # 安装 Maven sdk install maven # 验证 java -version # openjdk 21.0.3 javac -version # javac 21.0.3 mvn -version # Apache Maven 3.9.x

🏗️ 第一个 Java 程序

// Hello.java —— 文件名必须和 public class 名一致 public class Hello { public static void main(String[] args) { String name = "前端工程师"; System.out.println("Hello, " + name + "!欢迎来到 Java 后端世界。"); // 编译: javac Hello.java → 生成 Hello.class // 运行: java Hello → JVM 执行字节码 } }

📦 JDK / JRE / JVM

JVM

运行 .class 字节码的虚拟机。类比浏览器的 V8 引擎。

JRE

JVM + 标准库。只能运行程序,不能编译。

JDK

JRE + 编译器 + 工具。写代码必须装这个。

⚠️ 常见陷阱:① Java 文件名必须和 public class 名一致 ② Maven 有严格的目录约定 ③ IntelliJ IDEA 社区版免费且够用

📋 速查:常用命令

操作命令
编译javac Hello.java
运行java Hello
Maven 编译mvn compile
Maven 测试mvn test
Maven 打包mvn package
Spring Boot 运行mvn spring-boot:run
✏️ 填空:环境命令
编译 Java 文件: Hello.java 运行编译后的程序: Hello 查看 Java 版本:java

🔄 JS/TS vs Java 类型系统对比

概念JavaScript / TypeScriptJava
类型检查TS 编译时 / JS 无编译时强制检查,不可绕过
数字number(统一)int / long / double(区分整数/浮点)
布尔booleanboolean(原始)/ Boolean(包装)
字符无单独类型char(单字符,用单引号)
字符串stringString(引用类型,首字母大写)
空值null / undefinednull(仅一个,原始类型不能为 null)
可选类型string | nullOptional<String>
常量constfinal
类型推断let x = 1var x = 1(Java 10+)
any 逃逸any 可绕过无法绕过,必须类型正确

📌 八大原始类型 + 包装类

// ===== Java 原始类型(Primitive Types) ===== // 整数类型(4 种大小) byte tiny = 127; // 1 字节,-128 ~ 127 short small = 32000; // 2 字节 int age = 28; // 4 字节,最常用(≈ JS 的 number 整数部分) long big = 100_000_000L; // 8 字节,注意后缀 L // 浮点类型 float pi = 3.14f; // 4 字节,注意后缀 f double price = 19.99; // 8 字节,最常用(≈ JS 的 number) // 布尔 boolean isActive = true; // 只有 true / false // 字符(单引号!和字符串的双引号不同) char grade = 'A'; // 单个 Unicode 字符 // ===== 包装类(Wrapper Classes)—— 原始类型的对象版 ===== Integer boxedAge = 28; // int → Integer(自动装箱) Double boxedPrice = 19.99; // double → Double Boolean boxedFlag = true; // boolean → Boolean int unboxed = boxedAge; // Integer → int(自动拆箱) // 为什么需要包装类?—— 泛型和集合只接受对象 // List<int> ❌ List<Integer> ✅

🏷️ var 类型推断与 final 常量

// ===== var 关键字(Java 10+)—— 类似 TS 的 let 推断 ===== var name = "Alice"; // 编译器推断为 String var count = 42; // 推断为 int var prices = List.of(1.0, 2.0); // 推断为 List<Double> // var 只能用于局部变量,不能用于字段或参数 // var 不是动态类型!一旦推断就不能改变 // var x = 1; x = "hello"; ← 编译错误! // ===== final 常量(类似 const) ===== final int MAX_RETRY = 3; final String API_URL = "https://api.example.com"; // MAX_RETRY = 5; ← 编译错误! // ===== Optional —— 安全处理可能为空的值 ===== import java.util.Optional; Optional<String> maybeName = Optional.of("Alice"); Optional<String> empty = Optional.empty(); // 安全取值(不会 NullPointerException) String result = maybeName.orElse("默认值"); maybeName.ifPresent(n -> System.out.println("名字: " + n));
⚠️ 常见误区:intInteger 不一样!int 是原始类型不能为 null,Integer 是对象可以为 null。② Java 没有 undefined,只有 null。③ var 不是动态类型,只是让编译器推断。

📋 速查:Java 基础类型

类型大小包装类TS 对应示例
byte1BBytebyte b = 127;
short2BShortshort s = 32000;
int4BIntegernumberint x = 42;
long8BLongbigintlong l = 100L;
float4BFloatfloat f = 3.14f;
double8BDoublenumberdouble d = 3.14;
booleanBooleanbooleanboolean ok = true;
char2BCharacterchar c = 'A';
✏️ 填空:类型系统练习
// 声明一个不可变的整数常量 int MAX_SIZE = 100; // 用类型推断声明字符串 greeting = "Hello"; // 集合泛型必须用包装类 List<> numbers = new ArrayList<>();
🧠 小测验:Java 类型系统

以下哪个说法是正确的?

🔄 JS vs Java 字符串对比

操作JavaScriptJava
比较内容=== "hello".equals("hello")(不能用 ==)
模板字符串`Hello ${name}`"Hello %s".formatted(name)(Java 15+)
拼接"a" + "b""a" + "b"StringBuilder
长度str.lengthstr.length()(方法,有括号!)
包含str.includes("x")str.contains("x")
分割str.split(",")str.split(",")
去空格str.trim()str.strip()(推荐)或 .trim()
大写str.toUpperCase()str.toUpperCase()
索引str[0]str.charAt(0)
截取str.slice(1,3)str.substring(1,3)
多行字符串反引号 ``Text Block """..."""(Java 13+)

⚠️ equals() vs == —— 必须搞清楚

// ===== 字符串比较:最容易出错的地方 ===== String a = "hello"; String b = "hello"; String c = new String("hello"); // == 比较的是引用(内存地址),不是内容! System.out.println(a == b); // true(字符串常量池优化) System.out.println(a == c); // false!(new 创建了新对象) // .equals() 比较的是内容(正确做法!) System.out.println(a.equals(c)); // true ✅ // 防止 NullPointerException 的技巧 String name = null; // name.equals("test"); // ❌ 抛出 NullPointerException "test".equals(name); // ✅ 安全,返回 false // 或者用 Objects.equals(name, "test")

🔧 StringBuilder 与格式化

// ===== StringBuilder —— 高效拼接(循环中必须用) ===== // String 不可变,每次 + 都创建新对象,循环中性能很差 StringBuilder sb = new StringBuilder(); for (int i = 0; i < 5; i++) { sb.append("第 ").append(i).append(" 项\n"); } String result = sb.toString(); // ===== Text Block(Java 13+)—— 类似 JS 的反引号 ===== String json = """ { "name": "Alice", "age": 28, "role": "developer" } """; // ===== .formatted()(Java 15+)—— 类似 JS 模板字符串 ===== String msg = "你好 %s,你今年 %d 岁".formatted("Alice", 28); // 等价于 JS 的 `你好 ${name},你今年 ${age} 岁` // String.format() —— 传统方式 String old = String.format("价格: %.2f 元", 19.99); // ===== 常用方法 ===== String s = " Hello, World! "; s.strip(); // "Hello, World!"(去两端空白) s.toUpperCase(); // " HELLO, WORLD! " s.toLowerCase(); // " hello, world! " s.contains("World"); // true s.startsWith(" H"); // true s.replace("World", "Java"); // " Hello, Java! " s.substring(2, 7); // "Hello" "a,b,c".split(","); // String[]{"a","b","c"} String.join("-", "a","b"); // "a-b"
⚠️ 常见误区:① 永远用 .equals() 比较字符串,不要用 ==。② length() 是方法要加括号,不像 JS 的 .length 属性。③ 循环拼接字符串一定要用 StringBuilder,否则性能很差。

📋 速查:String 常用方法

方法作用JS 对应
equals(s)内容比较===
length()长度.length
charAt(i)取字符str[i]
substring(a,b)截取slice(a,b)
contains(s)是否包含includes(s)
indexOf(s)查找位置indexOf(s)
split(s)分割split(s)
strip()去空白trim()
replace(a,b)替换replace(a,b)
toUpperCase()转大写toUpperCase()
startsWith(s)前缀判断startsWith(s)
formatted(...)格式化(15+)模板字符串
String.join()合并arr.join()
✏️ 填空:字符串练习
// 正确比较字符串内容 String a = "hello"; boolean same = a.("hello"); // 取字符串第一个字符 char first = a.(0); // Java 15+ 格式化字符串 String msg = "Hello %s".("World");
🧠 小测验

new String("hi") == new String("hi") 的结果是?

🔄 JS vs Java 控制流对比

结构JavaScriptJava
if 条件if (x > 0) { }if (x > 0) { }(几乎一样)
三元x ? a : bx ? a : b(完全一样)
for 循环for (let i=0; i<n; i++)for (int i=0; i<n; i++)
增强 forfor (const x of arr)for (var x : arr)(冒号!)
whilewhile (cond) { }while (cond) { }(完全一样)
do-whiledo { } while(cond)do { } while(cond);
switchswitch + case + break箭头语法 case X ->(14+)
逻辑运算&& || !&& || !(完全一样)

🔀 if / else if / else

// ===== 条件判断(和 JS 几乎一样) ===== int score = 85; if (score >= 90) { System.out.println("优秀"); } else if (score >= 80) { System.out.println("良好"); // ← 输出这个 } else if (score >= 60) { System.out.println("及格"); } else { System.out.println("不及格"); } // 三元表达式(和 JS 完全一样) String level = score >= 60 ? "及格" : "不及格"; // 注意:Java 的 if 条件必须是 boolean! // if (1) { } ← 编译错误!Java 不会像 JS 那样隐式转换 // 必须写 if (x != 0) 或 if (x != null)

🎯 switch 箭头语法(Java 14+)

// ===== 新版 switch(推荐,不需要 break!) ===== int status = 404; // 箭头语法 —— 不会 fall-through,不需要 break switch (status) { case 200 -> System.out.println("OK"); case 301, 302 -> System.out.println("重定向"); // 多值匹配 case 404 -> System.out.println("未找到"); case 500 -> System.out.println("服务器错误"); default -> System.out.println("未知状态: " + status); } // switch 表达式 —— 可以返回值! String msg = switch (status) { case 200 -> "OK"; case 404 -> "Not Found"; case 500 -> "Server Error"; default -> "Unknown"; }; // 注意结尾有分号 // ===== 传统 switch(不推荐,容易忘 break) ===== switch (status) { case 200: System.out.println("OK"); break; // 忘了 break 会 fall-through! default: System.out.println("其他"); }

🔁 循环:for / for-each / while

// ===== 传统 for(和 JS 一模一样) ===== for (int i = 0; i < 5; i++) { System.out.println(i); // 0,1,2,3,4 } // ===== 增强 for(对应 JS 的 for...of) ===== String[] fruits = {"apple", "banana", "cherry"}; for (String fruit : fruits) { // 注意用冒号 : 不是 of System.out.println(fruit); } // 遍历 List List<String> names = List.of("Alice", "Bob"); for (var name : names) { System.out.println(name); } // ===== while 和 do-while ===== int count = 0; while (count < 3) { System.out.println(count); count++; } do { System.out.println("至少执行一次"); } while (false); // ===== break 和 continue(和 JS 一样) ===== for (int i = 0; i < 10; i++) { if (i == 3) continue; // 跳过 3 if (i == 7) break; // 到 7 停止 System.out.println(i); // 0,1,2,4,5,6 }
⚠️ 常见误区:① Java 的 if 条件必须是 boolean,不能像 JS 那样 if (1)if ("")。② 增强 for 用冒号 : 不是 of。③ 新版 switch 箭头语法不需要 break。

📋 速查:控制流语法

结构Java 语法注意点
ifif (bool) { }条件必须是 boolean
else ifelse if (bool) { }不是 elif
三元cond ? a : b和 JS 一样
forfor (int i=0; i<n; i++)声明类型
for-eachfor (T x : collection)用冒号
whilewhile (bool) { }和 JS 一样
do-whiledo { } while(bool);至少执行一次
switch 箭头case X -> expr;Java 14+,无需 break
switch 表达式var r = switch(x) {...};可返回值
breakbreak;退出循环
continuecontinue;跳过本次
✏️ 填空:控制流练习
// 增强 for 循环遍历数组 for (String item items) { System.out.println(item); } // switch 箭头语法(Java 14+) String text = switch (code) { case 200 "OK"; -> "Unknown"; };
🧠 小测验

Java 中 if (1) { } 会怎样?

🔄 JS vs Java 集合对比

概念JavaScriptJava
动态数组ArrayArrayList<T>
固定数组int[] / String[]
去重集合SetHashSet<T>
键值对Object / MapHashMap<K,V>
不可变列表Object.freeze([])List.of(...)(Java 9+)
不可变映射Object.freeze({})Map.of(k,v,...)(Java 9+)
遍历forEach / for...offor-each / .forEach()
排序arr.sort()Collections.sort(list)

📦 数组 vs ArrayList

// ===== 数组(固定长度,类型安全) ===== int[] numbers = {1, 2, 3, 4, 5}; // 声明并初始化 String[] names = new String[3]; // 声明长度为 3 的数组 names[0] = "Alice"; names[1] = "Bob"; System.out.println(numbers.length); // 5(注意:是属性,不是方法) // ===== ArrayList(动态长度,最常用!) ===== // 类比:JS 的 Array List<String> fruits = new ArrayList<>(); fruits.add("apple"); // push fruits.add("banana"); fruits.get(0); // "apple"(类比 arr[0]) fruits.set(0, "grape"); // 修改(类比 arr[0] = "grape") fruits.remove("banana"); // 删除 fruits.size(); // 长度(不是 length!) fruits.contains("grape"); // 是否包含 fruits.isEmpty(); // 是否为空 // ===== 不可变集合工厂(Java 9+,强烈推荐) ===== List<String> immutable = List.of("a", "b", "c"); // immutable.add("d"); ← 抛出 UnsupportedOperationException! Set<Integer> numSet = Set.of(1, 2, 3); Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 87);

🗂️ HashMap 与 HashSet

// ===== HashMap(类比 JS 的 Object / Map) ===== Map<String, Integer> userAge = new HashMap<>(); userAge.put("Alice", 28); // 添加 userAge.put("Bob", 32); userAge.get("Alice"); // 28 userAge.getOrDefault("Charlie", 0); // 0(不存在时返回默认值) userAge.containsKey("Alice"); // true userAge.size(); // 2 userAge.remove("Bob"); // 删除 // 遍历 Map for (var entry : userAge.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); } // ===== HashSet(自动去重) ===== Set<String> tags = new HashSet<>(); tags.add("java"); tags.add("java"); // 重复不会添加 tags.add("spring"); System.out.println(tags.size()); // 2 // ===== Collections 工具类 ===== List<Integer> nums = new ArrayList<>(List.of(3, 1, 4, 1, 5)); Collections.sort(nums); // [1, 1, 3, 4, 5] Collections.reverse(nums); // [5, 4, 3, 1, 1] Collections.max(nums); // 5 Collections.frequency(nums, 1); // 2(出现次数)
⚠️ 常见误区:① 数组用 .length(属性),ArrayList 用 .size()(方法)。② List.of() 创建的是不可变列表,不能 add/remove。③ 泛型只接受引用类型:List<int> ❌,List<Integer> ✅。

📋 速查:集合常用操作

操作ListMapSet
添加add(e)put(k,v)add(e)
获取get(i)get(k)
删除remove(i)remove(k)remove(e)
大小size()size()size()
包含contains(e)containsKey(k)contains(e)
清空clear()clear()clear()
遍历for (T x : list)for (var e : map.entrySet())for (T x : set)
不可变List.of()Map.of()Set.of()
✏️ 填空:集合操作
// 创建不可变列表(Java 9+) List<String> names = ("Alice", "Bob"); // 获取 HashMap 中的值,不存在时返回默认值 int age = map.("Tom", 0); // 获取 ArrayList 的长度 int len = list.();
🧠 小测验

List.of("a","b").add("c") 会怎样?

🔄 JS vs Java 方法对比

概念JavaScriptJava
定义函数function add(a, b) { }int add(int a, int b) { }
箭头函数(a, b) => a + bLambda: (a, b) -> a + b
默认参数function(x = 10)不支持,用方法重载代替
可变参数...argsint... args
返回类型不声明必须声明(void 表示无返回)
独立函数可以不可以,必须在类中
重载不支持支持(同名不同参数)

📌 方法定义与重载

// ===== 方法定义语法 ===== // 访问修饰符 + 返回类型 + 方法名(参数列表) { 方法体 } public int add(int a, int b) { return a + b; } public void sayHello(String name) { // void = 无返回值 System.out.println("Hello, " + name); } // ===== 方法重载(Overloading)—— JS 没有的特性 ===== // 同名方法,参数不同 → 编译器自动选择 public int add(int a, int b) { return a + b; } public double add(double a, double b) { return a + b; } public String add(String a, String b) { return a + b; } add(1, 2); // 调用 int 版本 → 3 add(1.5, 2.5); // 调用 double 版本 → 4.0 add("Hi", "!"); // 调用 String 版本 → "Hi!" // ===== 可变参数(varargs)—— 类似 JS 的 ...rest ===== public int sum(int... numbers) { // numbers 是 int[] int total = 0; for (int n : numbers) { total += n; } return total; } sum(1, 2, 3); // 6 sum(1, 2, 3, 4, 5); // 15

🔑 static vs 实例方法 & pass-by-value

// ===== static 方法(不需要 new 对象) ===== public class MathUtils { // 工具方法通常是 static public static int max(int a, int b) { return a > b ? a : b; } } // 直接用类名调用 MathUtils.max(3, 5); // 5 // ===== 实例方法(需要 new 对象) ===== public class Counter { private int count = 0; // 实例字段 public void increment() { // 实例方法 count++; } public int getCount() { return count; } } Counter c = new Counter(); // 必须先 new c.increment(); c.getCount(); // 1 // ===== Java 是 pass-by-value(值传递) ===== // 原始类型:传副本(和 JS 一样) public void change(int x) { x = 100; } int a = 1; change(a); System.out.println(a); // 1(不变) // 引用类型:传引用的副本(对象本身可被修改) public void addItem(List<String> list) { list.add("new"); // 会修改原始列表! } // 但不能让引用指向新对象: public void replace(List<String> list) { list = new ArrayList<>(); // 不影响外部引用 }
💡 记忆技巧:Java 方法必须在类里面。static 方法 ≈ JS 的普通函数,实例方法 ≈ JS class 中的方法。没有默认参数?用方法重载代替!

📋 速查:方法语法

语法说明示例
返回类型 名(参数)方法定义int add(int a, int b)
void无返回值void print(String s)
static类方法static int max(int a, int b)
重载同名不同参add(int,int) / add(double,double)
Type... args可变参数int sum(int... nums)
final 参数参数不可修改void f(final int x)
✏️ 填空:方法定义
// 定义一个无返回值的方法 public sayHi(String name) { System.out.println("Hi " + name); } // 定义可变参数方法 public int sum(int numbers) { // numbers 当作数组使用 } // 不需要 new 就能调用的方法 public int max(int a, int b) { return a > b ? a : b; }
🧠 小测验

Java 不支持默认参数值,应该用什么替代?

🔄 JS vs Java 异常对比

概念JavaScriptJava
捕获语法try/catch/finallytry/catch/finally(一样)
抛出throw new Error()throw new Exception()
强制处理checked exception 必须处理
异常类型只有 Error丰富的异常层次结构
资源清理手动 finallytry-with-resources(自动关闭)
声明异常throws IOException

📌 try-catch-finally

// ===== 基础异常处理(和 JS 很像) ===== try { int result = 10 / 0; // 抛出 ArithmeticException } catch (ArithmeticException e) { System.out.println("除零错误: " + e.getMessage()); } finally { System.out.println("无论如何都执行"); } // 捕获多种异常 try { String s = null; s.length(); // NullPointerException } catch (NullPointerException e) { System.out.println("空指针: " + e.getMessage()); } catch (Exception e) { System.out.println("其他异常: " + e.getMessage()); } // Java 7+ 多异常合并捕获 try { // ... } catch (IOException | SQLException e) { System.out.println("IO 或 SQL 异常: " + e.getMessage()); }

🔑 checked vs unchecked & try-with-resources

// ===== Checked Exception(编译器强制处理) ===== // IOException, SQLException, FileNotFoundException 等 // 必须 try-catch 或 throws 声明 public String readFile(String path) throws IOException { // 如果不加 throws 或 try-catch → 编译错误! return Files.readString(Path.of(path)); } // ===== Unchecked Exception(运行时异常,可选处理) ===== // NullPointerException, ArrayIndexOutOfBoundsException 等 // 不需要强制处理,但建议处理 int[] arr = {1, 2, 3}; arr[10]; // ArrayIndexOutOfBoundsException(运行时爆炸) // ===== try-with-resources(自动关闭资源,Java 7+) ===== // 类比:JS 没有对应机制,通常在 finally 中手动关闭 try (var reader = new BufferedReader(new FileReader("data.txt"))) { String line = reader.readLine(); System.out.println(line); } // ← 自动调用 reader.close(),无需 finally! // ===== 自定义异常 ===== public class UserNotFoundException extends RuntimeException { public UserNotFoundException(String userId) { super("用户不存在: " + userId); } } // 使用 throw new UserNotFoundException("12345");
⚠️ 常见误区:① Checked exception 必须处理(try-catch 或 throws),否则编译不过。② 不要用 catch (Exception e) {} 吞掉所有异常!③ 打开文件/网络连接一定用 try-with-resources。

📋 速查:异常层次结构

类型是否必须处理常见子类
ErrorError不处理OutOfMemoryError
CheckedException必须IOException, SQLException
UncheckedRuntimeException可选NullPointerException, IllegalArgumentException
✏️ 填空:异常处理
// 声明方法可能抛出 checked exception public void readData() IOException { // ... } // 自动关闭资源的语法 try ( reader = new FileReader("a.txt")) { // 使用 reader } // 自定义异常继承自 public class MyException extends { }
🧠 小测验

哪种异常必须用 try-catch 或 throws 处理?

🔄 TS vs Java 泛型对比

概念TypeScriptJava
泛型函数function f<T>(x: T): T<T> T f(T x)
泛型类class Box<T> { }class Box<T> { }(一样)
约束T extends FooT extends Foo(一样)
多约束T extends A & BT extends A & B(一样)
运行时类型保留擦除(type erasure)
通配符? extends T / ? super T

📌 泛型类与泛型方法

// ===== 泛型类(和 TS 几乎一样) ===== public class Box<T> { private T value; public Box(T value) { this.value = value; } public T getValue() { return value; } public void setValue(T value) { this.value = value; } } Box<String> strBox = new Box<>("Hello"); // 钻石运算符 <> Box<Integer> intBox = new Box<>(42); String s = strBox.getValue(); // 不需要强转! // ===== 泛型方法 ===== public static <T> T firstOf(List<T> list) { return list.isEmpty() ? null : list.get(0); } String first = firstOf(List.of("a", "b")); // 自动推断 T = String // ===== 有界泛型(Bounded Types) ===== // 类比 TS: function f<T extends Comparable>(x: T) public static <T extends Comparable<T>> T max(T a, T b) { return a.compareTo(b) > 0 ? a : b; } max(3, 5); // 5 max("abc", "xyz"); // "xyz"

🃏 通配符与类型擦除

// ===== 通配符 ?(Java 独有,TS 没有) ===== // ? extends T —— 上界通配符(只读) // "接受 T 或 T 的子类" public static double sum(List<? extends Number> list) { double total = 0; for (Number n : list) { total += n.doubleValue(); } return total; } sum(List.of(1, 2, 3)); // List<Integer> ✅ sum(List.of(1.5, 2.5)); // List<Double> ✅ // ? super T —— 下界通配符(只写) // "接受 T 或 T 的父类" public static void addNumbers(List<? super Integer> list) { list.add(1); list.add(2); } // ===== 类型擦除(Type Erasure) ===== // Java 泛型只在编译时检查,运行时被擦除! // 这意味着: List<String> strings = new ArrayList<>(); List<Integer> ints = new ArrayList<>(); // 运行时 strings.getClass() == ints.getClass() → true! // 都变成了 ArrayList(泛型信息消失了) // 因此不能: // if (obj instanceof List<String>) {} ← 编译错误 // new T() ← 编译错误 // new T[] ← 编译错误
💡 PECS 原则:Producer Extends, Consumer Super。从集合读取? extends T,向集合写入? super T。记不住就记:读用 extends,写用 super。

📋 速查:泛型语法

语法含义用途
<T>类型参数泛型类 / 泛型方法
<T extends X>上界约束T 必须是 X 的子类
<T extends A & B>多约束T 必须同时满足 A 和 B
? extends T上界通配符只读(Producer)
? super T下界通配符只写(Consumer)
<>钻石运算符类型推断简写
✏️ 填空:泛型练习
// 泛型类定义 public class Pair<, V> { private K key; private V value; } // 接受 Number 及其子类的列表(只读) public void print(List<? Number> list) { } // 泛型方法定义 public static T identity(T value) { return value; }
🧠 小测验

Java 泛型的"类型擦除"意味着什么?

🔄 JS vs Java 类对比

概念JavaScriptJava
类定义class User { }public class User { }
构造器constructor()与类同名的方法
字段声明构造器中 this.x = x类体中声明 private int x;
this动态绑定(容易丢失)始终指向当前对象
静态成员staticstatic(一样)
只读字段readonly(TS)final
数据类手动写record(Java 16+)
一个文件多个类可以一个文件只能有一个 public class

📌 类定义与构造器

// ===== 标准类定义 ===== public class User { // 字段(必须声明类型和访问修饰符) private String name; private int age; private static int userCount = 0; // 静态字段(所有实例共享) // 构造器(方法名 = 类名,无返回类型) public User(String name, int age) { this.name = name; // this 引用当前对象 this.age = age; userCount++; } // 构造器重载(类似默认参数的效果) public User(String name) { this(name, 0); // 调用另一个构造器 } // 实例方法 public String greet() { return "Hello, I'm %s, %d years old".formatted(name, age); } // 静态方法 public static int getUserCount() { return userCount; } // Getter / Setter public String getName() { return name; } public void setName(String name) { this.name = name; } } // 使用 User alice = new User("Alice", 28); alice.greet(); // "Hello, I'm Alice, 28 years old" User.getUserCount(); // 1(静态方法用类名调用)

📦 Record(Java 16+)—— 数据类神器

// ===== Record —— 不可变数据类(一行搞定) ===== // 自动生成:构造器、访问器方法、equals()、hashCode()、toString() public record UserDTO(String name, int age, String email) { } // 使用(和普通类一样 new) var user = new UserDTO("Alice", 28, "alice@example.com"); user.name(); // "Alice"(Record 生成的是 name() 这种访问器,不是 getName()) user.age(); // 28 System.out.println(user); // UserDTO[name=Alice, age=28, email=alice@example.com] // Record 是不可变的 // user.name = "Bob"; ← 编译错误!没有 setter // Record 可以有自定义方法和校验 public record Point(int x, int y) { // 紧凑构造器(compact constructor)用于校验 public Point { if (x < 0 || y < 0) throw new IllegalArgumentException("坐标不能为负"); } // 自定义方法 public double distance() { return Math.sqrt(x * x + y * y); } }
💡 Record vs Class:如果一个类只是用来"装数据"(DTO、值对象),优先用 Record。它自动实现了 equals/hashCode/toString,省掉大量样板代码。

📋 速查:类语法

语法说明示例
class普通类public class User { }
构造器与类同名public User(String name) { }
this当前对象this.name = name;
this()调用其他构造器this(name, 0);
static静态成员static int count;
final不可变字段final String id;
record不可变数据类record Point(int x, int y) { }
new创建对象new User("Alice");
✏️ 填空:类与 Record
// 定义一个 Record 数据类 public ProductDTO(String name, double price) { } // 调用其他构造器 public User(String name) { (name, 0); } // Record 的访问器没有 get 前缀 var p = new ProductDTO("手机", 999.0); String n = p.();
🧠 小测验

Java 的 Record 自动生成了哪些方法?

🔄 TS vs Java 继承对比

概念TypeScriptJava
继承extendsextends(一样)
实现接口implementsimplements(一样)
多继承不支持不支持(用接口代替)
抽象类abstract classabstract class(一样)
接口默认方法default 方法(Java 8+)
密封类sealed class(Java 17+)
调用父类super.method()super.method()(一样)

📌 extends 继承与 abstract

// ===== 抽象类(不能直接 new) ===== public abstract class Shape { private String color; public Shape(String color) { this.color = color; } // 抽象方法(子类必须实现) public abstract double area(); // 普通方法(子类可以直接用) public String getColor() { return color; } } // ===== 继承 ===== public class Circle extends Shape { private double radius; public Circle(String color, double radius) { super(color); // 必须调用父类构造器 this.radius = radius; } @Override // 注解:标记重写方法 public double area() { return Math.PI * radius * radius; } } Circle c = new Circle("red", 5); c.area(); // 78.54 c.getColor(); // "red"(继承自父类) // new Shape("blue"); ← 编译错误!抽象类不能实例化

🔌 接口与 sealed class

// ===== 接口(可以有默认实现!) ===== public interface Printable { void print(); // 抽象方法 default String format() { // 默认方法(Java 8+) return "Formatted: " + toString(); } static Printable empty() { // 静态方法 return () -> System.out.println("(empty)"); } } // 一个类可以实现多个接口 public class Report implements Printable, Serializable { @Override public void print() { System.out.println("打印报告"); } } // ===== Sealed Class(Java 17+)—— 限制谁能继承 ===== public sealed class Payment permits CreditCard, Cash, WePay { // 只有 permits 列出的类才能继承 } public final class CreditCard extends Payment { } // final: 不能再被继承 public final class Cash extends Payment { } public non-sealed class WePay extends Payment { } // non-sealed: 可以被继承
⚠️ 常见误区:① Java 只能 extends 一个类,但可以 implements 多个接口。② 子类构造器必须调用 super()。③ @Override 不是必须的,但强烈建议写上。

📋 速查:继承与接口

关键字说明用途
extends继承类单继承
implements实现接口多接口
abstract抽象抽象类/方法
@Override重写标记编译器检查
super父类引用调用父类方法/构造器
default接口默认方法Java 8+
sealed密封类限制继承(Java 17+)
final class不可继承终止继承链
✏️ 填空:继承与接口
// 子类构造器调用父类构造器 public Dog(String name) { (name); } // 重写方法的注解 public String toString() { return "..."; } // 接口的默认方法 public interface Loggable { void log() { System.out.println("日志"); } }
🧠 小测验

Java 一个类最多能 extends 几个类?

🔄 TS vs Java 访问控制对比

修饰符同类同包子类其他
public
protected
默认(不写)
private

📌 封装实践

// ===== 封装原则:字段 private,方法按需开放 ===== public class BankAccount { private String owner; // 只有本类能访问 private double balance; // 外部不能直接改 public BankAccount(String owner, double initialBalance) { this.owner = owner; this.balance = initialBalance; } // public getter —— 外部可以读 public double getBalance() { return balance; } public String getOwner() { return owner; } // public 方法 —— 通过方法控制修改逻辑 public void deposit(double amount) { if (amount <= 0) throw new IllegalArgumentException("金额必须为正"); balance += amount; } public void withdraw(double amount) { if (amount > balance) throw new IllegalStateException("余额不足"); balance -= amount; } // private 方法 —— 内部辅助方法 private void log(String action) { System.out.println(action + ": " + balance); } } // 使用 BankAccount acc = new BankAccount("Alice", 1000); acc.getBalance(); // 1000(通过 getter 访问) acc.deposit(500); // 余额 1500 // acc.balance = -999; ← 编译错误!private 不能直接访问

🏗️ 封装最佳实践

// ===== 好的封装 vs 坏的封装 ===== // ❌ 坏:字段 public,谁都能改 public class BadUser { public String name; // 任何地方都能 user.name = "" public int age; // 没有校验 } // ✅ 好:字段 private + getter/setter + 校验 public class GoodUser { private String name; private int age; public void setAge(int age) { if (age < 0 || age > 150) { throw new IllegalArgumentException("年龄不合法: " + age); } this.age = age; } public int getAge() { return age; } // 不提供 setName() → name 创建后不可修改(更安全) public String getName() { return name; } } // ===== 最佳实践总结 ===== // 1. 字段一律 private // 2. 只读字段:只提供 getter,不提供 setter // 3. setter 中加校验逻辑 // 4. 如果只是数据容器 → 用 Record(自动不可变) // 5. 工具类:构造器 private + 方法全 static
💡 记忆口诀:字段 private 不动摇,getter 按需来提供,setter 校验不能少,Record 优先更轻巧。

📋 速查:访问修饰符选择

场景推荐修饰符原因
类字段private封装核心原则
getter/setterpublic对外接口
内部辅助方法private实现细节
可被子类使用的方法protected继承扩展
同包内工具方法默认(不写)包级别复用
API 方法public对外暴露
✏️ 填空:访问修饰符
// 字段应该用什么修饰符? String name; // 外部读取字段的方法 String getName() { return name; } // 只在本类内部使用的辅助方法 void validate() { }
🧠 小测验

Java 的 default(不写修饰符)访问范围是?

🔄 TS vs Java 枚举对比

特性TypeScript enumJava enum
基本用法enum Color { Red, Green }enum Color { RED, GREEN }
带值Red = "red"通过构造器携带数据
有方法
实现接口
抽象方法✅(每个枚举值可以不同实现)

📌 枚举的完整用法

// ===== 基础枚举 ===== public enum Season { SPRING, SUMMER, AUTUMN, WINTER } Season s = Season.SPRING; System.out.println(s.name()); // "SPRING" System.out.println(s.ordinal()); // 0(序号) // ===== 带数据的枚举(Java 独有!) ===== public enum HttpStatus { OK(200, "成功"), NOT_FOUND(404, "未找到"), SERVER_ERROR(500, "服务器错误"); private final int code; private final String message; HttpStatus(int code, String message) { this.code = code; this.message = message; } public int getCode() { return code; } public String getMessage() { return message; } } HttpStatus status = HttpStatus.NOT_FOUND; status.getCode(); // 404 status.getMessage(); // "未找到" // 在 switch 中使用(非常常见) String msg = switch (status) { case OK -> "请求成功"; case NOT_FOUND -> "资源不存在"; case SERVER_ERROR -> "服务器崩了"; };

🏗️ Sealed Interface + Record 模式

// ===== Sealed Interface + Record = 代数数据类型 ===== // 类比 TS 的 discriminated union public sealed interface Shape permits Circle, Rectangle, Triangle { double area(); } public record Circle(double radius) implements Shape { public double area() { return Math.PI * radius * radius; } } public record Rectangle(double w, double h) implements Shape { public double area() { return w * h; } } public record Triangle(double base, double height) implements Shape { public double area() { return 0.5 * base * height; } } // 配合 switch 模式匹配(Java 21+) public String describe(Shape shape) { return switch (shape) { case Circle c -> "圆形,半径 " + c.radius(); case Rectangle r -> "矩形,%sx%s".formatted(r.w(), r.h()); case Triangle t -> "三角形,底 " + t.base(); }; }
💡 实战建议:用 enum 表示固定的状态集(如 HTTP 状态码、订单状态),用 sealed interface + record 表示有结构差异的类型(如不同图形、不同支付方式)。

📋 速查:枚举与 Record

特性enumrecordsealed interface
用途固定常量集不可变数据限定实现
可变不可变不可变
有方法
可继承限定子类
版本Java 5+Java 16+Java 17+
✏️ 填空:枚举练习
// 获取枚举值的名字字符串 Season s = Season.SPRING; String name = s.(); // "SPRING" // 带数据的枚举,字段用什么修饰? private int code; // 限制谁能实现接口 public interface Result permits Success, Failure { }
🧠 小测验

Java 的 enum 可以做什么?

🏗️ Builder 模式 —— 优雅构建复杂对象

// ===== Builder 模式 —— 解决构造器参数过多问题 ===== // 类比 JS:{ name: "Alice", age: 28, email: "..." }(对象字面量) // Java 没有对象字面量,Builder 是最佳替代 public class User { private final String name; private final int age; private final String email; private User(Builder builder) { this.name = builder.name; this.age = builder.age; this.email = builder.email; } public static class Builder { private String name; // 必填 private int age = 0; // 选填,有默认值 private String email; // 选填 public Builder(String name) { this.name = name; } public Builder age(int age) { this.age = age; return this; } public Builder email(String email) { this.email = email; return this; } public User build() { return new User(this); } } } // 使用:链式调用,非常清晰 User user = new User.Builder("Alice") .age(28) .email("alice@example.com") .build();

🔒 Singleton & Factory 模式

// ===== Singleton(单例)—— 在一个进程/容器中共享实例 ===== // 类比 JS:export const db = new Database()(模块单例) public class DatabaseConfig { private static final DatabaseConfig INSTANCE = new DatabaseConfig(); private DatabaseConfig() { } // private 构造器,禁止外部 new public static DatabaseConfig getInstance() { return INSTANCE; } } // 在 Spring Boot 中,@Service/@Component 默认通常是 singleton scope // 即同一个 Spring 容器里共享一个 Bean 实例,所以你很少需要手写 Singleton // ===== Factory(工厂)—— 根据条件创建不同对象 ===== public interface Notification { void send(String message); } public class EmailNotification implements Notification { /* ... */ } public class SmsNotification implements Notification { /* ... */ } public class NotificationFactory { public static Notification create(String type) { return switch (type) { case "email" -> new EmailNotification(); case "sms" -> new SmsNotification(); default -> throw new IllegalArgumentException("未知类型: " + type); }; } } Notification n = NotificationFactory.create("email"); n.send("Hello!");

🎯 Strategy 模式 —— 可替换的算法

// ===== Strategy(策略)—— 运行时切换算法 ===== // 类比 JS:传一个回调函数 arr.sort((a, b) => a - b) // 定义策略接口 public interface PricingStrategy { double calculate(double price); } // 不同策略实现 public class NormalPricing implements PricingStrategy { public double calculate(double price) { return price; } } public class VipPricing implements PricingStrategy { public double calculate(double price) { return price * 0.8; } // 8 折 } // 使用策略 public class OrderService { private PricingStrategy strategy; public OrderService(PricingStrategy strategy) { this.strategy = strategy; } public double getPrice(double original) { return strategy.calculate(original); } } // 运行时切换 var order = new OrderService(new VipPricing()); order.getPrice(100); // 80.0
💡 记忆技巧:Builder 解决"构造参数太多",Singleton 解决"全局唯一",Factory 解决"根据条件创建",Strategy 解决"算法可替换"。在 Spring Boot 中,DI 容器帮你做了大部分 Singleton 和 Factory。

📋 速查:设计模式选择

模式场景核心思想Spring Boot 对应
Builder参数多的对象链式构建Lombok @Builder
Singleton容器内共享实例私有构造器@Component 默认 singleton scope
Factory按条件创建工厂方法@Bean 工厂方法
Strategy可替换算法接口 + 多实现DI 注入不同实现
✏️ 填空:设计模式
// Builder 模式:链式调用最后调用 User user = new User.Builder("Alice").age(28).(); // Singleton:构造器的修饰符 DatabaseConfig() { } // Strategy:通过什么传入不同策略? public OrderService( strategy) { }
🧠 小测验

在 Spring Boot 中,@Service 注解的类默认是什么模式?

🔄 npm vs Maven 对比

概念npm / pnpmMaven
配置文件package.jsonpom.xml
依赖仓库npmjs.comMaven Central
安装依赖npm installmvn dependency:resolve
运行脚本npm run devmvn spring-boot:run
打包npm run buildmvn package
测试npm testmvn test
锁文件package-lock.json版本写在 pom.xml 中
devDependencies"devDependencies"<scope>test</scope>

📦 pom.xml 核心结构

<!-- pom.xml —— Maven 项目配置文件 --> <project> <!-- 项目坐标(类比 package.json 的 name + version) --> <groupId>com.example</groupId> <!-- 组织 --> <artifactId>my-app</artifactId> <!-- 项目名 --> <version>1.0.0</version> <!-- 版本 --> <!-- 依赖(类比 dependencies) --> <dependencies> <!-- 运行时依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.2.0</version> </dependency> <!-- 测试依赖(类比 devDependencies) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>

🔄 Maven 生命周期

# Maven 构建生命周期(按顺序执行) mvn validate # 检查项目是否正确 mvn compile # 编译源代码(≈ tsc) mvn test # 运行单元测试(≈ npm test) mvn package # 打包为 JAR/WAR(≈ npm run build) mvn install # 安装到本地仓库 mvn deploy # 发布到远程仓库(≈ npm publish) # 常用命令 mvn clean # 清除编译产物(删除 target 目录) mvn clean package # 先清除再打包(最常用) mvn spring-boot:run # 启动 Spring Boot 应用 mvn dependency:tree # 查看依赖树(排查冲突用) # 跳过测试(开发时偶尔用) mvn package -DskipTests
⚠️ 常见误区:① Maven 的依赖会自动传递(A 依赖 B,B 依赖 C,A 也能用 C)。② <scope>test</scope> 的依赖不会打进最终 JAR。③ 版本冲突用 mvn dependency:tree 排查。

📋 速查:Maven 常用命令

命令作用npm 对应
mvn compile编译tsc
mvn test测试npm test
mvn package打包npm run build
mvn clean清除rm -rf dist
mvn spring-boot:run启动npm run dev
mvn dependency:tree查看依赖树npm ls
✏️ 填空:Maven 命令
# 编译并打包项目 mvn clean # 只在测试时使用的依赖,scope 设为 <scope></scope> # 查看项目的依赖树 mvn
🧠 小测验

Maven 的 <scope>test</scope> 相当于 npm 的什么?

🔄 前端 vs Java 项目结构对比

前端项目Java 项目说明
src/src/main/java/源代码
__tests__/src/test/java/测试代码
public/src/main/resources/配置文件/静态资源
package.jsonpom.xml项目配置
node_modules/~/.m2/repository/依赖存放
dist/target/编译产物

📂 标准 Spring Boot 项目结构

my-project/ ├── pom.xml # 项目配置(= package.json) ├── src/ │ ├── main/ │ │ ├── java/com/example/myapp/ # Java 源代码 │ │ │ ├── MyAppApplication.java # 启动入口(= index.js) │ │ │ ├── controller/ # 控制器层(处理 HTTP 请求) │ │ │ │ └── UserController.java │ │ │ ├── service/ # 业务逻辑层 │ │ │ │ └── UserService.java │ │ │ ├── repository/ # 数据访问层(操作数据库) │ │ │ │ └── UserRepository.java │ │ │ ├── model/ # 数据模型(实体类) │ │ │ │ └── User.java │ │ │ ├── dto/ # 数据传输对象 │ │ │ │ └── UserDTO.java │ │ │ └── config/ # 配置类 │ │ │ └── SecurityConfig.java │ │ └── resources/ │ │ ├── application.properties # 配置文件(端口、数据库等) │ │ └── static/ # 静态资源 │ └── test/java/com/example/myapp/ # 测试代码(镜像 main 结构) │ └── service/ │ └── UserServiceTest.java └── target/ # 编译产物(= dist)

🏗️ 三层架构

// ===== Controller → Service → Repository 三层架构 ===== // 类比前端:Route Handler → Business Logic → API Call // Controller 层:接收请求,返回响应(不写业务逻辑!) @RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; // 通过构造器注入 Service public UserController(UserService userService) { this.userService = userService; } @GetMapping("/{id}") public UserDTO getUser(@PathVariable Long id) { return userService.findById(id); // 委托给 Service } } // Service 层:业务逻辑(核心!) @Service public class UserService { private final UserRepository userRepo; public UserService(UserRepository userRepo) { this.userRepo = userRepo; } public UserDTO findById(Long id) { User user = userRepo.findById(id) .orElseThrow(() -> new UserNotFoundException(id)); return new UserDTO(user.getName(), user.getEmail()); } } // Repository 层:数据访问(操作数据库) @Repository public interface UserRepository extends JpaRepository<User, Long> { // Spring Data JPA 自动实现基础 CRUD }
💡 核心原则:Controller 不写业务逻辑(只做请求/响应转换),Service 不直接操作数据库(通过 Repository),每层只依赖下一层。

📋 速查:分层职责

注解职责前端对应
Controller@RestControllerHTTP 入口Route Handler
Service@Service业务逻辑Utils / Hooks
Repository@Repository数据访问API Client
Model@Entity数据结构TypeScript Type
DTOrecord传输对象API Response Type
✏️ 填空:项目结构
// Java 源代码放在哪个目录? src//java/ // 配置文件放在哪个目录? src/main// // Service 层用什么注解? @ public class UserService { }
🧠 小测验

三层架构中,业务逻辑应该写在哪一层?

🔄 JS vs Java 函数式操作对比

操作JavaScriptJava Stream
遍历arr.forEach(x => ...)list.forEach(x -> ...)
映射arr.map(x => x * 2)list.stream().map(x -> x * 2)
过滤arr.filter(x => x > 0)list.stream().filter(x -> x > 0)
归约arr.reduce((a,b) => a+b)stream.reduce(0, Integer::sum)
查找arr.find(x => x > 3)stream.filter(x -> x > 3).findFirst()
排序arr.sort((a,b) => a-b)stream.sorted()
去重[...new Set(arr)]stream.distinct()
收集直接返回数组.collect(Collectors.toList())

📌 Lambda 表达式

// ===== Lambda(类似 JS 箭头函数) ===== // JS: (a, b) => a + b // Java: (a, b) -> a + b(箭头不一样!) // 排序(Comparator) List<String> names = new ArrayList<>(List.of("Charlie", "Alice", "Bob")); names.sort((a, b) -> a.compareTo(b)); // 字母排序 names.sort(String::compareTo); // 方法引用(更简洁) // forEach names.forEach(name -> System.out.println(name)); names.forEach(System.out::println); // 方法引用 // ===== 方法引用(::) ===== // 类名::静态方法 Integer::parseInt // 对象::实例方法 System.out::println // 类名::实例方法 String::toUpperCase // 类名::new ArrayList::new

🔄 Stream API 实战

// ===== Stream 三步走:创建 → 中间操作 → 终结操作 ===== List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 过滤偶数,乘以 2,收集为列表 List<Integer> result = numbers.stream() .filter(n -> n % 2 == 0) // 过滤:[2,4,6,8,10] .map(n -> n * 2) // 映射:[4,8,12,16,20] .collect(Collectors.toList()); // 收集 // 求和 int sum = numbers.stream() .filter(n -> n > 5) .reduce(0, Integer::sum); // 30 // 查找第一个匹配(返回 Optional) Optional<Integer> first = numbers.stream() .filter(n -> n > 7) .findFirst(); // Optional[8] // 转换为 Map List<String> words = List.of("hello", "world", "hi"); Map<Integer, List<String>> byLength = words.stream() .collect(Collectors.groupingBy(String::length)); // {2=[hi], 5=[hello, world]} // 字符串拼接 String joined = names.stream() .collect(Collectors.joining(", ")); // "Alice, Bob, Charlie" // 统计 long count = numbers.stream().filter(n -> n > 5).count(); boolean allPositive = numbers.stream().allMatch(n -> n > 0); boolean anyNegative = numbers.stream().anyMatch(n -> n < 0);
💡 关键区别:JS 的 map/filter 直接返回新数组,Java 的 Stream 是惰性求值,必须有终结操作(collect/reduce/forEach)才会执行。Stream 用完就废弃,不能重复使用。

📋 速查:Stream 常用操作

类型方法说明
中间filter()过滤
中间map()映射转换
中间sorted()排序
中间distinct()去重
中间limit(n)取前 n 个
终结collect()收集为集合
终结reduce()归约
终结forEach()遍历
终结count()计数
终结findFirst()找第一个
终结allMatch()全部匹配
✏️ 填空:Stream 操作
// 过滤 + 映射 + 收集 List<String> result = names.stream() .(n -> n.length() > 3) .map(String::toUpperCase) .(Collectors.toList()); // 方法引用:打印每个元素 names.forEach();
🧠 小测验

Java Stream 的 map() 是中间操作还是终结操作?

🔄 Jest vs JUnit 5 对比

概念Jest (JS)JUnit 5 (Java)
测试文件*.test.ts*Test.java
测试方法test("...", () => {})@Test void testXxx() {}
断言expect(x).toBe(y)assertEquals(y, x)(注意顺序!)
前置beforeEach()@BeforeEach
后置afterEach()@AfterEach
Mockjest.mock()Mockito.mock()
分组describe("...")@Nested class XxxTest
运行npm testmvn test

📌 JUnit 5 基础

import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; class CalculatorTest { private Calculator calc; @BeforeEach // 每个测试前执行(= beforeEach) void setUp() { calc = new Calculator(); } @Test @DisplayName("加法测试") void shouldAddTwoNumbers() { // Arrange(准备) int a = 2, b = 3; // Act(执行) int result = calc.add(a, b); // Assert(断言)—— 注意:期望值在前! assertEquals(5, result); // 期望值, 实际值 assertNotNull(result); assertTrue(result > 0); } @Test @DisplayName("除零应抛出异常") void shouldThrowOnDivideByZero() { assertThrows(ArithmeticException.class, () -> { calc.divide(10, 0); }); } @Nested @DisplayName("字符串操作测试") // 类似 describe() class StringTests { @Test void shouldUpperCase() { assertEquals("HELLO", "hello".toUpperCase()); } } }

🧪 Mockito 模拟

import static org.mockito.Mockito.*; // ===== Mockito —— Mock 外部依赖 ===== @ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock // 创建 Mock 对象(= jest.mock()) private UserRepository userRepo; @InjectMocks // 自动注入 Mock private UserService userService; @Test void shouldFindUser() { // 设定 Mock 行为(= jest.fn().mockReturnValue(...)) var user = new User("Alice", "alice@test.com"); when(userRepo.findById(1L)).thenReturn(Optional.of(user)); // 执行 UserDTO result = userService.findById(1L); // 断言 assertEquals("Alice", result.name()); // 验证方法被调用(= expect(fn).toHaveBeenCalled()) verify(userRepo).findById(1L); verify(userRepo, times(1)).findById(1L); } }
⚠️ 常见误区:assertEquals(期望值, 实际值)——期望值在前!和 Jest 相反。② 测试类名以 Test 结尾,Maven 才会自动发现。③ @Mock@InjectMocks 需要 @ExtendWith(MockitoExtension.class)

📋 速查:JUnit 5 常用注解

注解作用Jest 对应
@Test标记测试方法test()
@DisplayName显示名称test 描述字符串
@BeforeEach每个测试前beforeEach()
@AfterEach每个测试后afterEach()
@BeforeAll所有测试前beforeAll()
@Nested分组describe()
@Disabled跳过test.skip()
@ParameterizedTest参数化测试test.each()
✏️ 填空:JUnit 测试
// 标记测试方法 @ void shouldWork() { } // 断言相等(注意参数顺序) (期望值, 实际值); // Mock 对象返回指定值 (repo.findById(1L)).thenReturn(user);
🧠 小测验

JUnit 5 中 assertEquals(a, b) 的参数顺序是?

🔄 Express/Nest vs Spring Boot 对比

概念Express / NestJSSpring Boot
初始化npm init / nest newSpring Initializr(start.spring.io)
路由定义app.get("/path")@GetMapping("/path")
依赖注入NestJS @Injectable@Service + 构造器注入
中间件app.use()@Filter / @Interceptor
配置.envapplication.properties
启动npm run devmvn spring-boot:run
热重载nodemonspring-boot-devtools

📌 IoC 与 DI 核心概念

// ===== IoC(控制反转)—— 最核心的概念 ===== // 传统方式:自己 new 依赖(像 JS 里 import 然后 new) class UserService { private UserRepository repo = new UserRepository(); // ❌ 手动创建 } // IoC 方式:Spring 帮你创建和管理对象(Bean) @Service // 告诉 Spring:"帮我管理这个类的实例" class UserService { private final UserRepository repo; // ✅ 构造器注入(推荐!Spring 自动传入 UserRepository 的实例) public UserService(UserRepository repo) { this.repo = repo; } } // ===== 常用注解 ===== @Component // 通用组件(Spring 管理的 Bean) @Service // 业务层(语义化的 @Component) @Repository // 数据层(语义化的 @Component) @RestController // 控制器层(= @Controller + @ResponseBody) @Configuration // 配置类 // ===== application.properties 配置 ===== // 类比 .env 文件 // server.port=8080 // spring.datasource.url=jdbc:mysql://localhost:3306/mydb // spring.datasource.username=root

🚀 创建第一个 Spring Boot 项目

// ===== 启动类 ===== @SpringBootApplication // 核心注解,标记入口 public class MyAppApplication { public static void main(String[] args) { SpringApplication.run(MyAppApplication.class, args); } } // ===== 最简单的 Controller ===== @RestController public class HelloController { @GetMapping("/hello") public String hello() { return "Hello, Spring Boot!"; } @GetMapping("/hello/{name}") public String helloName(@PathVariable String name) { return "Hello, " + name + "!"; } } // 启动后访问:http://localhost:8080/hello // 返回:Hello, Spring Boot!
💡 记忆技巧:IoC = "你不用 new,Spring 帮你 new"。DI = "Spring 把你需要的东西塞进来"。用 NestJS 的同学会觉得非常熟悉——NestJS 就是照着 Spring 抄的。

📋 速查:Spring Boot 核心注解

注解作用NestJS 对应
@SpringBootApplication启动入口
@RestControllerREST 控制器@Controller()
@Service业务层@Injectable()
@Repository数据层@Injectable()
@Component通用组件@Injectable()
@Configuration配置类@Module()
@Bean手动注册 BeanuseFactory
✏️ 填空:Spring Boot 基础
// 标记启动类 @ public class MyApp { } // 推荐的依赖注入方式 public UserService( repo) { this.repo = repo; } // 配置文件名是 application.
🧠 小测验

Spring 推荐用什么方式进行依赖注入?

🔄 Express vs Spring Controller 对比

概念ExpressSpring Boot
GET 路由app.get("/users")@GetMapping("/users")
POST 路由app.post("/users")@PostMapping("/users")
路径参数req.params.id@PathVariable Long id
查询参数req.query.page@RequestParam int page
请求体req.body@RequestBody UserDTO dto
响应 JSONres.json(data)直接 return 对象(自动转 JSON)
路由前缀app.use("/api")@RequestMapping("/api")

📌 完整 CRUD Controller

@RestController @RequestMapping("/api/users") // 路由前缀 public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } // GET /api/users —— 获取列表 @GetMapping public List<UserDTO> listUsers( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { return userService.findAll(page, size); } // GET /api/users/123 —— 获取单个 @GetMapping("/{id}") public UserDTO getUser(@PathVariable Long id) { return userService.findById(id); } // POST /api/users —— 创建 @PostMapping @ResponseStatus(HttpStatus.CREATED) // 返回 201 public UserDTO createUser(@RequestBody CreateUserRequest request) { return userService.create(request); } // PUT /api/users/123 —— 更新 @PutMapping("/{id}") public UserDTO updateUser(@PathVariable Long id, @RequestBody UpdateUserRequest request) { return userService.update(id, request); } // DELETE /api/users/123 —— 删除 @DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) // 返回 204 public void deleteUser(@PathVariable Long id) { userService.delete(id); } }

🔧 请求参数绑定详解

// ===== @PathVariable —— 路径参数(/users/{id}) ===== @GetMapping("/users/{id}") public User getUser(@PathVariable Long id) { } // GET /users/123 → id = 123 // ===== @RequestParam —— 查询参数(?key=value) ===== @GetMapping("/search") public List<User> search( @RequestParam String keyword, // 必填 @RequestParam(required = false) String sort, // 可选 @RequestParam(defaultValue = "1") int page) { } // 默认值 // GET /search?keyword=java&page=2 // ===== @RequestBody —— 请求体(JSON → 对象) ===== public record CreateUserRequest(String name, String email, int age) { } @PostMapping("/users") public User create(@RequestBody CreateUserRequest req) { // req.name(), req.email(), req.age() 自动从 JSON 映射 return userService.create(req); } // POST /users Body: {"name":"Alice","email":"a@b.com","age":28} // ===== @RequestHeader —— 请求头 ===== @GetMapping("/profile") public User profile(@RequestHeader("Authorization") String token) { }
💡 关键理解:Spring 自动将返回的 Java 对象序列化为 JSON(通过 Jackson 库)。你不需要像 Express 那样手动 res.json(),直接 return 对象就行。

📋 速查:请求映射注解

注解HTTP 方法用途
@GetMappingGET查询
@PostMappingPOST创建
@PutMappingPUT全量更新
@PatchMappingPATCH部分更新
@DeleteMappingDELETE删除
@PathVariable路径参数
@RequestParam查询参数
@RequestBody请求体
✏️ 填空:Controller 注解
// 获取路径中的 id 参数 @GetMapping("/users/{id}") public User get(@ Long id) { } // 获取 JSON 请求体 @PostMapping("/users") public User create(@ UserDTO dto) { } // 设置路由前缀 @("/api/users") public class UserController { }
🧠 小测验

Spring Controller 返回对象时,自动序列化为 JSON 的库是?

🔄 注入方式对比

方式语法推荐度说明
构造器注入构造器参数⭐⭐⭐ 强烈推荐不可变、易测试、强制依赖
字段注入@Autowired 字段⭐ 不推荐隐藏依赖、难测试
Setter 注入@Autowired setter⭐⭐ 偶尔用可选依赖时使用

📌 Service 层标准写法

// ===== Service 层:业务逻辑的核心 ===== @Service // 告诉 Spring 管理这个 Bean public class UserService { private final UserRepository userRepo; private final EmailService emailService; // ✅ 构造器注入(Spring 自动注入所有参数) // 只有一个构造器时,@Autowired 可以省略 public UserService(UserRepository userRepo, EmailService emailService) { this.userRepo = userRepo; this.emailService = emailService; } public UserDTO findById(Long id) { User user = userRepo.findById(id) .orElseThrow(() -> new UserNotFoundException(id)); return toDTO(user); } public UserDTO create(CreateUserRequest request) { // 1. 校验 if (userRepo.existsByEmail(request.email())) { throw new DuplicateEmailException(request.email()); } // 2. 创建实体 var user = new User(request.name(), request.email()); // 3. 保存 User saved = userRepo.save(user); // 4. 发通知 emailService.sendWelcome(saved.getEmail()); // 5. 返回 DTO return toDTO(saved); } private UserDTO toDTO(User user) { return new UserDTO(user.getName(), user.getEmail()); } } // ❌ 不推荐:字段注入 @Service public class BadService { @Autowired // 隐藏依赖,单测时无法轻松注入 Mock private UserRepository repo; }

🔄 Bean 生命周期与作用域

// ===== Bean 生命周期 ===== @Service public class CacheService { @PostConstruct // Bean 创建后执行(类似 componentDidMount) public void init() { System.out.println("缓存服务初始化..."); } @PreDestroy // Bean 销毁前执行(类似 componentWillUnmount) public void cleanup() { System.out.println("清理缓存..."); } } // ===== @Configuration + @Bean —— 手动注册第三方库的 Bean ===== @Configuration public class AppConfig { @Bean // 方法返回值注册为 Bean public RestTemplate restTemplate() { return new RestTemplate(); } @Bean public ObjectMapper objectMapper() { return new ObjectMapper() .registerModule(new JavaTimeModule()); } }
⚠️ 常见误区:① 构造器注入的字段推荐设为 final,但不是语法强制。② Spring Bean 默认是 singleton scope(同一容器共享一个实例),不是 JVM 全局单例。③ 不要在 Service 中直接操作 HttpServletRequest/Response,那是 Controller 的事。

📋 速查:DI 相关注解

注解作用位置
@Service标记业务层 Bean
@Autowired自动注入构造器/字段/setter
@Bean手动注册 Bean@Configuration 类中的方法
@PostConstruct初始化回调方法
@PreDestroy销毁回调方法
@Qualifier指定注入哪个 Bean参数
@Primary默认 Bean类/@Bean 方法
✏️ 填空:Service 与 DI
// 构造器注入,字段用什么修饰? private UserRepository repo; // Bean 创建后自动执行的方法 @ public void init() { } // 手动注册第三方库的 Bean @ public RestTemplate restTemplate() { return new RestTemplate(); }
🧠 小测验

Spring Bean 的默认作用域是?

🔄 前端校验 vs Spring 校验对比

概念前端 (Zod/Yup)Spring Validation
必填z.string().min(1)@NotBlank
长度z.string().max(50)@Size(max=50)
邮箱z.string().email()@Email
范围z.number().min(0)@Min(0)
正则z.string().regex(/^/)@Pattern(regexp="^")
触发校验手动调用 parse()方法参数加 @Valid

📌 校验注解实战

// ===== 在 DTO 上添加校验注解 ===== public record CreateUserRequest( @NotBlank(message = "用户名不能为空") @Size(min = 2, max = 20, message = "用户名长度 2-20") String name, @NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不正确") String email, @Min(value = 0, message = "年龄不能为负数") @Max(value = 150, message = "年龄不能超过 150") int age, @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") String phone ) { } // ===== 在 Controller 中触发校验 ===== @PostMapping("/users") public UserDTO create(@Valid @RequestBody CreateUserRequest request) { // 如果校验失败,Spring 自动返回 400 + 错误信息 // 如果校验通过,才会执行到这里 return userService.create(request); }

🔧 自定义错误响应

// ===== 统一处理校验错误 ===== @RestControllerAdvice public class ValidationExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Map<String, Object> handleValidation( MethodArgumentNotValidException ex) { Map<String, String> errors = new HashMap<>(); ex.getBindingResult().getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage()) ); return Map.of( "code", 400, "message", "参数校验失败", "errors", errors ); } } // 返回示例: // { // "code": 400, // "message": "参数校验失败", // "errors": { "name": "用户名不能为空", "email": "邮箱格式不正确" } // }
💡 最佳实践:校验注解写在 DTO/Request 对象上,不要写在 Entity 上。Controller 只负责触发校验(@Valid),具体的校验规则由 DTO 定义。

📋 速查:常用校验注解

注解作用适用类型
@NotNull不能为 null任意
@NotBlank不能为空字符串String
@NotEmpty集合不能为空Collection/String
@Size(min,max)长度范围String/Collection
@Min/@Max数值范围数字
@Email邮箱格式String
@Pattern正则匹配String
@Valid触发校验方法参数
✏️ 填空:数据校验
// 字符串不能为空 @(message = "不能为空") String name; // 在 Controller 中触发校验 public User create(@ @RequestBody UserDTO dto) { } // 邮箱格式校验 @(message = "邮箱格式不正确") String email;
🧠 小测验

@NotBlank@NotNull 有什么区别?

🔄 Express vs Spring 错误处理对比

概念ExpressSpring Boot
全局错误处理app.use((err, req, res, next))@ControllerAdvice
特定错误处理if (err instanceof XxxError)@ExceptionHandler(XxxException.class)
自定义错误class AppError extends Errorclass AppException extends RuntimeException
错误响应res.status(404).json({})@ResponseStatus(404) + return 对象

📌 统一异常处理

// ===== 1. 定义统一错误响应 DTO ===== public record ErrorResponse( int code, String message, String path, LocalDateTime timestamp ) { public static ErrorResponse of(int code, String message, String path) { return new ErrorResponse(code, message, path, LocalDateTime.now()); } } // ===== 2. 自定义业务异常 ===== public class BusinessException extends RuntimeException { private final int code; public BusinessException(int code, String message) { super(message); this.code = code; } public int getCode() { return code; } } public class UserNotFoundException extends BusinessException { public UserNotFoundException(Long id) { super(404, "用户不存在: " + id); } } public class DuplicateEmailException extends BusinessException { public DuplicateEmailException(String email) { super(409, "邮箱已注册: " + email); } }

🛡️ @ControllerAdvice 全局处理

// ===== 3. 全局异常处理器 ===== @RestControllerAdvice public class GlobalExceptionHandler { // 处理自定义业务异常 @ExceptionHandler(BusinessException.class) public ResponseEntity<ErrorResponse> handleBusiness( BusinessException ex, HttpServletRequest request) { var error = ErrorResponse.of( ex.getCode(), ex.getMessage(), request.getRequestURI()); return ResponseEntity.status(ex.getCode()).body(error); } // 处理参数校验异常 @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleValidation( MethodArgumentNotValidException ex, HttpServletRequest request) { String msg = ex.getBindingResult().getFieldErrors().stream() .map(e -> e.getField() + ": " + e.getDefaultMessage()) .collect(Collectors.joining("; ")); return ErrorResponse.of(400, msg, request.getRequestURI()); } // 兜底:处理所有未捕获的异常 @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse handleAll(Exception ex, HttpServletRequest req) { // 生产环境不要暴露真实错误信息! return ErrorResponse.of(500, "服务器内部错误", req.getRequestURI()); } } // 在 Service 中直接抛异常,ControllerAdvice 自动捕获 // throw new UserNotFoundException(123L); // → {"code":404,"message":"用户不存在: 123","path":"/api/users/123"}
⚠️ 常见误区:① 生产环境的 500 错误不要暴露堆栈信息。② @ExceptionHandler 的匹配顺序是从具体到通用。③ 异常类继承 RuntimeException 不需要 throws 声明。

📋 速查:异常处理注解

注解作用位置
@RestControllerAdvice全局异常处理器
@ExceptionHandler处理特定异常方法
@ResponseStatus设置 HTTP 状态码方法/类
ResponseEntity自定义响应返回值
✏️ 填空:异常处理
// 全局异常处理器注解 @ public class GlobalHandler { } // 处理特定类型的异常 @(UserNotFoundException.class) public ErrorResponse handle(UserNotFoundException ex) { } // 自定义异常继承 public class MyException extends { }
🧠 小测验

@RestControllerAdvice 的作用范围是?

🔄 Prisma vs Spring Data JPA 对比

概念Prisma (TS)Spring Data JPA
模型定义schema.prisma@Entity Java 类
CRUDprisma.user.findMany()userRepo.findAll()
条件查询prisma.user.findFirst({where})userRepo.findByEmail(email)
关联schema 中定义@ManyToOne / @OneToMany
迁移prisma migrateddl-auto / Flyway

📌 Entity 实体定义

// ===== Entity —— 对应数据库表 ===== @Entity @Table(name = "users") // 表名 public class User { @Id // 主键 @GeneratedValue(strategy = GenerationType.IDENTITY) // 自增 private Long id; @Column(nullable = false, length = 50) private String name; @Column(nullable = false, unique = true) private String email; @Column(name = "created_at") private LocalDateTime createdAt; // JPA 要求无参构造器 protected User() { } public User(String name, String email) { this.name = name; this.email = email; this.createdAt = LocalDateTime.now(); } // Getter / Setter 省略... public Long getId() { return id; } public String getName() { return name; } public String getEmail() { return email; } }

🔧 Repository 接口与查询

// ===== Repository —— 只需定义接口,JPA 自动实现! ===== @Repository public interface UserRepository extends JpaRepository<User, Long> { // JpaRepository 自带方法: // findAll(), findById(id), save(entity), deleteById(id), count() // 方法名查询(JPA 根据方法名自动生成 SQL) Optional<User> findByEmail(String email); List<User> findByNameContaining(String keyword); boolean existsByEmail(String email); List<User> findByCreatedAtAfter(LocalDateTime time); // 自定义 JPQL 查询 @Query("SELECT u FROM User u WHERE u.email LIKE %:domain%") List<User> findByEmailDomain(@Param("domain") String domain); } // ===== application.properties 数据库配置 ===== // spring.datasource.url=jdbc:mysql://localhost:3306/mydb // spring.datasource.username=root // spring.datasource.password=123456 // spring.jpa.hibernate.ddl-auto=update // 仅建议本地学习/原型环境使用 // spring.jpa.show-sql=true // ===== 在 Service 中使用 ===== @Service public class UserService { private final UserRepository repo; public UserService(UserRepository repo) { this.repo = repo; } public List<User> search(String keyword) { return repo.findByNameContaining(keyword); } public User create(String name, String email) { if (repo.existsByEmail(email)) { throw new DuplicateEmailException(email); } return repo.save(new User(name, email)); } }
💡 方法名查询规则:findBy + 字段名 + 条件关键词。例如 findByNameContainingWHERE name LIKE %?%findByCreatedAtAfterWHERE created_at > ?。JPA 会根据方法名生成对应查询。

📋 速查:JPA 常用注解

注解作用位置
@Entity标记实体类
@Table指定表名
@Id主键字段
@GeneratedValue自增策略字段
@Column列属性字段
@ManyToOne多对一关联字段
@OneToMany一对多关联字段
@Query自定义查询Repository 方法
✏️ 填空:JPA 操作
// 标记实体类 @ public class User { } // Repository 继承的接口 public interface UserRepo extends <User, Long> { } // 按邮箱查找(方法名查询) Optional<User> (String email);
🧠 小测验

JPA 的 findByNameContaining("test") 生成的 SQL 条件是?

📐 RESTful 设计规范

HTTP 方法路径操作状态码
GET/api/users获取用户列表200
GET/api/users/{id}获取单个用户200 / 404
POST/api/users创建用户201
PUT/api/users/{id}全量更新200
PATCH/api/users/{id}部分更新200
DELETE/api/users/{id}删除204

📦 统一响应格式

// ===== 统一响应包装 ===== // 前端最讨厌的事:每个接口返回格式不一样! public record ApiResponse<T>( int code, String message, T data ) { public static <T> ApiResponse<T> ok(T data) { return new ApiResponse<>(200, "success", data); } public static <T> ApiResponse<T> created(T data) { return new ApiResponse<>(201, "created", data); } public static <T> ApiResponse<T> error(int code, String message) { return new ApiResponse<>(code, message, null); } } // Controller 中使用 @GetMapping("/{id}") public ApiResponse<UserDTO> getUser(@PathVariable Long id) { return ApiResponse.ok(userService.findById(id)); } // 返回:{"code":200,"message":"success","data":{"name":"Alice","email":"a@b.com"}}

📄 分页与 DTO 模式

// ===== 分页响应 ===== public record PageResponse<T>( List<T> items, int page, int size, long total, int totalPages ) { public static <T> PageResponse<T> from(Page<T> page) { return new PageResponse<>( page.getContent(), page.getNumber() + 1, page.getSize(), page.getTotalElements(), page.getTotalPages() ); } } // Controller 分页查询 @GetMapping public ApiResponse<PageResponse<UserDTO>> listUsers( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { Page<UserDTO> result = userService.findAll(page - 1, size); return ApiResponse.ok(PageResponse.from(result)); } // ===== DTO 模式:永远不要直接返回 Entity! ===== // Entity 暴露数据库结构,DTO 只暴露需要的字段 public record UserDTO(String name, String email) { } // ✅ // 不要 return userEntity; ← 暴露了 password 等敏感字段 ❌ // ===== 命名规范 ===== // 路径:复数名词,kebab-case /api/user-profiles // 字段:camelCase {"userName": "Alice"} // 参数:camelCase ?pageSize=10&sortBy=name
💡 前端视角:统一的响应格式让前端可以写一个通用的 API 请求拦截器。分页格式固定后,前端的 Table 组件就能通用化。DTO 保护了后端数据结构不泄露。

📋 速查:接口设计清单

项目规范示例
路径复数名词/api/users 不是 /api/user
响应格式统一包装{"code","message","data"}
分页统一格式{"items","page","total"}
错误统一格式{"code","message","path"}
DTO不暴露 Entity用 Record 定义
版本URL 或 Header/api/v1/users
✏️ 填空:接口设计
// 统一成功响应 public static <T> ApiResponse<T> ok(T data) { return new ApiResponse<>(, "success", data); } // 创建成功应返回的 HTTP 状态码 @ResponseStatus(HttpStatus.) // RESTful 路径用什么形式? /api/ // 复数名词
🧠 小测验

为什么不应该直接返回 Entity 对象给前端?

🔐 认证 vs 授权

概念英文说明类比
认证Authentication你是谁?出示身份证
授权Authorization你能做什么?检查门禁权限

🎫 JWT 工作原理

// ===== JWT(JSON Web Token)认证流程 ===== // 你在前端已经用过 JWT,现在学后端怎么生成和验证 // 1. 用户登录 → 后端验证密码 → 返回 JWT // POST /api/auth/login // Body: {"email":"alice@test.com","password":"123456"} // 返回: {"token":"eyJhbGciOiJIUzI1NiJ9.xxxxx.yyyyy"} // 2. 前端存储 token,后续请求携带 // GET /api/users/me // Header: Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxxxx.yyyyy // 3. 后端验证 token → 提取用户信息 → 处理请求 // ===== JWT 结构(三段式) ===== // Header.Payload.Signature // eyJhbGciOiJIUzI1NiJ9 ← Header(算法) // .eyJzdWIiOiJhbGljZSJ9 ← Payload(数据,Base64 编码) // .SflKxwRJSMeKKF2QT4f ← Signature(签名,防篡改) // ===== Spring Security 概览 ===== // Spring Security 是 Java 最主流的安全框架 // 核心概念:Filter Chain(过滤器链) // 请求 → 认证过滤器 → 授权过滤器 → Controller @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) // 仅在无状态 API(如 Bearer Token、不依赖 Cookie)中才常见 .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() // 登录无需认证 .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() // 其他需要认证 ); return http.build(); } }

🛡️ 常见安全漏洞

// ===== SQL 注入防护 ===== // ❌ 拼接 SQL(严禁!) String sql = "SELECT * FROM users WHERE name = '" + name + "'"; // 攻击者输入:' OR '1'='1 → 查出所有用户 // ✅ 参数化查询(使用参数绑定时通常安全) @Query("SELECT u FROM User u WHERE u.name = :name") List<User> findByName(@Param("name") String name); // ===== XSS 防护 ===== // 返回 JSON ≠ 自动完成 XSS 防护 // 真正的风险点通常在前端是否把数据插入 innerHTML 等不安全上下文 // 如果服务端要拼接 HTML,再考虑转义或模板引擎的安全输出 // ===== 密码安全 ===== // 永远不要明文存储密码!使用 BCrypt 哈希 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // 加密:encoder.encode("123456") → "$2a$10$..." // 验证:encoder.matches("123456", hashedPassword) → true/false // ===== 敏感数据 ===== // 数据库密码、JWT 密钥等放在环境变量或配置中心 // application.properties: // jwt.secret=${JWT_SECRET} ← 从环境变量读取 // 不要硬编码在代码里!
⚠️ 安全红线:① 永远不要明文存储密码(用 BCrypt)。② 永远不要拼接 SQL(用参数化查询)。③ JWT 密钥不要硬编码(用环境变量)。④ 不要在错误信息中暴露堆栈。

📋 速查:安全检查清单

漏洞防护措施Java 工具
SQL 注入参数化查询JPA / PreparedStatement
XSS输出转义HtmlUtils.htmlEscape()
CSRFToken 验证Spring Security
密码泄露哈希存储BCryptPasswordEncoder
越权访问权限检查@PreAuthorize
密钥泄露环境变量application.properties
✏️ 填空:安全基础
// 密码加密用什么算法? new (); // JWT 放在请求头的哪个字段? Authorization: eyJhbGci... // 允许所有人访问的路径配置 .requestMatchers("/api/auth/**").()
🧠 小测验

为什么不能明文存储用户密码?

🗺️ 项目演进路线

版本功能涉及知识预计时间
V1User CRUDController + Service + JPA2-3 天
V2+ 校验 + 异常处理@Valid + @ControllerAdvice + DTO2-3 天
V3+ 认证 + 分页 + 测试JWT + PageResponse + JUnit3-5 天

📋 V1:基础 CRUD

// ===== V1 任务清单 ===== // □ 1. Spring Initializr 创建项目(Web + JPA + MySQL) // □ 2. 配置 application.properties(数据库连接) // □ 3. 创建 User Entity // □ 4. 创建 UserRepository(extends JpaRepository) // □ 5. 创建 UserService(基础 CRUD 方法) // □ 6. 创建 UserController(GET/POST/PUT/DELETE) // □ 7. 用 Postman/curl 测试所有接口 // ===== V1 核心代码参考 ===== @RestController @RequestMapping("/api/users") public class UserController { private final UserService service; public UserController(UserService service) { this.service = service; } @GetMapping public List<User> list() { return service.findAll(); } @GetMapping("/{id}") public User get(@PathVariable Long id) { return service.findById(id); } @PostMapping @ResponseStatus(HttpStatus.CREATED) public User create(@RequestBody User user) { return service.create(user); } @DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void delete(@PathVariable Long id) { service.delete(id); } }

📋 V2 & V3 任务

// ===== V2 任务清单 ===== // □ 1. 创建 CreateUserRequest / UpdateUserRequest(加 @Valid 注解) // □ 2. 创建 UserDTO(返回值不暴露密码等敏感字段) // □ 3. 创建 ApiResponse 统一包装 // □ 4. 创建 GlobalExceptionHandler // □ 5. 创建 UserNotFoundException 等自定义异常 // □ 6. Entity ↔ DTO 转换方法 // □ 7. 测试异常场景:404、重复邮箱、参数校验失败 // ===== V3 任务清单 ===== // □ 1. 添加 Spring Security + JWT 依赖 // □ 2. 实现 /api/auth/register 和 /api/auth/login // □ 3. JWT 生成和验证工具类 // □ 4. SecurityConfig 配置路径权限 // □ 5. 分页查询 + PageResponse // □ 6. 写 UserServiceTest(JUnit + Mockito) // □ 7. 写 UserControllerTest(MockMvc) // □ 8. 确保测试覆盖率 > 60%
🎯 立即开始:创建 V1 项目

1. 访问 start.spring.io 创建项目

2. 选择:Maven + Java 21 + Spring Boot 3.2+

3. 添加依赖:Spring Web, Spring Data JPA, MySQL Driver

4. 下载并用 IntelliJ IDEA 打开

5. 按照 V1 任务清单逐步实现

💡 学习建议:每完成一个版本,用 Postman 完整测试一遍所有接口。遇到问题先看报错信息(Java 的错误信息通常很详细),再 Google "Spring Boot + 关键词"。

📋 速查:项目依赖

版本依赖用途
V1spring-boot-starter-webWeb 基础
V1spring-boot-starter-data-jpa数据库 ORM
V1mysql-connector-jMySQL 驱动
V2spring-boot-starter-validation数据校验
V3spring-boot-starter-security安全框架
V3jjwt-apiJWT 工具
V3spring-boot-starter-test测试框架
✏️ 填空:项目结构
// 创建项目时选择的构建工具 + Java 21 + Spring Boot 3.2 // V1 必须的三个依赖 Spring Web, Spring Data , MySQL Driver // 测试接口推荐用什么工具? 或 curl
🧠 小测验

Spring Initializr 的网址是?

🗺️ Java vs TypeScript 概念总对照

概念TypeScriptJava
包管理npm / pnpmMaven / Gradle
类型系统编译时(可绕过)编译时(强制)
运行方式解释执行编译为字节码 → JVM 执行
空值null / undefinednull(+ Optional)
字符串比较===.equals()
接口interface(纯类型)interface(可有默认方法)
枚举enum(简单常量)enum(完整类)
泛型保留到运行时编译后擦除
异常运行时checked + unchecked
函数一等公民方法必须在类中
异步async/await + PromiseCompletableFuture / 虚拟线程
框架Express / NestJSSpring Boot
ORMPrisma / TypeORMSpring Data JPA / MyBatis
测试Jest / VitestJUnit 5 + Mockito

💬 高频面试题速答

// ===== Java 基础 ===== // Q: == 和 equals() 的区别? // A: == 比较引用地址,equals() 比较内容。 // 基本类型用 ==,对象(尤其 String)用 equals()。 // Q: int 和 Integer 的区别? // A: int 是原始类型(栈上,不能为 null), // Integer 是包装类(堆上,可以为 null,能用于泛型)。 // Q: final 的三种用法? // A: final 变量(常量)、final 方法(不能重写)、final 类(不能继承)。 // Q: 抽象类和接口的区别? // A: 抽象类可以有状态(字段),单继承。 // 接口不能有状态(Java 8+ 可以有默认方法),多实现。 // ===== Spring Boot ===== // Q: IoC 是什么? // A: 控制反转。对象的创建和管理交给 Spring 容器, // 而不是自己 new。 // Q: Spring 推荐哪种注入方式? // A: 构造器注入。字段用 final,不可变,易测试。 // Q: @RestController vs @Controller? // A: @RestController = @Controller + @ResponseBody // 方法返回值直接序列化为 JSON。 // Q: Spring Bean 的默认作用域? // A: singleton(单例),同一个 Spring 容器默认共享一个实例。

📝 更多面试题

// ===== 集合 ===== // Q: ArrayList vs LinkedList? // A: ArrayList 基于数组,随机访问 O(1),插入删除 O(n)。 // LinkedList 基于链表,随机访问 O(n),插入删除 O(1)。 // 99% 的场景用 ArrayList。 // Q: HashMap 的工作原理? // A: 基于哈希表。key.hashCode() → 桶位置 → 解决冲突(链表/红黑树)。 // Java 8+ 链表长度 > 8 转红黑树。 // ===== JPA ===== // Q: JPA 的 N+1 问题? // A: 查 1 次主表 + N 次关联表。 // 解决:@Query 用 JOIN FETCH,或 @EntityGraph。 // Q: @Transactional 注解的作用? // A: 声明式事务管理。方法内的所有数据库操作在一个事务中, // 异常时自动回滚。 // ===== 设计模式 ===== // Q: Spring 中用了哪些设计模式? // A: 单例(Bean 默认)、工厂(BeanFactory)、 // 代理(AOP)、模板方法(JdbcTemplate)、 // 观察者(事件机制)。
💡 面试技巧:① 回答时先说结论再解释原因。② 用 TypeScript 类比帮助面试官理解你的思路。③ 不确定的说"我理解的是……"而不是瞎猜。④ 实际项目经验 > 背概念。

📋 速查:学习路线总结

阶段核心知识产出
语言基础类型/字符串/控制流/集合/方法/异常/泛型能写 200 行 Java
面向对象类/继承/封装/枚举/设计模式面向对象思维
工程能力Maven/项目结构/Stream/测试工程化能力
Spring BootIoC/Controller/Service/校验/异常/JPAREST API
实战接口设计/安全/项目/面试完整项目
✏️ 填空:核心概念回顾
// Java 字符串比较用什么方法? str1.(str2) // Spring 的核心思想:控制反转的英文缩写 (Inversion of Control) // JUnit 断言中,参数顺序是 assertEquals(, 实际值)
🧠 最终测验

以下哪个不是 Spring Boot 的核心注解?

📝 进入考试 →