这就是又能写安卓,又能写mc mod的函数名一长串的语言啊!

简介

分类

java分为三个版本Java SEJava EE(现更名为Jakarta EE)、Java ME,他们之间关系就好像Python 和 MicroPython。

特性 JSE JRE JEE JRE JME JRE
定位 标准版 企业版 微型版
库和 API 核心 Java 库 核心库 + 企业级 API 精简库 + 设备特定 API
JVM 标准 JVM 标准 JVM 定制化 JVM(如 CLDC HotSpot)
适用场景 桌面应用、小型服务器 企业应用、Web 服务 移动设备、嵌入式系统
  • JSE JRE 只能运行基于 JSE 开发的程序。
  • JEE JRE 可以运行 JSE 和 JEE 程序,因为它包含了 JSE 的所有功能。
  • JME JRE 只能运行基于 JME 开发的程序,因为它的库和 JVM 是专门为资源受限设备设计的。

官网地址显示的就是**JDK 21 is the latest Long-Term Support (LTS) release of the Java SE Platform.**正常我们用的就是java se。上述内容不重要。

我学的好像是java8的老版本,但是为了fabric 1.21.4,我使用jdk21来进行实操。

C++的过渡

  1. Java没有指针,有引用,但是它的引用和C++的引用不是相同的概念
  2. Java能够自动管理内存
  3. Java多平台数据类型是统一大小的
  4. Java类似python,声明和实现在一起,不需要头文件h和实现cpp这种形式
  5. 没有define这种宏
  6. 不支持多重继承,即不能有多个爹,为了解决钻石继承,引入了接口这个性质
  7. 没有全局变量,作用域最大就是类里面
  8. 没有struct和union,有class就足够了
  9. 没有goto

运行

xxx.java编译成字节码xxx.class,在不同平台上使用不同的解释器来运行

JRE = JVE + API JDK = JRE + Tools

基本工具如通过javac.exe编译,java.exe运行。

引入

hello

先写一个经典的hello world

1
2
3
4
5
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World"); // 输出 Hello World
    }
}

注意:类名和文件名要相同

对于一个**.java文件**我们要知道其组成部分:

  1. package (0 or 1)
  2. import (>= 0)
  3. class (>= 1) 但是 public class (= 1 并且与文件同名)

注:

  1. package 的作用就是 c++ 的 namespace 的作用,防止名字相同的类产生冲突。
  2. java因强制要求类名(唯一的public类)和文件名统一,因此在引用其它类时无需显式声明。在编译时,编译器会根据类名去寻找同名文件。

from runoob

注释

和c差不多,Javadoc提供根据文档注释生成文档的功能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 单行注释

/*
多行注释
*/

/**
* 文档注释
* @author cbksb
* @version 1.2
*/

类和对象

  1. 类 Class :含有 方法method (成员函数), 字段 field (成员变量)
  2. 对象 Object
  3. 继承 通过 class A extends B {…}
  4. 封装:通过public方法访问私有字段
  5. 多态
  6. 抽象:类似纯虚函数
  7. 接口:定义类必须实现的方法,支持多重继承。
  8. 重载(Overload):同c++,同名不同参
  9. 重写(Overwrite): 子类覆盖父类

可变参数

基本语法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public void methodName(Type... parameterName) {
    // 方法体
}

public static void printInfo(String message, int... values) {
    System.out.println(message);
    for (int value : values) {
        System.out.println(value);
    }
}
  • 使用三个点(...)表示可变参数
  • 可变参数必须是方法参数列表中的最后一个参数
  • 在方法内部,可变参数被当作数组处理

和数组参数的对比

特性 可变参数 数组参数
声明方式 Type... parameterName Type[] parameterName
调用方式 可以传递多个单独参数或数组 必须传递数组对象

注意一下特殊情况:

  1. 当不传参时,默认为空数组,不是 null
  2. 当传入一个 null 时,参数为 null

引用

Java中没有真正的引用传递,如C++中的&。所以通常就是返回一个值,然后重新赋值。

对于基本类型我们可以通过数组来实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Main {
    public static void modify(int[] arr) {
        arr[0] = 100; // 修改数组元素
    }
    
    public static void main(String[] args) {
        int[] num = {10};
        modify(num);
        System.out.println(num[0]); // 输出100
    }
}

对于其他的类型如String或者自己定义的类,传入是原始值的内存地址,因为Java一切皆引用,所以本身被传入的那个变量也不过是指向内存中的真正变量的位置,这种情况下函数内的修改会对外面也产生影响。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        String bob = "Bob";
        p.setName(bob); // 传入bob变量
        System.out.println(p.getName()); // "Bob"
        bob = "Alice"; // bob改名为Alice
        System.out.println(p.getName()); // "Bob"还是"Alice"?
    }
}

class Person {
    private String name;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

上面这段程序两次输出都是"Bob",虽然它确实传递的是引用类型但是String,但是String对象一旦创建便不可修改。bob = "Alice"实际上是创建新字符串对象,而非修改原"Bob"的内容。因此,p.name引用的"Bob"始终未被改变。

最后解释一下经典的引用类型参数传递的是引用的拷贝,这里的引用相当于一个指针,指向真正类变量存储存储的地方,引用的拷贝也就是相当于另一个指针同样指向那个地方,所以进行操作也可以对其值进行修改。

数据类型

Java由于没有指针,所以全是引用,除了基本的数据类型,全是引用数据类型。

内置

1. 整数类型

用于存储整数值,包括正数、负数和零。

数据类型 大小(字节) 取值范围 默认值
byte 1 -128 到 127 0
short 2 -32,768 到 32,767 0
int 4 -2³¹ 到 2³¹-1(约 -2.1亿 到 2.1亿) 0
long 8 -2⁶³ 到 2⁶³-1 0L

long在用数字表示的时候后面一定要加L,如123456789123L,否则视作int

2. 浮点类型

用于存储带小数部分的数值。

数据类型 大小(字节) 取值范围 默认值
float 4 约 ±3.4e-38 到 ±3.4e38 0.0f
double 8 约 ±1.7e-308 到 ±1.7e308 0.0d

3. 字符类型

用于存储单个字符。

数据类型 大小(字节) 取值范围 默认值
char 2 0 到 65,535(Unicode 字符) ‘\u0000’

4. 布尔类型

用于存储逻辑值,只有两个可能的值:truefalse

不可以0或非0的整数替代true和false ,boolean不能与整数类型相互转换。

数据类型 大小(字节) 取值范围 默认值
boolean 1(实际大小取决于 JVM 实现,可能被优化为 1 位) truefalse false

引用

类,接口,数组都是引用类型。

1
2
Person p = new Person(); // Person的实例在堆区,p在栈区,p指向它
Person p2 = p; // 复制的是引用

数组

支持两种写法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
数据类型[] 数组名;  // 推荐写法
数据类型 数组名[];  // 不推荐,兼容 C/C++ 风格

int[] numbers;  // 声明一个整型数组
String[] names; // 声明一个字符串数组

int[] numbers = {1, 2, 3, 4, 5}; // 静态初始化
String[] names = {"Alice", "Bob", "Charlie"};

int[] numbers = new int[5]; // 动态初始化,长度为 5
String[] names = new String[3]; // 动态初始化,长度为 3
//动态初始化时,数组的元素会被赋予默认值

可以通过 数组名.length 获取数组的长度。

遍历数组,一种是下标,一种是类似python的for i in xxx,和c++11之后的for auto i : xxx那样的,但是是只读的。

1
2
3
4
5
6
7
8
int[] numbers = {10, 20, 30, 40, 50};
for (int i = 0; i < numbers.length; i++) {
    System.out.println(numbers[i]);
}

for (int num : numbers) {
    System.out.println(num);
}

二维数组

1
2
3
4
5
6
int[][] matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; // 静态初始化
System.out.println(matrix[1][2]); // 输出: 6

int[][] matrix2 = new int[3][3]; // 动态初始化
matrix2[0][0] = 1;
matrix2[1][1] = 5;

以下是和C++不同的不规则数组(锯齿数组)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int[][] t = new int[3][]; // 声明一个二维数组,第一维长度为 3,第二维长度未定
t[0] = new int[2]; // 第一行长度为 2
t[1] = new int[3]; // 第二行长度为 3
t[2] = new int[4]; // 第三行长度为 4

// 访问
for (int i = 0; i < t.length; i++) {
    for (int j = 0; j < t[i].length; j++) {
        System.out.print(t[i][j] + " ");
    }
    System.out.println();
}

// 所以下面的C++写法在Java是不行的
// int[][] t = new int[][3]; 

数组拷贝

1
2
3
4
5
6
7
public static void arraycopy(
    Object src,  // 源数组
    int srcPos,  // 源数组的起始位置
    Object dest, // 目标数组
    int destPos, // 目标数组的起始位置
    int length   // 要复制的元素个数
)

System.arraycopy是最高效的!

流程控制

同c++

switch也需要break

还有我的大括号不换行的码风是Java毋庸置疑的正统!

变量常量

1
type identifier [ = value][, identifier [= value] ...] ;
  • type – 数据类型。
  • identifier – 是变量名,可以使用逗号 , 隔开来声明多个同类型变量。

按Java惯例, 类名首字母用大写( Pascal) 其余的(包名、方法名、变量名) 首字母都小写(camel)

静态 static 常量 final 还可以组合 static final 数据类型 常量名 = 值;

类和接口

虽然说你要是用C++那一套也都能听懂,但是吧人家都有翻译规范了就规范点吧。

以下是Java类相关的术语中英文对照:

对照

  1. 类 (Class)

  2. 对象 (Object)

  3. 实例 (Instance)

  4. 属性/域/字段 (Attribute/Field)

  5. 方法 (Method)

  6. 构造函数 (Constructor)

  7. 继承 (Inheritance)

  8. 父类/超类 (Superclass/Parent Class)

  9. 子类 (Subclass/Child Class)

  10. 封装 (Encapsulation)

  11. 多态 (Polymorphism)

  12. 抽象类 (Abstract Class)

  13. 接口 (Interface)

  14. 重载 (Overloading)

  15. 重写/覆盖 (Overriding)

  16. 访问修饰符 (Access Modifier)

    • public
    • private
    • protected
    • default (package-private)
  17. 静态 (Static)

  18. final

  19. this

  20. super

  21. 包 (Package)

  22. 导入 (Import)

  23. 成员变量 (Member Variable)

  24. 局部变量 (Local Variable)

  25. 实例变量 (Instance Variable)

  26. 类变量 (Class Variable)

  27. 方法签名 (Method Signature)

  28. 参数 (Parameter)

  29. 返回值 (Return Value)

  30. 异常 (Exception)

  31. 泛型 (Generics)

  32. 注解 (Annotation)

  33. 枚举 (Enum)

  34. 内部类 (Inner Class)

  35. 匿名类 (Anonymous Class)

  36. Lambda表达式 (Lambda Expression)

  37. 流 (Stream)

  38. 集合 (Collection)

  39. 数组 (Array)

  40. 反射 (Reflection)

  41. 序列化 (Serialization)

  42. 反序列化 (Deserialization)

  43. 线程 (Thread)

  44. 同步 (Synchronization)

  45. 异步 (Asynchronous)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[public] [abstract|final] class className [extends superClass] 
[implements InterfaceNameList] { // 类声明
    [public | protected | private] [static] [final] [transient] [volatile] type variableName;
    // 成员变量声明,可为多个

    [public | protected | private] [static] [final | abstract] [native] [synchronized] returnType methodName ( [paramList] ) 
    // 方法定义及实现,可为多个
        
    [throws exceptionList] {
        statements
    }
}

1. 访问修饰符和类修饰符

  • public:表示该类可以被任何其他类访问。如果省略,则默认为包级私有(仅在同一个包内可见)。

  • abstract:表示这是一个抽象类,不能被实例化,只能被继承。抽象类可以包含抽象方法和具体方法。

  • final:表示这是一个最终类,不能被继承。所有方法默认也是final的,不能被子类重写。

2. 类声明

  • class className:定义一个名为className的类。

  • extends superClass:表示当前类继承自superClass。一个类只能继承一个父类。

  • implements InterfaceNameList:表示当前类实现了InterfaceNameList中列出的一个或多个接口。一个类可以实现多个接口,接口之间用逗号分隔。

3. 成员变量声明

1
[type] variableName;
  • 修饰符

    • publicprotectedprivate:控制变量的访问权限。
    • static:表示该变量属于类本身,而不是某个实例。
    • final:表示该变量的值一旦赋值后不可更改。
    • transient:表示该变量不会被序列化。
    • volatile:确保多线程环境下对该变量的修改对所有线程可见。
  • type:变量的数据类型,如intString等。

  • variableName:变量的名称。

4. 方法声明与定义

1
2
3
[returnType] methodName ( [paramList] ) [throws exceptionList] {
    statements
}
  • 修饰符

    • publicprotectedprivate:控制方法的访问权限。
    • static:表示该方法属于类本身,可以通过类名直接调用。
    • final:表示该方法不能被子类重写。
    • abstract:表示该方法没有实现,必须在子类中被实现(仅适用于抽象类)。
    • native:表示该方法的实现由本地代码(如C/C++)提供。
    • synchronized:表示该方法在同一时间只能被一个线程访问,用于线程同步。
  • returnType:方法的返回类型,如voidintString等。如果是void,表示方法不返回任何值。

  • methodName:方法的名称。

  • paramList:方法的参数列表,多个参数用逗号分隔。例如:(int a, String b)

  • throws exceptionList:声明方法可能抛出的异常类型,多个异常用逗号分隔。例如:throws IOException, SQLException

  • statements:方法体内的代码块,包含具体的操作和逻辑。

示例

以下是一个结合上述元素的完整示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public final class Person implements Serializable, Cloneable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public synchronized void setAge(int age) {
        if(age >= 0){
            this.age = age;
        }
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

抽象类

如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法。

抽象方法用abstract修饰。因为无法执行抽象方法,因此这个类也必须申明为抽象类(abstract class)。

使用abstract修饰的类就是抽象类。我们无法实例化一个抽象类。

所以抽象类只是单纯为了被继承。

多态

针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。

和cpp一样,派生类重载基类的方法时,是基类引用指向派生类,通过基类引用调用,然后根据其类型类动态调用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Animal {
    public void speak() {
        System.out.println("Animal speaks");
    }
}

class Dog extends Animal {
    @Override
    public void speak() { // 重写父类方法
        System.out.println("Dog barks");
    }
}

class Cat extends Animal {
    @Override
    public void speak() { // 重写父类方法
        System.out.println("Cat meows");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal; // 父类引用

        animal = new Dog(); // 指向子类对象
        animal.speak(); // 输出: Dog barks(动态调用Dog的speak)

        animal = new Cat(); // 指向另一个子类对象
        animal.speak(); // 输出: Cat meows(动态调用Cat的speak)
    }
}

接口

接口其实就是某种程度上的抽象类,抽象的不能再抽象,但是它不再是具有相同特征的一类事物,而是类似一些事物具有相同的特点,我们再在子类中实现这些它的特性。侧重于行为的定义

1
2
3
4
5
6
7
[public] interface InterfaceName [extends superInterfaceList] {
    // 常量声明,可为多个
    type CONSTANT_NAME = value;
    
    // 方法声明,可为多个
    returnType methodName([paramList]);
}

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public interface Animal {
    String NAME = "Animal"; // 常量声明
    
    void eat(); // 抽象方法声明
    
    void sleep(int hours); // 另一个抽象方法声明
}

public interface Mammal extends Animal {
    void giveBirth(); // 继承自Animal接口,并添加新的抽象方法
}

注意,在Java 8之前,接口中的所有方法都是抽象的(即没有方法体)。Java 8之后,可以有默认方法和静态方法。

三个默认方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// (1) 构造方法,声明为:
className( [paramlist] ){

}
// (2) main( )方法,声明为:
public static void main ( String args[ ] ){

}
//3. finalize( )方法,声明为:
protected void finalize( ) throws throwable{

}

注意从Java9开始finalize就被标记成弃用,Java18正式干掉它。

继承

子类:subclass

父类/超类:superclass

Java只支持单继承: 一个类只能有一个直接父类

在面向对象编程中,继承(Inheritance) 是代码复用和逻辑分层的重要机制。Java 的继承机制遵循以下核心要点:


🧬 继承的本质

  • 子类(subclass) 继承 父类/超类(superclass) 的属性和方法(private 成员除外)
  • 子类可以 扩展(extend) 父类的功能,添加新属性和方法
  • 子类可以 重写(override) 父类的方法(非 private 和非 final 的方法)

⚠️ Java 单继承限制

  • 一个类只能有一个直接父类(通过 extends 关键字)
  • 但可以通过接口(implements)实现多重继承的效果
  • 继承关系形成树状层次结构(非网状结构)
1
2
3
4
5
6
// 单继承示例
class Animal { /* 父类 */ }
class Dog extends Animal { /* 子类 */ }

// 错误示例:尝试多继承
// class Cat extends Animal, Mammal {} // 编译错误

🔑 关键语法要素

  1. extends 关键字

    1
    
    class Subclass extends Superclass { ... }
    
  2. super 关键字

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    public class Dog extends Animal {
        public Dog(String name) {
            super(name); // 调用父类构造器
        }
    
        @Override
        public void eat() {
            super.eat(); // 调用父类方法
            System.out.println("狗狗在啃骨头");
        }
    }
    
  3. 构造器调用规则

    • 子类构造器必须首先调用父类构造器
    • 默认调用父类无参构造器(若父类没有无参构造器,必须显式调用)

📜 访问权限

Java的继承只有extends这一种方式,和cpp不同,访问权限只由父类的修饰符决定。

修饰符 同类访问 同包访问 子类访问(不同包) 不同包非子类
public
protected
默认(无修饰符)
private

🧩 继承中的访问规则

1️⃣ public 成员
  • 完全开放访问

  • 子类可以直接访问父类的 public 成员

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    class Parent {
        public String publicField = "Public";
    }
    
    class Child extends Parent {
        void print() {
            System.out.println(publicField); // 直接访问
        }
    }
    
2️⃣ protected 成员
  • 子类特权访问

  • 子类可以直接访问父类的 protected 成员(即使位于不同包)

  • 示例:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    // Parent.java(包 com.example)
    package com.example;
    public class Parent {
        protected String protectedField = "Protected";
    }
    
    // Child.java(包 com.test)
    package com.test;
    import com.example.Parent;
    
    public class Child extends Parent {
        void print() {
            System.out.println(protectedField); // 不同包子类可直接访问
        }
    }
    
3️⃣ 默认(包私有)成员
  • 同包访问限制

  • 只有相同包内的子类可以访问

  • 示例:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    // Parent.java(包 com.example)
    class Parent {
        String packageField = "Package-private";
    }
    
    // ChildSamePackage.java(包 com.example)
    public class ChildSamePackage extends Parent {
        void print() {
            System.out.println(packageField); // 同包子类可访问
        }
    }
    
    // ChildDiffPackage.java(包 com.test)→ 无法访问
    
4️⃣ private 成员
  • 完全继承隔离

  • 子类无法直接访问父类的 private 成员

  • 只能通过父类提供的 public/protected 方法间接访问

  • 示例:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    class Parent {
        private String secret = "Confidential";
    
        public String getSecret() {
            return secret;
        }
    }
    
    class Child extends Parent {
        void printSecret() {
            // System.out.println(secret); // 编译错误
            System.out.println(getSecret()); // 通过方法访问
        }
    }
    

🔧 继承中的构造方法

  • 构造方法不被继承(即使父类构造方法是 public

  • 子类构造器必须调用父类构造器(通过 super()

  • 示例:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    class Parent {
        protected String name;
    
        public Parent(String name) {
            this.name = name;
        }
    }
    
    class Child extends Parent {
        public Child(String name) {
            super(name); // 必须显式调用父类构造器
        }
    }
    

💡 关键注意事项

  1. 跨包继承时

    • protected 成员在不同包子类中可以直接访问
    • 但不同包的非子类不能访问 protected 成员
  2. 方法重写规则

    • 重写方法的访问权限不能 缩小(如父类方法是 public,子类重写方法不能改为 protected
  3. 成员变量访问

    • 子类可以定义与父类同名的成员变量(但不推荐,会产生隐藏现象)
  4. Java 没有 C++ 式的访问继承

    1
    2
    
    // Java 不支持这种写法!
    // class Child extends private Parent {} // 非法语法
    
  5. **构造方法是不能继承的 **

    • 但是可以调用
  6. **super访问父类的域 **

    • 通过super.{field_name}来访问
    • 使用super可以访问被子类所隐藏了的同名变量。
    • 当覆盖父类的同名方法的同时,又要调用父类的方法,就必须使用super。

反射

反射有什么用,目前是不知道的,但是先学着再说。最基本的比如说访问私有字段,私有方法,这个在mc fabric 1.21.3到1.21.4模组升级的时候ai给出过代码,原本public方法在麻将瞎更新后变成private了,这样直接通过反射暴力升级一波,反正也能用,虽然看作者代码最后是api变了,但是咱也不会写模组,能用就行。

然后就是典中典的Spring依赖注入,所谓依赖注入看起来很高级,但是就是cpp当中聚合类,然后将其他的类作为参数传入构造函数而已,不过就是这样听起来很高级,然后通过一个叫做Anotation的东西,Spring就可以识别出这个类里面的字段,然后一顿赋值,就非常的省心。

再有就是测试,也是通过Anotation来标记,然后使用反射来发现和执行测试方法。

原理

众所周知把Java运行起来需要两步,显示转换成字节码,然后才是变成机器码。反射是在字节码层面上实现的。

因为Java是一个纯度拉满的面向对象语言,所以一切皆对象,所有的对象都是从Object继承而来的,所有的类也是从Class继承而来的。

但是基本类型(int, double, char 等)不是对象,但它们的包装类(Integer, Double, Character)是对象。

数组是对象,即使是 int[] 也是 Object 的子类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Object
├── String
├── Integer
├── MyCustomClass
├── ...
└── Class  // Class 本身也是 Object 的子类
    ├── Class<MyClass1>  // 每个类的 Class 对象
    ├── Class<MyClass2>
    └── ...

当 JVM 加载一个类(如 com.example.MyClass)时:

  1. 类加载器(ClassLoader) 读取 .class 字节码文件。

  2. 解析字节码,在 方法区(Method Area) 存储类的元数据(字段、方法、父类、接口等)。

  3. 堆(Heap) 中生成一个 Class 对象,作为该类的运行时表示。

Class 对象 是反射的入口,它包含:

  • 类名、修饰符(public/final 等)
  • 字段(Field 信息)
  • 方法(Method 信息)
  • 构造器(Constructor 信息)
  • 注解(Annotation 信息)

获取对象

  1. 类名.class
  2. 对象.getClass()
  3. Class.forname("**全限定类名**(Fully Qualified Name)") 注 : 包含包名和类名的完整类名。
方式 Class.forName("全类名") 类名.class 对象.getClass()
使用场景 动态加载类,常用于配置文件读取类名后加载 编译时已知类名,直接获取Class对象 已有对象实例,获取其Class对象
类是否加载 会执行类的静态代码块(初始化类) 不会初始化类(静态代码块不执行) 对象已存在,类肯定已加载并初始化
性能 相对较慢(需查找类路径并可能初始化类) 最快(直接获取) 快(对象已存在)
适用性 最灵活,类名可作为字符串参数传递 需要编译时知道具体类 必须有对象实例
异常处理 需要处理ClassNotFoundException 无检查异常 无检查异常
数组类型 不能直接获取数组类型的Class(需特殊处理) 可以直接获取(如int[].class 可以直接获取数组对象的Class
基本类型 不能用于基本类型 可以用于基本类型(如int.class 不能用于基本类型(需包装类对象)
示例代码 Class cls = Class.forName("java.lang.String"); Class cls = String.class; String s = ""; Class cls = s.getClass();

下面提及一下int.class, int.class乍一看还是很抽象的,明明int就不是类为什么还会有class呢,其主要用途之一就是作为反射的一些方法的参数,如你的类有一个方法参数正好是int, 就可以getMethod("methodName", int.class),其存在还是有一定必要性的.

方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 获取 Class 对象
Class.forName(String className)
Object.getClass()
.class 语法 (如 String.class)

// 获取类信息
String getName()
String getSimpleName()
String getCanonicalName()
Class<? super T> getSuperclass()
Class<?>[] getInterfaces()
int getModifiers() // 配合 Modifier 类使用
Package getPackage()

// 创建实例
T newInstance() // 已废弃,Java 9+
Constructor<T> getConstructor(Class<?>... parameterTypes)
Constructor<?>[] getConstructors()

// 获取字段
Field getField(String name)
Field[] getFields() // 仅公共字段
Field getDeclaredField(String name)
Field[] getDeclaredFields() // 所有字段

// 获取方法
Method getMethod(String name, Class<?>... parameterTypes)
Method[] getMethods() // 包括继承的公共方法
Method getDeclaredMethod(String name, Class<?>... parameterTypes)
Method[] getDeclaredMethods() // 仅本类声明的方法
    
// 获取接口
boolean isInterface()         // 判断是否为接口
Class<?>[] getInterfaces()    // 获取类实现的所有接口(对于接口则返回其扩展的接口)
default Method[] getDefaultMethods() // 获取接口的默认方法(Java 8+)

// 获取注解
<A extends Annotation> A getAnnotation(Class<A> annotationClass) // 获取指定类型的注解
Annotation[] getAnnotations() // 获取所有注解
Annotation[] getDeclaredAnnotations() // 获取直接声明的注解(不包括继承的)
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) // 检查是否存在指定类型的注解  
    
// 其他
boolean isArray()
boolean isEnum()
boolean isInterface()
boolean isPrimitive()
Class<?> getComponentType() // 数组类型)

最后补充一下想到的一个问题,怎么获得父类的父类甚至祖祖祖祖祖爷爷的私有方法?其实就是一直getSuperClass到为null就可以.

例子

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.Arrays;

// 定义一个自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@interface MyAnnotation {
    String value() default "default";
    int version() default 1;
}

// 定义一个接口
interface MyInterface {
    void interfaceMethod();
}

// 父类
class ParentClass {
    private String parentPrivateField = "parent private";
    protected String parentProtectedField = "parent protected";
    
    public void parentPublicMethod() {
        System.out.println("Parent public method");
    }
}

// 子类,实现接口并使用注解
@MyAnnotation(value = "ClassAnnotation", version = 2)
class ChildClass extends ParentClass implements MyInterface {
    private String privateField = "private value";
    public String publicField = "public value";
    @MyAnnotation("FieldAnnotation")
    public String annotatedField = "annotated field";
    
    public ChildClass() {}
    
    public ChildClass(String privateField) {
        this.privateField = privateField;
    }
    
    @Override
    public void interfaceMethod() {
        System.out.println("Implemented interface method");
    }
    
    @MyAnnotation("MethodAnnotation")
    public void publicMethod() {
        System.out.println("Public method");
    }
    
    private void privateMethod() {
        System.out.println("Private method: " + privateField);
    }
    
    public void methodWithArgs(String arg1, int arg2) {
        System.out.println("Method with args: " + arg1 + ", " + arg2);
    }
    
    // 静态方法
    public static void staticMethod() {
        System.out.println("Static method");
    }
}

public class ReflectionExample {
    public static void main(String[] args) throws Exception {
        // 1. 获取Class对象的几种方式
        Class<?> clazz1 = ChildClass.class;
        Class<?> clazz2 = Class.forName("ChildClass");
        ChildClass obj = new ChildClass();
        Class<?> clazz3 = obj.getClass();
        
        System.out.println("Class name: " + clazz1.getName());
        System.out.println("Simple name: " + clazz1.getSimpleName());
        System.out.println("Canonical name: " + clazz1.getCanonicalName());
        
        // 2. 获取父类和接口
        System.out.println("\nSuperclass: " + clazz1.getSuperclass().getName());
        System.out.println("Interfaces: " + Arrays.toString(clazz1.getInterfaces()));
        
        // 3. 注解操作
        System.out.println("\nClass annotations:");
        Annotation[] annotations = clazz1.getAnnotations();
        for (Annotation annotation : annotations) {
            if (annotation instanceof MyAnnotation) {
                MyAnnotation myAnnotation = (MyAnnotation) annotation;
                System.out.println("MyAnnotation value: " + myAnnotation.value() + 
                                 ", version: " + myAnnotation.version());
            }
        }
        
        // 检查类是否有特定注解
        boolean hasAnnotation = clazz1.isAnnotationPresent(MyAnnotation.class);
        System.out.println("Has MyAnnotation: " + hasAnnotation);
        
        // 4. 字段操作
        System.out.println("\nFields:");
        // 获取所有public字段(包括继承的)
        Field[] publicFields = clazz1.getFields();
        System.out.println("Public fields: " + Arrays.toString(publicFields));
        
        // 获取所有声明的字段(不包括继承的)
        Field[] declaredFields = clazz1.getDeclaredFields();
        System.out.println("Declared fields: " + Arrays.toString(declaredFields));
        
        // 获取特定字段并访问值
        Field publicField = clazz1.getField("publicField");
        System.out.println("Public field value: " + publicField.get(obj));
        
        // 访问私有字段
        Field privateField = clazz1.getDeclaredField("privateField");
        privateField.setAccessible(true); // 设置可访问
        System.out.println("Private field value: " + privateField.get(obj));
        
        // 修改字段值
        publicField.set(obj, "new public value");
        System.out.println("Modified public field value: " + publicField.get(obj));
        
        // 获取字段上的注解
        Field annotatedField = clazz1.getField("annotatedField");
        MyAnnotation fieldAnnotation = annotatedField.getAnnotation(MyAnnotation.class);
        System.out.println("Field annotation value: " + fieldAnnotation.value());
        
        // 5. 方法操作
        System.out.println("\nMethods:");
        // 获取所有public方法(包括继承的)
        Method[] publicMethods = clazz1.getMethods();
        System.out.println("Public methods: " + Arrays.toString(publicMethods));
        
        // 获取所有声明的方法(不包括继承的)
        Method[] declaredMethods = clazz1.getDeclaredMethods();
        System.out.println("Declared methods: " + Arrays.toString(declaredMethods));
        
        // 调用public方法
        Method publicMethod = clazz1.getMethod("publicMethod");
        publicMethod.invoke(obj);
        
        // 调用private方法
        Method privateMethod = clazz1.getDeclaredMethod("privateMethod");
        privateMethod.setAccessible(true);
        privateMethod.invoke(obj);
        
        // 调用带参数的方法
        Method methodWithArgs = clazz1.getMethod("methodWithArgs", String.class, int.class);
        methodWithArgs.invoke(obj, "test", 123);
        
        // 调用静态方法
        Method staticMethod = clazz1.getMethod("staticMethod");
        staticMethod.invoke(null); // 静态方法可以传null作为对象
        
        // 获取方法上的注解
        MyAnnotation methodAnnotation = publicMethod.getAnnotation(MyAnnotation.class);
        System.out.println("Method annotation value: " + methodAnnotation.value());
        
        // 6. 构造器操作
        System.out.println("\nConstructors:");
        // 获取所有public构造器
        Constructor<?>[] publicConstructors = clazz1.getConstructors();
        System.out.println("Public constructors: " + Arrays.toString(publicConstructors));
        
        // 获取所有声明的构造器
        Constructor<?>[] declaredConstructors = clazz1.getDeclaredConstructors();
        System.out.println("Declared constructors: " + Arrays.toString(declaredConstructors));
        
        // 使用反射创建对象
        Constructor<?> noArgConstructor = clazz1.getConstructor();
        Object newObj1 = noArgConstructor.newInstance();
        System.out.println("Created object with no-arg constructor: " + newObj1);
        
        Constructor<?> stringArgConstructor = clazz1.getConstructor(String.class);
        Object newObj2 = stringArgConstructor.newInstance("constructor value");
        System.out.println("Created object with String arg constructor: " + newObj2);
        
        // 7. 其他反射操作
        System.out.println("\nOther operations:");
        // 检查修饰符
        int modifiers = clazz1.getModifiers();
        System.out.println("Is public: " + Modifier.isPublic(modifiers));
        System.out.println("Is abstract: " + Modifier.isAbstract(modifiers));
        
        // 检查数组类型
        System.out.println("Is array: " + clazz1.isArray());
        
        // 创建数组实例
        Object array = Array.newInstance(String.class, 3);
        Array.set(array, 0, "first");
        Array.set(array, 1, "second");
        System.out.println("Array length: " + Array.getLength(array));
        System.out.println("Array[1]: " + Array.get(array, 1));
        
        // 8. 访问父类的成员
        System.out.println("\nAccessing parent members:");
        Class<?> parentClass = clazz1.getSuperclass();
        Field parentProtectedField = parentClass.getDeclaredField("parentProtectedField");
        System.out.println("Parent protected field: " + parentProtectedField.get(obj));
        
        try {
            // 尝试访问父类私有字段
            Field parentPrivateField = parentClass.getDeclaredField("parentPrivateField");
            parentPrivateField.setAccessible(true);
            System.out.println("Parent private field: " + parentPrivateField.get(obj));
        } catch (Exception e) {
            System.out.println("Cannot access parent private field: " + e.getMessage());
        }
    }
}

注解

注释是写给人看的,那么注解就是写给编译器看的。

Java一共有三类注解:

一、内置注解

  1. @Override - 表示方法覆盖了父类的方法
  2. @Deprecated - 表示元素已过时,不推荐使用
  3. @SuppressWarnings - 抑制编译器警告
  4. @SafeVarargs - 表示方法不会对其可变参数执行不安全的操作
  5. @FunctionalInterface - 表示接口是函数式接口(Java 8)

对于一些注解他们不会进入到class文件中,编译之后就会被抛弃,这取决于它们的 @Retention 策略。

SOURCE 级别注解(如 @Override@SuppressWarnings):

  • 仅在 编译阶段 有效,编译后不会进入 .class 文件。
  • 主要用于 静态检查(如防止拼写错误、抑制警告)。

RUNTIME 级别注解(如 @Deprecated@SafeVarargs@FunctionalInterface):

  • 会保留到 运行时,可以通过 反射 读取。
  • 常用于 框架(如 Spring、JUnit)和 运行时动态处理

CLASS 级别注解(较少见):

  • 会进入 .class 文件,但 运行时不可见(除非使用字节码工具如 ASM)

二、元注解

元注解是指用于注解其他注解的注解,如上面的@Retention其实就是。

  • @Target - 指定注解可以应用的位置
  • @Retention - 指定注解的保留策略
  • @Documented - 表示注解应该包含在Javadoc中
  • @Inherited - 表示注解可以被继承
  • @Repeatable (Java 8+) - 表示注解可以重复应用在同一元素上

三、自定义注解

定义注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import java.lang.annotation.*;

// 自定义注解
// 元注解部分
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
// 自己的部分
public @interface CodeInfo {
    String author(); 
    String date(); 
    int version() default 1; // 有默认值
}

// 使用注解
public class MyClass {
    @CodeInfo(author = "Alice", date = "2023-10-01", version = 2)
    public void myMethod() {
        System.out.println("Hello, Annotation!");
    }
}

元注解

下面展开自定义注解中用得到的元注解部分

  1. **@Target**用于指定注解作用的位置 参数ElementType[] value(),指定目标元素类型数组 只有一个参数的例子: @Target(ElementType.METHOD) 多个作用位置:

    1
    2
    3
    4
    
    @Target({
        ElementType.METHOD,
        ElementType.FIELD
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    
    ElementType.TYPE能修饰类接口或枚举类型
    ElementType.FIELD能修饰成员变量
    ElementType.METHOD能修饰方法
    ElementType.PARAMETER能修饰参数
    ElementType.CONSTRUCTOR能修饰构造器
    ElementType.LOCAL_VARIABLE能修饰局部变量
    ElementType.ANNOTATION_TYPE能修饰注解
    ElementType.PACKAGE能修饰包
    
  2. **@Retention**指定注解的保留策略 参数RetentionPolicy value() RetentionPolicy 取值

    • SOURCE:仅在源代码中保留,编译时被丢弃
    • CLASS:在class文件中保留,但运行时不可获取(默认值)
    • RUNTIME:在class文件中保留,运行时可通过反射获取
  3. **@Inherited**表示注解可以继承

    如果一个类使用了带有@Inherited的注解,那么它的子类将自动继承该注解。

    注意

    • 只对类注解(@Target(ElementType.TYPE))有效
    • 对接口或方法注解无效
  4. **@Repeatable**表示注解可重复定义

    @Repeatable 表示注解可以在同一个元素上重复使用。需要定义一个容器注解来存储重复的注解。

    参数Class<? extends Annotation> value(),指定容器注解类

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    // 定义可重复注解
    @Repeatable(MyAnnotations.class)
    public @interface MyAnnotation {
        String value();
    }
    
    // 定义容器注解
    public @interface MyAnnotations {
        MyAnnotation[] value();
    }
    
    // 使用方式
    @MyAnnotation("foo")
    @MyAnnotation("bar")
    public class MyClass {
        // ...
    }
    

注解使用

注解和反射密不可分,具体的method前面的反射中已经提及过了。无非就是如果这个方法或者域被注解了,然后我就对其进行一些判断和操作。

综合案例

注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.itranswarp.learnjava;

import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {

	int min() default 0;

	int max() default 255;

}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.itranswarp.learnjava;

public class Person {

	@Range(min = 1, max = 20)
	public String name;

	@Range(max = 10)
	public String city;

	@Range(min = 1, max = 100)
	public int age;

	public Person(String name, String city, int age) {
		this.name = name;
		this.city = city;
		this.age = age;
	}

	@Override
	public String toString() {
		return String.format("{Person: name=%s, city=%s, age=%d}", name, city, age);
	}
}

主函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.itranswarp.learnjava;

import java.lang.reflect.Field;

/**
 * Learn Java from https://www.liaoxuefeng.com/
 * 
 * @author liaoxuefeng
 */
public class Main {

	public static void main(String[] args) throws Exception {
		Person p1 = new Person("Bob", "Beijing", 20);
		Person p2 = new Person("", "Shanghai", 20);
		Person p3 = new Person("Alice", "Shanghai", 199);
		for (Person p : new Person[] { p1, p2, p3 }) {
			try {
				check(p);
				System.out.println("Person " + p + " checked ok.");
			} catch (IllegalArgumentException e) {
				System.out.println("Person " + p + " checked failed: " + e);
			}
		}
	}

	static void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
		for (Field field : person.getClass().getFields()) {
			Range range = field.getAnnotation(Range.class);
			if (range != null) {
				Object value = field.get(person);
				if (value instanceof String) {
					String str = (String) value;
					if (str.length() < range.min() || str.length() > range.max()) {
						throw new IllegalArgumentException("Field " + field.getName() + " out of range: " + str);
					}
				} else if (value instanceof Integer) {
					int intValue = (Integer) value;
					if (intValue < range.min() || intValue > range.max()) {
						throw new IllegalArgumentException("Field " + field.getName() + " out of range: " + intValue);
					}
				}
			}
		}
	}
}

泛型

泛型对应cpp当中的模板。相应的vector对应java里面的ArrayList,先简单看一下其使用方法。

1
2
3
4
5
6
7
ArrayList<String> fruits = new ArrayList<>();
// Add elements
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// 整数型
ArrayList<Integer> numbers = new ArrayList<>(100); // initial capacity 100

泛型类

单参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public T getLast() { ... }
	/** 写法1
    public static <T> Pair<T> create(T first, T last) {
        return new Pair<T>(first, last);
    }
    **/
    public static <TT> Pair<TT> create(TT first, TT last) {
    	return new Pair<TT>(first, last);
    }

}

注意:这里的静态方法写法1是合理的,也是能编译通过的,但是和这个类的T是毫无关系的,等效于下面的写法。所以静态方法最好是要独立地写出。

多参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Pair<T, K> {
    private T first;
    private K last;
    public Pair(T first, K last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public K getLast() { ... }
}
// use
Pair<String, Integer> p = new Pair<>("test", 123);

实现原理和局限

Java 的泛型是通过 类型擦除(Type Erasure) 实现的,这意味着泛型信息仅在编译时存在,而在运行时会被擦除。所以折腾半天其实和Object直接实现没什么区别,只是变得更安全。

泛型信息在编译后被擦除,替换为 上界类型(通常是 Object 或指定的边界)。这也就导致运行时,通过对于不同的<T>反射getClass,返回的也是相同的。

特性 使用 Object 使用泛型(List<T>
类型安全 ❌ 需手动检查,可能 ClassCastException ✅ 编译时检查,避免运行时错误
代码可读性 ❌ 类型不明确,维护困难 ✅ 类型清晰,增强可读性
编译时检查 ❌ 运行时才暴露错误 ✅ 编译时发现错误
强制转换 ❌ 每次取值都要手动转换 ✅ 自动转换,减少冗余代码
泛型方法 ❌ 返回 Object,需手动转换 ✅ 自动推断类型
继承与多态 ❌ 需手动检查类型 ✅ 编译器生成桥接方法

存在下列问题

局限性 问题描述 替代方案
运行时类型擦除 无法直接获取 T 的实际类型 传递 Class<T> 或反射
不能 new T() 无法实例化泛型类型 反射或工厂模式
不能 new T[] 无法创建泛型数组 Array.newInstance()
不支持基本类型 List<int> 不合法 使用包装类(Integer
不能继承 Throwable 泛型异常类不可行 非泛型异常
方法重载冲突 类型擦除导致签名相同 修改方法名或参数
静态变量限制 静态变量不能依赖泛型 使用独立泛型方法
泛型不变性 List<String> 不是 List<Object> 通配符(? extends / ? super

通配符

extends

首先回顾一下,子类可以转换成父类,因为父类有的子类一定也有,然后NumberInteger的父类.

然后要明确这个关键字的目的,加入一个泛型方法,其参数是泛型的Number按照常理,如果我们传入一个Integer应该是合法的,但是这确实是无法编译的,所以这是应该被解决的。

注意Pair<? extends Number> p这个参数可以实现上面的功能。Pair<Number> p则无法通过编译。

这种使用<? extends Number>的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型T的上界限定在Number了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Main {
    public static void main(String[] args) {
        Pair<Integer> p = new Pair<>(123, 456);
        int n = add(p);
        System.out.println(n);
    }

    static int add(Pair<? extends Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        return first.intValue() + last.intValue();
    }
}

class Pair<T> {
	private T first;
	private T last;

	public Pair(T first, T last) {
		this.first = first;
		this.last = last;
	}

	public T getFirst() {
		return first;
	}
	public T getLast() {
		return last;
	}
	public void setFirst(T first) {
		this.first = first;
	}
	public void setLast(T last) {
		this.last = last;
	}
}

然而这就完美了吗?并非如此。Pair同上省略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Main {
	public static void main(String[] args) {
		Pair<Integer> p = new Pair<>(123, 456);
		add(p);
	}
	static void add(Pair<? extends Number> p) {
		Number first = p.getFirst();
		Number last = p.getLast();
		p.setFirst(new Integer(first.intValue() + 100));
		p.setLast(new Integer(last.intValue() + 100));
	}
}

这样也是会报错的。

  • ? extends Number只读的,适合读取数据,但不能写入(编译器无法保证类型安全)。
  • 如果需要修改数据,应该避免使用通配符,改用明确的泛型类型或返回新对象。

注意上面这个只读并不一定是坏事,也可以用来保证安全。

上面是对于参数的传入来讲的。对于类来讲,也可应通过这个来限制类的示例类型。如:

1
public class Pair<T extends Number> { ... }

这样泛型类型限定为Number以及Number的子类。

super

extends相反,这个是确定下界的(包含)—— 下界限定符。

特性 ? extends T (上界) ? super T (下界)
读取 可以安全读取为 T 只能当作 Object
写入 不能写入(类型不确定) 可以写入 T 或其子类
适用场景 生产者(Producer) 消费者(Consumer)
示例 List<? extends Number> List<? super Integer>

Pair的定义同上一条, 下面的代码时合法的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Main {
    public static void main(String[] args) {
        Pair<Number> p1 = new Pair<>(12.3, 4.56);
        Pair<Integer> p2 = new Pair<>(123, 456);
        setSame(p1, 100);
        setSame(p2, 200);
        System.out.println(p1.getFirst() + ", " + p1.getLast());
        System.out.println(p2.getFirst() + ", " + p2.getLast());
    }

    static void setSame(Pair<? super Integer> p, Integer n) {
        p.setFirst(n);
        p.setLast(n);
    }
}

遵循 PECS 原则

  • 需要读取数据 → 用 ? extends T
  • 需要写入数据 → 用 ? super T

如果既要读又要写,直接用明确的泛型类型(如 <T>),不要用通配符。

无限定通配符

到这里,应该对反射的Class<?>的写法有所理解了。

无限定通配符 <?> 表示泛型类型可以是任意类型,类似于 <? extends Object>(因为所有类都继承自 Object)。它主要用于不关心具体类型的场景,比如:

  • 只调用与泛型无关的方法(如 List.size())。
  • 读取数据时当作 Object 处理(因为类型未知)。
  • 适用于泛型类的非泛型方法
通配符类型 用途 读取 写入
<?> 任意类型(只读或非泛型操作) Object ❌ 不允许
<? extends T> 安全读取 T 或其子类 T ❌ 不允许
<? super T> 安全写入 T 或其父类 Object T 或其子类

安全事项

一、Java 反射 API 广泛使用了泛型来提供类型安全:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 类型安全的 Class 使用
Class<String> stringClass = String.class;
String str = stringClass.newInstance(); // 无需强制转换

// getSuperclass() 返回 Class<? super T>
Class<? super String> superClass = String.class.getSuperclass();

// Constructor 也是泛型的
Constructor<Integer> intConstructor = Integer.class.getConstructor(int.class);
Integer i = intConstructor.newInstance(123); // 类型安全

二、Java 不允许直接创建泛型数组,这是由类型擦除机制决定的: Java 在运行时无法知道 T 的具体类型,数组需要在创建时知道其确切的组件类型。

  1. 使用 Object 数组然后强制转型
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@SuppressWarnings("unchecked")
public class GenericArray<T> {
    private T[] array;
    
    public GenericArray(int size) {
        array = (T[]) new Object[size];  // 创建 Object 数组然后转型
    }
    
    public void set(int index, T item) {
        array[index] = item;
    }
    
    public T get(int index) {
        return array[index];
    }
    
    public T[] getArray() {
        return array;
    }
}

注意:这种方法在从 getArray() 返回数组时可能会引发 ClassCastException,因为实际类型是 Object[]

错误示范:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Main {
    public static void main(String[] args) {
       String[] arr = asArray("one", "two", "three");
       System.out.println(Arrays.toString(arr));
       // ClassCastException:
       String[] firstTwo = pickTwo("one", "two", "three");
       System.out.println(Arrays.toString(firstTwo));
    }

    static <K> K[] pickTwo(K k1, K k2, K k3) {
       return asArray(k1, k2);
    }

    static <T> T[] asArray(T... objs) {
       return objs;
    }
}

解释:

  1. 可变参数的泛型数组问题:当可变参数与泛型结合时,Java实际上会创建一个泛型数组。但由于Java泛型的类型擦除,运行时无法知道确切的类型信息。

  2. 类型擦除的影响:在pickTwo方法中,K的类型被擦除为Object,所以asArray(k1, k2)实际上创建的是一个Object[]数组,而不是String[]数组。

  3. 隐式类型转换失败:当尝试将这个Object[]赋值给String[] firstTwo时,Java需要进行隐式类型转换,但由于数组的协变性,这种转换在运行时失败,抛出ClassCastException

  4. 使用 Array.newInstance() 反射创建数组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class GenericArrayWithReflection<T> {
    private T[] array;
    
    @SuppressWarnings("unchecked")
    public GenericArrayWithReflection(Class<T> type, int size) {
        array = (T[]) Array.newInstance(type, size);  // 使用反射创建数组
    }
    
    // 其他方法同上
}

使用示例:

1
2
GenericArrayWithReflection<String> stringArray = 
    new GenericArrayWithReflection<String>(String.class, 10);
  1. 使用 List 替代数组
1
List<T> list = new ArrayList<T>();

这是最推荐的方案,因为:

  • 避免了数组的所有问题
  • 提供了更多的灵活性
  • 是类型安全的

IO

目前先学同步IO, java.io包里面的

文件

Java提供了一个File对象来操作文件目录

构造形如下面, 路径既可以是绝对路径也可以是相对路径, 注意windows的路径需要\\转译. 对于不同的平台可以通过File.separator来输出\或者/,但是统一用/就可以。

1
2
3
4
5
6
7
8
File(String pathname) // 通过路径名创建File对象
File(String parent, String child) // 通过父路径和子路径创建
File(File parent, String child) // 从父抽象路径名和子路径名字符串创建新实例
    
File file1 = new File("D:/example/1.txt");
File file2 = new File("D:/example", "1.txt");
File file3 = new File("D:/example");
File file4 = new File(file3, "1.txt");

常用方法(都是File对象的方法,所以是.调用哦)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
boolean exists() // 判断文件或目录是否存在
    
boolean isFile() // 判断是否是文件
boolean isDirectory() // 判断是否是目录
    
String getName() // 获取文件或目录名称
    
String getPath() // 获取路径字符串
String getAbsolutePath() // 获取绝对路径
String getCanonicalPath() // 获取规范路径
    
long length() // 获取文件大小(字节数)
boolean canRead() // 是否可读
boolean canWrite() // 是否可写
boolean canExecute() //是否可执行
long lastModified() // 获取最后修改时间(毫秒值)
    
boolean createNewFile() // 创建新文件
boolean mkdir() // 创建单级目录
boolean mkdirs() // 创建多级目录
boolean delete() // 删除文件或空目录(注意目录必须为空)
boolean renameTo(File dest) // 重命名或移动文件
    
// 临时文件写法
File f = File.createTempFile("tmp", ".txt"); // 提供临时文件的前缀和后缀
f.deleteOnExit(); // JVM退出时自动删除

对于Path表示路径时,可以遍历目录文件和子目录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        File f = new File("C:\\Windows");
        File[] fs1 = f.listFiles(); // 列出所有文件和子目录
        printFiles(fs1);
        File[] fs2 = f.listFiles(new FilenameFilter() { // 仅列出.exe文件
            public boolean accept(File dir, String name) {
                return name.endsWith(".exe"); // 返回true表示接受该文件
            }
        });
        printFiles(fs2);
    }

    static void printFiles(File[] files) {
        System.out.println("==========");
        if (files != null) {
            for (File f : files) {
                System.out.println(f);
            }
        }
        System.out.println("==========");
    }
}

对于一些配置文件、资源文件我们可以把它们放在classpath下,它们的路径表示与读取如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
try (InputStream is = getClass().getResourceAsStream("/template.html")) {
    // 这个getClass是this.getClass()已经存在实例的情况下进行调用的
    if (is == null) {
        throw new FileNotFoundException("资源文件未找到");
    }
    String content = new String(is.readAllBytes(), StandardCharsets.UTF_8);
    // 使用文件内容...
} catch (IOException e) {
    e.printStackTrace();
}

流就是数据的一部分,流式处理就是来一点处理一点。

根据数据流方向的不同分为:输入流输出流。根据处理的数据单位不同可分为:字节流字符流

字节流 字符流
输入流 InputStream Reader
输出流 OutputStream Writer

字节流

输入流

InputStream是Java提供的最基本的输入流,它甚至只是一个抽象类。所以我们常用的是FileStream

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public void readFile() throws IOException {
    InputStream input = null;
    try {
        input = new FileInputStream("readme.txt");
        int n;
        while ((n = input.read()) != -1) { // 利用while同时读取并判断
            System.out.println(n);
        }
    } finally {
        if (input != null) { input.close(); }
    }
}

当然还可以写的现代一些Java7+,编译器看try(resource = ...)中的对象是否实现了java.lang.AutoCloseable接口,如果实现了,就自动加上finally语句并调用close()方法。

1
2
3
4
5
6
7
8
public void readFile() throws IOException {
    try (InputStream input = new FileInputStream("readme.txt")) {
        int n;
        while ((n = input.read()) != -1) {
            System.out.println(n);
        }
    }
}

当然也可以一次性多读一些,read是可以有参数的,完全形态是int read(byte[] b, int off, int len)

1
2
3
4
5
6
7
8
File file = new File("1.txt");
try (InputStream inputStream = new FileInputStream(file)) {
    int size = inputStream.available();
    byte[] array = new byte[size];
    inputStream.read(array);
    result = new String(array);

} 

还有其他的实现类ByteArrayInputStream用于Byte数组转Stream可以用来测试

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws IOException {
    byte[] data = { 72, 101, 108, 108, 111, 33 };
    try (InputStream input = new ByteArrayInputStream(data)) {
        int n;
        while ((n = input.read()) != -1) {
            System.out.println((char)n);
        }
    }
}

输出流

和输入流对应,同样的最基本的抽象类是OutputStream

常见的实现类也是FileOutputStream

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
OutputStream output = new FileOutputStream("readme.txt");
output.write(72); // H
output.write(101); // e
output.write(108); // l
output.write(108); // l
output.write(111); // o
output.close();

// 多个
public void writeFile() throws IOException {
    OutputStream output = new FileOutputStream("readme.txt");
    output.write("Hello".getBytes("UTF-8")); // Hello
    output.close();
}

// 自动关闭 原理同输入
public void writeFile() throws IOException {
    try (OutputStream output = new FileOutputStream("readme.txt")) {
        output.write("Hello".getBytes("UTF-8")); // Hello
    } // 编译器在此自动为我们写入finally并调用close()
}

还有ByteArrayOutputStream,配合前面的相当于Byte[]Stream互相转换圆满了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Main {
    public static void main(String[] args) throws IOException {
        byte[] data;
        try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
            output.write("Hello ".getBytes("UTF-8"));
            output.write("world!".getBytes("UTF-8"));
            data = output.toByteArray();
        }
        System.out.println(new String(data, "UTF-8"));
    }
}

try的自动关闭是同时支持多个对象的。

1
2
3
4
5
try (InputStream input = new FileInputStream("input.txt");
     OutputStream output = new FileOutputStream("output.txt"))
{
    input.transferTo(output); // Java9+语法
}

装饰器模式

首先先了解它存在的背景是用来解决什么问题的。

  • 需要为基本的输入/输出流添加多种功能(如缓冲、签名、加密等)
  • 如果使用继承方式实现,会导致类爆炸(n 种功能组合需要 2^n 个子类)
  • 功能组合的灵活性差,难以动态添加/移除功能

装饰器模式(Decorator Pattern)是一种结构型设计模式,它通过动态地包装对象来扩展功能,而不是通过继承。就好比从ABC的组合种类变成ABC的多选框一样,当元素增多时不会爆炸增长。

以流为例的实现方式:

  • 基础组件:InputStream/OutputStream(抽象组件)
  • 具体组件:FileInputStream、ByteArrayInputStream 等(具体实现)
  • 装饰器基类:FilterInputStream/FilterOutputStream(保持组件接口)
  • 具体装饰器:BufferedInputStream、GZIPInputStream 等(添加功能)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 基础组件
public abstract class InputStream {
    public abstract int read() throws IOException;
}

// 具体组件
public class FileInputStream extends InputStream {
    public int read() { /* 实际文件读取 */ }
}

// 装饰器基类
public class FilterInputStream extends InputStream {
    protected InputStream in;
    
    protected FilterInputStream(InputStream in) {
        this.in = in;
    }
    
    public int read() throws IOException {
        return in.read();
    }
}

// 具体装饰器
public class BufferedInputStream extends FilterInputStream {
    private byte[] buffer = new byte[8192];
    private int pos, count;
    
    public BufferedInputStream(InputStream in) {
        super(in);
    }
    
    public int read() throws IOException {
        if(pos >= count) {
            fillBuffer();
        }
        return buffer[pos++] & 0xff;
    }
    
    private void fillBuffer() { /* 填充缓冲区 */ }
}
1
2
3
4
5
6
7
// 多层装饰
InputStream input = new BufferedInputStream(
                    new GZIPInputStream(
                     new FileInputStream("test.gz")));

// 透明使用
int data = input.read(); // 会自动经过缓冲、解压等处理

本质在于其没有改变基础组件的属性,只是在不同的装饰类中添加一些方法修饰本身存在的域而已

解压缩

ZipInputStream也是一种FilterInputStream

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 读取
try (ZipInputStream zip = new ZipInputStream(new FileInputStream(...))) {
    ZipEntry entry = null;
    while ((entry = zip.getNextEntry()) != null) {
        String name = entry.getName();
        if (!entry.isDirectory()) {
            int n;
            while ((n = zip.read()) != -1) {
                ...
            }
        }
    }
}
// 写入
try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(...))) {
    File[] files = ...
    for (File file : files) {
        zip.putNextEntry(new ZipEntry(file.getName()));
        zip.write(Files.readAllBytes(file.toPath()));
        zip.closeEntry();
    }
}

序列化

终于看到序列化了,之前学类的时候一定看到过,但是不知道这是个什么玩应。

Java 序列化是将 Java 对象转换为字节流的过程,以便可以将其保存到文件中、通过网络传输或在内存中缓存。反序列化则是将字节流转换回 Java 对象的过程。

就是将对象和Byte []互相转换的一个过程。

一个Java对象要能序列化,必须实现java.io.Serializable接口:

1
2
public interface Serializable {
}

Serializable接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。

序列化

ObjectOutputStream既可以写入基本类型,如intboolean,也可以写入String(以UTF-8编码),还可以写入实现了Serializable接口的Object

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import java.io.*;
import java.util.Arrays;

public class Main {
    public static void main(String[] args) throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
            // 写入int:
            output.writeInt(12345);
            // 写入String:
            output.writeUTF("Hello");
            // 写入Object:
            output.writeObject(Double.valueOf(123.456));
        }
        System.out.println(Arrays.toString(buffer.toByteArray()));
    }
}

反序列化

1
2
3
4
5
try (ObjectInputStream input = new ObjectInputStream(...)) {
    int n = input.readInt();
    String s = input.readUTF();
    Double d = (Double) input.readObject();
}

readObject()可能抛出的异常有:

  • ClassNotFoundException:没有找到对应的Class;出现于接收端没有这个类的定义。
  • InvalidClassException:Class不匹配。出现于class变动。

为了避免这种class定义变动导致的不兼容,java引入了serialVersionUID机制

1
2
3
4
5
6
7
public class Person implements Serializable {
    // 显式声明版本ID
    private static final long serialVersionUID = 1L;
    
    // 当类结构发生兼容性变更时,手动递增版本号
    // private static final long serialVersionUID = 2L;
}

生成方式

  1. IDE 自动生成(通常基于类结构哈希)
  2. 手动指定简单序列号(从1L开始)

注意:反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。

安全性

  1. 不要反序列化不受信任的数据:可能导致远程代码执行(RCE)

  2. 使用过滤机制(Java 9+):

    1
    2
    3
    4
    5
    6
    7
    
    ObjectInputFilter filter = info -> 
        info.serialClass() == null ? Status.UNDECIDED : 
        info.serialClass().getName().equals("com.example.Person") ? 
            Status.ALLOWED : Status.REJECTED;
    
    ObjectInputStream ois = new ObjectInputStream(...);
    ois.setObjectInputFilter(filter);
    

字符流

输入流

ReadInputStream相似,不过他是以char为单位读取,那个是以Byte为单位读取。

同样他也有相应的实现类FileReader CharArrayReader StringReader

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public void readFile() throws IOException {
    try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8)) {
        char[] buffer = new char[1000];
        int n;
        while ((n = reader.read(buffer)) != -1) {
            System.out.println("read " + n + " chars.");
        }
    }
}

try (Reader reader = new CharArrayReader("Hello".toCharArray())) {
}

try (Reader reader = new StringReader("Hello")) {
}

还有一个神奇的类叫做InputStreamReader,可以把一个InputStream转换成一个Reader。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
try (InputStream is = new FileInputStream("file.txt");
     InputStreamReader isr = new InputStreamReader(is, "UTF-8");
     BufferedReader br = new BufferedReader(isr)) {
    
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}

至于为什么呢?因为普通的Reader实际上是基于InputStream构造的,仍然是先读取字节流然后根据编码再转换为char

输出流

Writer就是带编码转换器的OutputStream,它把char转换为byte并输出。

已经学习了三个了,这剩下这最后一个明显可以同理可知。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
    writer.write('H'); // 写入单个字符
    writer.write("Hello".toCharArray()); // 写入char[]
    writer.write("Hello"); // 写入String
}
try (CharArrayWriter writer = new CharArrayWriter()) {
    writer.write(65);
    writer.write(66);
    writer.write(67);
    char[] data = writer.toCharArray(); // { 'A', 'B', 'C' }
}

StringWriter writer = new StringWriter();
writer.write("Hello");
writer.write(", ");
writer.write("World!");
String result = writer.toString();  // "Hello, World!"
System.out.println(result);

try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) {
    // TODO:
}

带格式化的流

PrintStream和PrintWriter和c里面的fscanf和fprintf功能是相似的。

然后就是最开始就用到的System.out也终于知道这是什么东西了和cout是一样的,也是个对象,同样也有System.err表示错误输出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.io.*;

public class PrintStreamExample {
    public static void main(String[] args) {
        try {
            // 1. 创建文件输出流
            FileOutputStream fos = new FileOutputStream("output.txt");
            
            // 2. 创建PrintStream并关联到文件输出流
            PrintStream ps = new PrintStream(fos);
            
            // 3. 使用PrintStream输出数据
            ps.println("Hello, PrintStream!");
            ps.printf("当前时间: %tT", new java.util.Date());
            ps.println();
            ps.print("PI的值大约是: ");
            ps.println(Math.PI);
            
            // 4. 关闭流
            ps.close();
            
            System.out.println("数据已写入output.txt");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import java.io.*;

public class Main {
    public static void main(String[] args)     {
        StringWriter buffer = new StringWriter();
        try (PrintWriter pw = new PrintWriter(buffer)) {
            pw.println("Hello");
            pw.println(12345);
            pw.println(true);
        }
        System.out.println(buffer.toString());
    }
}

Files

也是打脸了,前面说好不学.nio的,这个是Java7+的内容现在也不可能不兼容吧(。

Files提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个G的大文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
byte[] data = Files.readAllBytes(Path.of("/path/to/file.txt"));

// 默认使用UTF-8编码读取:
String content1 = Files.readString(Path.of("/path/to/file.txt"));
// 可指定编码:
String content2 = Files.readString(Path.of("/path", "to", "file.txt"), StandardCharsets.ISO_8859_1);
// 按行读取并返回每行内容:
List<String> lines = Files.readAllLines(Path.of("/path/to/file.txt"));

// 写入二进制文件:
byte[] data = ...
Files.write(Path.of("/path/to/file.txt"), data);
// 写入文本并指定编码:
Files.writeString(Path.of("/path/to/file.txt"), "文本内容...", StandardCharsets.ISO_8859_1);
// 按行写入文本:
List<String> lines = ...
Files.write(Path.of("/path/to/file.txt"), lines);

用到什么函数到时候再查吧。

单元测试

使用Junit进行学习

测试驱动开发TDD

只存在于理论中的理想开发模式,

测试驱动开发(TDD,Test-Driven Development)的核心过程遵循 “红-绿-重构” 循环,具体步骤如下:

1. 编写失败的测试(Red)

  • 先写测试:在写实现代码之前,先针对某个功能点编写测试用例。
  • 测试应明确:定义输入、预期输出和边界条件。
  • 运行测试:此时测试应该失败(因为功能尚未实现),进入“红”状态。
1
2
def test_add():
    assert add(2, 3) == 5  # 假设 add() 还不存在

运行测试 → 失败(Red)

2. 编写最少代码使测试通过(Green)

  • 只写能让测试通过的代码,不追求完美实现。
  • 甚至可以硬编码返回值,先让测试变绿。
  • 目标:快速验证测试逻辑是否正确。
1
2
def add(a, b):
    return 5  # 硬编码,仅为了让测试通过

运行测试 → 通过(Green)

3. 重构代码(Refactor)

  • 优化实现:去除硬编码,改进算法、提高可读性。
  • 确保测试仍通过:重构后必须重新运行测试。
  • 保持代码整洁:消除重复、优化结构。
1
2
def add(a, b):
    return a + b  # 正确的实现

运行测试 → 仍通过(Green)

4. 重复循环

  • 继续下一个功能点,重复红→绿→重构流程。
  • 逐步构建完整系统,确保每个功能都有测试覆盖。

JUnit初识

首先我们编写一个简单的阶乘类。

先建立一个软件包Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package Example;

public class Factorial {
    public static long fact(long n) {
        if (n == 0) {
            return 1;
        } else {
            return n * fact(n - 1);
        }
    }
}

在代码里面,对于Factorial右键,点击生成,测试…,然后选择Junit5,然后修复,IDEA会帮你把包自动下好,然后勾选需要测试的函数(这里只有这一个)生成即可。

你便得到了下面

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package Example;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class FactorialTest {

    @Test
    void fact() {
    }
}

然后你需要自己写@Test下面的函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package Example;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class FactorialTest {

    @Test
    void fact() {
        assertEquals(1, Factorial.fact(0));
        assertEquals(1, Factorial.fact(1));
        assertEquals(2, Factorial.fact(2));
        assertEquals(6, Factorial.fact(3));
        assertEquals(24, Factorial.fact(4));
        assertEquals(120, Factorial.fact(5));
        assertEquals(114514, Factorial.fact(6));
    }
}

这里故意错了一个,为了看看错误的输出是什么样子的

1
2
3
4
5
6
7
"C:\Program Files\Java\jdk-23\bin\java.exe" ...
org.opentest4j.AssertionFailedError: 
预期:114514
实际:720
<点击以查看差异>
......
进程已结束,退出代码为 -1

一坨断言

在 JUnit 5 中,org.junit.jupiter.api.Assertions 类提供了丰富的断言方法

基本断言assertEquals(expected, actual):验证期望值和实际值相等。

1
2
assertEquals(5, 2 + 3);
assertEquals(double expected, double actual, double delta); // 浮点数方法重载,需要指定精度

assertNotEquals(unexpected, actual):验证两个值不相等。

1
assertNotEquals(0, list.size());

assertSame(expected, actual):验证两个对象引用同一个实例。

1
assertSame(obj1, obj1);

assertNotSame(unexpected, actual):验证两个对象引用不同实例。

1
assertNotSame(obj1, obj2);

assertTrue(condition):验证条件为 true,支持 Lambda。

1
assertTrue(() -> x > 0);

assertFalse(condition):验证条件为 false

1
assertFalse(list.isEmpty());

assertNull(object):验证对象为 null

1
assertNull(error);

assertNotNull(object):验证对象不为 null

1
assertNotNull(result);

集合和数组断言assertArrayEquals(expectedArray, actualArray):验证两个数组内容相同。

1
assertArrayEquals(new int[]{1, 2}, new int[]{1, 2});

assertIterableEquals(expected, actual):验证两个 Iterable 的元素和顺序一致。

1
assertIterableEquals(List.of(1, 2), Arrays.asList(1, 2));

assertLinesMatch(expectedLines, actualLines):验证字符串列表按行匹配,支持正则表达式。

1
assertLinesMatch(List.of("Hello", "\\d+"), List.of("Hello", "123"));

异常断言assertThrows(expectedExceptionType, executable):验证代码抛出指定异常,并返回异常实例。

1
2
3
4
Exception e = assertThrows(IllegalArgumentException.class, () -> {
    throw new IllegalArgumentException("error");
});
assertEquals("error", e.getMessage());

assertDoesNotThrow(executable):验证代码不抛出异常。

1
assertDoesNotThrow(() -> calculator.add(1, 2));

组合断言assertAll(heading, executables...):组合多个断言,全部执行后报告所有失败。

1
2
3
4
5
assertAll(
    () -> assertEquals(2, 1 + 1),
    () -> assertTrue(list.isEmpty()),
    () -> assertNotNull(user)
);

超时断言assertTimeout(duration, executable):验证代码在指定时间内完成(阻塞式)。

1
2
3
assertTimeout(Duration.ofSeconds(2), () -> {
    Thread.sleep(1000);
});

assertTimeoutPreemptively(duration, executable):超时后立即终止测试(非阻塞式)。

1
2
3
assertTimeoutPreemptively(Duration.ofMillis(100), () -> {
    Thread.sleep(200); // 超时失败
});

强制失败fail(message):直接使测试失败。

1
fail("Not implemented yet");

Text Fixture

想必在之前的例子当中,在创建测试的GUI界面中看到了setUp/@BeforetearDown/@After,下面就来讲解其用处。

之前的例子是静态方法,如果我们要测试一个类的方法,那么就需要先初始化一个对象,然后再调用其方法,假如有很多个方法,都要这么干,麻烦!所以将这种做准备和清理现场的事情函数化,测试的前后进行调用即可。

看一遍代码就知道有什么用了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package Example;

public class Calculator {
    private long result = 0;

    public static int add(int a, int b) {
        return a + b;
    }

    public static int subtract(int a, int b) {
        return a - b;
    }

}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package test;

import Example.Calculator;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {
    Calculator calculator;
    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }

    @AfterEach
    void tearDown() {
        calculator = null;
    }

    @Test
    void add() {
        assertEquals(0, calculator.add(0));
        assertEquals(1, calculator.add(1));
        assertEquals(3, calculator.add(2));
        assertEquals(6, calculator.add(3));
        assertEquals(10, calculator.add(4));
    }

    @Test
    void subtract() {
        assertEquals(0, calculator.subtract(0));
        assertEquals(-1, calculator.subtract(1));
        assertEquals(-3, calculator.subtract(2));
        assertEquals(-6, calculator.subtract(3));
        assertEquals(-10, calculator.subtract(4));
    }
}

除了@BeforeEach@AferEach我们还有BeforeAllAfterAll,它们运行在所有@Test前后,也就是最开始,和最后面,并且也因此只运行一次,这导致它只能初始化静态变量,它的意义就是如果类存在一个静态变量的初始化很慢,我们还总用到它,就能快一些,其他也没啥用。

异常测试

这里的异常不是说测试有异常,而是测试你的类抛出的Exception

还是之前的阶乘类,众所周知,负数没有阶乘,所以如果参数是负数,就应该抛出异常。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package Example;

public class Factorial {
    public static long fact(long n) {
        if (n < 0) {
            throw new IllegalArgumentException("Negative numbers are not allowed");
        }
        
        if (n == 0) {
            return 1;
        } else {
            return n * fact(n - 1);
        }
    }
}

然后在对应的Test类添加函数,这里采用lamda匿名函数来写,之前的古老方法不太行,有点麻烦了,语法和js的匿名函数有点像,但是明显java才是爸爸。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package Example;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.lang.reflect.Executable;

import static org.junit.jupiter.api.Assertions.*;

class FactorialTest {

    @Test
    void fact() {
        assertEquals(1, Factorial.fact(0));
        assertEquals(1, Factorial.fact(1));
        assertEquals(2, Factorial.fact(2));
        assertEquals(6, Factorial.fact(3));
        assertEquals(24, Factorial.fact(4));
        assertEquals(120, Factorial.fact(5));
    }

    @Test
    void testNegative() {
        assertThrows(IllegalArgumentException.class, () -> {
            Factorial.fact(-1);
        });
    }
}

条件测试

一些测试可能只存在于某些系统,版本等,但是我们也不能在不同情况下给测试代码注释掉,这太麻烦了,所以就可以使用条件判断是否应该执行这些测试。

首先我们可以自定义条件@EnabledIf @DisabledIfcustomCondition可以接受ExtensionContext context, Method method参数,也可以无参,具体参数用法建议查官方文档

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Test
@EnabledIf("customCondition")
void enabled() {
    // ...
}

@Test
@DisabledIf("customCondition")
void disabled() {
    // ...
}

boolean customCondition() {
    return true;
}

然后是内置的一些常用条件:系统,版本等

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Test
@EnabledOnOs(MAC)
void onlyOnMacOs() {
}

@TestOnMac
void testOnMac() {
}

@Test
@EnabledOnOs({ LINUX, MAC })
void onLinuxOrMac() {
}

@Test
@DisabledOnOs(WINDOWS)
void notOnWindows() {
}

@Test
@DisabledOnJre(JRE.JAVA_8)
void testOnJava9OrAbove() {
}

@Test
@DisabledForJreRange(min = JAVA_9, max = JAVA_11)
void notFromJava9to11() {
}

@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void testOnlyOn64bitSystem() {
    // TODO: this test is only run on 64 bit system
}

//环境变量
@Test
@EnabledIfEnvironmentVariable(named = "DEBUG", matches = "true")
void testOnlyOnDebugMode() {
    // TODO: this test is only run on DEBUG=true
}

参数化测试

参数化测试可以显著减少重复测试代码,使测试更加清晰和易于维护。官方教程

首先我们用@ParameterizedTest替换@Test,然后用ValueSource(类型 = {数据})来传参。(类型只支持基本7种详见官方)

1
2
3
4
5
@ParameterizedTest
@ValueSource(ints = { -1, -5, -100 })
void testAbsNegative(int x) {
    assertEquals(-x, Math.abs(x));
}

上面的参数还是太简单了,如果复杂一点呢,比如多个参数传入。

最简单的方法是通过@MethodSource注解,通过编写一个同名的静态方法或者自己指定方法名来识别:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@ParameterizedTest
@MethodSource
void testCapitalize(String input, String result) {
    assertEquals(result, StringUtils.capitalize(input));
}

static List<Arguments> testCapitalize() {
    return List.of( // arguments:
            Arguments.of("abc", "Abc"), //
            Arguments.of("APPLE", "Apple"), //
            Arguments.of("gooD", "Good"));
}

还可以通过@CsvSource,它的每一个字符串表示一行,一行包含的若干参数用,分隔。

1
2
3
4
5
@ParameterizedTest
@CsvSource({ "abc, Abc", "APPLE, Apple", "gooD, Good" })
void testCapitalize(String input, String result) {
    assertEquals(result, StringUtils.capitalize(input));
}

进一步,我们还可以使用csv文件@CsvFileSource,注意path是从classpath开始的

1
2
3
4
5
@ParameterizedTest
@CsvFileSource(resources = { "/test-capitalize.csv" })
void testCapitalizeUsingCsvFile(String input, String result) {
    assertEquals(result, StringUtils.capitalize(input));
}

其它种种用的时候再去看文档吧。

多线程

基本概念

进程(Process)

  • 资源分配的基本单位,拥有独立的内存空间(如代码、数据、系统资源)。
  • 进程间相互隔离,通信需通过IPC(如管道、共享内存等)。
  • 创建、切换开销大。

线程(Thread)

  • CPU调度的基本单位,属于同一进程的线程共享进程资源(如内存、文件)。
  • 线程间可直接读写同一进程的数据,通信效率高,但需同步机制(如锁)避免冲突。
  • 创建、切换开销小。

区别

  1. 资源:进程独立,线程共享进程资源。
  2. 开销:线程更轻量,切换更快。
  3. 安全性:进程崩溃不影响其他进程;线程崩溃可能导致整个进程终止。

关系

  • 一个进程包含≥1个线程,线程是进程的执行单元。
  • 多线程可提升程序并发性能(如并行任务),多进程适合需要高隔离的场景(如浏览器多标签)。

类比:进程像工厂(拥有资源),线程像工厂中的工人(共享资源,协同工作)。

Java本身支持多线程,Java程序就是通过JVM启动主线程执行main()方法启动的。JVM 的并发模型主要依赖多线程,而不是多进程。JVM 本身是单进程的,但可以管理多线程或多进程。所以我们倾向去多线程编程来并发。

特性 定义 问题描述 解决方案 示例
可见性(Visibility) 一个线程对变量的修改,其他线程能否立即看到这个修改。 线程可能读取的是工作内存中的旧值,导致看不到其他线程的更改。 使用 volatilesynchronizedLockAtomic 类型。 主线程修改标志位,子线程未退出。
原子性(Atomicity) 一个操作要么全部完成,要么完全不执行,不能被中断。 多个线程同时执行非原子操作(如 i++),会导致数据不一致。 使用 synchronizedLock、或 AtomicInteger 等原子类。 count++ 在并发下出现计数错误。
指令重排序(Instruction Reordering) 编译器/处理器为了优化性能,可能会改变代码的实际执行顺序。 可能导致在多线程中看到不合逻辑的结果。 使用 volatile(禁止特定重排序)、synchronizedLock 单例模式的双重检查加锁,没有 volatile 会出错。

创建线程

  1. Thread类派生出一个子类,然后重写run()方法

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    // 多线程
    public class Main {
        public static void main(String[] args) {
            Thread t = new MyThread();
            t.start(); // 启动新线程
        }
    }
    
    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("start new thread!");
        }
    }
    
  2. 实例化一个Thread,然后传入一个Runnable的实例

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    public class Main {
        public static void main(String[] args) {
            Thread t = new Thread(new MyRunnable());
            t.start(); // 启动新线程
        }
    }
    
    class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("start new thread!");
        }
    }
    

    对于小型示例,可以采用lamada语法来化简

    1
    2
    3
    4
    5
    6
    7
    8
    
    public class Main {
        public static void main(String[] args) {
            Thread t = new Thread(() -> {
                System.out.println("start new thread!");
            });
            t.start(); // 启动新线程
        }
    }
    

线程拥有优先级范围是1~10,默认是5,通过Thread.setPriority(int)来实现.

线程状态

枚举常量 (Enum Constant) 描述 (Description)
BLOCKED 线程因等待监视器锁而阻塞的状态
NEW 线程尚未启动的状态
RUNNABLE 线程可运行的状态(可能正在执行或等待CPU资源)
TERMINATED 线程已终止的状态
TIMED_WAITING 线程在指定等待时间内等待的状态(如调用了 sleep(n)wait(n)
WAITING 线程无限期等待的状态(如调用了无超时的 wait()join()

中断线程

跑着跑着我不想跑了,所以干掉它。只需要该实例调用Thread.interrupt()这个方法即可。

还有一种就是使用一个flag,根据它来判断是否允许。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
    public static void main(String[] args)  throws InterruptedException {
        HelloThread t = new HelloThread();
        t.start();
        Thread.sleep(1);
        t.running = false; // 标志位置为false
    }
}

class HelloThread extends Thread {
    public volatile boolean running = true;
    public void run() {
        int n = 0;
        while (running) {
            n ++;
            System.out.println(n + " hello!");
        }
        System.out.println("end!");
    }
}

这里要主要volatile这个点,volatile 关键字是 Java 语言中的最轻量级的同步机制。在 Java 中,为了提高性能,JVM 引入了线程工作内存(Working Memory 的概念,每个线程都有自己的工作内存,而不是直接读写主内存(Main Memory)中的变量。

volatile关键字的目的是告诉虚拟机:

  • 每次访问变量时,总是获取主内存的最新值;
  • 每次修改变量后,立刻回写到主内存。

这样就实现了可见性。

在学习了线程同步后我会发现虽然t.running = false是原子操作,但是却没有实现可见性,所以还是得叠上volatitle的buff。

守护线程

daemon古希腊掌管守护的神(大雾。根据ODE Living Online: Origin 1980s: perhaps from d(isk) a(nd) e(xecution) mon(itor) or from de(vice) mon(itor)

这种线程一般是无限循环的,用来服务于其它线程,但是通常没人来负责结束它,当其它线程都结束的时候,为了能够让程序正常退出,我们需要让它也同样终止,所以我们不关心它是否执行完毕。这种无论是否结束,虚拟机都会退出的线程就是守护线程。创建守护线程只需要在start前调用setDaemon(true)即可。

注意:守护线程不应该使用任何需要关闭的资源,因为会导致有概率没有正常关闭而丢失数据。

线程同步

对于多个线程同时读写相同的变量,由于系统调度的不确定性,结果也是未知的,所以我们需要控制其什么时候能够读写。

临界区(Critical Section) 是指一段访问共享资源(如共享变量、文件、外设等)的代码。为了防止多个线程或进程同时执行这段代码导致数据不一致或其他并发问题,我们必须保证同一时刻只能有一个线程进入临界区

1
2
3
4
5
lock();            // 加锁
// --- 临界区开始 ---
// 访问共享资源(比如修改一个全局变量)
// --- 临界区结束 ---
unlock();          // 解锁

对于被JVM定义为原子操作的,并且只执行一步原子操作我们不需要考虑同步问题。但是仍需要注意可见性问题。

  1. 读取(read)和写入(write)基本类型的变量 (除了 longdouble 以外)是原子的。

    • 比如:boolean, byte, short, char, int, float, reference(引用类型
  2. volatile 类型的 longdouble 的读写是原子的

  3. volatile 变量的读写操作具有内存可见性保证

下面学习一下相关方法。

synchronized

三种方式
  1. 实例方法同步(对象锁)
1
2
3
4
5
6
7
public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}
  • 锁的是当前对象实例(this);
  • 如果多个线程操作的是同一个对象,则这些线程会按顺序执行该方法;
  • 不同实例之间互不影响。
  1. 静态方法同步(类锁)
1
2
3
4
5
6
7
public class Counter {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }
}
  • 锁的是类对象(Counter.class);
  • 所有实例共享这个锁;
  • 即使不同实例调用此方法,也会被阻塞。
  1. 同步代码块
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Counter {
    private int count = 0;
    private Object lock = new Object(); // 自定义锁对象

    public void increment() {
        synchronized (lock) { // 或者 synchronized(this)
            count++;
        }
    }
}
  • lock也可以是外部的,可以是任何Object
死锁

首先Java的锁定义成可重入锁,从而实现一个线程可以多次获取同一个锁而不会造成死锁。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class ReentrantExample {
    public synchronized void methodA() {
        System.out.println("Inside methodA");
        methodB(); // 调用另一个 synchronized 方法
    }

    public synchronized void methodB() {
        System.out.println("Inside methodB");
    }
}

分析上述代码,这个锁的object是this,如果A方法显然已经持有锁了,如果不可重入的话那么调用B的时候就无法成功获取锁进而阻塞。

下面是糟糕的死锁代码例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public void add(int m) {
    synchronized(lockA) { // 获得lockA的锁
        this.value += m;
        synchronized(lockB) { // 获得lockB的锁
            this.another += m;
        } // 释放lockB的锁
    } // 释放lockA的锁
}

public void dec(int m) {
    synchronized(lockB) { // 获得lockB的锁
        this.another -= m;
        synchronized(lockA) { // 获得lockA的锁
            this.value -= m;
        } // 释放lockA的锁
    } // 释放lockB的锁
}

假设有两个线程

  1. Thread A 进入 add()

    • 获取 lockA
    • 尝试获取 lockB 锁 ——此时还没有被释放,继续等待…
  2. Thread B 进入 dec()

    • 获取 lockB
    • 尝试获取 lockA 锁 ——此时被 Thread A 占用,也继续等待…

结果:

  • Thread A 拿着 lockA → 等待 lockB
  • Thread B 拿着 lockB → 等待 lockA
  • 互相等待对方释放锁 → 死锁发生!

示例代码恰好满足以下四个死锁发生的必要条件:

条件 描述
互斥 锁资源不能共享,一次只能被一个线程持有
请求与保持 线程在等待其他锁时不会释放已持有的锁
不可抢占 锁不能被强制从线程手中夺走
循环等待 存在一个线程链,每个线程都在等待下一个线程所持有的资源

要避免死锁,只需要打破其中一个即可。

1
2
3
4
5
6
7
8
public void dec(int m) {
    synchronized(lockA) { // 获得lockA的锁
        this.value -= m;
        synchronized(lockB) { // 获得lockB的锁
            this.another -= m;
        } // 释放lockB的锁
    } // 释放lockA的锁
}
wait和notify

如果只有锁,是无法满足我们多线程的所有需求的。比如对于一个多线程的队列,我们有pushpop两个操作,注意这个pop操作的目的是阻塞的,一定要获取一个返回值。如果朴素地实现如下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public synchronized String getTask() {
    if (queue.isEmpty()) {
        return null;
    }
    return queue.remove();
}

while (true) {
    String task = taskQueue.getTask();
    if (task != null) {
        process(task);
    }
}

这样就成了忙等待,就算没有任务也会不断循环占用cpu,效率低下。所以我们引入了wait和notify

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class TaskQueue {
    private final Queue<String> queue = new LinkedList<>();

    public synchronized void addTask(String task) {
        queue.add(task);
        notify();  // 唤醒一个等待的线程
    }

    public synchronized String getTask() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();  // 等待直到有任务
        }
        return queue.poll();
    }
}

下面给出其定义。

方法名 作用 是否释放锁 推荐用法
wait() 无限等待 ✅ 是 while(条件不满足) wait();
wait(long) 指定毫秒内等待 ✅ 是 用于超时控制
notify() 唤醒一个等待线程 ❌ 否 生产者通知消费者
notifyAll() 唤醒所有等待线程 ❌ 否 多线程安全通知

wait()notify() 必须在临界区(即同步上下文)中调用

永远使用 while 而不是 if 判断条件

如果只有一个消费者/生产者,notify() 足够;

多线程环境下推荐使用 notifyAll(),避免死锁或线程饥饿。

关于notify和notifyAll的区别,这个还是有点难理解的,对于notify是所有的在等待池wait进行竞争,拿到通知的的线程再去获得锁,而notifyAll是所有的wait都跳出等待池,然后去竞争锁,然后再去执行。比如有两个消费者,一个生产者,然后一次性生产两个,这是notifyAll就很合理。

然后以一个经典生产者-消费者的模型为例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class ProducerConsumer {
    private int count = 0;
    private final int MAX = 5;
    private final Object lock = new Object();

    class Producer implements Runnable {
        public void run() {
            while (true) {
                synchronized (lock) {
                    while (count == MAX) {
                        try {
                            System.out.println("仓库已满,生产者等待...");
                            lock.wait();
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }

                    count++;
                    System.out.println("生产了一个产品,当前库存:" + count);
                    lock.notifyAll(); // 唤醒所有等待线程(包括消费者)
                }

                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

    class Consumer implements Runnable {
        public void run() {
            while (true) {
                synchronized (lock) {
                    while (count == 0) {
                        try {
                            System.out.println("仓库为空,消费者等待...");
                            lock.wait();
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }

                    count--;
                    System.out.println("消费了一个产品,当前库存:" + count);
                    lock.notifyAll(); // 唤醒所有等待线程(包括生产者)
                }

                try {
                    Thread.sleep(800);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

    public static void main(String[] args) {
        ProducerConsumer pc = new ProducerConsumer();
        new Thread(pc.new Producer()).start();
        new Thread(pc.new Consumer()).start();
    }
}

ReentrantLock

其实是re-entrant Lock,reentrant就是可重入的意思。

synchronized看起来还是太底层了,有没有方便点的写法呢,有的有的,我们用ReentrantLock来实现,位于java.util.concurrent.locks

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Counter {
    private final Lock lock = new ReentrantLock();
    private int count;

    public void add(int n) {
        lock.lock();
        try {
            count += n;
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock位于java.util.concurrent.locks,只是内置的一个库,所以jvm不会帮你解决烂摊子,需要我们自己来处理异常,所以要try一下,记住一定要finally解锁无论是否获取成功。

一共有三种

方法 是否阻塞 是否可中断 是否支持超时
lock()
tryLock()
tryLock(long, TimeUnit) 等待但不无限 可以响应中断 支持超时
1
2
3
4
5
6
7
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        ...
    } finally {
        lock.unlock();
    }
}
Condition

想要在ReentrantLock实现等待通知我们需要使用Condition,它也是一个对象。

Conditionawait()signal()signalAll()synchronizedwait()notify()notifyAll()相对应。

Condition对象必须从Lock对象获取。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class TaskQueue {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private Queue<String> queue = new LinkedList<>();

    public void addTask(String s) {
        lock.lock();
        try {
            queue.add(s);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public String getTask() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                condition.await();
            }
            return queue.remove();
        } finally {
            lock.unlock();
        }
    }
}

ReadWriteLock

对于多线程来讲,在没有写入的时候,多线程读是允许的,所以ReentranLock管的太宽了。我们可以通过ReadWriteLock实现只允许一个线程写(其它禁止读写),允许多个线程同时只读

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Counter {
    private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
    // 注意: 一对读锁和写锁必须从同一个rwlock获取:
    private final Lock rlock = rwlock.readLock();
    private final Lock wlock = rwlock.writeLock();
    private int[] counts = new int[10];

    public void inc(int index) {
        wlock.lock(); // 加写锁
        try {
            counts[index] += 1;
        } finally {
            wlock.unlock(); // 释放写锁
        }
    }

    public int[] get() {
        rlock.lock(); // 加读锁
        try {
            return Arrays.copyOf(counts, counts.length);
        } finally {
            rlock.unlock(); // 释放读锁
        }
    }
}

注意允许同时读,并不意味着不需要加锁,因为它需要阻塞写的锁,只是可以共享而已。

StampedLock

乐观锁:乐观锁认为并发冲突发生的概率较低,不会一开始就加锁,而是在更新数据时检查是否有冲突。

悲观锁:悲观锁认为在并发环境中,数据被修改的概率很高,因此在访问数据时就会上锁,防止其他线程或事务修改数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.concurrent.locks.StampedLock;

public class SimpleExample {
    private int value = 0;
    private final StampedLock lock = new StampedLock();

    public void writeValue(int v) {
        long stamp = lock.writeLock();
        try {
            value = v;
        } finally {
            lock.unlockWrite(stamp);
        }
    }

    public int readValue() {
        // 尝试乐观读
        long stamp = lock.tryOptimisticRead();
        int val = value;
        if (!lock.validate(stamp)) {
            // 版本发生更新,变成悲观读
            stamp = lock.readLock();
            try {
                val = value;
            } finally {
                lock.unlockRead(stamp);
            }
        }
        return val;
    }
}

如果没有写入validate版本号是不变的,否则验证失败再次悲观读。StampedLock将读锁分成乐观锁和悲观锁,代价是StampedLock是不可重入锁,不能在一个线程中反复获取同一个锁。

其实望文生义即可,stamped即为有戳的,时间戳、版本戳等等,对应着其通过版本号来实现判断是否发生写入。

Semaphore

/ˈseməfɔː/直译过来是旗语,但是在cs就被翻译成信号量。

使用方法形如:

1
2
3
4
5
6
semaphore.acquire();  // 可能会阻塞等待
try {
    // 执行临界区代码(访问共享资源)
} finally {
    semaphore.release();  // 无论是否发生异常,都保证释放
}

同样也可以指定超时时间,如tryAcquire(3, TimeUnit.SECONDS)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class AccessLimitControl {
    // 任意时刻仅允许最多3个线程获取许可:
    final Semaphore semaphore = new Semaphore(3);

    public String access() throws Exception {
        // 如果超过了许可数量,其他线程将在此等待:
        semaphore.acquire();
        try {
            // TODO:
            return UUID.randomUUID().toString();
        } finally {
            semaphore.release();
        }
    }
}

Concurrent集合

对于常用的集合,Java已经贴心地帮你写了一堆线程安全版本的。

集合 常规 并发
List ArrayList CopyOnWriteArrayList
Map HashMap ConcurrentHashMap
Set HashSet / TreeSet CopyOnWriteArraySet
Queue ArrayDeque / LinkedList ArrayBlockingQueue / LinkedBlockingQueue
Deque ArrayDeque / LinkedList LinkedBlockingDeque

用法和常规的一样,这就是封装的好处(

Collections.synchronizedXXX() 方法

虽然性能不如专用并发集合,但可以将普通集合包装成线程安全版本:

1
2
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());

Atomic

java.util.concurrent.atomic包提供了一组原子操作的封装类。注意它只能保证单个变量的原子性

这个包的核心思想是利用 CAS(Compare-And-Swap) 算法来实现无锁并发控制,从而提高程序性能。至于CAS是什么再说,然后它会导致一个ABA问题。

以下是 一些常用类:

类名 用途
AtomicBoolean 原子更新布尔值
AtomicInteger 原子更新整型
AtomicLong 原子更新长整型
AtomicIntegerArray 原子更新整型数组里的元素
AtomicLongArray 原子更新长整型数组里的元素
AtomicReference<V> 原子更新引用类型
AtomicReferenceArray<E> 原子更新引用类型的数组
AtomicMarkableReference<V> 原子更新带有标记位的引用类型ABA争议
AtomicStampedReference<V> 原子更新带版本号的引用类型(解决 ABA 问题)

例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExampleWithoutThreadPool {
    // 创建一个原子整型变量,初始值为 0
    private static AtomicInteger count = new AtomicInteger(0);
    // 定义一个方法,让线程执行多次自增操作
    public static void increment() {
        for (int i = 0; i < 1000; i++) {
            count.incrementAndGet(); // 原子自增
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        // 创建并启动第一个线程
        Thread thread1 = new Thread(() -> {
            increment();
        });
        // 创建并启动第二个线程
        Thread thread2 = new Thread(() -> {
            increment();
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        // 输出最终结果
        System.out.println("Final count: " + count.get());
    }
}

总的来说它就相当于提供了一个线程安全的变量。

下面提及一下CAS

Java中 CAS 主要通过 sun.misc.Unsafe 类来实现,这是一个很底层的类,用不到它,并且其中的方法也是native的,其实不是用java写的。

CAS 操作包含三个操作数:

  1. 内存位置(V)
  2. 预期原值(A)
  3. 拟写入的新值(B)

只有当内存位置 V 的当前值等于预期值 A 时,才将内存位置 V 的值更新为 B;否则不做任何操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 // 手动用 CAS 实现 incrementAndGet
public int incrementAndGet() {
    int current;
    int next;
    do {
        current = value.get();       // 当前值
        next = current + 1;          // 目标新值
    } while (!value.compareAndSet(current, next)); // CAS 更新

    return next;
}

boolean compareAndSet(int expect, int update)

这个方法的意思是:

如果当前值等于预期值 expect,那么就将它更新为新值 update;否则不更新。 这个操作是原子性的 ,即整个判断和更新过程不会被其他线程打断。

由于 CAS 操作在并发修改共享变量操作失败是一种常见现象,因此通常会在失败后不断重试。

这就引出下一个概念自旋(Spinning):一个线程在尝试获取某个资源失败后,并不立即放弃 CPU 或进入阻塞状态,而是反复尝试、持续检查条件是否满足 ,这个过程就叫做“自旋”。

这时候你就或说了,这不就是之前被唾弃的忙等待吗?没错,从形式上看,自旋确实和传统的忙等待类似。但关键在于结果的不同。在自旋中,挂起和唤醒线程的开销较大 ,尤其是在资源被占用时间很短的情况下,频繁切换线程上下文反而效率很低,这样相比反而等待一会儿效率更高,因此,在特定场景下,自旋是一种合理的优化选择。

线程池

多线程的频繁创建和销毁需要消耗较多资源,此时可以通过线程池来复用一组线程,改善上述情况。线程池里面有若干线程,新任务出现时,分配到空闲线程,若均为忙碌状态则放入队列等待或者开辟新线程。

ExecutorService是是线程池的主要操作接口。线程池分为四种。

类型 方法 特点
固定大小线程池 newFixedThreadPool(n) 固定线程数,适合稳定负载
缓存线程池 newCachedThreadPool() 自动扩容,适合短期任务
单线程执行器 newSingleThreadExecutor() 顺序执行,保证串行
定时调度线程池 newScheduledThreadPool(n) 支持定时/周期任务

例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.util.concurrent.*;

public class FixedThreadPoolWithResult{

    public static void main(String[] args) {
        // 创建一个固定大小为3的线程池
        ExecutorService executor = Executors.newFixedThreadPool(3);

        // 用于保存任务返回结果的 Future 对象数组
        Future<Integer>[] futures = new Future[5];

        // 提交5个带返回值的任务
        for (int i = 0; i < 5; i++) {
            final int taskId = i + 1;
			Callable<Integer> task = new MyTask(taskId);
            // 提交任务并保存 Future 引用
            futures[i] = executor.submit(task);
        }

        // 获取每个任务的结果(这一步会阻塞,直到对应任务完成)
        for (int i = 0; i < futures.length; i++) {
            try {
                Integer result = futures[i].get(); // get() 会阻塞直到任务完成
                System.out.println("任务 " + (i + 1) + " 的结果是: " + result);
            } catch (InterruptedException | ExecutionException e) {
                System.out.println("获取任务结果时发生异常:" + e.getMessage());
                e.printStackTrace();
            }
        }

        // 关闭线程池
        executor.shutdown();
        System.out.println("主线程结束,所有任务已完成。");
    }
}

class MyTask implements Callable<Integer> {
    private final int taskId;
    public MyTask(int taskId) {
        this.taskId = taskId;
    }
    @Override
    public Integer call() throws Exception {
        System.out.println("正在执行任务 " + taskId + ",线程名:" + Thread.currentThread().getName());
        Thread.sleep(2000);
        return taskId * 2;
    }
}

CachedThreadPool可以实现动态扩容,其定义如下

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ScheduledThreadPoolExample {

    public static void main(String[] args) {
        // 创建一个 ScheduledThreadPool,核心线程数为2
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);

        System.out.println("任务开始时间:" + System.currentTimeMillis());

        // 1. 延迟执行一次任务
        scheduledExecutorService.schedule(() -> {
            System.out.println("延迟任务执行时间:" + System.currentTimeMillis());
            System.out.println("执行延迟任务");
        }, 3, TimeUnit.SECONDS); // 延迟3秒执行

        // 2. 周期性执行任务(初始延迟1秒后,每2秒执行一次)
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            System.out.println("周期任务执行时间:" + System.currentTimeMillis());
            System.out.println("执行周期任务");
        }, 1, 2, TimeUnit.SECONDS); // 初始延迟1秒,之后每2秒执行一次

        // 主线程休眠一段时间,确保看到输出结果
        try {
            Thread.sleep(10000); // 等待10秒让任务执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 关闭线程池
        scheduledExecutorService.shutdown();
    }
}
  • scheduleAtFixedRate固定频率执行 ,即使上一次任务还没完成,也会按设定时间启动下一次。
  • scheduleWithFixedDelay固定延迟执行 ,即每次任务结束后再等 delay 时间才开始下一次。

注意:对于FixedRate,调度器会尝试在下一个“预定时间点”启动任务 ,但如果前一个任务还没结束,它就不会真正执行这个任务,而是等它结束后再立刻执行。其本质是“单线程串行化”,这个线程池是用来同时执行不同的任务的而不是相同时间执行相同任务。

Future

对于需要返回值的情况,如果使用共享变量无疑需要考虑线程同步的问题,所以Java提供一个Future实例来方便地获得返回值。

我们只需要在任务类中实现Callable接口即可(就不用实现Runnable了)

1
2
3
4
5
class Task implements Callable<String> {
    public String call() throws Exception {
        return longTimeCalculation(); 
    }
}

使用如下

1
2
3
4
5
6
7
ExecutorService executor = Executors.newFixedThreadPool(4); 
// 定义任务:
Callable<String> task = new Task();
// 提交任务并获得Future(非阻塞)
Future<String> future = executor.submit(task);
// 从Future获取异步执行返回的结果(可能阻塞)
String result = future.get(); 

CompletableFuture

这是Java8+引入的,用于处理异步任务和非阻塞编程,增加回调功能。

最基本的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class CompletableFutureExample {
    public static void main(String[] args) {
        // 创建一个异步任务,返回一个CompletableFuture对象
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("正在执行异步任务...");
            try {
                Thread.sleep(1000); // 模拟耗时操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 10 + 20; // 返回结果
        });
        // 注册回调,在任务完成后处理结果
        future.thenAccept(result -> {
            System.out.println("异步任务完成,结果是: " + result);
        });
        System.out.println("主线程继续执行...");
        // 防止主线程提前退出
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

它的强大之处并不在此,而在于可以支持链式调用,我们模拟一个异步流程:

  1. 异步获取用户ID;
  2. 根据用户ID查询用户名;
  3. 最后将用户名转为大写并输出。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class ChainingExample {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // 第一步:获取用户ID
            System.out.println("第一步:获取用户ID");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 123;
        })
        .thenApply(userId -> {
            // 第二步:根据用户ID获取用户名
            System.out.println("第二步:根据用户ID查找用户名");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Alice";
        })
        .thenApply(username -> {
            // 第三步:处理用户名,转为大写
            System.out.println("第三步:将用户名转为大写");
            return username.toUpperCase();
        });

        future.thenAccept(result -> {
            System.out.println("最终结果: " + result);
        });

        System.out.println("主线程继续执行...");

        // 等待足够时间让异步任务完成(实际项目中应使用 join 或 thenAccept 阻塞)
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

你有一个程序,需要从两个网站(比如 A 和 B)查询用户信息,然后保存这两个结果。但为了提高响应速度:

  • 同时向 主网站 A备用网站 B 发起请求;
  • 只要其中一个返回了结果,就先处理它(anyOf());
  • 最终还是要等两个都完成(比如比较两个信息是否一致),把数据一起保存到数据库(allOf());
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class AllOfAndAnyOfExample {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        // 模拟从网站A获取用户信息
        CompletableFuture<String> futureFromA = CompletableFuture.supplyAsync(() -> {
            try {
                System.out.println("从网站A获取数据...");
                Thread.sleep(2000); // 模拟网络延迟
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "User Info from A";
        });

        // 模拟从网站B获取用户信息
        CompletableFuture<String> futureFromB = CompletableFuture.supplyAsync(() -> {
            try {
                System.out.println("从网站B获取数据...");
                Thread.sleep(3000); // 模拟网络延迟
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "User Info from B";
        });

        // 1 使用 anyOf:只要有一个网站返回了结果,就先输出提示
        CompletableFuture<Object> anyFuture = CompletableFuture.anyOf(futureFromA, futureFromB);

        anyFuture.thenAccept(result -> {
            System.out.println("第一个完成的结果是: " + result);
        });

        // 2 使用 allOf:等待两个网站都返回后,进行保存操作
        CompletableFuture<Void> allFutures = CompletableFuture.allOf(futureFromA, futureFromB);

        // 阻塞等待所有任务完成
        allFutures.join();

        // 获取两个结果并保存
        String resultA = futureFromA.get();
        String resultB = futureFromB.get();

        saveUserInfo(resultA, resultB);

        System.out.println("主线程结束");
    }

    private static void saveUserInfo(String infoA, String infoB) {
        System.out.println("开始保存两个用户信息到数据库...");
        System.out.println("保存信息 A: " + infoA);
        System.out.println("保存信息 B: " + infoB);
        System.out.println("保存完成!");
    }
}

ForkJoin

ForkJoin 是 Java 7 引入的一个用于并行执行任务的框架,它特别适合“分治”类型的任务。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class SumTask extends RecursiveTask<Integer> {
    private static final int THRESHOLD = 10; // 每个任务最多计算10个数
    private final int[] array;
    private final int start;
    private final int end;

    public SumTask(int[] array, int start, int end) {
        this.array = array;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        if (end - start <= THRESHOLD) {
            // 小任务直接计算
            int sum = 0;
            for (int i = start; i < end; i++) {
                sum += array[i];
            }
            return sum;
        } else {
            // 拆分成两个子任务
            int mid = (start + end) >>> 1;
            SumTask left = new SumTask(array, start, mid);
            SumTask right = new SumTask(array, mid, end);

            // 并行执行子任务
            left.fork();
            right.fork();

            // 合并结果
            return left.join() + right.join();
        }
    }
}

public class ForkJoinExample {
    public static void main(String[] args) {
        // 初始化一个包含100个元素的数组
        int[] array = new int[100];
        for (int i = 0; i < array.length; i++) {
            array[i] = i + 1;
        }

        // 创建 ForkJoinPool
        ForkJoinPool pool = new ForkJoinPool();

        // 提交任务
        SumTask task = new SumTask(array, 0, array.length);
        int result = pool.invoke(task);

        System.out.println("数组总和为: " + result);
    }
}

任务需要由RecursiveTask<ResultType>派生出来,然后重写compute方法。

ThreadLocal

ThreadLocal用于实现线程局部变量,有时候我们希望某些变量只属于当前线程私有即可,不需要同步机制,这是便可以使用它。

线程上下文:即每个线程执行任务时需要知道的一些信息。它帮助你在不同层次之间共享关键数据而不必层层传递。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class ThreadLocalDemo {
    // 创建一个 ThreadLocal 变量
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        Runnable task = () -> {
            // 每个线程设置自己的值
            threadLocal.set((int) (Math.random() * 100));
            System.out.println(Thread.currentThread().getName() + " 的值:" + threadLocal.get());
        };

        new Thread(task).start();
        new Thread(task).start();
    }
}
方法 描述
void set(T value) 设置当前线程的本地变量值
T get() 获取当前线程的本地变量值
void remove() 删除当前线程的本地变量值
protected T initialValue() 初始化默认值(可重写)

线程池中的线程会被复用,如果不清理 ThreadLocal,可能会导致数据污染。**每次使用完 ThreadLocal 后务必调用 remove();**即使不是线程池也是推荐这样做的。

也可以像之前的字节流一样实现AutoCloseable

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class UserContext implements AutoCloseable {
    static final ThreadLocal<String> ctx = new ThreadLocal<>();

    public UserContext(String user) {
        ctx.set(user);
    }

    public static String currentUser() {
        return ctx.get();
    }

    @Override
    public void close() {
        ctx.remove();
    }
}
1
2
3
4
try (var ctx = new UserContext("Bob")) {
    // 可任意调用UserContext.currentUser():
    String currentUser = UserContext.currentUser();
} // 在此自动调用UserContext.close()方法释放ThreadLocal关联对象

ThreadLocal 的底层实现类似于一个线程级别的 Map,其中 key 是 ThreadLocal 实例本身,value 是当前线程绑定的值。虽然可以类比为 Map<ThreadLocal, Object>,但不是以线程为 key ,而是每个线程内部保存了属于自己的 Map

虚拟线程

虚拟线程(Virtual Threads)是 Java 21 中引入的一项重大改进,在Java 19/20是预览功能。可以轻松支持成千上万个并发任务,占用内存更少,上下文切换开销更低。相当于jvm实现的协程。

多线程一般用于cpu密集型程序,遇到io密集型的话,传统线程有点过于重量级,线程上下文切换代价高(和单线程差不了多少,就好比早期redis用单线程),此时虚拟线程更好。

下面是创建方法

一、Thread.ofVirtual().start(Runnable)/Thread.startVirtualThread(Runnable)

1
2
3
4
Thread virtualThread = Thread.ofVirtual().start(() -> {
    System.out.println("Hello from virtual thread");
});
virtualThread.join(); // 可选:等待线程完成
1
2
3
4
5
Thread vt = Thread.startVirtualThread(() -> {
    System.out.println("Start virtual thread...");
    Thread.sleep(10);
    System.out.println("End virtual thread.");
});

二、Thread.ofVirtual().factory()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ThreadFactory factory = Thread.ofVirtual().factory();
Runnable task = () -> System.out.println("Task running in virtual thread");

Thread t1 = factory.newThread(task);
Thread t2 = factory.newThread(task);

t1.start();
t2.start();

t1.join();
t2.join();

三、 ExecutorService + 虚拟线程工厂

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 创建调度器:
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
// 创建大量虚拟线程并调度:
ThreadFactory tf = Thread.ofVirtual().factory();
for (int i=0; i<100000; i++) {
    Thread vt = tf.newThread(() -> { ... });
    executor.submit(vt);
    // 也可以直接传入Runnable或Callable:
    executor.submit(() -> {
        System.out.println("Start virtual thread...");
        Thread.sleep(1000);
        System.out.println("End virtual thread.");
        return true;
    });
}

四、结构化并发(Structured Concurrency)Java21+

1
2
3
4
5
6
7
8
9
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> future1 = scope.fork(() -> "Result from task 1");
    Future<Integer> future2 = scope.fork(() -> 42);

    scope.join();  // 等待所有子任务完成

    System.out.println(future1.result());
    System.out.println(future2.result());
}

Spring 核心

Spring 是一个轻量级的 Java 开发框架,用于构建企业级应用程序。

容器(Container)是一种特殊的软件组件,负责管理其他组件的生命周期、依赖关系和配置。在Spring框架中,容器是IoC(控制反转)原则的实现核心。

首先记录一下Spring项目的环境搭建,在IDEA记住选择maven编译器,然后在pom.xml里面添加

1
2
3
4
5
6
7
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>6.2.3</version>
    </dependency>
</dependencies>

注意没事多刷新刷新,然后tun模式打开,等等这迷惑的网络,一会就好了。

IoC容器

基本概念

Ioc即Inversion of Control,翻译成控制反转,这里的控制即程序流程的控制权,反转则是从对象A自己构造对象B,变成了对象A只需声明需要B,然后再通过IoC容器将对象B自动注入到对象A中。这也是所谓的好莱坞原则:Don’t call us, we’ll call you。

在传统编程中,对象自己控制其依赖的创建和管理:通常先执行类成员变量的构造函数,再执行自己的构造函数。

IoC 容器管理中,对象的创建和依赖注入由容器控制,初始化顺序有所不同,具体后面再展开。

例子如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 相当于代码中的传统写法:
class 我自己 {
    public void 做饭() {
        菜 白菜 = new 去菜市场买的白菜(); // 主动获取依赖
        白菜.切菜();
        白菜.炒菜();
    }
}

// 相当于IoC的写法:
class  {
    @Autowired // 声明"我需要一份鱼香肉丝"
    private 鱼香肉丝 外卖; // 但不关心怎么做、谁送来的
    
    public void 吃饭() {
        外卖.(); // 直接使用
    }
}

可以看出依赖注入(DI - Dependency Injection)是IoC的一种具体实现方式,核心思想是:“你需要什么,别人(容器)就给你什么,而不是你自己去拿”

同时Spring设计的IoC容器时一个无侵入容器,你的类不需要实现某个特定接口或继承某个特定类,就算你哪一天不想用了,直接删掉配置文件,注解即可。

装配Bean-xml(outdate)

首先回忆一下什么是Java Bean,封装了一堆数据和行为的一种类,这种类显然是用于作为其它类的成员变量的,所以为了体现IoC,我们需要让Spring来注入它,而不是自己构造。

代码过于长了放到仓库里吧

首先是最古老的xml文件配置:

可以看到对于UserService这个类,我们里面有MailService这个Bean类,然后xml有如下配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userService" class="org.cbk.service.UserService">
        <property name="mailService" ref="mailService" />
    </bean>

    <bean id="mailService" class="org.cbk.service.MailService" />
</beans>
  • ref的值是其它类的id
  • 每个类的id是唯一的
  • name 属性的值对应的是 Java 类中的 setter 方法名。如对于这个代码,查找名为 setMailService 的方法(根据 JavaBean 命名规范)

当然被注入的不一定要是Bean类,基本数据类型也是可以的

1
2
3
4
5
6
7
8
9
<bean id="example" class="org.test.example">
    <property name="zoneId" value="Asia/Shanghai" />
    <!-- 注入字符串 -->
    <property name="name" value="张三" />
    <!-- 注入数字 -->
    <property name="age" value="25" />
    <!-- 注入布尔值 -->
    <property name="active" value="true" />
</bean>

但是这里不是ref而是value

Main函数如下

1
2
3
4
5
6
7
8
9
public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
        // 从容器中获取已配置好的UserService实例
        UserService userService = context.getBean(UserService.class);
        User user = userService.login("bob@example.com", "password");
        System.out.println(user.getName());
    }
}

其实和传统写法唯一不同的就是前两行,虽然看着很复杂,其实第一个就是简单的获取IoC容器,Spring应用上下文,然后第二行相当于传统中的无参构造new一个对象出来。

传统方式:

1
2
3
UserService userService = new UserService();
MailService mailService = new MailService();
userService.setMailService(mailService);

对于上面的ApplicationContext,是我们日常常用的,但是其顶层接口BeanFactory也需要有所了解,BeanFactory的实现是按需创建,即第一次获取Bean时才创建这个Bean,而ApplicationContext会一次性创建所有的Bean。

注解法

上面的太麻烦了,我们有更爽的写法,但是也是更难找bug的写法。

我们常用@Component标注在被Spring管理的组件,@Autowired标注需要被注入的字段。

1
2
3
4
5
6
7
@Component
public class UserService {
    @Autowired
    MailService mailService;

    ...
}

但是呢,xml没了,我们仍需要一个配置文件,这时候它就是配置类,它需要注解@Configuration@ComponentScan两项。(类名叫什么都行)

@Configuration,表示它是一个配置类,@ComponentScan限定了搜索Bean类包的范围。假如想要扫描多个包的话,对于Java8+可以使用多个 @ComponentScan 注解,或者采用古老的@ComponentScan(basePackages = {"com.example.package1", "com.example.package2", "com.example.package3"})

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package org.cbk;

@Configuration
@ComponentScan("org.cbk")
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        UserService userService = context.getBean(UserService.class);
        User user = userService.login("bob@example.com", "password");
        System.out.println(user.getName());
    }
}

主要的变化就是 ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

补充:

除了@ComponetSpring还提供了更具体的派生注解,用于不同层次的组件:

  • @Service: 用于服务层
  • @Repository: 用于数据访问层(DAO)
  • @Controller: 用于控制器层(MVC)

@Autowired可以注解的方法有:

  1. 构造方法

    • 作用:用于依赖注入,Spring 会调用该构造方法创建 Bean,并自动注入参数依赖。
    • 特点:从 Spring 4.3 开始,如果类只有一个构造方法,@Autowired 可省略。
  2. Setter 方法

    • 作用:自动调用 Setter 方法注入依赖,方法参数为待注入的 Bean。
    • 特点:常用于可选依赖或需要动态变更依赖的场景。
  3. 普通方法(任意名称)

    • 作用:Spring 会在创建 Bean 后自动调用该方法,并注入方法参数中的依赖。
    • 特点:方法名无限制,但需有 @Autowired 注解。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    @Service
    public class OrderService {
        private final PaymentService paymentService;
        private DiscountService discountService;
        private LoggerService loggerService;
    
        // 1. 构造方法注入
        @Autowired
        public OrderService(PaymentService paymentService) {
            this.paymentService = paymentService;
        }
    
        // 2. Setter 方法注入
        @Autowired
        public void setDiscountService(DiscountService discountService) {
            this.discountService = discountService;
        }
    
        // 3. 普通方法注入
        @Autowired
        public void initLogger(LoggerService loggerService) {
            this.loggerService = loggerService;
            System.out.println("LoggerService 初始化完成!");
        }
    }
    

然后是查找注入依赖的顺序:

  1. 按类型查找byType
    • 默认行为,优先匹配唯一的 Bean。
  2. 按名称匹配byName
    • 若存在多个同类型 Bean,用方法参数名匹配 Bean 名称(如 setX(Y y) 会找名为 y 的 Bean)。
  3. @Qualifier 显式指定
    • 直接指定要注入的 Bean 名称(如 @Qualifier("beanName"))。
  4. @Primary 优先注入
    • 若有多个同类型 Bean,优先选择标记 @Primary 的。
  5. 抛出异常
    • 若无法确定唯一 Bean,报 NoUniqueBeanDefinitionException

最后补充一下Bean名称这个东西,也就是id属性,对于单例来讲直接注解即可,其它的则必须需要在配置类里面手写Bean方法。

Bean管理

Scope

默认的Bean都是单例的,如果想要每次getBean获得全新的一个,就需要添加@Scope注解,既可以@Scope("prototype"),也可以@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)

作用域 适用场景 常量 注解
Singleton 默认,全局单例 SCOPE_SINGLETON @Scope("singleton")
Prototype 每次请求新实例 SCOPE_PROTOTYPE @Scope("prototype")
Request HTTP 请求级别 SCOPE_REQUEST @RequestScope
Session HTTP 会话级别 SCOPE_SESSION @SessionScope
Application ServletContext 级别 SCOPE_APPLICATION @ApplicationScope
WebSocket WebSocket 会话级别 "websocket" @Scope("websocket")
Custom 自定义作用域 - @Scope("custom")
List注入

Spring 有个很强大的功能:如果你注入一个 List<interface>,它会自动把所有实现了 interface接口的类都放进这个列表里!

Java 的 List 是有序的,如果我们想控制这些List中实现接口类的的顺序,可以在类上加 @Order(数字) 注解。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public interface Validator {
    void validate(String email, String password, String name);
}

@Component
@Order(1)
public class EmailValidator implements Validator { ... }

@Component
@Order(2)
public class PasswordValidator implements Validator { ... }

@Component
@Order(3)
public class NameValidator implements Validator { ... }

@Component
public class Validators {

    @Autowired
    List<Validator> validators;

    public void validate(String email, String password, String name) {
        for (Validator v : validators) {
            v.validate(email, password, name); // 挨个验证
        }
    }
}
可选注入

@Autowired(required = false)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Component
public class MailService {

    @Autowired(required = false)
    private ZoneId zoneId = ZoneId.systemDefault();

    public void sendMail() {
        System.out.println("Using timezone: " + zoneId);
    }
}
  • 如果Spring容器中存在类型为 ZoneId 的Bean,则注入该Bean。
  • 如果不存在,就不会注入,而是使用代码中设定的默认值 ZoneId.systemDefault()
包外Bean

对于不在自己package管理的类(这样你就没法改源代码加@Component),可以在配置类中,自己定义一个返回实例的方法。

1
2
3
4
5
6
7
8
9
@Configuration
public class AppConfig {

    @Bean
    public ZoneId zoneId() {
        // 返回你需要的时区,比如系统默认时区或指定的 "Asia/Shanghai"
        return ZoneId.of("Asia/Shanghai");
    }
}
初始化和销毁

需要jakarta.annotation:jakarta.annotation-api,如果是SpringBoot应该是配置好的,但是Spring自己配一下吧。

我们经常需要在一个Bean完成依赖注入后执行一些初始化操作(如启动监听器、加载缓存),或者在容器关闭前进行资源清理工作(如关闭数据库连接池、注销服务注册等)。

通过@PostConstruct@PreDestroy实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Component
public class MailService {
    @Autowired(required = false)
    ZoneId zoneId = ZoneId.systemDefault();

    @PostConstruct
    public void init() {
        System.out.println("Init mail service with zoneId = " + this.zoneId);
    }

    @PreDestroy
    public void shutdown() {
        System.out.println("Shutdown mail service");
    }
}

对于这些方法要求:

  • 没有参数(即形参列表为空)
  • 返回类型为 void
  • 可以是 publicprotectedprivate 的访问权限
  • 方法名随意
FactoryBean

因为还没有详细地学设计模式,先简单学习一下工厂模式。其结构:

  1. 抽象产品(Product)接口或基类 定义产品的公共行为。
  2. 具体产品(Concrete Product)类 实现产品接口的具体类。
  3. 工厂类(Factory) 包含一个创建产品的方法,根据参数返回不同的产品实例。

实现增加新产品时不需要修改已有代码。其核心思想是:将对象的创建过程封装到一个工厂类中,客户端通过调用工厂方法来获取对象,而无需关心具体的创建逻辑。

FactoryBean<T> 是 Spring 提供的一个接口,用于自定义工厂 Bean:

1
2
3
4
5
6
7
public interface FactoryBean<T> {
    T getObject() throws Exception;
    Class<?> getObjectType();
    default boolean isSingleton() {
        return true;
    }
}
方法名 含义
T getObject() 返回由该工厂创建的对象实例
Class<?> getObjectType() 返回该工厂创建的对象类型
boolean isSingleton() 指定创建的 Bean 是否为单例(默认是)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Component
public class ZoneIdFactoryBean implements FactoryBean<ZoneId> {
    String zone = "Z";
    @Override
    public ZoneId getObject() throws Exception {
        return ZoneId.of(zone);
    }
    @Override
    public Class<?> getObjectType() {
        return ZoneId.class;
    }
}

注意

  • 在 Spring 容器中,getBean获得的的并不是 ZoneIdFactoryBean 本身,而是其 getObject() 返回的 ZoneId 实例。

  • 如果你想获取 ZoneIdFactoryBean 本体,可以通过加前缀 &

    1
    
    ZoneIdFactoryBean factory = context.getBean("&zoneIdFactoryBean", ZoneIdFactoryBean.class);
    

Spring 社区约定俗成:工厂类命名以 XxxFactoryBean 结尾

由于可以用@Bean方法创建第三方Bean,本质上@Bean方法就是工厂方法,所以,FactoryBean已经用得越来越少了。

注入Resource

对于外部文件,同样也可以注入。在org.springframework.core.io.Resource中,通过@Value(Path)来注入。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Component
public class AppService {
    @Value("classpath:/logo.txt")
    private Resource resource;

    private String logo;

    @PostConstruct
    public void init() throws IOException {
        try (var reader = new BufferedReader(
                new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
            this.logo = reader.lines().collect(Collectors.joining("\n"));
        }
    }
}

路径既可以是classpath,也可以是文件路径。

然后复习一下classpath的位置,对于maven项目

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
src/
├── main/
│   ├── java/
│   │   └── com/example/App.java
│   └── resources/
│       ├── application.properties
│       └── config/
│           └── data.json
└── test/
    ├── java/
    │   └── com/example/AppTest.java
    └── resources/
        └── test-data.xml

编译后

1
2
3
4
5
6
7
8
9
target/
├── classes/
│   ├── com/example/App.class
│   ├── application.properties
│   └── config/
│       └── data.json
└── test-classes/
    ├── com/example/AppTest.class
    └── test-data.xml

classpath即为target/classes/

注入配置

程序的配置文件是不可或缺的,当然Sping也帮我们考虑到了,对于配置类只需要加上@PropertySource即可。然后它就会帮我们读取这个key=value类型的配置文件。

1
2
app.name=My Application
app.description=This is a test application.
1
2
3
app:
  name: My Application
  description: This is a test application.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Configuration
@ComponentScan
@PropertySource("app.properties") // 表示读取classpath的app.properties
public class AppConfig {

    @Value("${app.name:Default name}")
    private String appName;

    @Value("${app.description}")
    private String appDescription;

    // Getters and setters
}

我们使用@Value进行注入,如果你用的是 YAML 格式,Spring 会自动识别嵌套结构。

${app.name} 是占位符语法,表示从配置文件中读取 app.name 的值。同时支持默认值。

对于只用到过一次的变量我们还可以直接在方法的参数里面进行注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Configuration
@ComponentScan
@PropertySource("app.properties") // 表示读取classpath的app.properties
public class AppConfig {
    String zoneId;

    @Bean
    ZoneId createZoneId(@Value("${app.zone:Z}") String zoneId) {
        return ZoneId.of(zoneId);
    }
}

Spring 会自动将配置值注入到zoneId参数中。

还有一种写法是将所有相关配置项封装在一个JavaBean中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Component
public class SmtpConfig {
    @Value("${smtp.host}")
    private String host;

    @Value("${smtp.port:25}")
    private int port;

    public String getHost() {
        return host;
    }

    public int getPort() {
        return port;
    }
}

然后,在需要读取的地方,使用#{smtpConfig.属性}

1
2
3
4
5
6
7
8
@Component
public class MailService {
    @Value("#{smtpConfig.host}")
    private String smtpHost;

    @Value("#{smtpConfig.port}")
    private int smtpPort;
}

#{smtpConfig.host}Spring Expression Language (SpEL) 的语法,它的含义是:从 Spring 容器中查找名为 smtpConfig 的 Bean,调用该 Bean 的 getHost() 方法(或直接访问 host 属性,如果可见),并返回其值。

其优势在于如果配置来源从静态文件(如application.properties)改为动态源(如数据库、API),只需调整SmtpConfig的逻辑(如添加@PostConstruct初始化方法),其他依赖的类无需改动。

但是其实有个疑问,这个SmtpConfig是从哪读的这个配置文件呢?

优先级 配置源 示例 特点说明
1 命令行参数 --smtp.host=smtp.example.com 启动时通过 --key=value 传递,优先级最高。
2 Profile 专属配置 application-prod.yml 通过 spring.profiles.active=prod 激活,覆盖默认配置。
3 主配置文件 application.ymlapplication.properties 位于 src/main/resources,通用配置。
4 环境变量 export SMTP_HOST=smtp.example.com 系统环境变量,支持大写和下划线(如 SMTP_HOST 对应 smtp.host)。
5 Java 系统属性 -Dsmtp.host=smtp.example.com 通过 -D 参数传递,优先级低于环境变量。

这里面需要解释一下的就是Profile 专属配置了。

Profile 是 Spring 提供的环境隔离机制,允许为不同环境(如开发、测试、生产)定义不同的配置。

首先它需要被激活

配置文件激活application.properties/application.yml 中指定:

1
spring.profiles.active=prod

命令行激活

1
java -jar app.jar --spring.profiles.active=prod,cloud

环境变量激活

1
export SPRING_PROFILES_ACTIVE=prod

然后是它的命名规则application-{profile}.yml

最后是它支持多个同时激活,后面覆盖前面的(正如上面的命令行激活一样)。

条件装配

对于外部类的bean,通常在配置类里面写一个构造方法,然后可以通过@Profile("参数")来实现是否构造它。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Configuration
@ComponentScan
public class AppConfig {
    @Bean
    @Profile("!test")
    ZoneId createZoneId() {
        return ZoneId.systemDefault();
    }

    @Bean
    @Profile({ "test", "master" })
    ZoneId createZoneIdForTest() {
        return ZoneId.of("America/New_York");
    }
}

除此之外我们还以使用Conditional注解来实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class OnSmtpEnvCondition implements Condition {
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return "true".equalsIgnoreCase(System.getenv("smtp"));
    }
}

@Component
@Conditional(OnSmtpEnvCondition.class)
public class SmtpMailService implements MailService {
    ...
}

这样每次写个判断都要写个类,还是太麻烦了,内置也存在一些

@ConditionalOnProperty(name="xxxx", havingValue="xxx")判断配置文件中某个属性的值是什么。

@ConditionalOnClass(name = "classname")判断类是否存在

AOP

AOP即Aspect Oriented Programming,翻译成面向切面编程,这里的aspect指的是cross-cutting concern(横切关注点),它主要就是和很多模块都有关联,但是无法合理的把它当作一个模块去实现的东西(如打印日志),Oriented是抄袭的OOP命名方式,翻译为面向。

最简单的例子就是Python的装饰器,可以装饰在函数的前后,就是一种AOP的思想。

Spring一共有两种实现方式

  • JDK 动态代理:基于接口(默认),通过 Proxy.newProxyInstance() 生成代理。

  • CGLIB 代理:基于子类继承(无接口时),通过字节码增强生成子类代理。

装配AOP

首先maven添加org.springframework:spring-aspects:6.0.0

通常使用AspectJ实现AOP,比较方便。

切面类通过添加注解@Aspect来声明。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.example.service;

public class UserService {
    
    public void addUser(String username) {
        System.out.println("添加用户: " + username);
    }
    
    public void deleteUser(String username) {
        System.out.println("删除用户: " + username);
    }
    
    public String getUser(String username) {
        System.out.println("获取用户: " + username);
        return username;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.example.aspect;

@Aspect
@Component
public class LoggingAspect {

    // 定义切点,匹配UserService中的所有方法
    @Pointcut("execution(* com.example.service.UserService.*(..))")
    public void userServicePointcut() {}
    
    // 前置通知
    @Before("userServicePointcut()")
    public void beforeAdvice(JoinPoint joinPoint) {
        System.out.println("[前置通知] 准备执行方法: " + joinPoint.getSignature().getName());
        System.out.println("[前置通知] 参数: " + java.util.Arrays.toString(joinPoint.getArgs()));
    }
    
    // 后置通知
    @After("userServicePointcut()")
    public void afterAdvice(JoinPoint joinPoint) {
        System.out.println("[后置通知] 方法执行完成: " + joinPoint.getSignature().getName());
    }
    
    // 返回后通知
    @AfterReturning(pointcut = "userServicePointcut()", returning = "result")
    public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
        System.out.println("[返回通知] 方法返回值: " + result);
    }
    
    // Around通知
    @Around("userServicePointcut()")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("[Around 开始] ==== 进入方法拦截 ====");
        
        // 1. 方法执行前逻辑
        System.out.println("[Around 前] 方法名: " + joinPoint.getSignature().getName());
        System.out.println("[Around 前] 参数: " + java.util.Arrays.toString(joinPoint.getArgs()));
        
        // 2. 执行目标方法(关键点)
        Object result = null;
        try {
            result = joinPoint.proceed(); // 这里实际调用目标方法
            
            // 3. 方法正常返回后逻辑
            System.out.println("[Around 后] 方法返回: " + result);
        } catch (Exception e) {
            // 4. 方法抛出异常时逻辑
            System.out.println("[Around 异常] 发生异常: " + e.getMessage());
            throw e; // 可以选择重新抛出或处理异常
        } finally {
            // 5. 最终执行逻辑
            System.out.println("[Around 结束] ==== 退出方法拦截 ====");
        }
        
        return result;
    }
    
}

然后配置类添加@EnableAspectJAutoProxy来启用

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "com.example")
@EnableAspectJAutoProxy
public class AppConfig {
}

当调用 UserService 的方法时,各通知的执行顺序为:

  1. @Around 的开始部分(proceed() 之前)
  2. @Before 通知
  3. 实际方法执行
  4. @AfterReturning 通知(如果方法正常返回)
  5. @After 通知
  6. @Around 的结束部分(proceed() 之后)

Bean注解装配

上面的匹配过于宽泛了,不太精准,判断一个Bean是否需要被装配的最好方法就是Bean自己知道自己是否装配。使用@Transactional注解即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
public class UserService {
    
    @Transactional
    public void addUser(String username) {
        System.out.println("添加用户: " + username);
        // 这里执行数据库操作
    }
    
    @Transactional
    public void deleteUser(String username) {
        System.out.println("删除用户: " + username);
        // 这里执行数据库操作
    }
    
    @Transactional(readOnly = true)
    public String getUser(String username) {
        System.out.println("获取用户: " + username);
        // 这里执行数据库查询操作
        return username;
    }
}

或者在类上注解

1
2
3
@Component
@Transactional
public class UserService {}

但是@Transactional 只能应用到 public 方法上

此外我们还可以通过自定义注解来限定匹配切点方法。见下面代码中的@Around("@annotation(metricTime)")

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Target(METHOD)
@Retention(RUNTIME)
public @interface MetricTime {
    String value();
}

@Component
public class UserService {
    // 监控register()方法性能:
    @MetricTime("register")
    public User register(String email, String password, String name) {
    }
}

@Aspect
@Component
public class MetricAspect {
    @Around("@annotation(metricTime)")
    public Object metric(ProceedingJoinPoint joinPoint, MetricTime metricTime) throws Throwable {
        String name = metricTime.value(); // 体现出注解的用处之二
        long start = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } finally {
            long t = System.currentTimeMillis() - start;
            // 写入日志或发送至JMX:
            System.err.println("[Metrics] " + name + ": " + t + "ms");
        }
    }
}

易错点

Spring在创建AOP代理时(无论是JDK动态代理还是CGLIB代理)不会初始化从目标类继承的字段,主要原因包括:

  1. 代理的本质:代理对象是目标类的包装器,实际字段值存储在目标对象中
  2. 性能考虑:避免不必要的字段初始化开销
  3. 一致性保证:确保通过代理访问字段时行为与直接访问目标对象一致
  4. 避免状态重复:防止代理对象和目标对象有各自独立的字段状态

这导致了一个问题,如果直接通过.访问字段,由于字段未初始化,就可能抛出空指针异常。

所以为了避免这个问题:

  1. 访问被注入的Bean时,总是调用方法而非直接访问字段;
  2. 编写Bean时,如果可能会被代理,就不要编写public final方法。

下面是进一步解释。

当创建AOP代理的时候,首先通过反射调用原类的构造函数,然后再使用CGLIB创建一个继承于原类的子类,然后这个子类里面含有一个原始实例的引用和切面特有的额LoggingAspect。这个代理类会覆写所有publicprotected方法,然后委托给里面的原始实例。所以这里出现了两个实例,一个是原始实例,另一个是代理后暴露给外面用的,当我们使用.来访问Bean的字段时候,访问的并非是原始Bean实例,而是代理的切面子类的字段,由于没有被构造,当然是空的。

Servlet 简介

Java Servlet 是用于构建 Web 应用程序的服务器端技术,在此不作详细学习,仅为后面的Spring Boot学习衔接一下,实际基本不会用这个裸的。

Servlet 容器是负责管理 Servlet 生命周期的服务器端组件。负责加载、初始化和销毁 Servlet,和接收客户端请求、调用 Servlet 的方法处理请求,并将响应返回给客户端。常见的 Servlet 容器包括:

  • Apache Tomcat:最流行的开源 Servlet 容器。
  • Jetty:一个轻量级的 Servlet 容器,适合嵌入式应用。
  • GlassFish:官方的 Java EE 容器,支持完整的 Java EE 规范。

我们通常不会直接继承Servlet而是继承其派生类,如HttpServlet, HttpServletResponseWrapper,ServletResponseWrapper ,然后覆写doGet()doPost()doPut()等方法来处理请求。

其方法签名也比较固定,形如(HttpServletRequest request, HttpServletResponse response)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;

@WebServlet("/example")
public class MyServlet extends HttpServlet {

    // 处理 GET 请求
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 获取查询参数
        String name = request.getParameter("name");
        String age = request.getParameter("age");
        
        // 设置响应内容类型
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        
        // 根据参数生成响应
        if (name != null && age != null) {
            out.println("<h1>GET Request Received!</h1>");
            out.println("<p>Name: " + name + "</p>");
            out.println("<p>Age: " + age + "</p>");
        } else {
            out.println("<h1>GET Request Received without parameters</h1>");
        }
    }

    // 处理 POST 请求
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 获取表单参数
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        
        // 设置响应内容类型
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        
        // 处理 POST 请求逻辑(如简单的用户名密码验证)
        if ("admin".equals(username) && "password123".equals(password)) {
            out.println("<h1>POST Request Received!</h1>");
            out.println("<p>Login Successful</p>");
        } else {
            out.println("<h1>POST Request Received!</h1>");
            out.println("<p>Invalid Login</p>");
        }
    }
}

最后说一下,怎么在IDEA使用Tomacat跑一下它。首先你需要装一个Tomcat它是独立的。然后在运行的地方:编辑配置->添加一个Tomcat服务器,在部署里面添加工件,然后对于应用程序上下文按心情进行修改,默认是带项目名的,这个相当于需要localhost/上下文/页面,然后运行即可。

然后遇到的坑,我下载的是最新的Tomcat 11版本,不再支持javaee,需要引入的依赖变成了jakarta.servlet。而且Servlet是大小写敏感的。

Spring 数据库

JdbcTemplate

Spring对Java原生的JDBC进行了一定的封装,让我们可以更方便的使用。

步骤如下:

  1. 在配置类,写一个初始化一个DataSource的Bean实例的方法

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/testdb");
        config.setUsername("root");
        config.setPassword("yourpassword");
        config.setDriverClassName("com.mysql.cj.jdbc.Driver");
    
        // 可选配置
        config.setMaximumPoolSize(10);
        config.setIdleTimeout(30000);
        config.setConnectionTestQuery("SELECT 1");
    
        return new HikariDataSource(config);
    }
    
  2. 在配置类,再写一个初始化一个JdbcTemplate的Bean实例的方法(@Autowired可省略)

    1
    2
    3
    4
    
    @Bean
    JdbcTemplate createJdbcTemplate( [@Autowired] DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
    
  3. 编写实体类——用JAVA字段来映射数据库表结构,包含get、set等方法

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    
    public class Employee {
        private int id;
        private String name;
        private String email;
    
        public Employee() {}
    
        public Employee(int id, String name, String email) {
            this.id = id;
            this.name = name;
            this.email = email;
        }
    
        public int getId() {
            return id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getEmail() {
            return email;
        }
    
        public void setEmail(String email) {
            this.email = email;
        }
        @Override
        public String toString() {
            return "Employee{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", email='" + email + '\'' +
                    '}';
        }
    }
    
  4. 编写DAO(Data Access Object),用于封装对数据库的访问逻辑的组件,将数据从数据库映射为 Java 对象,并对外提供统一的数据访问接口(JdbcDaoSupport 不再被推荐)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    
    @Repository
    public class EmployeeDao {
    
        private final JdbcTemplate jdbcTemplate;
    
        @Autowired
        public EmployeeDao(JdbcTemplate jdbcTemplate) {
            this.jdbcTemplate = jdbcTemplate;
        }
    
        // 创建员工表
        public void createTable() {
            String sql = "CREATE TABLE IF NOT EXISTS employees (" +
                         "id INT PRIMARY KEY AUTO_INCREMENT, " +
                         "name VARCHAR(100) NOT NULL, " +
                         "email VARCHAR(100) UNIQUE NOT NULL)";
            jdbcTemplate.execute(sql);
        }
    
        // 添加员工
        public void addEmployee(Employee employee) {
            String sql = "INSERT INTO employees (name, email) VALUES (?, ?)";
            jdbcTemplate.update(sql, employee.getName(), employee.getEmail());
        }
    
        // 根据ID获取员工
        public Employee getEmployeeById(int id) {
            String sql = "SELECT * FROM employees WHERE id = ?";
            return jdbcTemplate.queryForObject(sql, new Object[]{id}, new EmployeeRowMapper());
        }
    
        // 获取所有员工
        public List<Employee> getAllEmployees() {
            String sql = "SELECT * FROM employees";
            return jdbcTemplate.query(sql, new EmployeeRowMapper());
        }
    
        // 更新员工信息
        public void updateEmployee(Employee employee) {
            String sql = "UPDATE employees SET name = ?, email = ? WHERE id = ?";
            jdbcTemplate.update(sql, employee.getName(), employee.getEmail(), employee.getId());
        }
    
        // 删除员工
        public void deleteEmployee(int id) {
            String sql = "DELETE FROM employees WHERE id = ?";
            jdbcTemplate.update(sql, id);
        }
    
        // 根据邮箱查询员工
        public Employee findEmployeeByEmail(String email) {
            String sql = "SELECT * FROM employees WHERE email = ?";
            try {
                return jdbcTemplate.queryForObject(sql, new Object[]{email}, new EmployeeRowMapper());
            } catch (Exception e) {
                return null; // 如果没有找到返回null
            }
        }
    
        // 行映射器
        private static final class EmployeeRowMapper implements RowMapper<Employee> {
            @Override
            public Employee mapRow(ResultSet rs, int rowNum) throws SQLException {
                Employee employee = new Employee();
                employee.setId(rs.getInt("id"));
                employee.setName(rs.getString("name"));
                employee.setEmail(rs.getString("email"));
                return employee;
            }
        }
    }
    
  5. 对于需要操作数据库的Bean只需要注入DAO即可

下面对其CRUD方法做一些总结:

首先是最通用的JdbcTemplate.execute,用于执行任意sql语句

1
2
3
4
void execute(String sql);
T execute(String sql, PreparedStatementCallback<T> action)
T execute(ConnectionCallback<T> action);
T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 通常用于执行 DDL(数据定义语言)语句,不返回任何结果。如果 SQL 是查询语句或更新语句,可能不会返回有意义的数据,但如果有异常会抛出。
jdbcTemplate.execute("CREATE TABLE my_table (id INT PRIMARY KEY, name VARCHAR(255))");

// 它隐藏了创建 PreparedStatement 的细节,让你专注于参数设置和结果处理
Integer count = jdbcTemplate.execute(
    "SELECT COUNT(*) FROM users WHERE status = ?",
    ps -> {
        ps.setInt(1, 1); // 设置参数
        ResultSet rs = ps.executeQuery();
        rs.next();
        return rs.getInt(1); // 返回结果
    }
);

// 返回泛型 T 类型的结果,由你提供的 action 回调决定返回什么。
Integer count = jdbcTemplate.execute((Connection conn) -> {
    Statement stmt = conn.createStatement();
    ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM my_table");
    rs.next();
    return rs.getInt(1);
});

// 创建一个 PreparedStatement,然后交给回调进行处理。适用于需要预编译 SQL 并进行自定义处理的情况。
String sql = "SELECT COUNT(*) FROM users WHERE status = ?";
Integer count = jdbcTemplate.execute(
    con -> {
        PreparedStatement ps = con.prepareStatement(sql);
        ps.setInt(1, 1);
        return ps;
    },
    ps -> {
        ResultSet rs = ps.executeQuery();
        rs.next();
        return rs.getInt(1);
    }
);

插入(Insert更新(Update)还是删除(Delete),统一使用的方法都是int update(String sql, Object... args),返回受影响的行数。

1
2
3
4
5
6
7
8
String sql = "INSERT INTO user (name, age) VALUES (?, ?)";
jdbcTemplate.update(sql, name, age);

String sql = "UPDATE user SET name = ?, age = ? WHERE id = ?";
jdbcTemplate.update(sql, name, age, id);

String sql = "DELETE FROM user WHERE id = ?";
jdbcTemplate.update(sql, id);

但是还有一种情况是,插入后需要获取主键,我们就要使用KeyHolder

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public int addUserAndReturnId(User user) {
    String sql = "INSERT INTO user (name, age) VALUES (?, ?)";
    
    KeyHolder keyHolder = new GeneratedKeyHolder();

    jdbcTemplate.update(connection -> {
        PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
        ps.setString(1, user.getName());
        ps.setInt(2, user.getAge());
        return ps;
    }, keyHolder);

    // 返回生成的主键值
    return keyHolder.getKey().intValue();
}

主要复杂的是查询数据上,我们要考虑到把查回来的数据,重新变回POJO。对于单个对象或者值我们使用queryForObject,对于多个对象返回列表我们使用query,但是其实列表也可以只含有一个对象。

1
2
3
4
<T> List<T> query(String sql, RowMapper<T> rowMapper)
<T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args)
<T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args)
<T> T queryForObject(String sql, Class<T> requiredType, Object... args)

其主要实现的其实就是将sql查到的ResultSet,通过RowMapper构造出一个返回的对象。

首先我们可以使用BeanPropertyRowMapper<T>

要求:

  • 数据库字段名 和 POJO 属性名必须一致(或使用别名匹配)
  • POJO 必须有 无参构造函数
  • 字段类型要能自动转换(比如 int ➜ Integer
1
2
3
4
5
6
String sql = "SELECT * FROM user WHERE id = ?";
User user = jdbcTemplate.queryForObject(
    sql, 
    new BeanPropertyRowMapper<>(User.class), 
    1
);

如果字段名和java属性名不一致,但是没那么复杂的情况下,我们可以使用sql别名来简单处理一下。AS一下即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
CREATE TABLE user (
    user_id INT,
    user_name VARCHAR(50),
    user_age INT
);

public class User {
    private int id;
    private String name;
    private int age;

    // getter 和 setter 必须有
}

String sql = "SELECT user_id AS id, user_name AS name, user_age AS age FROM user WHERE user_id = ?";
User user = jdbcTemplate.queryForObject(
    sql,
    new BeanPropertyRowMapper<>(User.class),
    1
);

然后就是最常见的RowMapper<T>,用于字段名不一致或复杂映射。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class UserRowMapper implements RowMapper<User> {
    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = new User();
        user.setId(rs.getInt("uid"));        // 数据库字段叫 uid
        user.setName(rs.getString("uname")); // 数据库字段叫 uname
        user.setAge(rs.getInt("age"));
        return user;
    }
}

String sql = "SELECT * FROM user";
List<User> list = jdbcTemplate.query(sql, new UserRowMapper());

或者可以使用lamda表达式进行轻量查询。

1
2
3
4
5
6
7
8
String sql = "SELECT * FROM user WHERE id = ?";
User user = jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
    return new User(
        rs.getInt("id"),
        rs.getString("name"),
        rs.getInt("age")
    );
}, 1);

通常我们倾向于使用高级封装方法,实在不行再使用excute。

MyBatis集成

官方文档

其实上面的DAO类作为实现类是一种更底层的JdbcTemplate + 手动映射的方式。

我们还可以让DAO作为接口层,MyBatis作为映射层来实现同样操作。想要同样实现上面功能的代码,我们用相同的实体类(包含getter setter方法),然后创造Mapper接口。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Mapper
public interface EmployeeMapper {

    @Insert("INSERT INTO employees (name, email) VALUES (#{name}, #{email})")
    void addEmployee(Employee employee);

    @Select("SELECT * FROM employees WHERE id = #{id}")
    Employee getEmployeeById(int id);

    @Select("SELECT * FROM employees")
    List<Employee> getAllEmployees();

    @Update("UPDATE employees SET name = #{name}, email = #{email} WHERE id = #{id}")
    void updateEmployee(Employee employee);

    @Delete("DELETE FROM employees WHERE id = #{id}")
    void deleteEmployee(int id);

    @Select("SELECT * FROM employees WHERE email = #{email}")
    Employee findEmployeeByEmail(String email);
}

这里面我们用的是注解的方式,但是传统来讲用的是xml配置,xml方式对于较为复杂的语句有缩进之类的格式可以更加美观一些。所以也给出xml写法。注意文件名称要和Mapper类一致EmployeeMapper.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<mapper namespace="com.example.mapper.EmployeeMapper">

    <insert id="addEmployee">
        INSERT INTO employees (name, email) VALUES (#{name}, #{email})
    </insert>

    <select id="getEmployeeById" resultType="Employee">
        SELECT * FROM employees WHERE id = #{id}
    </select>

    <select id="getAllEmployees" resultType="Employee">
        SELECT * FROM employees
    </select>

    <update id="updateEmployee">
        UPDATE employees SET name = #{name}, email = #{email} WHERE id = #{id}
    </update>

    <delete id="deleteEmployee">
        DELETE FROM employees WHERE id = #{id}
    </delete>

    <select id="findEmployeeByEmail" resultType="Employee">
        SELECT * FROM employees WHERE email = #{email}
    </select>

</mapper>

由于在Spring中用MyBatis我懒得看了,所以直接来Spring Boot启用方式。添加依赖,配置文件,然后启动类加上扫描注解即可。

1
2
3
4
5
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.model
1
2
3
4
5
6
7
@SpringBootApplication
@MapperScan("com.example.mapper")
public class MyApp {
    public static void main(String[] args) {
        SpringApplication.run(MyApp.class, args);
    }
}

Hibernate集成

在使用 JdbcTemplate 进行数据库查询时,核心机制是通过 RowMapper 实现 ORM(Object-Relational Mapping),即在关系型数据与 Java 对象之间进行转换。Hibernate 是一个封装程度高、全自动的 ORM 框架。

环境配置略,建议现配现搜。这个框架同样支持注解和xml,但是我们只学习新的注解。

首先需要定义一个实体类:注意到这个实体类通过注解实现数据库的字段与对象属性的映射。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Entity
@Table(name = "student")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(name = "name")
    private String name;

    @Column(name = "age")
    private int age;

    // 构造器、getter、setter 省略
}

由于是在Spring里面集成,所以我们不使用传统的hibernate.cfg.xml进行配置,在配置类通过LocalSessionFactoryBean管理即可。

如果用的是Spring Boot就更简单了,只要写好配置文件即可,会帮你自动创建Bean。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test_db
    username: root
    password: root123
    driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

下面就是对表进行CRUD:

我们既可以直接使用SessionFactory,也可以实现一个DAO层更分离、清晰地调用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Service
@Transactional
public class StudentService {

    @Autowired
    private SessionFactory sessionFactory;

    // CREATE
    public void addStudent(Student student) {
        sessionFactory.getCurrentSession().save(student);
    }

    // READ
    public Student getStudent(int id) {
        return sessionFactory.getCurrentSession().get(Student.class, id);
    }

    // UPDATE
    public void updateStudent(Student student) {
        sessionFactory.getCurrentSession().update(student);
    }

    // DELETE
    public void deleteStudent(int id) {
        Student s = getStudent(id);
        if (s != null) {
            sessionFactory.getCurrentSession().delete(s);
        }
    }
}

也可以

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Repository
public class StudentDao {

    @Autowired
    private SessionFactory sessionFactory;

    public void save(Student student) {
        sessionFactory.getCurrentSession().save(student);
    }

    public Student findById(int id) {
        return sessionFactory.getCurrentSession().get(Student.class, id);
    }

    public void update(Student student) {
        sessionFactory.getCurrentSession().update(student);
    }

    public void delete(Student student) {
        sessionFactory.getCurrentSession().delete(student);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
@Transactional
public class StudentService {

    @Autowired
    private StudentDao studentDao;

    public void addStudent(Student student) {
        studentDao.save(student);
    }

    public Student getStudent(int id) {
        return studentDao.findById(id);
    }

    public void updateStudent(Student student) {
        studentDao.update(student);
    }

    public void deleteStudent(int id) {
        Student s = getStudent(id);
        if (s != null) {
            studentDao.delete(s);
        }
    }
}

总结:Hibernate的全自动,主要体现在调用的时候会根据实体类自动生成了SQL语句。但是我们也可以使用HQL或者原生SQL自己写。

1
2
session.createQuery("FROM Student WHERE age > 18").list();
session.createSQLQuery("SELECT * FROM student").list();

所以说,实体类的注解很重要,下面展开来详细说一下。

@Column是核心注解

属性名 含义 默认值
name 映射到的数据库列名 字段名(自动映射)
nullable 是否允许为 NULL true
length 字符串最大长度(对应 VARCHAR) 255
unique 是否唯一 false
insertable 是否参与插入语句 true
updatable 是否参与更新语句 true
columnDefinition 自定义 SQL 类型
precision 精度(仅限数值) 0
scale 小数点位数(仅限数值) 0

对于常用注解的整理

注解 用于 主要参数及默认值 说明
@Column 字段 namenullable=trueunique=falselength=255 精细控制列名、是否为空、唯一、长度等
@Table nameindexes 指定表名和索引
@Id 字段 主键标识
@GeneratedValue 字段 strategy=GenerationType.AUTO 主键生成策略,常用策略有AUTO、IDENTITY、SEQUENCE、TABLE
@Enumerated 字段 value=EnumType.ORDINAL(默认)或STRING 枚举值保存方式:ORDINAL数字,STRING字符串
@Temporal 字段 value=TemporalType.TIMESTAMP(默认) 日期类型,常用DATE、TIME、TIMESTAMP
@Transient 字段 不映射到数据库
@Lob 字段 大字段,映射为TEXT或BLOB

JPA集成

JPA是JAVA EE的一个ORM标准,上面使用HIbernate是其一种实现方式。JPA 作为Java 官方定义的标准接口,只要你使用 JPA API,底层可以随时切换成不同的实现,比如 Hibernate、EclipseLink、OpenJPA 等,代码层面无需改动(理论上),也就是可以不在一颗树上吊死。

Hibernate作为其常见实现,我们还是以它为例,只是写法和上面略有不同。

一直到实体类和上面的配置都是一摸一样的。

区别在于我们要实现JPA的接口,不再直接使用LocalSessionFactoryBean。

JpaRepository<T, ID> 是 Spring Data JPA 提供的一个接口,内置了常用的数据库操作方法,比如:

  • save(S entity) — 保存或更新实体
  • findById(ID id) — 根据ID查找
  • findAll() — 查找所有
  • deleteById(ID id) — 根据ID删除
  • count() — 统计数量
  • 等等…

只需要:

1
2
3
@Repository
public interface StudentRepository extends JpaRepository<Student, Integer> {
}

就已经有了所有基础操作,不用写任何实现代码,Spring Data JPA 在运行时会帮你生成代理类自动实现。

如果想要手动写自定义的查询可以声明按照一定**命名规则的方法**或者使用JPQL/原生SQL

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Repository
public interface StudentRepository extends JpaRepository<Student, Integer> {
    // 查询名字完全匹配的学生列表
    List<Student> findByName(String name);

    // 查询年龄大于指定值的学生列表
    List<Student> findByAgeGreaterThan(int age);

    // 查询名字包含指定字符串的学生列表(模糊查询)
    List<Student> findByNameContaining(String keyword);

    // 查询年龄在指定范围内的学生
    List<Student> findByAgeBetween(int startAge, int endAge);
    
    // JPQL
    
    @Query("select s from Student s where s.name = :name")
    List<Student> findByNameJPQL(@Param("name") String name);
    
    @Query("select s from Student s where s.age > :age order by s.age desc")
    List<Student> findOlderStudentsOrderByAgeDesc(@Param("age") int age);
    
    // SQL 可能有莫名其妙的bug?
    
    @Query(value = "SELECT * FROM student WHERE name = :name", nativeQuery = true)
    List<Student> findByNameNative(@Param("name") String name);

    @Query(value = "SELECT * FROM student WHERE age BETWEEN :start AND :end", nativeQuery = true)
    List<Student> findByAgeBetweenNative(@Param("start") int start, @Param("end") int end);
       
}

最后在StudentService中,将原先的SessionFactory换成StudentRepository即可。

MySQL原理

MySQL是常见的关系型数据库,即二维表格的结构。在5.1版本之后默认存储引擎是 InnoDB,该引擎提供了:

  • 行级锁(Row-Level Locking)

  • 支持事务(Transactions)

  • 自动崩溃恢复(Crash Recovery)

  • 外键支持(Foreign Key Constraints)

InnoDB

存储

InnoDB 的存储结构层次可以分为四个核心概念,从大到小依次是:

Tablespace(表空间) → Segment(段) → Extent(区) → Page(页)

这些层次构成了 InnoDB 数据存储的物理结构,下面逐一详细解释它们的作用和关系:

Tablespace(表空间)是 InnoDB 存储数据的最高层单位,物理上是一个或多个文件:

  • 共享表空间(默认):系统表空间 ibdata1,存储系统表、undo log、doublewrite buffer 等。
  • 独立表空间:每个表一个 .ibd 文件(需要 innodb_file_per_table=ON
  • 临时表空间:处理临时表和排序等中间结果

Segment(段)是表或索引的一种逻辑结构单位,每个段由多个区(extent)组成,用于按需扩展存储。一个表至少对应一棵 B+ 树(主键或隐式主键)每个二级索引再各自对应一棵 B+ 树。这些B+树各自使用不同的 segment页空间 来组织各自的数据结构。

类型 所属结构 存储内容 分配单位 是否可见数据
Leaf node segment 索引(B+ 树) 表的实际数据或索引键+主键值 Extents(1MB) ✅ 是
Non-leaf segment 索引(B+ 树) 键值 + 指向下级节点的指针 Extents(1MB) ❌ 否
Rollback segment Undo 日志系统 数据修改前的版本记录 Page(16KB) ✅(通过 MVCC)

当叶子节点段初始的时候,只分配一个page,当初始的单个Page(16KB)用完时,InnoDB会分配一个完整的Extent(通常为1MB,包含64个连续的16KB Page),但不会立即使用整个Extent,而是按需从中分配Page。

Extent(区)是 InnoDB 空间分配的基本单位,一个 extent 包含多个页:

  • 每个 extent 大小为 1MB
  • 一个 extent 包含 64个连续的页(每页 16KB)

随着数据量增长,InnoDB采用以下分配策略:

  1. 碎片Page分配阶段(最初32个Page):
    • 每次只分配单个Page
    • 直到段中已分配Page总数达到32个(共512KB)
  2. 完整Extent分配阶段(超过32个Page后):
    • 改为每次分配完整的Extent(1MB,64个Page)
    • 这种批量分配提高了空间管理效率

Page 是 InnoDB 中最小的存储单位,每页默认大小为 16KB。对应着HDD硬盘的扇区。

其布局如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
┌────────────────────────────┐
│ File Header(38 bytes)     │ ← 页头
├────────────────────────────┤
│ Page Header(56 bytes)     │ ← 页元数据(如页类型、记录数等)
├────────────────────────────┤
│ Infimum & Supremum(26 bytes)│ ← 特殊记录:最小/最大哨兵
├────────────────────────────┤
│ User Records(Rows)         │ ← 你的表中实际行记录就在这里
├────────────────────────────┤
│ Free Space                  │ ← 插入时使用
├────────────────────────────┤
│ Page Directory              │ ← 存储 slot 数组,快速定位 row
├────────────────────────────┤
│ File Trailer(8 bytes)      │ ← 校验和 & 页双写缓冲校验
└────────────────────────────┘

一行记录(Row)并不是只存用户的字段值,它包含了 系统信息(Extra Info)+ 用户列(col1, col2…),结构如下:

1
2
3
4
5
┌───────────────────────────┐
│ Row Header (Extra Info)   │ ← 系统字段,固定或变长
├───────────────────────────┤
│ User Columns (col1, col2) │ ← 你表中定义的字段
└───────────────────────────┘

索引

InnoDB 所有的索引(聚簇和二级索引)都是基于 B+ 树(Balanced Plus Tree) 实现的:

  • 所有数据都保存在叶子节点
  • 非叶节点只存储**键值(key)**和指向子节点的指针
  • 节点之间通过页指针(page number)连接

索引分为:

  1. 聚簇索引(Clustered Index) 默认索引:

    • 表的数据物理顺序就是按聚簇索引排的
    • 每个表只能有一个聚簇索引
    • 聚簇索引的 叶子节点保存整行数据

    如果没有显式主键,InnoDB 会选择:

    • 一个 非 NULL 唯一键
    • 否则会自动创建一个 6字节隐藏 ROW_ID
  2. 二级索引(辅助索引)(Secondary Index) 可有多个

    建在非主键字段上

    叶子节点只存储索引列值 + 聚簇索引主键值(逻辑指针)

    查全行时需要回表(回到聚簇索引)

事务

事务是数据库操作的最小单位,InnoDB 完全支持 ACID:

  1. ACID 特性
  • A(Atomicity,原子性):事务中的所有操作,要么全部成功,要么全部失败。
  • C(Consistency,一致性):事务执行前后,数据库状态应保持一致。
  • I(Isolation,隔离性):多个事务之间应互不干扰。
  • D(Durability,持久性):事务提交后其结果应永久保存。

InnoDB有两种锁

  • 共享锁(S Lock)– 读锁 • 允许多个事务对同一条记录加共享锁
  • 排他锁(X Lock)– 写锁 • 只允许一个事务对一条记录加锁
  1. 事务的控制
  • BEGIN / START TRANSACTION:开启事务
  • COMMIT:提交事务
  • ROLLBACK:回滚事务

MySQL 支持四种标准的事务隔离级别:

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED
READ COMMITTED ×
REPEATABLE READ × ×
SERIALIZABLE × × ×

InnoDB 默认的隔离级别是 REPEATABLE READ,通过 MVCC 解决不可重复读和部分幻读问题。

下面是对隔离级别的解释,为了平衡性能和一致性,定义了四种级别,每种级别定义了允许那些并发现象发生。

问题名 说明
脏读 (Dirty Read) 事务A读取了事务B尚未提交的数据。如果B回滚了,那A读到的数据就是“脏”的。
不可重复读 (Non-repeatable Read) 事务A两次读取同一条记录,但中间事务B修改了该记录并提交,导致A读到不一致的数据。
幻读 (Phantom Read) 事务A两次执行相同的查询,第二次却看到“幻影”一样新增或删除的记录(比如B插入或删除了符合条件的新记录)。

不可重复读对应着update的操作,是相对于一行的属性而言的,幻读则是对应着insertdelete操作,是相对获取的总数据量而言的。

MVCC

多版本并发控制(Multiversion concurrency control),是用来实现并发读的一种机制。其思想和StampedLock是很像的。

对于每一条记录,其实还存着两个隐藏的字段:

  • trx_id:创建或修改该行的事务ID

  • roll_pointer:指向该记录旧版本的Undo Log

Undo Log作用:

  • Undo日志保存了行的旧版本数据。
  • 当行被修改或删除时,新版本的trx_id指向当前事务,roll_pointer指向旧版本的Undo日志。
  • 通过roll_pointer链,InnoDB能访问该行之前的多个版本

每个事务在启动时会获取一个事务ID(trx_id),以及一个快照,快照记录了“当前活动事务”集合。事务在读数据时,不直接访问最新行版本,而是根据自身快照判断该版本是否可见。

字段 作用
min_trx_id 定义版本的“开始时间”,用来判断该版本是否对当前事务可见。
max_trx_id 定义版本的“结束时间”,如果当前事务ID大于等于此值,版本不可见。

其可见性判断逻辑如下,假设事务T读取行R:

  • 如果 R 是由 T 自己生成的:可见
  • “版本可见性条件:R.min_trx_id ≤ T.trx_id < R.max_trx_id
  • 如果 R 被未提交事务修改:不可见
  • 如果 R 已被删除且删除操作在 T 前:不可见

Spring Web

此处内容皆为了解即可,实际都是使用Spring Boot再封装一层再调用。

MVC架构

之前已经用swing写过桌面端的程序了,web端的mvc概念和其类似。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
project-name/
├── src/
   ├── main/
      ├── java/               # Java 源代码
         └── com/
             └── example/
                 ├── config/ # Spring 配置类
                 ├── controller/ # 控制器
                 ├── service/    # 业务逻辑层
                 ├── dao/        # 数据访问层
                 ├── model/      # 实体类
                 └── Application.java # 主启动类
      ├── resources/          # 资源文件
         ├── static/        # 静态资源 (JS, CSS, 图片等)
         ├── templates/     # 模板文件 (Thymeleaf, FreeMarker等)
         ├── application.properties # 应用配置
         └── messages.properties # 国际化消息
      └── webapp/
          ├── WEB-INF/
             ├── views/     # JSP 视图文件
             └── web.xml    # 传统部署描述符
          └── index.jsp      # 首页
   └── test/                  # 测试代码
       ├── java/              # 测试 Java 代码
       └── resources/         # 测试资源
├── target/                    # 构建输出目录
├── pom.xml                    # Maven 项目配置文件
└── README.md                  # 项目说明文件project-name/
├── src/
   ├── main/
      ├── java/               # Java 源代码
         └── com/
             └── example/
                 ├── config/ # Spring 配置类
                 ├── controller/ # 控制器
                 ├── service/    # 业务逻辑层
                 ├── dao/        # 数据访问层
                 ├── model/      # 实体类
                 └── Application.java # 主启动类
      ├── resources/          # 资源文件
         ├── static/        # 静态资源 (JS, CSS, 图片等)
         ├── templates/     # 模板文件 (Thymeleaf, FreeMarker等)
         ├── application.properties # 应用配置
         └── messages.properties # 国际化消息
      └── webapp/
          ├── WEB-INF/
             ├── views/     # JSP 视图文件 里面是.html
             └── web.xml    # 传统部署描述符
          └── index.jsp      # 首页
   └── test/                  # 测试代码
       ├── java/              # 测试 Java 代码
       └── resources/         # 测试资源
├── target/                    # 构建输出目录
├── pom.xml                    # Maven 项目配置文件
└── README.md                  # 项目说明文件

需要在配置类加上@EnableWebMvc来启用。

然后使用来两个Bean用来做配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
 * 配置静态资源处理
 */
@Bean
WebMvcConfigurer createWebMvcConfigurer() {
    return new WebMvcConfigurer() {
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            // 处理静态资源路径映射
            registry.addResourceHandler("/static/**")
                    .addResourceLocations("/static/")
                    .setCachePeriod(3600);  // 缓存时间(秒)

            // 可以添加更多资源路径
            registry.addResourceHandler("/assets/**")
                    .addResourceLocations("classpath:/assets/");
        }
    };
}

/**
 * 配置Pebble模板视图解析器
 */
@Bean
ViewResolver createViewResolver(@Autowired ServletContext servletContext) {
    // 创建Pebble引擎
    PebbleEngine engine = new PebbleEngine.Builder()
            .autoEscaping(true)  // 自动HTML转义
            .cacheActive(false)  // 开发时禁用缓存
            .loader(new Servlet5Loader(servletContext))  // Servlet 5.0+ 加载器
            .build();

    // 配置Pebble视图解析器
    PebbleViewResolver viewResolver = new PebbleViewResolver(engine);
    viewResolver.setPrefix("/WEB-INF/templates/");  // 模板前缀
    viewResolver.setSuffix("");  // 模板无后缀,或可设置为".pebble"
    viewResolver.setCache(false);  // 开发时禁用缓存
    viewResolver.setContentType("text/html;charset=UTF-8");  // 设置编码
    return viewResolver;
}

其它的就按照正常的MVC类来,里面的方法通过添加@GetMapping("url")处理指定url的请求,也可以在类上使用@RequestMapping("/url")实现对整个类填一个url前缀。类似的我们也有@PostMapping,@PutMapping等。

RESTful API

REST(Representational State Transfer)是一种软件架构风格,强调基于 HTTP 的无状态通信。它不绑定任何编程语言,在 Java 中广泛使用。微软文档

HTTP 方法 Java 注解(Spring Boot) 示例用途
GET @GetMapping 获取数据
POST @PostMapping 创建资源
PUT @PutMapping 更新整个资源
DELETE @DeleteMapping 删除资源

资源建模:资源是 REST 的核心,比如:用户 (User)、订单 (Order)、商品 (Product),在 Java 中通常用 POJO(Plain Old Java Object)表示。

其基本思想如下:

  • 使用名词而非动词表示资源
  • 使用复数形式表示集合
  • 使用小写字母和连字符(-)分隔单词

它相当于把HTTP方法视作动词,资源为名字,连接成为一个操作。然后传输的数据的格式为json

CORS跨域

现在通常前后端分离部署,甚至都部署在一个服务器上,这导致ip也会不同,为了安全,默认是禁止跨域访问的,所以需要配置一下。

  1. 使用@CrossOrigin注解

可以在控制器类或方法上使用@CrossOrigin注解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "http://example.com") // 类级别
public class MyController {
    
    @GetMapping("/items")
    @CrossOrigin(origins = {"http://example.com", "http://another-domain.com"}) // 方法级别
    public List<Item> getItems() {
        // ...
    }
}
  1. 全局CORS配置

使用CorsRegistry进行全局配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void addCorsMappings(CorsRegistry registry) {
      registry.addMapping("/api/**")
          .allowedOrigins("http://example.com", "http://another-domain.com")
          .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
          .allowedHeaders("*")
          .allowCredentials(true)
          .maxAge(3600);
  }
}
  1. 使用CorsConfigurationSource (适用于Spring Security) 略

Spring 缓存

MyBatis 缓存

这里补充一下SqlSession的概念,前面的MyBatis其实是没有显式使用SqlSeesion的,是通过,因为Spring Boot的配置会自动帮我们做很多事情,在调用@Mapper接口方法的时候会自动获取SqlSession实例,然后执行语句,自动提交关闭事务。

MyBatis 的缓存机制主要分为 一级缓存二级缓存,它们用于减少数据库访问次数、提高性能。

一级缓存(本地缓存):

  • 默认开启
  • SqlSession 级别的缓存(同一个 SqlSession 中的查询会缓存起来)
  • 缓存的作用范围仅限于当前 SqlSession 对象内

触发条件:

当执行查询时,如果本 SqlSession 中执行过相同的查询(SQL + 参数相同),则会从缓存中读取结果。

失效的情况:

  1. 执行了 INSERTUPDATEDELETE 操作(会清空一级缓存)
  2. SqlSession 关闭(缓存随之销毁)
  3. 查询条件不同
  4. 手动清空缓存(如 sqlSession.clearCache()

二级缓存(全局缓存):

  • 需要手动开启
  • 基于 Mapper 映射级别,多个 SqlSession 共享
  • 使用场景:同一个 Mapper 中多个 SqlSession 查询相同数据

支持的淘汰策略:

  • LRU:最近最少使用(默认)
  • FIFO:先进先出
  • SOFT:软引用
  • WEAK:弱引用

失效的情况:

  • Mapper 中的 update/delete/insert 操作默认会刷新对应缓存
  • 查询时未命中缓存或参数不同

默认我们不开启二级缓存

总体感觉就是MyBatis的缓存没什么用啊。MyBatis 缓存的默认行为确实比较保守、作用范围有限、不易察觉,甚至很多时候根本“没命中”

Redis

全称 Remote Dictionary Server,常用的key-value型数据库,支持支持 String、List、Set、Hash、ZSet(有序集合)等。

我们有老牌的Jedis库,但是我们通常使用Spring Data Redis来支持它。

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
1
2
3
4
5
6
7
8
@Autowired
private StringRedisTemplate redisTemplate;

public void test() {
    redisTemplate.opsForValue().set("name", "Bob");
    String name = redisTemplate.opsForValue().get("name");
    System.out.println(name); // Bob
}

下面是常用的数据结构操作

类型 操作方法示例
String opsForValue().set(key, val) / opsForValue().get(key)
Hash opsForHash().put(hk, field, val) / opsForHash().get(hk, field)
List opsForList().leftPush(key, val) / opsForList().rightPop(key)
Set opsForSet().add(key, val) / opsForSet().members(key)
ZSet opsForZSet().add(key, val, score) / opsForZSet().range(key, 0, -1)
Bitmap opsForValue().setBit(key, offset, true) / opsForValue().getBit(key, offset)

除此之外Redis还支持lua脚本,lua脚本的执行具有原子性,和早期Redis的单线程模型很匹配。通过EVAL命令来执行。但是也正是其原子性,导致其如果很复杂的话会导致阻塞,并且lua作为动态语言出错调试成本高,所以应避免写复杂脚本。

例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Service
public class StockService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public String deductStock(String productId, int quantity) {
        String key = "stock:" + productId;

        String luaScript =
                "local stock = tonumber(redis.call('get', KEYS[1]))\n" +
                "local purchase = tonumber(ARGV[1])\n" +
                "if stock == nil then return -2 end\n" +
                "if stock < purchase then return -1 end\n" +
                "redis.call('decrby', KEYS[1], purchase)\n" +
                "return stock - purchase";

        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(luaScript);
        redisScript.setResultType(Long.class);

        Long result = redisTemplate.execute(
                redisScript,
                Collections.singletonList(key),
                String.valueOf(quantity)
        );

        if (result == null) {
            return "执行失败";
        } else if (result == -2) {
            return "库存未初始化";
        } else if (result == -1) {
            return "库存不足";
        } else {
            return "扣减成功,剩余库存:" + result;
        }
    }
}

下面实现四个典型功能:

一、缓存

1
2
3
4
5
6
// 缓存商品信息
redisTemplate.opsForValue().set("product:1001", "iPhone 15", Duration.ofMinutes(10));

// 获取缓存
String product = (String) redisTemplate.opsForValue().get("product:1001");
System.out.println("商品信息:" + product);

二、计数器

1
2
3
4
5
// 每访问一次就计数
Long count = redisTemplate.opsForValue().increment("visit:user:123");

// 查询当前访问次数
System.out.println("用户访问次数:" + count);

三、集合运算

1
2
3
4
5
6
7
8
9
// 用户 1、2、3 点赞了帖子 A、B、C
redisTemplate.opsForSet().add("like:user1", "A", "B");
redisTemplate.opsForSet().add("like:user2", "B", "C");
redisTemplate.opsForSet().add("like:user3", "A", "C");

// 计算所有用户点赞的帖子合集(并集)
Set<Object> union = redisTemplate.opsForSet().union("like:user1", Arrays.asList("like:user2", "like:user3"));

System.out.println("总共点赞过的帖子:" + union);

四、布隆过滤器

布隆过滤器(Bloom Filter)是一种非常高效的空间节省型概率数据结构,用来判断一个元素是否可能存在于一个集合中。

它的特点是:

  • 可能存在:返回 true 说明 可能存在
  • 一定不存在:返回 false 说明 一定不存在

使用Redisson

1
2
3
4
<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Autowired
private RedissonClient redissonClient;

public void useBloomFilter() {
    RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("bloom:email");
    bloomFilter.tryInit(1000000L, 0.01); // 初始化大小和误判率
    bloomFilter.add("alice@example.com");

    boolean mightExist = bloomFilter.contains("alice@example.com");
    System.out.println("是否可能存在:" + mightExist);
}

Spring 自带缓存

虽然它跨平台做的很好,但是默认功能十分基础。

  • 不支持 分布式缓存(如 Redis 集群)

  • 不支持 过期策略(默认实现中缓存永不过期)

  • 不支持 缓存预热、淘汰机制

  • 不支持 异步加载、自动刷新

  • 不支持 缓存穿透、雪崩、击穿等容错机制

所以没人用它,无法写出一些测试代码。

Redis 内存管理

  1. 内存压缩
  • 对某些数据结构(如 hash, list, set)启用压缩表示(如 ziplist, intset)。
  • 可以通过参数 hash-max-ziplist-entries 等设置阈值。
  1. 对象共享
  • 小整数和空字符串等对象会被共享,避免重复创建。
  1. 惰性删除与定期删除
  • 当 key 过期时,不是立即删除,而是:
    • 惰性删除(lazy deletion):访问 key 时检查是否过期,过期则删除。
    • 定期删除(active deletion):周期性扫描一部分 key 进行删除。

内存淘汰策略(Eviction Policy)

当内存达到 maxmemory 限制时,Redis 会根据设定的策略释放旧数据:

策略名 描述
noeviction 不淘汰(默认),新写入将返回错误。
allkeys-lru 所有 key 中,最少使用的淘汰。
volatile-lru 仅设置了过期时间的 key 中进行 LRU 淘汰。
allkeys-random 所有 key 中,随机淘汰。
volatile-random 设置了过期时间的 key 中随机淘汰。
volatile-ttl 优先淘汰即将过期的 key。

allkeys-lru是最常用、使用的策略。

缓存穿透、 缓存雪崩是两个在实际系统中非常常见、也很容易导致系统崩溃或数据库崩溃的问题。

缓存穿透:请求的数据,不存在于缓存中(Redis),也不存在于数据库中。

某攻击者/爬虫发起如下请求:

1
GET /user?id=999999999
  • user:999999999 不存在于 Redis,也不存在于 MySQL。
  • Redis 无法命中,回源数据库也没有结果 → 无法写入缓存(不能缓存 null)。
  • 每次请求都走数据库,造成数据库压力激增。

导致:

  • 数据库压力陡增
  • 如果是攻击流量,还可能带来恶意 DDOS 效果
方法 说明
缓存空值(推荐) 将查询结果为 null 的 key 也缓存一段时间,如 60 秒。防止重复查库。
参数校验(防御第一步) 拒绝非法参数,如 id < 0 或字符格式非法的请求。
布隆过滤器(大规模场景) 使用布隆过滤器提前判断请求 key 是否可能存在,不存在则直接拒绝。
接入层限流或验证码 对请求频率高的接口增加保护措施。

缓存雪崩:大量缓存数据在同一时间失效,导致所有请求都绕过缓存,打到数据库。

与穿透不同,雪崩通常发生在:

  • 合法的 key,但突然都过期了。
  • 比如大量 key 的 TTL 设置为同一时间(比如系统重启后一小时过期)。

后果:

  • 数据库短时间内承受数倍流量。

  • 系统雪崩 → 服务宕机。

方法 说明
TTL 随机分散(核心) 设置过期时间时添加随机偏移,避免集中过期。
热点数据永不过期 对于关键数据不设置 TTL,而是定期手动刷新。
多级缓存 Redis 缓存失效后,从本地缓存读取,减少回源。
设置请求互斥锁(如缓存重建锁) 避免多个线程同时回源数据库,只允许一个查库,其他等待或复用结果。
限流与降级 拒绝部分请求,返回默认值,保护数据库。

Spring Boot

Spring还是太繁琐了,作为牛马CRUD的一员,还是面向Spring Boot开发更无脑。

采用Spring Boot 3.0 + 版本进行学习,需要JDK >= 17

这里通过跑通springbootadmin来体会一下

配置文件

1
2
server:
  port: 8081

注意admin版本要和Spring Boot匹配,不能太低了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<dependencies>
    <dependency>
        <groupId>de.codecentric</groupId>
        <artifactId>spring-boot-admin-server</artifactId>
        <version>3.2.2</version>
    </dependency>
    <dependency>
        <groupId>de.codecentric</groupId>
        <artifactId>spring-boot-admin-server-ui</artifactId>
        <version>3.2.2</version>
    </dependency>

</dependencies>

然后包的位置也有所改变import de.codecentric.boot.admin.server.config.EnableAdminServer;

再来一个主函数

1
2
3
4
5
6
7
8
@Configuration
@EnableAutoConfiguration
@EnableAdminServer
public class SpringBootAdminApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootAdminApplication.class, args);
    }
}

通过maven里面的springboot插件run即可

然后再跑一个client,其配置文件如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 服务器端口
server:
  port: 8082

# Actuator 配置(暴露全部端点)
management:
  endpoints:
    web:
      exposure:
        include: "*"

# Spring Boot Admin 客户端配置
spring:
  application:
    name: autowiredemo-client  # 应用名称,在 Admin 界面上显示
  boot:
    admin:
      client:
        url: http://localhost:8081  # Admin 服务器地址

RocketMQ集成

RocketMQ 是一种高性能、高可靠的分布式消息中间件,用于实现分布式系统中各个模块之间的通信。主要用于以下三个方面:

  • 解耦:通过引入消息队列,生产者与消费者之间不再直接调用,修改一方无需影响另一方,提升系统灵活性与可维护性。
  • 异步:生产者发送消息后即可返回,不必等待消费者处理完毕,降低耦合与响应延迟,提高系统吞吐量。
  • 削峰:在突发高流量场景下,消息队列可以暂存大量请求,消费者按能力逐步处理,防止系统被瞬时高并发压垮。

在 RocketMQ 中,消息服务器主要由两个核心组件组成:

  • Broker(代理/消息服务器)(这个单词不是break的过去式得来的,是来自法语brocour。经纪人,掮客)
  • NameServer(名称服务器)

对概念进行一些简介:

  1. Producer(消息生产者)
  • 负责将消息发送到 Broker。
  • Producer Group 分组,可快速失败和容灾。
  • 同一组 Producer 可发送多个 Topic,建议业务类型一致
  1. Consumer(消息消费者)
  • 从 Broker 拉取消息并处理。
  • Consumer Group 形式存在,实现负载均衡与容错。
  • 消费者数量 ≤ 订阅 Topic 的队列数量,否则多出的消费者会闲置。
  • Group 内消费者订阅相同topic。
  1. Topic(主题)
  • 逻辑概念,对应多个消息队列(MessageQueue)。
  • 多个 Producer/Consumer 可共享 Topic。
  • 发送与消费均以 队列为单位 进行,支持并发处理。
  1. NameServer(注册中心)
  • 保存 Broker 路由信息,供 Producer/Consumer 查询。
  • 无状态设计,每个 NameServer 节点独立。
  • Broker 会每 30 秒发送心跳更新状态。
  • 客户端每 30 秒主动拉取路由信息(Pull 模式)。
  1. Broker(消息服务器)
  • 接收并存储消息;支持读写、持久化、索引、HA。
  • 主从集群结构:Master 负责读写,Slave 做备份。
  • Master 与 Slave 通过相同 BrokerName、不同 BrokerId 绑定。
  • 支持 Topic 自动创建(默认创建 4 个队列)。
  1. Topic 与队列的读写区别
  • 读/写队列数量可不同,但需谨慎设计以避免资源浪费或消息丢失。
  • 核心目的是 支持 Topic 缩容 ——先减写队列,再减读队列。
  • Topic 是队列(Queue)的集合。

然后介绍两种通讯方式:点对点、订阅/发布

  1. 点对点

生产者将消息发送到某个队列(Queue),消息只能被一个消费者接收并处理。强调“一条消息只被消费一次”。

RocketMQ 没有显式的“队列”概念,而是用 Topic + Queue(多个消息队列) 实现:

  • Producer 向某个 Topic 发送消息
  • 若多个 Consumer 属于同一个消费组,并且是 集群模式(Clustering),那么同一个 Topic 的消息会被这些消费者分摊消费(即点对点)
  1. 发布/订阅通信

生产者将消息发布到一个 主题(Topic),多个消费者可以订阅该 Topic,并各自都收到完整的消息副本。

  • Topic 是主题,Producer 发布消息到 Topic
  • Consumer 订阅 Topic
  • 若 Consumer 设置为 广播模式(Broadcasting),那么每个消费者实例都能收到全部消息(即发布/订阅模式)

注意:如果是集群模式,则变成点对点(分摊消息)

示例

添加依赖rocketmq-spring-boot-starter

配置好application.yml

1
2
3
4
5
spring:
  rocketmq:
    name-server: 127.0.0.1:9876
    producer:
      group: demo-producer-group

自定义消息实体类(Lombok 是一个通过注解来简化 Java 代码的工具,它能够在编译时自动生成常用的方法,如 getter 和 setter,构造函数等。)

1
2
3
4
5
6
import lombok.Data;
@Data
public class UserMessage {
    private String userId;
    private String action;
}

生产者:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Service
public class UserActionProducer {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public void sendUserAction(UserMessage message) {
        rocketMQTemplate.convertAndSend("user-action-topic", message);
        System.out.println("消息发送成功:" + message);
    }
}

消费者:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Service
@RocketMQMessageListener(
        topic = "user-action-topic",
        consumerGroup = "demo-consumer-group"
)
public class UserActionConsumer implements RocketMQListener<UserMessage> {

    @Override
    public void onMessage(UserMessage message) {
        System.out.println("接收到消息:" + message);
    }
}

测试类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@SpringBootApplication
public class DemoApplication implements CommandLineRunner {

    @Autowired
    private UserActionProducer producer;

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        UserMessage msg = new UserMessage();
        msg.setUserId("u1001");
        msg.setAction("LOGIN");
        producer.sendUserAction(msg);
    }
}

微服务

微服务:

  • 单一职责:一个微服务解决一个问题(一两个表)
  • 松耦合:轻量级通信(如RESTful API)、不依赖其它服务代码
  • 独立存储:独立数据结构和数据库

对于一个面向对象体系设计的程序,如果微服务切的过小的话,就会将面向对象模型变成面向功能,对象之间的关系就会消失

微服务原则:

  • 独立部署
  • 集中配置
  • 客户透明
  • 复杂均衡、容错

Spring Cloud

其实这个东西不是专有名词,应该理解成Spring的Cloud组件。它包含了一系列组件,用来解决微服务架构中常见问题。在国内我们常用Spring Cloud Alibaba,其中有一些常见的中间件:

  • Nacos :服务注册与发现 + 配置中心
  • Sentinel :流量控制、熔断降级(多台服务器)
  • Seata :分布式事务(性能较低,只用于一致性要求高、并发不高的场景)
  • RocketMQ/Kafka :消息队列
  • Dubbo :高性能 RPC 框架

下面介绍Spring Cloud中关于微服务原则的实现

集中配置

在传统的单体应用中,配置通常写在 application.ymlapplication.properties 文件中。但在微服务架构中,随着服务数量增加,这种方式会带来配置困难、修改困难、环境差异、动态更新难等问题。

这里采用阿里的nacos为例

因为配置都在远程服务器上所以我们就需要使用bootstrap来读取nacos配置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
spring:
  application:
    name: nacos-config-client
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
        file-extension: yaml # 配置文件格式
        group: DEFAULT_GROUP
        namespace: public # 可以指定命名空间

然后nacos既可以使用外部的sql数据库,也有一个嵌入的数据用用来测试。

假装添加一个配置

1
2
3
user:
  name: Alice
  age: 30

然后就可以在类里面使用了,@RefreshScope是支持热更新的注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@RestController
@RefreshScope
public class ConfigController {

    @Value("${user.name}")
    private String name;

    @Value("${user.age}")
    private int age;

    @GetMapping("/user")
    public String getUser() {
        return "Name: " + name + ", Age: " + age;
    }
}

服务发现

起到类似DNS的作用,让我们通过服务名就可以找到目标服务的示例。

服务提供者在启动时,主动将自己的信息(服务名、IP、端口、元数据等)注册到 Nacos Server。每隔一段时间(默认 5 秒)发送一次 心跳,告知 Nacos“活着”。

服务消费者在调用时,从 Nacos 中获取服务名对应的所有实例列表然后进行本地缓存(这实现了就算nacos服务器崩了之前的服务如果无变化也能用)。客户端或 Ribbon 负载均衡器根据策略(如轮询、随机)选择一个实例进行调用。

1
2
3
4
5
@FeignClient(name = "user-service")
public interface UserClient {
    @GetMapping("/user/{id}")
    User getUser(@PathVariable Long id);
}

服务熔断

  • 微服务调用链非常长

  • 一个服务挂掉可能连锁影响所有上游

  • 必须保护调用者:不要因下游故障而拖垮整个系统

Sentinel 的核心思想:

  1. 统计服务调用的状态指标(QPS、异常率、RT等)
  2. 当某个指标超过阈值,就进入“熔断状态”(阻止调用)
  3. 一段时间后自动尝试半开状态,探测服务是否恢复
  4. 如果服务恢复正常,则关闭熔断,恢复调用

通过简单的轮询和熔断就可以实现效果不错的负载均衡。

OpenFeign

OpenFeign 是一个声明式的 HTTP 客户端,主要用于在 Spring Cloud 微服务架构中简化服务之间的调用。使用接口来声明远程调用的细节,不再手动编写 RestTemplateWebClient

1
2
3
4
5
6
@FeignClient(name = "user-service") // 指向注册中心的服务名
public interface UserClient {

    @GetMapping("/users/{id}")
    User getUserById(@PathVariable("id") Long id);
}

@FeignClient(name = “…”) 中的 name 是服务提供者在注册中心注册的名称。

GateWay

Spring Cloud Gateway 是 Spring 官方推出的网关框架,是 Spring Cloud 生态中的 API 网关解决方案,用于构建微服务架构中的 统一入口(如 API 代理、负载均衡、限流、权限验证等功能)。

功能 说明
路由转发 根据路径、请求头等规则将请求转发到对应服务
统一认证与鉴权 配合 JWT 或 OAuth2 拦截未授权请求
限流 支持 Redis + Token Bucket
熔断与容错 与 Resilience4j 配合
过滤器链(Filters) 可添加请求/响应日志、Header 修改等

配置例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
spring:
  application:
    name: gateway-server
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      discovery:
        locator:
          enabled: true # 启用服务发现自动路由
          lower-case-service-id: true