首页
关于
Search
1
同步本地Markdown至Typecho站点
88 阅读
2
微服务
41 阅读
3
苍穹外卖
32 阅读
4
JavaWeb——后端
25 阅读
5
消息队列MQ
20 阅读
后端学习
项目
杂项
科研
论文
默认分类
登录
推荐文章
推荐
拼团设计模式
项目
8月11日
0
8
0
推荐
拼团交易系统
项目
6月20日
0
26
1
推荐
Smile云图库
项目
6月7日
0
37
0
热门文章
88 ℃
同步本地Markdown至Typecho站点
项目
3月22日
0
88
0
41 ℃
微服务
后端学习
3月21日
0
41
0
32 ℃
苍穹外卖
项目
3月21日
0
32
0
最新发布
2025-03-21
Java笔记本
Java笔记本 IDEA基础操作 Intellij Idea创建Java项目: 创建空项目 创建Java module 创建包 package edu.whut.xx 创建类,类名首字母必须大写! IDEA快捷键: Ctrl + L 格式化代码 Ctrl + / 注释/取消注释当前行 Ctrl + D 复制当前行或选中的代码块 Ctrl + N 查找类 shift+shift 在文件中查找代码 alt+ enter “意图操作” “快捷修复” 可以1:service接口类跳转到实现 2:补全函数的返回值 调试快捷键: 快捷键 功能 Shift + F9 调试当前程序 F8 单步执行(不进入方法) F7 单步执行(进入方法) Shift + F8 跳出当前方法 Alt + F9 运行到光标处 Ctrl + F2 停止调试 缩写 生成的代码 说明 psvm public static void main(String[] args) {} 生成 main 方法 sout System.out.println(); 打印到控制台 fori for (int i = 0; i < ; i++) {} 生成 for 循环 iter for (Type item : iterable) {} 生成增强 for 循环 new Test().var Test test = new Test(); 自动补全变量声明 从exsiting file中导入模块: 方法一:复制整个模块到项目文件夹,并导入模块的 *.iml 文件,这种方式保留了模块原有的配置信息。 方法二:新建一个模块,然后将原模块的 src 文件夹下的包复制过去,这种方式更灵活,可以手动调整模块设置。 删除模块: 模块右键,remove module,这只是把它从项目中移除,然后!!打开模块所在文件夹,物理删除,才是真正完全删除。 Java基础 二进制:0b 八进制:0 十六进制:0x 在 System.out.println() 方法中,"ln" 代表 "line",表示换行。因此,println 实际上是 "print line" 的缩写。这个方法会在输出文本后自动换行. System.out.println("nihao "+1.3331); #Java 会自动将数值转换为字符串 一维数组创建: // 方式1:先声明,再指定长度(默认值为0、null等) int[] arr1 = new int[10]; // 创建一个长度为10的int数组 // 方式2:使用初始化列表直接创建数组 int[] arr2 = {1, 2, 3, 4, 5}; // 创建并初始化一个包含5个元素的int数组 String[] strs = {"eat", "tea", "tan", "ate", "nat", "bat"}; // 方式3:结合new关键字和初始化列表创建数组(常用于明确指定类型时) int[] arr3 = new int[]{1, 2, 3, 4, 5}; // 与方式2效果相同 字符串创建 String str = "Hello, World!"; //(1)直接赋值 String str = new String("Hello, World!"); //使用 new 关键字 char[] charArray = {'H', 'e', 'l', 'l', 'o'}; String str = new String(charArray); //通过字符数组创建 switch-case public class SwitchCaseExample { public static void main(String[] args) { // 定义一个 int 类型变量,作为 switch 的表达式 int day = 3; String dayName; // 根据 day 的值执行相应的分支 switch(day) { case 1: dayName = "Monday"; // 当 day 为 1 时 break; // 结束当前 case case 2: dayName = "Tuesday"; // 当 day 为 2 时 break; case 3: dayName = "Wednesday"; // 当 day 为 3 时 break; case 4: dayName = "Thursday"; // 当 day 为 4 时 break; case 5: dayName = "Friday"; // 当 day 为 5 时 break; case 6: dayName = "Saturday"; // 当 day 为 6 时 break; case 7: dayName = "Sunday"; // 当 day 为 7 时 break; default: // 如果 day 不在 1 到 7 之间 dayName = "Invalid day"; } // 输出最终结果 System.out.println("The day is: " + dayName); } } 强制类型转换 double sqrted=Math.sqrt(n); int soft_max=(int) sqrted; Math库常用方法 Math.pow(3, 2)); Math.sqrt(9)); Math.abs(a)); Math.max(a, b)); Math.min(a, b)); 转义符的作用 防止字符被误解: 在字符串中,一些字符(如 " 和 \)有特殊的含义。例如,双引号用于标识字符串的开始和结束,反斜杠通常用于转义。所以当你希望在字符串中包含这些特殊字符时,你需要使用转义符来告诉解析器这些字符是字符串的一部分,而不是特殊符号。 例如,\" 表示在字符串中包含一个双引号字符,而不是字符串的结束标志。 "Hello \"World\"" => 结果是:Hello "World" (双引号被转义) "C:\\Program Files\\App" => 结果是:C:\Program Files\App(反斜杠被转义) 如果只是"C:\Program Files\App" 那么路径就会报错 表示非打印字符: 转义符可以用于表示一些不可见的或非打印的控制字符,如换行符(\n)、制表符(\t)等。这些字符无法直接通过键盘输入,所以使用转义符来表示它们。 枚举 //纯状态枚举 常见于 switch-case、简单条件判断。 public enum OperationType { /** * 更新操作 */ UPDATE, /** * 插入操作 */ INSERT } OperationType opType = OperationType.INSERT; // 声明并初始化 public void execute(OperationType type, Object entity) { switch (type) { case INSERT: insertEntity(entity); break; case UPDATE: updateEntity(entity); break; default: throw new IllegalArgumentException("Unsupported operation: " + type); } } // 携带数据的枚举, 适合“常量 + 不变数据”的场景,如 星期、货币、错误码等。 public enum DayOfWeek { //创建7个 DayOfWeek 类型的对象,分别传入构造参数chineseName和dayNumber,它们叫“枚举常量” MONDAY("星期一", 1), TUESDAY("星期二", 2), WEDNESDAY("星期三", 3), THURSDAY("星期四", 4), FRIDAY("星期五", 5), SATURDAY("星期六", 6), SUNDAY("星期日", 7); // 枚举属性 private final String chineseName; private final int dayNumber; // 构造方法 DayOfWeek(String chineseName, int dayNumber) { this.chineseName = chineseName; this.dayNumber = dayNumber; } // 方法 public String getChineseName() { return chineseName; } public int getDayNumber() { return dayNumber; } } // 使用示例 public class Main { public static void main(String[] args) { DayOfWeek today = DayOfWeek.MONDAY; System.out.println(today.getChineseName()); // 输出: 星期一 System.out.println(today.getDayNumber()); // 输出: 1 } } 枚举类的构造方法必须是 private的,默认就是private的,这意味着只能在枚举内部使用这个构造方法。 枚举类你只需要使用,而不用创建对象,类内部已经定义好了MONDAY、TUESDAY...对象。 Java传参方式 基本数据类型 传递方式:按值传递 每次传递的是变量的值的副本**,对该值的修改不会影响原变量**。例如:int、double、boolean 等类型。 引用类型(对象) 传递方式:对象引用的副本传递 传递的是对象引用的一个副本,指向同一块内存区域。因此,方法内部通过该引用修改对象的状态,会影响到原对象。如数组、集合、String、以及其他所有对象类型。 Integer 属于引用类型,变量 Integer a = 10;中的 a是一个引用,它指向堆中存储的 Integer对象。 注意 StringBuilder s = new StringBuilder(); s.append("hello"); String res = s.toString(); // res = "hello" s.append(" world"); // s = "hello world" System.out.println(res); // 输出还是 "hello" 浅拷贝深拷贝 不可变对象 一个对象一旦被创建并初始化,它的状态(其内部代表的数据)就再也无法被改变,如Integer、String。 不可变”在代码中的具体表现:所有修改操作都返回新对象 String s1 = "Hello"; String s2 = s1.concat(" World"); // 不是修改s1,而是创建新字符串 字符串常量池 String a = "1"; String b = "1"; 变量 a和变量 b会指向同一个内存中的 String对象。 原理:常量池中有,就直接返回其引用;没有,就创建一个放进去再返回。 存放位置: Java 7 之前:字符串常量池逻辑上属于方法区(Method Area) 的运行时常量池(Runtime Constant Pool) 的一部分。而方法区的具体实现是 永久代(PermGen)。 问题:永久代大小有限且难以调整,容易发生 OutOfMemoryError: PermGen space。 Java 7 开始:字符串常量池被从永久代移动到了 Java 堆(Heap) 中。 Java 8 及以后:永久代被彻底移除,取而代之的是元空间(Metaspace)(用于存类元信息、方法码等)。而字符串常量池依然留在堆中。 浅拷贝 拷贝对象本身,但内部成员(例如集合中的元素)只是复制引用,新旧对象的内部成员指向同一份内存。如果内部元素是不可变的(如 Integer、String 等),这种拷贝通常足够。如果元素是可变对象,修改其中一个对象可能会影响另一个。 回溯法用的就是浅拷贝,因为List<Integer> path; 中间的Integer是不可变对象。 List<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); //new ArrayList<>(list)的底层行为: //1.新建一个空的 ArrayList实例。 //2.将原集合 list中的所有元素引用逐个复制到新集合中。 //3.返回这个新集合。 //4.新集合和原集合是两个完全独立的容器,只是内容(元素引用)相同。 List<Integer> shallowCopy = new ArrayList<>(list); 可变对象,浅拷贝修改对象会出错! List<Box> list = new ArrayList<>(); list.add(new Box(1)); list.add(new Box(2)); list.add(new Box(3)); List<Box> shallowCopy = new ArrayList<>(list); shallowCopy.get(0).value = 10; // 修改 shallowCopy 中第一个 Box 的 value System.out.println(list); // 输出: [10, 2, 3],因为同一 Box 对象被修改 System.out.println(shallowCopy); // 输出: [10, 2, 3] 深拷贝 不仅复制对象本身,还递归地复制其所有内部成员,从而生成一个完全独立的副本。即使内部元素是可变的,修改新对象也不会影响原始对象。 // 深拷贝 List<MyObject> 的例子 List<MyObject> originalList = new ArrayList<>(); originalList.add(new MyObject(10)); originalList.add(new MyObject(20)); List<MyObject> deepCopy = new ArrayList<>(); for (MyObject obj : originalList) { deepCopy.add(new MyObject(obj)); // 每个元素都创建一个新的对象 } 日期 在Java中: 代表年月日的类型是 LocalDate。LocalDate 类位于 java.time 包下,用于表示没有时区的日期,如年、月、日。 代表年月日时分秒的类型是 LocalDateTime。LocalDateTime 类也位于 java.time 包下,用于表示没有时区的日期和时间,包括年、月、日、时、分、秒。 LocalDateTime.now(),获取当前时间 静态成员变量与静态方法 一、静态成员变量 静态成员变量使用 static 修饰,属于类级别,而非对象,即所有对象共享同一份静态变量。 在类加载时完成初始化,且只执行一次。 可以通过类名直接访问,无需创建对象。 二、静态变量的初始化方式 1)静态代码块,用于对静态变量进行复杂初始化,在类第一次加载到 JVM 时执行一次。 public class MyClass { static int num1, num2; static { num1 = 1; System.out.println("静态代码块1执行"); } static { num2 = 3; System.out.println("静态代码块2执行"); } public static void main(String[] args) { System.out.println("main方法执行"); } } 输出: 静态代码块1执行 静态代码块2执行 main方法执行 类加载时按代码顺序依次执行所有静态代码块。 由于 main() 是静态方法,调用前类已被加载,因此静态代码块先执行。 示例 2:静态代码块 vs 构造方法 class Demo { static { System.out.println("静态代码块"); } Demo() { System.out.println("构造方法"); } } public class Test { public static void main(String[] args) { System.out.println("main开始"); Demo d1 = new Demo(); Demo d2 = new Demo(); } } 输出: main开始 静态代码块 构造方法 构造方法 说明:静态代码块仅在类首次加载时执行一次,构造方法在每次 new 对象时执行。 2)声明时直接初始化 public class MyClass { public static int staticVariable = 42; } 类加载时 staticVariable 直接被赋值为 42。 3)通过静态方法赋值(运行时) public class GlobalCounter { public static int currentCount; public static void initializeCounter(int startValue) { currentCount = startValue; } public static void incrementCounter() { currentCount++; } } public class Main { public static void main(String[] args) { GlobalCounter.initializeCounter(100); System.out.println(GlobalCounter.currentCount); // 输出 100 GlobalCounter.incrementCounter(); System.out.println(GlobalCounter.currentCount); // 输出 101 } } 只有 静态方法 才能在不创建对象的情况下访问或修改静态变量。 三、静态方法 用 static 修饰的方法,属于类本身,不依赖任何实例。 通过类名直接调用,无需创建对象。 public class MathUtils { public static int square(int n) { return n * n; } } int result = MathUtils.square(5); // 直接通过类名调用 四、访问静态成员变量 int value = MyClass.staticVariable; // ✅ 推荐 MyClass obj = new MyClass(); System.out.println(obj.staticVariable); // ✅ 可行,但不推荐 静态成员属于类,与对象无关;推荐使用类名访问。 变量修饰符final、static... 在Java中,变量的修饰符应该按照规定的顺序出现,通常是这样的: 访问修饰符:public、protected、private,或者不写(默认为包级访问)。 非访问修饰符:final、static、abstract、synchronized、volatile等。 数据类型:变量的数据类型,如 int、String、class 等。 变量名:变量的名称。 public static final int MAX_COUNT = 100; #定义常量 protected static volatile int counter; #定义成员变量 虽然final、static都是非访问修饰符,但是一般都是 static final ,不推荐反过来!!! final关键字 final 关键字,意思是最终的、不可修改的,最见不得变化 ,用来修饰类、方法和变量,具有以下特点: 修饰类:类不能继承,final 类中的所有成员方法都会被隐式的指定为 final 方法; 修饰变量:该变量为常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。 修饰符方法:方法不能重写 全限定名 全限定名(Fully Qualified Name,简称 FQN)指的是一个类或接口在 Java 中的完整名称,包括它所在的包名。例如: 对于类 Integer,其全限定名是 java.lang.Integer。 对于自定义的类 DeptServiceImpl,如果它位于包 edu.zju.zy123.service.impl 中,那么它的全限定名就是 edu.zju.zy123.service.impl.DeptServiceImpl。 使用全限定名可以消除歧义,确保指定的类型在整个项目中唯一无误。 使用场景: Spring AOP 的 Pointcut 表达式 MyBatis的XML映射文件的namespace属性 synchronized 概念 它的核心在于选择一个对象作为“锁”(也称为“监视器”或“互斥量”)。 synchronized (lockObject) { // 需要同步的代码块(临界区) } lockObject:这是一个对象引用,它作为锁。任何Java对象都可以充当锁。 { ... }:大括号内的代码就是“临界区”。JVM保证同一时刻,只有一个线程可以持有 lockObject这把锁并执行临界区内的代码。其他试图进入的线程必须等待,直到当前线程释放锁。 常见使用形式 类型 写法 锁对象 用途 同步方法(实例) public synchronized void foo() this 保护实例变量 同步方法(静态) public static synchronized void foo() 类对象(ClassName.class) 保护静态变量 同步代码块 synchronized (obj) {...} 任意对象 灵活控制锁粒度 1)同步代码块: class Counter { private int count = 0; private final Object lock = new Object(); public void increment() { synchronized (lock) { count++; System.out.println(Thread.currentThread().getName() + " -> " + count); } } } public class Demo { public static void main(String[] args) { Counter counter = new Counter(); Runnable task = counter::increment; new Thread(task, "A").start(); new Thread(task, "B").start(); } } 2)同步方法 class Counter { private int count = 0; public synchronized void increment() { count++; System.out.println(Thread.currentThread().getName() + " -> " + count); } } public class Test { public static void main(String[] args) { Counter c = new Counter(); for (int i = 0; i < 5; i++) { new Thread(c::increment).start(); } } } synchronized 修饰实例方法时,锁的是当前对象(this)。 同一个对象的同步方法是互斥的。 同一个对象(p1),T1 和 T2 都调用 p1.print(),因为 synchronized 锁的是 p1,所以 T1 和 T2 必须一个执行完,另一个才能进入方法。 不同对象(p2),T3 和 T4 调用的是另一个对象 p2.print() 因此,T1、T2 顺序执行,T3、T4 也顺序执行,它们之间互不影响。 Lambda表达式 函数式接口:有且仅有一个抽象方法的接口。 @FunctionalInterface 注解:这是一个可选的注解,用于表示接口是一个函数式接口。虽然不是强制的,但它可以帮助编译器识别意图,并检查接口是否确实只有一个抽象方法。 这个时候可以用Lambda代替匿名内部类!!! public class LambdaExample { // 定义函数式接口,doSomething 有两个参数 @FunctionalInterface interface MyInterface { void doSomething(int a, int b); } public static void main(String[] args) { // 使用匿名内部类实现接口方法 MyInterface obj = new MyInterface() { @Override public void doSomething(int a, int b) { System.out.println("参数a: " + a + ", 参数b: " + b); } }; obj.doSomething(5, 10); } public static void main(String[] args) { // 使用 Lambda 表达式实现接口方法 MyInterface obj = (a, b) -> { System.out.println("参数a: " + a + ", 参数b: " + b); }; obj.doSomething(5, 10); } } lambda表达式格式:(参数列表) -> { 代码块 }或 (参数列表) ->表达式 如果上述MyInterface接口的doSomething()方法不接受任何参数并且没有返回值: // Lambda 表达式(无参数) MyInterface obj = () -> { System.out.println("doSomething 被调用,无参数!"); }; 以下是lambda表达式的重要特征: 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。 可选的参数圆括号():一个参数无需定义圆括号,但无参数或多个参数需要定义圆括号。 可选的大括号{}:如果主体只有一个语句,可以不使用大括号。 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,使用大括号需显示retrun;如果函数是void则不需要返回值。 // 定义一个函数式接口,只有一个抽象方法 interface Calculator { int add(int a, int b); } public class LambdaReturnExample { public static void main(String[] args) { // 例子1:单个表达式,不使用大括号和 return 关键字 Calculator calc1 = (a, b) -> a + b; System.out.println("calc1: " + calc1.add(5, 3)); // 输出:8 // 例子2:使用大括号,需要显式使用 return 关键字 Calculator calc2 = (a, b) -> { return a + b; }; System.out.println("calc2: " + calc2.add(5, 3)); // 输出:8 } } 示例1: list.forEach这个方法接受一个函数式接口作为参数。它只有一个抽象方法 accept(T t)因此,可以使用 lambda 表达式来实现。 @FunctionalInterface public interface Consumer<T> { void accept(T t); } public class Main { public static void main(String[] args) { List<String> list = Arrays.asList("Apple", "Banana", "Cherry", "Date"); // 使用 Lambda 表达式迭代列表,这段 lambda,就是在“实现” void accept(String item) 这个方法——把每个元素传给 accept,然后打印它。 list.forEach(item -> System.out.println(item)); } } 示例2:为什么可以使用 Lambda 表达式自定义排序? 因为**Comparator<T> 是一个函数式接口**,只有一个抽象方法 compare(T o1, T o2) @FunctionalInterface public interface Comparator<T> { int compare(T o1, T o2); // 唯一的抽象方法 // 其他方法(如 thenComparing、reversed)都是默认方法或静态方法,不影响函数式接口特性 } public class Main { public static void main(String[] args) { List<String> names = Arrays.asList("John", "Jane", "Adam", "Dana"); // 使用Lambda表达式排序 Collections.sort(names, (a, b) -> a.compareTo(b)); // 输出排序结果 names.forEach(name -> System.out.println(name)); } } JAVA面向对象 public class Dog { // 成员变量 private String name; // 构造函数 public Dog(String name) { this.name = name; } // 一个函数:让狗狗“叫” public void bark() { System.out.println(name + " says: Woof! Woof!"); } // (可选)获取狗狗的名字 public String getName() { return name; } // 测试主方法 public static void main(String[] args) { Dog myDog = new Dog("Buddy"); myDog.bark(); // 输出:Buddy says: Woof! Woof! System.out.println("Name: " + myDog.getName()); } } 访问修饰符 public(公共的): 使用public修饰的成员可以被任何其他类访问,无论这些类是否属于同一个包。 例如,如果一个类的成员被声明为public,那么其他类可以通过该类的对象直接访问该成员。 protected(受保护的): 使用protected修饰的成员可以被同一个包中的其他类访问,也可以被不同包中的子类访问。 与包访问级别相比,protected修饰符提供了更广泛的访问权限。 default (no modifier)(默认的,即包访问级别): 如果没有指定任何访问修饰符,则默认情况下成员具有包访问权限。 在同一个包中的其他类可以访问默认访问级别的成员,但是在不同包中的类不能访问。 private(私有的): 使用private修饰的成员只能在声明它们的类内部访问,其他任何类(子类也不行!)都不能访问这些成员。 这种访问级别提供了最高的封装性和安全性。 如果您在另一个类中实例化了包含私有成员的类,那么您无法直接访问该类的私有成员。但是,您可以通过公共方法来间接地访问和操作私有成员。 public class PrivateExample { private int privateVar = 30; // 公共方法,用于访问私有成员 public int getPrivateVar() { return privateVar; } } 则每个实例都有自己的一份拷贝,只有当变量被声明为 static 时,变量才是类级别的,会被所有实例共享。 修饰符不仅可以用来修饰成员变量和方法,也可以用来修饰类。顶级类只能使用 public 或默认(即不写任何修饰符,称为包访问权限)。内部类可以使用所有访问修饰符(public、protected、private 和默认),这使得你可以更灵活地控制嵌套类的访问范围。 public class OuterClass { // 内部类使用private,只能在OuterClass内部访问 private class InnerPrivateClass { // ... } // 内部类使用protected,同包以及其他包中的子类可以访问 protected class InnerProtectedClass { // ... } // 内部类使用默认访问权限,只在同包中可见 class InnerDefaultClass { // ... } // 内部类使用public,任何地方都可访问(但访问时需要通过OuterClass对象) public class InnerPublicClass { // ... } } JAVA三大特性 封装 封装指隐藏对象的状态信息(属性),不允许外部对象直接访问对象的内部信息(private实现)。但是可以提供一些可以被外界访问的方法(public)来操作属性。 优点:隐藏内部实现细节,提升安全性。 继承 [修饰符] class 子类名 extends 父类名{ 类体部分 } //class C extends A, B // 错误:C 不能同时继承 A 和 B Java只支持单继承,不支持多继承。一个类只能有一个父类,不可以有多个父类。 Java支持多层继承(A → B → C )。 Java继承了父类非私有的成员变量和成员方法,但是请注意:子类是无法继承父类的构造方法的。 优点:实现代码复用,减少重复代码。子类可以在继承基础上扩展或修改功能。 多态 指在面向对象编程中,同样的消息(方法调用)可以在不同的对象上触发不同的行为。 方法重写(Override):动态多态;子类从父类继承的某个实例方法无法满足子类的功能需要时,需要在子类中对该实例方法进行重新实现,这样的过程称为重写,也叫做覆写、覆盖。 要求: 必须存在继承关系(子类继承父类)。 子类重写的方法的访问修饰符不能比父类更严格(可以相同或更宽松)。 方法名、参数列表和返回值类型必须与父类中的方法完全相同(Java 5 以后支持协变返回类型,即允许返回子类型)。 向上转型(Upcasting):动态多态;子类对象可以赋值给父类引用,这样做可以隐藏对象的真实类型,只能调用父类中声明的方法。 class Animal { public void makeSound() { System.out.println("Animal makes sound"); } } class Dog extends Animal { @Override public void makeSound() { System.out.println("Dog barks"); } public void fetch() { System.out.println("Dog fetches the ball"); } } public class Test { public static void main(String[] args) { Animal animal = new Dog(); // 向上转型 animal.makeSound(); // 调用的是 Dog 重写的 makeSound() 方法 // animal.fetch(); // 编译错误:Animal 类型没有 fetch() 方法 } } 多态实现总结:继承 + 重写 + 父类引用指向子类对象 = 多态 方法重载(Overload):静态多态;方法名相同但参数列表不同。当调用这些方法时,会根据传递的参数类型或数量选择相应的方法。 参数类型不同;参数数量不同:参数顺序不同;返回类型可以不同,但仅返回类型不同不构成重载 void print(int a) { ... } void print(String a) { ... } // 合法重载 void log(String msg) { ... } void log(String msg, int level) { ... } // 合法重载 void save(int id, String name) { ... } void save(String name, int id) { ... } // 合法重载 优点:提高代码的灵活性和可扩展性。通过统一接口调用不同实现,提高可替换性。 继承与super关键字 类加载与对象实例化顺序 class Parent { static { System.out.println("A"); } Parent() { System.out.println("B"); } } class Child extends Parent { static { System.out.println("C"); } Child() { System.out.println("D"); } } 加载类时: A // 父类静态块先执行 C // 再执行子类静态块 实例化子类对象时: B // 父类构造函数先执行 D // 再执行子类构造函数 记忆口诀:静态先父后子,构造先父后子。(析构则相反,先子后父) new Child()的时候-> 输出顺序为 ACBD Super关键字 super 用于在子类中访问父类的内容: 1)访问父类的成员 当子类与父类存在同名成员时,子类中的成员会**隐藏(shadow)**父类的同名成员。可用 super 显式访问父类版本。 2)调用父类的构造方法 创建子类对象时,一定会先调用父类构造函数。若父类没有无参构造函数,则子类必须显式调用父类构造函数,否则编译报错。使用 super(参数) 调用父类构造方法。 示例: class Parent { int num = 10; Parent(int num) { this.num = num; System.out.println("Parent constructor: num = " + num); } void display() { System.out.println("Parent method"); } } class Child extends Parent { int num = 20; Child(int num) { super(num); // 调用父类构造函数 System.out.println("Child constructor: num = " + num); } void print() { System.out.println("Child num: " + num); // 子类字段 System.out.println("Parent num: " + super.num); // 父类字段 super.display(); // 调用父类方法 } } public class Main { public static void main(String[] args) { Child obj = new Child(30); System.out.println("---- 调用 print() ----"); obj.print(); } } 运行结果: Parent constructor: num = 30 Child constructor: num = 30 ---- 调用 print() ---- Child num: 20 Parent num: 30 Parent method 注意:若父类定义了构造函数(无论是否有参),编译器不会再自动生成默认无参构造函数。 如果父类仅有有参构造函数,子类必须显式调用 super(...) 来选择合适的父类构造函数。 如果有有参+无参,那子类可以不显示调用super(...),系统会自动帮你调用父类的无参构造函数。 抽象类 用 abstract 修饰的类,不能被实例化。可以包含抽象方法(无方法体)和具体方法(有实现)。抽象类可以实现接口,也可以继续保持抽象。 注意:抽象类可以一个抽象方法都没有。 abstract class Animal { public abstract void makeSound(); // 抽象方法 public void sleep() { // 普通方法 System.out.println("Sleeping..."); } } class Dog extends Animal { @Override public void makeSound() { System.out.println("Dog barks"); } } 如何使用抽象类 由于抽象类不能直接实例化,我们通常有两种方法来使用抽象类: 通过子类继承并实现抽象方法: Animal animal = new Dog(); animal.makeSound(); // 输出:Dog barks 使用匿名内部类 Animal a = new Animal() { @Override public void makeSound() { System.out.println("Anonymous sound"); } }; 如何算作实现抽象方法 抽象类的子类必须实现全部抽象方法,否则该子类也必须声明为抽象类。 实现接口时,可选择性实现方法,未实现的交由子类完成。 接口 一组行为规范或契约。接口中的变量默认是 public static final 常量。方法默认是 public abstract(Java 8 之后可包含 default 和 static 方法)。 interface SmartDevice { String DEFAULT_BRAND = "Generic"; void turnOn(); // 抽象方法 default void updateFirmware() { System.out.println("Updating firmware..."); } static void checkConnection() { System.out.println("Checking network..."); } } class SmartLight implements SmartDevice { @Override public void turnOn() { System.out.println("Light is on"); } } 接口特性 支持 多继承(一个类可以实现多个接口)。 接口之间可以使用 extends 继承。 默认方法(default)可提供接口级的默认实现。 抽象类 vs 接口 特性 抽象类 接口 关键字 abstract + class interface 实例化 ❌ 不能 ❌ 不能 方法类型 抽象 + 具体 抽象(默认)、默认方法、静态方法 变量 普通成员变量 public static final 常量 继承 extends(单继承) implements(多实现) 关系 可实现接口 可被类实现、可继承接口 适用场景 表示**“是什么”**(共性与实现) 表示**“能做什么”**(行为约定) 四种内部类 1.成员内部类 定义位置:成员内部类定义在外部类的成员位置。 访问权限:可以无限制地访问外部类的所有成员,包括私有成员、静态成员变量。 实例化方式:需要先创建外部类的实例,然后才能创建内部类的实例。 修改限制:不能有静态字段和静态方法(除非声明为常量final static)。成员内部类属于外部类的一个实例,不能独立存在于类级别上。 用途:适用于内部类与外部类关系密切,需要频繁访问外部类成员的情况。 public class Outer { private static int staticVar = 10; // 外部类静态变量 private int instanceVar = 20; // 外部类实例变量 // 非静态内部类 class Inner { void print() { System.out.println("静态变量: " + staticVar); // 直接访问外部类静态变量 System.out.println("实例变量: " + instanceVar); // 直接访问外部类实例变量 } } public static void main(String[] args) { Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); // 创建内部类实例 inner.print(); } } 易错题: class A { static class B { void hello() { System.out.println("hello from static A.B"); } } } class C implements A.B { public void hello() { System.out.println("hello from C"); } } 会编译报错,A.B 是一个 非静态内部类(inner class),它隐式地依赖外部类 A 的实例。 2.静态内部类 没有静态类,但是有静态内部类! 定义位置:定义在外部类内部,但使用static修饰。 访问权限:只能直接访问外部类的静态成员,访问非静态成员需要通过外部类实例。 实例化方式:可以直接创建,不需要外部类的实例。 修改限制:可以有自己的静态成员。 用途:适合当内部类工作不依赖外部类实例时使用,常用于实现与外部类关系不那么密切的帮助类。 public class Solution { // 外部类的静态成员 private static String author = "AlgoMaster"; // 外部类的实例成员 private int solveCount = 0; // 静态内部类:二叉树节点 public static class TreeNode { int val; TreeNode left, right; TreeNode(int val) { this.val = val; } public void showInfo() { // ✅ 可以直接访问外部类的静态成员 System.out.println("Created by: " + author); // ❌ 不能直接访问外部类的实例成员 // System.out.println("solveCount: " + solveCount); } // 算法函数:递归求最大深度 public int maxDepth(TreeNode root) { solveCount++; // 访问实例成员 if (root == null) return 0; return 1 + Math.max(maxDepth(root.left), maxDepth(root.right)); } // 测试 public static void main(String[] args) { // ✅ 静态内部类可以脱离外部类实例独立创建 TreeNode root = new TreeNode(1); root.left = new TreeNode(2); root.right = new TreeNode(3); root.left.left = new TreeNode(4); // 展示静态内部类对外部类成员的访问特性 root.showInfo(); // 使用外部类实例来跑算法 Solution sol = new Solution(); System.out.println("Max Depth: " + sol.maxDepth(root)); // 输出 3 } } 如果把辅助类直接写成顶级类,可能导致包里出现一堆“小工具类”。 把它们作为静态内部类放到外部类里,可以让代码结构更紧凑,命名更直观。 例子:在算法题里定义 TreeNode、ListNode 常作为 Solution 的静态内部类。 3.局部内部类 定义位置:局部内部类定义在一个方法或任何块内(如:if语句、循环语句内)。 访问权限:只能访问所在方法的final或事实上的final(即不被后续修改的)局部变量和外部类的成员变量(同成员内部类)。 实例化方式:只能在定义它们的块中创建实例。 修改限制:同样不能有静态字段和方法。 用途:适用于只在方法或代码块中使用的类,有助于将实现细节隐藏在方法内部。 public class OuterClass { public void startThread() { class LocalInnerClass implements Runnable { @override public void run() { System.out.println("局部内部类中的线程正在运行..."); } } LocalInnerClass localInner = new LocalInnerClass(); Thread thread = new Thread(localInner); thread.start(); } public static void main(String[] args) { OuterClass outer = new OuterClass(); outer.startThread(); } } 4.匿名内部类 new 父类/接口() { // 实现或重写方法 @Override void method() { ... } } 在定义的同时直接实例化,而不需要显式地声明一个子类的名称。 用途:适用于创建一次性使用的实例,通常用于接口或抽象类的实现。但匿名内部类并不限于接口或抽象类,只要是非 final 的普通类,都有机会通过匿名内部类来“现场”创建一个它的子类实例。 abstract class Animal { public abstract void makeSound(); } public class Main { public static void main(String[] args) { // 匿名内部类:临时创建一个 Animal 的子类并实例化 Animal dog = new Animal() { // 注意这里的 new Animal() { ... } @Override public void makeSound() { System.out.println("汪汪汪!"); } }; dog.makeSound(); // 输出:汪汪汪! } } 如何理解?可以对比普通子类(显式定义),即显示定义了Dog来继承Animal // 抽象类或接口 abstract class Animal { public abstract void makeSound(); } // 显式定义一个具名的子类 class Dog extends Animal { @Override public void makeSound() { System.out.println("汪汪汪!"); } } public class Main { public static void main(String[] args) { // 实例化具名的子类 Animal dog = new Dog(); dog.makeSound(); // 输出:汪汪汪! } } 容器 Collection 在 Java 中,Collection 是一个接口,它表示一组对象的集合。Collection 接口是 Java 集合框架中最基本的接口之一,定义了一些操作集合的通用方法,例如添加、删除、遍历等。 所有集合类(例如 List、Set、Queue 等)都直接或间接地继承自 Collection 接口。 boolean add(E e):将指定的元素添加到集合中(可选操作)。 boolean remove(Object o):从集合中移除指定的元素(可选操作)。 boolean contains(Object o):如果集合中包含指定的元素,则返回 true。 int size():返回集合中的元素个数。 void clear():移除集合中的所有元素。 boolean isEmpty():如果集合为空,则返回 true。 public class CollectionExample { public static void main(String[] args) { // 创建一个 Collection 对象,使用 ArrayList 作为实现类 Collection<String> fruits = new ArrayList<>(); // 添加元素到集合中 fruits.add("Apple"); fruits.add("Banana"); fruits.add("Cherry"); System.out.println("添加元素后集合大小: " + fruits.size()); // 输出集合大小 // 检查集合是否包含某个元素 System.out.println("集合中是否包含 'Banana': " + fruits.contains("Banana")); // 从集合中移除元素 fruits.remove("Banana"); System.out.println("移除 'Banana' 后集合大小: " + fruits.size()); // 清空集合 fruits.clear(); System.out.println("清空集合后,集合是否为空: " + fruits.isEmpty()); } } Iterator 在 Java 中,Iterator 是一个接口,遍历集合元素。Collection 接口中定义了 iterator() 方法,返回一个 Iterator 对象。 Iterator 接口中包含以下主要方法: hasNext():如果迭代器还有下一个元素,则返回 true,否则返回 false。 next():返回迭代器的下一个元素,并将迭代器移动到下一个位置。 remove():从迭代器当前位置删除元素。该方法是可选的,不是所有的迭代器都支持。 import java.util.ArrayList; import java.util.Iterator; public class Main { public static void main(String[] args) { // 创建一个 ArrayList 集合 ArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); int size = list.size(); // 获取列表大小 System.out.println("Size of list: " + size); // 输出 3 // 获取集合的迭代器 Iterator<Integer> iterator = list.iterator(); // 使用迭代器遍历集合并输出元素 while (iterator.hasNext()) { Integer element = iterator.next(); System.out.println(element); } } } ArrayList ArrayList 是 List 接口的一种实现,而 List 接口又继承自 Collection 接口。包括 add()、remove()、contains() 等。 HashSet HashMap // 使用 entrySet() 方法获取 Map 中所有键值对的集合,并使用增强型 for 循环遍历键值对 System.out.println("Entries in the map:"); for (Map.Entry<String, Integer> entry : map.entrySet()) { String key = entry.getKey(); Integer value = entry.getValue(); System.out.println("Key: " + key + ", Value: " + value); } PriorityQueue 默认是小根堆,输出1,2,5,8 import java.util.PriorityQueue; public class Main { public static void main(String[] args) { // 创建一个 PriorityQueue 对象 PriorityQueue<Integer> pq = new PriorityQueue<>(); // 添加元素到队列 pq.offer(5); pq.offer(2); pq.offer(8); pq.offer(1); // 打印队列中的元素 System.out.println("Elements in the priority queue:"); while (!pq.isEmpty()) { System.out.println(pq.poll()); } } } offer() 方法用于将元素插入到队列中 poll() 方法用于移除并返回队列中的头部元素 peek() 方法用于返回队列中的头部元素但不移除它。 JAVA异常处理 public class ExceptionExample { // 方法声明中添加 throws 关键字,指定可能抛出的异常类型 public static void main(String[] args) throws SomeException, AnotherException { try { // 可能会抛出异常的代码块 if (someCondition) { throw new SomeException("Something went wrong"); } } catch (SomeException e) { // 处理 SomeException 异常 System.out.println("Caught SomeException: " + e.getMessage()); } catch (AnotherException e) { // 处理 AnotherException 异常 System.out.println("Caught AnotherException: " + e.getMessage()); } finally { // 不管是否发生异常,都会执行的代码块 System.out.println("End of try-catch block"); } } } // 自定义异常类,继承自 Exception 类 public class SomeException extends Exception { // 构造方法,用于设置异常信息 public SomeException(String message) { // 调用父类的构造方法,设置异常信息 super(message); } } JAVA泛型 在类、接口或方法定义时,用类型参数来替代具体的类型,编译时检查类型安全,运行时通过类型擦除映射到原始类型。 <T>: 用于 定义泛型类型。 在 类、接口、方法 的定义中,<T> 是用来指定一个占位符,表示这个类或方法可以接受任何类型。 T 在这里是 类型参数,你可以在类、接口或方法内使用它来代替具体的类型。 public class Box<T> { // <T> 定义了一个泛型类,T 是类型参数 private T value; // 使用 T 来表示某种类型 public void set(T value) { // 使用 T 来表示参数类型 this.value = value; } public T get() { // 使用 T 来表示返回类型 return value; } } 定义一个泛型类 // 定义一个“盒子”类,可以装任何类型的对象 public class Box<T> { private T value; public Box() {} public Box(T value) { this.value = value; } public void set(T value) { this.value = value; } public T get() { return value; } } T 是类型参数(Type Parameter),可任意命名(常见还有 E、K、V 等)。 使用: public class Main { public static void main(String[] args) { // 创建一个只装 String 的盒子 Box<String> stringBox = new Box<>(); stringBox.set("Hello Generics"); String s = stringBox.get(); // 自动类型推断为 String System.out.println(s); // 创建一个只装 Integer 的盒子 Box<Integer> intBox = new Box<>(123); Integer i = intBox.get(); System.out.println(i); } } 定义一个泛型方法 有时候我们只想让某个方法支持多种类型,而不必为此写泛型类,就可以在方法前加上类型声明: public class Utils { //[修饰符] <T> 返回类型 方法名(参数列表) { … } // 泛型方法:打印任意类型的一维数组 public static <T> void printArray(T[] array) { for (T element : array) { System.out.println(element); } } } 方法签名中 <T> 表示这是一个泛型方法,注意这里不是指返回值!!!这个返回值是void!!! 调用时,编译器会根据传入实参自动推断 T。 使用 public class Main { public static void main(String[] args) { String[] names = {"Alice", "Bob", "Charlie"}; Utils.printArray(names); // 等价于 Utils.<String>printArray(names); Integer[] nums = {10, 20, 30}; Utils.printArray(nums); // 等价于 Utils.<Integer>printArray(nums); } } 好用的方法 toString() **Arrays.toString()**转一维数组 **Arrays.deepToString()**转二维数组 这个方法是是用来将数组转换成String类型输出的,入参可以是long,float,double,int,boolean,byte,object 型的数组。 import java.util.Arrays; public class Main { public static void main(String[] args) { // 一维数组示例 int[] oneD = {1, 2, 3, 4, 5}; System.out.println("一维数组输出: " + Arrays.toString(oneD)); // 二维数组示例 int[][] twoD = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }; // 使用 Arrays.deepToString() 输出二维数组 System.out.println("二维数组输出: " + Arrays.deepToString(twoD)); } } 自定义对象的toString() 方法 每个 Java 对象默认都有 toString() 方法(可以根据需要覆盖) 当直接打印一个没有重写 toString() 方法的对象时,其输出格式通常为: java.lang.Object@15db9742 当打印重写toString() 方法的对象时: class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Person{name='" + name + "', age=" + age + "}"; } } public class Main { public static void main(String[] args) { Person person = new Person("Alice", 30); System.out.println(person); //会自动调用对象的 toString() 方法 //Person{name='Alice', age=30} } } 对象拷贝属性 public void save(EmployeeDTO employeeDTO) { Employee employee = new Employee(); //对象属性拷贝 BeanUtils.copyProperties(employeeDTO, employee,"id"); } employeeDTO的内容拷贝给employee,跳过字段为"id"的属性。 StartOrStopDTO dto = new StartOrStopDTO(1, 100L); // 用 Builder 拷贝 id 和 status Employee employee = Employee.builder() .id(dto.getId()) .status(dto.getStatus()) .build(); Java 8 Stream API SpaceUserRole role = SPACE_USER_AUTH_CONFIG.getRoles() .stream() // 1 .filter(r -> r.getKey().equals(spaceUserRole)) // 2 .findFirst() // 3 .orElse(null); // 4 stream() 把 List<SpaceUserRole> 转换成一个 Stream<SpaceUserRole>,Stream 是 Java 8 引入的对集合进行函数式操作的管道。 .filter(r -> r.getKey().equals(spaceUserRole)) filter 接受一个 Predicate<T>(这里是从每个 SpaceUserRole r 中调用 r.getKey().equals(...)),只保留“满足该条件”的元素,其余都丢弃。 .findFirst() 在过滤后的流中,取第一个元素,返回一个 Optional<SpaceUserRole>。即使流是空的,它也会返回一个空的 Optional,而不会抛异常。 .orElse(null) 从 Optional 中取值:如果存在就返回该值,不存在就返回 null。 等价于下面的老式写法(Java 7 及以前): SpaceUserRole role = null; for (SpaceUserRole r : SPACE_USER_AUTH_CONFIG.getRoles()) { if (r.getKey().equals(spaceUserRole)) { role = r; break; } } 类加载器和获取资源文件路径 在Java中,类加载器的主要作用是根据**类路径(Classpath)**加载类文件以及其他资源文件。 启动类加载器(Bootstrap ClassLoader):加载 Java 运行时环境的核心类库,包括 Java 的标准库和 JVM 必须的类,比如 java.lang.* 包中的类。 扩展类加载器:加载 Java 扩展目录中的类库,这些库通常是 ext 目录中的 JAR 文件(例如,$JAVA_HOME/jre/lib/ext/)。 系统类加载器:加载应用程序的类路径(classpath)下的类和资源文件,通常用于加载项目中的类和 JAR 包。 自定义类加载器:这个类加载器是由开发者自定义实现的,用于加载非标准的类或动态加载类。它是 Java 提供的类加载机制的扩展,允许你根据特定需求来自定义类加载的行为。 双亲委派机制的基本原理: 双亲委派机制的核心思想是:一个类加载器在加载类时,首先将请求委托给它的父类加载器,只有当父类加载器无法加载该类时,当前加载器才会自己去加载。 原因: 1.避免类的重复加载 2.保证核心类的安全性,如 java.lang.Object 类等是 Java 核心类库的一部分,必须由启动类加载器(Bootstrap ClassLoader)加载。 类路径是JVM在运行时用来查找类文件和资源文件的一组目录或JAR包。在许多项目(例如Maven或Gradle项目)中,src/main/resources目录下的内容在编译时会被复制到输出目录(如target/classes),src/main/java 下编译后的 class 文件也会放到这里。 src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── example/ │ │ └── App.java │ └── resources/ │ ├── application.yml │ └── static/ │ └── logo.png └── test/ ├── java/ │ └── com/ │ └── example/ │ └── AppTest.java └── resources/ └── test-data.json 映射到 target/ 后: target/ ├── classes/ ← 主代码和资源的输出根目录 │ ├── com/ │ │ └── example/ │ │ └── App.class ← 编译自 src/main/java/com/example/App.java │ ├── application.yml ← 复制自 src/main/resources/application.yml │ └── static/ │ └── logo.png ← 复制自 src/main/resources/static/logo.png └── test-classes/ ← 测试代码和测试资源的输出根目录 ├── com/ │ └── example/ │ └── AppTest.class ← 编译自 src/test/java/com/example/AppTest.java └── test-data.json ← 复制自 src/test/resources/test-data.json // 获取 resources 根目录下的 emp.xml 文件路径 String empFileUrl = this.getClass().getClassLoader().getResource("emp.xml").getFile(); // 获取 resources/static 目录下的 tt.img 文件路径 URL resourceUrl = getClass().getClassLoader().getResource("static/tt.img"); String ttImgPath = resourceUrl != null ? resourceUrl.getFile() : null; this.getClass():获取当前对象(即调用该代码的对象)的 Class 对象。 .getClassLoader():获取该 Class 对象的类加载器(ClassLoader)。 .getResource("emp.xml"):从类路径中获取名为 "emp.xml" 的资源,并返回一个 URL 对象,该 URL 对象指向 "emp.xml" 文件的位置。 .getFile():从 URL 对象中获取文件路径部分,即获取 "emp.xml" 文件的绝对路径字符串。 **类路径(Classpath)**是 Java 虚拟机(JVM)用于查找类文件和其他资源文件的一组路径。 是的,类加载器的主要作用之一确实是从类路径中加载类文件(.class 文件)以及其他资源(如图片、配置文件等)。在 Java 项目启动时,类加载器不仅会加载类文件,还会把这些类文件转换为 Java 程序可以使用的 Class 对象,并将它们放入 运行时数据区,即 方法区(Method Area) 和 堆区(Heap)。 反射 反射技术的关键之一是能够在 运行时动态加载 类的字节码并将其转换为 Class 对象,并以编程的方法解刨出类中的各个成分(成员变量、方法、构造器等)。 .class 是静态的文件,加载到内存并解析后,就会创建一个对应的 java.lang.Class 实例即 Class对象,是动态的。 反射技术例子:IDEA通过反射技术就可以获取到类中有哪些方法,并且把方法的名称以提示框的形式显示出来,所以你能看到这些提示了。 获取类的字节码(Class对象) 有三种方法: public class Test1Class{ public static void main(String[] args){ Class c1 = Student.class; System.out.println(c1.getName()); //获取全类名:edu.whut.pojo.Student System.out.println(c1.getSimpleName()); //获取简单类名: Student Class c2 = Class.forName("edu.whut.pojo.Student"); //全类名 System.out.println(c1 == c2); //true Student s = new Student(); Class c3 = s.getClass(); System.out.println(c2 == c3); //true } } 类的 Class 对象(字节码对象)是类的模具,不会直接存放数据。 类的 实例对象obj是用 new 出来的,它代表着 实际的运行时对象,会在堆(Heap)里开辟内存,存放实例字段的数据。每次 new 一下,就会有一个独立的实例,它们共享同一个 Class,但各自的数据独立。 1.获取类的元信息 类名:getName()、getSimpleName()、getPackage() 父类和接口:getSuperclass()、getInterfaces() 修饰符:getModifiers()(配合 Modifier 工具类解析 public、private、abstract 等) 注解:getAnnotation() / getAnnotations() 获取类上的注解!!! 2.获取类的构造器 定义类 public class Cat{ private String name; private int age; public Cat(){} private Cat(String name, int age){ } } 获取构造器列表 public class TestConstructor { @Test public void testGetAllConstructors() { // 1. 获取类的 Class 对象 Class<?> c = Cat.class; // 2. 获取类的全部构造器(包括public、private等) Constructor<?>[] constructors = c.getDeclaredConstructors(); // 3. 遍历并打印构造器信息 for (Constructor<?> constructor : constructors) { System.out.println( constructor.getName() + " --> 参数个数:" + constructor.getParameterCount() ); } } } c.getDeclaredConstructors() 会返回所有声明的构造器(包含私有构造器),而 c.getConstructors() 只会返回公共构造器。 constructor.getParameterCount() 用于获取该构造器的参数个数。 获取某个构造器:指定参数类型! public class Test2Constructor(){ @Test public void testGetConstructor(){ //1、反射第一步:必须先得到这个类的Class对象 Class c = Cat.class; /2、获取private修饰的有两个参数的构造器,第一个参数String类型,第二个参数int类型 Constructor constructor = c.getDeclaredConstructor(String.class,int.class); constructor.setAccessible(true); //禁止检查访问权限,可以使用private构造函数 Cat cat=(Cat)constructor.newInstance("叮当猫",3); //初始化Cat对象 } } c.getDeclaredConstructor(String.class, int.class):根据参数列表获取特定的构造器。 如果构造器是private修饰的,先需要调用setAccessible(true) 表示禁止检查访问控制,然后再调用newInstance(实参列表) 就可以执行构造器,完成对象的初始化了。 Constructor 本身就是用来创建对象实例的,它的职责是生成实例,而不是操作某个已经存在的实例。 3.获取类的成员变量 获取类的成员变量 方法 说明 public Field[] getFields() 获取类的全部成员变量(只能获取 public 修饰的) public Field[] getDeclaredFields() 获取类的全部成员变量(只要存在就能拿到) public Field getField(String name) 获取类的某个成员变量(只能获取 public 修饰的) public Field getDeclaredField(String name) 获取类的某个成员变量(只要存在就能拿到) 设置与获取字段值 方法 说明 void set(Object obj, Object value) 设置字段值 Object get(Object obj) 获取字段值 public void setAccessible(boolean flag) 设置为 true,表示禁止检查访问控制(暴力反射) 不管是设置值还是获取值,都需要: 获取 Field 对象 —— 先通过 Class 对象拿到目标字段的 Field 实例。 指定目标实例 —— 操作字段时必须传入具体的对象实例,告诉 JVM 要修改或读取哪一个对象的该字段。 处理访问权限 —— 如果字段是私有的,需要调用 setAccessible(true) 来关闭 Java 的访问检查(俗称“暴力反射”)。 import java.lang.reflect.Field; public class ReflectionExample { // 示例类 public static class MyClass { public String publicField = "Public Field"; private String privateField = "Private Field"; } public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { // 创建 MyClass 的实例 MyClass obj = new MyClass(); // 获取 MyClass 的 Class 对象 Class<?> clazz = obj.getClass(); // 获取 public 字段 Field publicField = clazz.getField("publicField"); System.out.println("Public Field: " + publicField.get(obj)); // 获取并输出 publicField 的值 // 获取 private 字段(使用 getDeclaredField) Field privateField = clazz.getDeclaredField("privateField"); // 设置私有字段为可访问(通过 setAccessible) privateField.setAccessible(true); System.out.println("Private Field: " + privateField.get(obj)); // 获取并输出 privateField 的值 // 修改 private 字段的值 privateField.set(obj, "New Private Value"); System.out.println("Updated Private Field: " + privateField.get(obj)); // 获取修改后的值 } } 4.获取类的成员方法 获取单个指定的成员方法:第一个参数填方法名、第二个参数填方法中的参数类型 执行:第一个参数传入一个对象实例,然后是若干方法参数(无参可不写)... 示例:Cat 类与测试类 public class Cat { private String name; public int age; public Cat() { this.name = "Tom"; this.age = 1; } public void meow() { System.out.println("Meow! My name is " + this.name); } private void purr() { System.out.println("Purr... I'm a happy cat!"); } } public class FieldReflectionTest { @Test public void testMethodAccess() throws Exception { // 1. 获取 Cat 类的 Class 对象 Class<?> catClass = Cat.class; // 2. 创建 Cat 对象实例 Cat cat = new Cat(); // ---------------------- // A. 获取并调用 public 方法 // ---------------------- // 获取名为 "meow"、无参数的方法 Method meowMethod = catClass.getMethod("meow"); // 调用该方法 meowMethod.invoke(cat); // ---------------------- // B. 获取并调用 private 方法 // ---------------------- // 获取名为 "purr"、无参数的私有方法 Method purrMethod = catClass.getDeclaredMethod("purr"); purrMethod.setAccessible(true); // 关闭权限检查 purrMethod.invoke(cat); } } 注解 在 Java 中,注解用于给程序元素(类、方法、字段等)添加元数据,这些元数据可被编译器、工具或运行时反射读取,以实现配置、检查、代码生成以及框架支持(如依赖注入、AOP 等)功能,而不直接影响代码的业务逻辑。 比如:Junit框架的 @Test 注解可以用在方法上,用来标记这个方法是测试方法,被@Test标记的方法能够被Junit框架执行。 再比如:@Override 注解可以用在方法上,用来标记这个方法是重写方法,被@Override注解标记的方法能够被IDEA识别进行语法检查。 使用注解 元注解 是修饰注解的注解。 @Retention(RetentionPolicy.SOURCE) //只在源码阶段保留,编译后 `.class` 文件中不会有这个注解信息。 @Retention(RetentionPolicy.RUNTIME) //指定注解的生命周期,即在运行时有效,可用于反射等用途。 @Target(ElementType.TYPE) //类上的注解(包含类、接口、枚举等类型) @Target(ElementType.METHOD) //方法上的注解 @Target(ElementType.FIELD) //字段上的注解 注意,若想在运行时通过反射读取,只能设置@Retention(RetentionPolicy.RUNTIME) 定义注解 使用 @interface 定义注解 // 定义注解 @Retention(RetentionPolicy.RUNTIME) // 生命周期:运行时保留,可反射获取 @Target(ElementType.METHOD) // 目标:作用于方法 public @interface MyAnnotation { String description() default "This is a default description"; int value() default 0; } 用法: // 1. 只传 value,可省略属性名 @MyAnnotation(5) public void someMethod() {} // 2. 多属性赋值必须指明名称 @MyAnnotation(value = 5, description = "Specific description") public void anotherMethod() {} // 3. 使用默认值 @MyAnnotation public void defaultMethod() {} 解析注解 在 Java 中,注解本质上是类的元数据,要在运行时获取注解信息,必须依赖反射 API 来读取。 下面示例展示了如何通过反射获取方法上的自定义注解 @MyAnnotation。 1.定义示例类与注解 // 自定义注解 @Retention(RetentionPolicy.RUNTIME) // 运行时保留,可反射获取 @Target(ElementType.METHOD) // 仅可作用于方法 public @interface MyAnnotation { String value(); } // 使用注解 public class MyClass { @MyAnnotation(value = "specific value") public void myMethod() { // 方法实现 } } 2.通过反射获取注解 import java.lang.reflect.Method; public class AnnotationReader { public static void main(String[] args) throws NoSuchMethodException { // 获取MyClass的Class对象 Class<MyClass> obj = MyClass.class; // 获取myMethod方法的Method对象 Method method = obj.getMethod("myMethod"); // 获取方法上的MyAnnotation注解实例 MyAnnotation annotation = method.getAnnotation(MyAnnotation.class); if (annotation != null) { // 输出注解的value值 System.out.println("注解的value: " + annotation.value()); } } } // 反射调用链: // Class → 通过类加载器获取类的运行时描述对象; // Method → 从 Class 对象中获取方法的反射对象; // getAnnotation() → 从方法反射对象中获取注解实例。 3.快速判断注解是否存在 if (method.isAnnotationPresent(MyAnnotation.class)) { // 如果存在MyAnnotation注解,则执行相应逻辑 } Junit 单元测试 步骤 1.导入依赖 将 JUnit 框架的 jar 包添加到项目中(注意:IntelliJ IDEA 默认集成了 JUnit,无需手动导入)。 2.编写测试类 为待测业务方法创建对应的测试类。 测试类中定义测试方法,要求方法必须为 public 且返回类型为 void。 3.添加测试注解 在测试方法上添加 @Test 注解,确保 JUnit 能自动识别并执行该方法。 4.运行测试 在测试方法上右键选择“JUnit运行”。 测试通过显示绿色标志; 测试失败显示红色标志。 public class UserMapperTest { @Test public void testListUser() { UserMapper userMapper = new UserMapper(); List<User> list = userMapper.list(); Assert.assertNotNull("User list should not be null", list); list.forEach(System.out::println); } } 注意,如果需要使用依赖注入,需要在测试类上加@SpringBootTest注解 它会启动 Spring 应用程序上下文,并在测试期间模拟运行整个 Spring Boot 应用程序。这意味着你可以在集成测试中使用 Spring 的各种功能,例如自动装配、依赖注入、配置加载等 @RunWith(SpringRunner.class) @SpringBootTest public class UserMapperTest { @Autowired private UserMapper userMapper; @Test public void testListUser() { List<User> list = userMapper.list(); Assert.assertNotNull("User list should not be null", list); list.forEach(System.out::println); } } 写了@Test注解,那么该测试函数就可以直接运行!若一个测试类中写了多个测试方法,可以全部执行! 原理可能是: //自定义注解 @Retention(RetentionPolicy.RUNTIME) //指定注解在运行时可用,这样才能通过反射获取到该注解。 @Target(ElementType.METHOD) //指定注解可用于方法上。 public @interface MyTest { } public class AnnotationTest4 { @MyTest public void test() { System.out.println("===test4==="); } public static void main(String[] args) throws Exception { AnnotationTest4 instance = new AnnotationTest4(); // 1. 获取 Class 对象 Class<?> clazz = AnnotationTest4.class; // 2. 获取类中声明的所有方法 Method[] methods = clazz.getDeclaredMethods(); // 3. 遍历方法,执行带 @MyTest 的方法 for (Method method : methods) { if (method.isAnnotationPresent(MyTest.class)) { method.invoke(instance); // 反射调用方法 } } } } 在Springboot中,如何快速生成单元测试? 选中类名,右键: JAVA笔试题总结 1)构造函数用于初始化当前类的对象,不能被继承。子类会调用父类的构造函数(通过 super),但这不是继承。 2)try-finally public static int func() { try { return 1; // 这个返回值被"吞噬" } finally { return 0; // 这个返回值覆盖了try中的值 } } // 结果: 返回 0 因此极不推荐在 finally 中使用 return 3)关于 Java 的包和部署,下列说法正确的是(单选题)? A. Java 虚拟机在使用 JAR 的 class 之前必须先将其解压缩 B. 编译包中的类时,完整名称必须和目录结构一致 C. 同一源文件中的不同类可以放到不同的包中 D. JAR 是保存 .class 文件的标准目录 解析: A 错误:JVM 可以直接读取 JAR 中的 .class 文件,无需解压(JAR 本质是 ZIP 格式,JVM 能直接加载)。 B 正确:Java 要求包名必须与文件目录结构完全匹配(例如 com.example.Test需放在 com/example/Test.java中),否则编译失败。 C 错误:同一 .java 源文件中的多个类默认属于同一个包,且只能有一个 public 类,不能拆分到不同包中。 D 错误:JAR 是压缩文件(归档格式),不是标准目录;标准目录是文件系统本身的文件夹结构。
后端学习
zy123
3月21日
0
12
0
2025-03-21
JavaWeb——后端
JavaWeb——后端 好用的操作 右键文件/文件夹选择Copy Path/Reference,可以获得完整的包路径 Java版本解决方案 单个Java文件运行: Edit Configurations 针对单个运行配置:每个 Java 运行配置(如主类、测试类等)可以独立设置其运行环境(如 JRE 版本、程序参数、环境变量等)。 不影响全局项目:修改某个运行配置的环境不会影响其他运行配置或项目的全局设置。 如何调整全局项目的环境 打开 File -> Project Structure -> Project。 在 Project SDK 中选择全局的 JDK 版本(如 JDK 17)。 在 Project language level 中设置全局的语言级别(如 17)。 Java Compiler File -> Settings -> Build, Execution, Deployment -> Compiler -> Java Compiler Maven Runner File -> Settings -> Build, Execution, Deployment -> Build Tools -> Maven -> Runner 三者之间的关系 全局项目环境 是基准,决定项目的默认 JDK 和语言级别。 Java Compiler 控制编译行为,可以覆盖全局的 Project language level。 Maven Runner 控制 Maven 命令的运行环境,可以覆盖全局的 Project SDK。 Maven 项目: 确保 pom.xml 中的 <maven.compiler.source> 和 <maven.compiler.target> 与 Project SDK 和 Java Compiler 的配置一致。 确保 Maven Runner 中的 JRE 与 Project SDK 一致。 如果还是不行,pom文件右键点击maven->reload project HTTP协议 响应状态码 状态码分类 说明 1xx 响应中 --- 临时状态码。表示请求已经接受,告诉客户端应该继续请求或者如果已经完成则忽略 2xx 成功 --- 表示请求已经被成功接收,处理已完成 3xx 重定向 --- 重定向到其它地方,让客户端再发起一个请求以完成整个处理 4xx 客户端错误 --- 处理发生错误,责任在客户端,如:客户端的请求一个不存在的资源,客户端未被授权,禁止访问等 5xx 服务器端错误 --- 处理发生错误,责任在服务端,如:服务端抛出异常,路由出错,HTTP版本不支持等 状态码 英文描述 解释 200 OK 客户端请求成功,即处理成功,这是我们最想看到的状态码 302 Found 指示所请求的资源已移动到由Location响应头给定的 URL,浏览器会自动重新访问到这个页面 304 Not Modified 告诉客户端,你请求的资源至上次取得后,服务端并未更改,你直接用你本地缓存吧。隐式重定向 400 Bad Request 客户端请求有语法错误,不能被服务器所理解 403 Forbidden 服务器收到请求,但是拒绝提供服务,比如:没有权限访问相关资源 404 Not Found 请求资源不存在,一般是URL输入有误,或者网站资源被删除了 405 Method Not Allowed 请求方式有误,比如应该用GET请求方式的资源,用了POST 429 Too Many Requests 指示用户在给定时间内发送了太多请求(“限速”),配合 Retry-After(多长时间后可以请求)响应头一起使用 500 Internal Server Error 服务器发生不可预期的错误。服务器出异常了,赶紧看日志去吧 503 Service Unavailable 服务器尚未准备好处理请求,服务器刚刚启动,还未初始化好 开发规范 REST风格 在前后端进行交互的时候,我们需要基于当前主流的REST风格的API接口进行交互。 什么是REST风格呢? REST(Representational State Transfer),表述性状态转换,它是一种软件架构风格。 传统URL风格如下: http://localhost:8080/user/getById?id=1 GET:查询id为1的用户 http://localhost:8080/user/saveUser POST:新增用户 http://localhost:8080/user/updateUser PUT:修改用户 http://localhost:8080/user/deleteUser?id=1 DELETE:删除id为1的用户 我们看到,原始的传统URL,定义比较复杂,而且将资源的访问行为对外暴露出来了。 基于REST风格URL如下: http://localhost:8080/users/1 GET:查询id为1的用户 http://localhost:8080/users POST:新增用户 http://localhost:8080/users PUT:修改用户 http://localhost:8080/users/1 DELETE:删除id为1的用户 其中总结起来,就一句话:通过URL定位要操作的资源,通过HTTP动词(请求方式)来描述具体的操作。 REST风格后端代码: @RestController @RequestMapping("/depts") //定义当前控制器的请求前缀 public class DeptController { // GET: 查询资源 @GetMapping("/{id}") public Dept getDept(@PathVariable Long id) { ... } // POST: 新增资源 @PostMapping public void createDept(@RequestBody Dept dept) { ... } // PUT: 更新资源 @PutMapping public void updateDept(@RequestBody Dept dept) { ... } // DELETE: 删除资源 @DeleteMapping("/{id}") public void deleteDept(@PathVariable Long id) { ... } } GET:查询,用 URL 传参,不能带 body。 POST:创建/提交,可以用 body 传数据(JSON、表单)。 PUT:更新,可以用 body 。 DELETE:删除,一般无 body,只要 -X DELETE。 开发流程 查看页面原型明确需求 根据页面原型和需求,进行表结构设计、编写接口文档(已提供) 阅读接口文档 思路分析 功能接口开发 就是开发后台的业务功能,一个业务功能,我们称为一个接口(Controller 中一个完整的处理请求的方法) 功能接口测试 功能开发完毕后,先通过Postman进行功能接口测试,测试通过后,再和前端进行联调测试 前后端联调测试 和前端开发人员开发好的前端工程一起测试 SpringBoot Servlet 容器 是用于管理和运行 Web 应用的环境,它负责加载、实例化和管理 Servlet 组件,处理 HTTP 请求并将请求分发给对应的 Servlet。常见的 Servlet 容器包括 Tomcat、Jetty、Undertow 等。 SpringBoot的WEB默认内嵌了tomcat服务器,非常方便!!! 浏览器与 Tomcat 之间通过 HTTP 协议进行通信,而 Tomcat 则充当了中间的桥梁,将请求路由到你的 Java 代码,并最终将处理结果返回给浏览器。 查看springboot版本:查看pom文件 <parent> <artifactId>spring-boot-starter-parent</artifactId> <groupId>org.springframework.boot</groupId> <version>2.7.3</version> </parent> 版本为2.7.3 快速启动 新建spring initializr project 删除以下文件 新建HelloController类 @RestController public class HelloController { @RequestMapping("/hello") public String hello(){ System.out.println("hello"); return "hello"; } } 然后启动服务器,main程序 package edu.whut; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SprintbootQuickstartApplication { public static void main(String[] args) { SpringApplication.run(SprintbootQuickstartApplication.class, args); } } 然后浏览器访问 localhost:8080/hello。 SpringBoot请求 简单参数 在Springboot的环境中,对原始的API进行了封装,接收参数的形式更加简单。 如果是简单参数,参数名与形参变量名相同,定义同名的形参即可接收参数。 @RestController public class RequestController { // http://localhost:8080/simpleParam?name=Tom&age=10 // 第1个请求参数: name=Tom 参数名:name,参数值:Tom // 第2个请求参数: age=10 参数名:age , 参数值:10 //springboot方式 @RequestMapping("/simpleParam") public String simpleParam(String name , Integer age ){//形参名和请求参数名保持一致 System.out.println(name+" : "+age); return "OK"; } } 如果方法形参名称与请求参数名称不一致,controller方法中的形参还能接收到请求参数值吗? 解决方案:可以使用Spring提供的@RequestParam注解完成映射 在方法形参前面加上 @RequestParam 然后通过value属性执行请求参数名,从而完成映射。代码如下: @RestController public class RequestController { // http://localhost:8080/simpleParam?name=Tom&age=20 // 请求参数名:name //springboot方式 @RequestMapping("/simpleParam") public String simpleParam(@RequestParam("name") String username , Integer age ){ System.out.println(username+" : "+age); return "OK"; } } 实体参数 复杂实体对象指的是,在实体类中有一个或多个属性,也是实体对象类型的。如下: User类中有一个Address类型的属性(Address是一个实体类) 复杂实体对象的封装,需要遵守如下规则: 请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套实体类属性参数。 注意:这里User前面不能加@RequestBody是因为请求方式是 Query 或 路径 参数;如果是JSON请求体(Body)就必须加。 @RequestMapping("/complexpojo") public String complexpojo(User user){ System.out.println(user); return "OK"; } @Data @NoArgsConstructor @AllArgsConstructor public class User { private String name; private Integer age; private Address address; } @Data @NoArgsConstructor @AllArgsConstructor public class Address { private String province; private String city; } 数组参数 数组参数:请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数 @RestController public class RequestController { //数组集合参数 @RequestMapping("/arrayParam") public String arrayParam(String[] hobby){ System.out.println(Arrays.toString(hobby)); return "OK"; } } 路径参数 请求的URL中传递的参数 称为路径参数。例如: http://localhost:8080/user/1 http://localhost:880/user/1/0 注意,路径参数使用大括号 {} 定义 @RestController public class RequestController { //路径参数 @RequestMapping("/path/{id}/{name}") public String pathParam2(@PathVariable Integer id, @PathVariable String name){ System.out.println(id+ " : " +name); return "OK"; } } 在路由定义里用 {id} 只是一个占位符,实际请求时 不要 带大括号 JSON格式参数 { "backtime": [ "与中标人签订合同后 5日内", "投标截止时间前撤回投标文件并书面通知招标人的,2日内", "开标现场投标文件被拒收,开标结束后,2日内" ], "employees": [ { "firstName": "John", "lastName": "Doe" }, { "firstName": "Anna", "lastName": "Smith" }, { "firstName": "Peter", "lastName": "Jones" } ] } JSON 格式的核心特征 接口文档中的请求参数中是 'Body' 发送数据 数据为键值对:数据存储在键值对中,键和值用冒号分隔。在你的示例中,每个对象有两个键值对,如 "firstName": "John"。 使用大括号表示对象:JSON 使用大括号 {} 包围对象,对象可以包含多个键值对。 使用方括号表示数组:JSON 使用方括号 [] 表示数组,数组中可以包含多个值,包括数字、字符串、对象等。在该示例中:"employees" 是一个对象数组,数组中的每个元素都是一个对象。 Postman如何发送JSON格式数据: 服务端Controller方法如何接收JSON格式数据: 传递json格式的参数,在Controller中会使用实体类进行封装。 封装规则:JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数。需要使用 @RequestBody标识。 @Data @NoArgsConstructor @AllArgsConstructor public class DataDTO { private List<String> backtime; private List<Employee> employees; } @Data @NoArgsConstructor @AllArgsConstructor public class Employee { private String firstName; private String lastName; } @RestController public class DataController { @PostMapping("/data") public String receiveData(@RequestBody DataDTO data) { System.out.println("Backtime: " + data.getBacktime()); System.out.println("Employees: " + data.getEmployees()); return "OK"; } } JSON格式工具包 用于高效地进行 JSON 与 Java 对象之间的序列化和反序列化操作。 引入依赖: <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.76</version> </dependency> 使用: import com.alibaba.fastjson.JSON; public class FastJsonDemo { public static void main(String[] args) { // 创建一个对象 User user = new User("Alice", 30); // 对象转 JSON 字符串 String jsonString = JSON.toJSONString(user); System.out.println("JSON String: " + jsonString); // JSON 字符串转对象 User parsedUser = JSON.parseObject(jsonString, User.class); System.out.println("Parsed User: " + parsedUser); } } // JSON String: {"age":30,"name":"Alice"} // Parsed User: User(name=Alice, age=30) SpringBoot响应 @ResponseBody注解: 位置:书写在Controller方法上或类上 作用:将方法返回值直接响应给浏览器 如果返回值类型是实体对象/集合,将会转换为JSON格式后在响应给浏览器 @RestController = @Controller + @ResponseBody 统一响应结果: 下图返回值分别是字符串、对象、集合。 定义统一返回结果类 响应状态码:当前请求是成功,还是失败 状态码信息:给页面的提示信息 返回的数据:给前端响应的数据(字符串、对象、集合) 定义在一个实体类Result来包含以上信息。代码如下: @Data @NoArgsConstructor @AllArgsConstructor public class Result { private Integer code;//响应码,1 代表成功; 0 代表失败 private String msg; //响应信息 描述字符串 private Object data; //返回的数据 //增删改 成功响应 public static Result success(){ return new Result(1,"success",null); } //查询 成功响应 public static Result success(Object data){ return new Result(1,"success",data); } //失败响应 public static Result error(String msg){ return new Result(0,msg,null); } } Spring分层架构 三层架构 Controller层接收请求,调用Service层;Service层先调用Dao层获取数据,然后实现自己的业务逻辑处理部分,最后返回给Controller层;Controller层再响应数据。可理解为递归的过程。 **传统模式:**对象的创建、管理和依赖关系都由程序员手动编写代码完成,程序内部控制对象的生命周期。 例如: public class A { private B b; public A() { b = new B(); // A 自己创建并管理 B 的实例 } } 假设有类 A 依赖类 B,在传统方式中,类 A 可能在构造方法或方法内部直接调用 new B() 来创建 B 的实例。 如果 B 的创建方式发生变化,A 也需要修改代码。这就导致了耦合度较高。 软件设计原则:高内聚低耦合。 高内聚指的是:一个模块中各个元素之间的联系的紧密程度,如果各个元素(语句、程序段)之间的联系程度越高,则内聚性越高,即 "高内聚"。 低耦合指的是:软件中各个层、模块之间的依赖关联程序越低越好。 IOC控制反转 将对象的创建和依赖关系的管理交给容器,而不是由程序中的各个组件自行管理。 在 IOC 中,容器负责以下几件事: 管理对象的创建:容器根据配置或注解实例化项目中的类,将它们变成“Bean”。 维护依赖关系:容器跟踪各个 Bean 之间的依赖,并在需要时自动注入它们的依赖。 管理生命周期:容器负责控制对象的生命周期,包括对象的初始化和销毁。 依赖注入DI 类 A 不再自己创建 B,而是声明自己需要一个 B,容器在创建 A 时会自动将 B 的实例提供给 A。 DI 是实现 IOC 的一种方式。 public class A { private B b; // 通过构造器注入依赖 public A(B b) { this.b = b; } } 1.Autowird注入 @Autowired private PaymentClient paymentClient; // 字段直接加 @Autowired 2.构造器注入(推荐) 1)手写构造器 public class OrderService { private final PaymentClient paymentClient; // 在构造器上无需加 @Autowired(Spring Boot 下可省略) public OrderService(PaymentClient paymentClient) { this.paymentClient = paymentClient; } } 2)Lombok @RequiredArgsConstructor 用 Lombok 自动为所有 final 字段生成构造器,进一步简化写法: @RequiredArgsConstructor public class OrderService { private final PaymentClient paymentClient; // Lombok 会在编译期生成构造器 } controller层应注入接口类,而不是子类,如果只有一个子类实现类,那么直接注入即可,否则需要指定注入哪一个 @Service("categoryServiceImplV1") public class CategoryServiceImplV1 implements CategoryService { … } @Service("categoryServiceImplV2") public class CategoryServiceImplV2 implements CategoryService { … } @RestController @RequiredArgsConstructor // 推荐构造器注入 public class CategoryController { @Qualifier("categoryServiceImplV2") // 指定注入 V2 private final CategoryService categoryService; } 分层解耦 Bean 对象:在 Spring 中,被容器管理的对象称为 Bean。通过注解(如 @Component, @Service, @Repository, @Controller),可以将一个普通的 Java 类声明为 Bean,容器会负责它的创建、初始化以及生命周期管理。 Component衍生注解 注解 说明 位置 @Controller @Component的衍生注解 标注在控制器类上Controller @Service @Component的衍生注解 标注在业务类上Service @Repository @Component的衍生注解 标注在数据访问类上(由于与mybatis整合,用的少)DAO @Component 声明bean的基础注解 不属于以上三类时,用此注解 注:@Mapper 注解本身并不是 Spring 框架提供的,是用于 MyBatis 数据层的接口标识,但效果类似。 SpringBoot原理 配置文件 配置优先级 在SpringBoot项目当中,常见的属性配置方式有5种, 3种配置文件,加上2种外部属性的配置(Java系统属性、命令行参数)。优先级(从低到高): application.yaml(忽略) application.yml application.properties java系统属性(-Dxxx=xxx) 命令行参数(--xxx=xxx) 在 Spring Boot 项目中,通常使用的是 application.yml 或 application.properties 文件,这些文件通常放在项目的 src/main/resources 目录下。 如果项目已经打包上线了,这个时候我们又如何来设置Java系统属性和命令行参数呢? java -Dserver.port=9000 -jar XXXXX.jar --server.port=10010 在这个例子中,由于命令行参数的优先级高于 Java 系统属性,最终生效的 server.port 是 10010。 properties 位置:src/main/resources/application.properties 将配置信息写在application.properties,用注解@Value获取配置文件中的数据 yml配置文件(推荐!!!) 位置:src/main/resources/application.yml 了解下yml配置文件的基本语法: 大小写敏感 数据前边必须有空格,作为分隔符 使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(idea中会自动将Tab转换为空格) 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可 #表示注释,从这个字符一直到行尾,都会被解析器忽略 对象/map集合 user: name: zhangsan detail: age: 18 password: "123456" 数组/List/Set集合 hobby: - java - game - sport //获取示例 @Value("${hobby}") private List<String> hobby; 以上获取配置文件中的属性值,需要通过@Value注解,有时过于繁琐!!! @ConfigurationProperties 是用来将外部配置(如 application.yml)映射到一个 POJO 上的。 在 Spring Boot 中,根据 驼峰命名转换规则,自动将 YAML 配置文件中的 键名(例如 user-token-name user_token_name)映射到 Java 类中的属性(例如 userTokenName)。 @Data @Component @ConfigurationProperties(prefix = "aliyun.oss") public class AliOssProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; } Spring提供的简化方式套路: 需要创建一个实现类,且实体类中的属性名和配置文件当中key的名字必须要一致 比如:配置文件当中叫endpoints,实体类当中的属性也得叫endpoints,另外实体类当中的属性还需要提供 getter / setter方法 ==》@Data 需要将实体类交给Spring的IOC容器管理,成为IOC容器当中的bean对象 ==>@Component 在实体类上添加@ConfigurationProperties注解,并通过perfix属性来指定配置参数项的前缀 (可选)引入依赖pom.xml (自动生成配置元数据,让 IDE 能识别并补全你在 application.properties/yml 中的自定义配置项,提高开发体验,不加不影响运行!) <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </dependency> 隐私数据配置 通常设置一个 application-local.yml 存放隐私数据,且加入 .gitignore ,表示仅存储在本地;然后application.yml 通过占位符引入该文件,eg: application.yml # 主配置文件,导入本地隐私文件,并通过占位符引用 spring: config: # Spring Boot 2.4+ 推荐用 import import: optional:classpath:/application-local.yml myapp: datasource: url: jdbc:mysql://localhost:3306/pay_mall # 下面两个会从 application-local.yml 里拿 username: ${datasource.username} password: ${datasource.password} application-local.yml # 本地专属配置,激活时才会加载 datasource: username: root password: 123456 这里有个松散绑定的原则,对于 ${datasource.username} 这里匹配有效: userName username user-name user_name 这四个是等价的 底层做法是: 全部字符转小写 → username 去掉分隔符(-、_、.、空格) → username 再匹配到 Java Bean 里的驼峰字段 userName(或直接 username,视你写的字段而定) Bean 的获取和管理 获取Bean 1.自动装配(@Autowired) @Service public class MyService { @Autowired private MyRepository myRepository; // 自动注入 MyRepository Bean } 2.手动获取(ApplicationContext) ApplicationContext 是 Spring 的 IoC 容器,可以手动获取 Bean: Spring 会默认采用类名并将首字母小写作为 Bean 的名称。例如,类名为 DeptController 的组件默认名称就是 deptController。 @RunWith(SpringRunner.class) @SpringBootTest public class SpringbootWebConfig2ApplicationTests { @Autowired private ApplicationContext applicationContext; // IoC 容器 @Test public void testGetBean() { // 根据 Bean 名称获取 DeptController bean = (DeptController) applicationContext.getBean("deptController"); System.out.println(bean); } } 默认情况下,Spring 在容器启动时会创建所有单例 Bean(饿汉模式);使用 @Lazy 注解则可实现延迟加载(懒汉模式) bean的作用域 作用域 说明 singleton 容器内同名称的bean只有一个实例(单例)(默认) prototype 每次使用该bean时会创建新的实例(非单例) 在设计单例类时,通常要求它们是无状态的,不仅要确保成员变量不可变,还需要确保成员方法不会对共享的、可变的状态进行不受控制的修改,从而实现整体的线程安全。 @Service public class CalculationService { // 不可变的成员变量 private final double factor = 2.0; // 成员方法仅依赖方法参数和不可变成员变量 public double multiply(double value) { return value * factor; } } 更改作用域方法: 在bean类上加注解@Scope("prototype")(或其他作用域标识)即可。 第三方 Bean配置 如果需要管理的 Bean 不是自己写的类(如第三方库提供的对象),不能用 @Component,必须借助 @Bean 注册: @Configuration // 配置类 public class CommonConfig { // 定义第三方 Bean,并交给 IoC 容器管理 @Bean public SAXReader reader(DeptService deptService) { System.out.println(deptService); return new SAXReader(); } } 默认 Bean 名称:方法名 → "reader"。 Bean 类型:返回值类型 → SAXReader。 使用: @Service public class XmlProcessingService { // 按类型注入 @Autowired private SAXReader reader; public void parse(String xmlPath) throws DocumentException { Document doc = reader.read(new File(xmlPath)); // ... 处理 Document ... } } 只有一个同类型 Bean 时:@Autowired 按类型注入,字段名如(reader)不必和 @Bean 方法名一致。 多个同类型 Bean 时:需要用 @Qualifier 或 @Resource 指定 Bean 名称。 SpirngBoot原理 如果我们直接基于Spring框架进行项目的开发,会比较繁琐。SpringBoot框架之所以使用起来更简单更快捷,是因为SpringBoot框架底层提供了两个非常重要的功能:一个是起步依赖,一个是自动配置。 起步依赖 Spring Boot 只需要引入一个起步依赖(例如 springboot-starter-web)就能满足项目开发需求。这是因为: Maven 依赖传递: 起步依赖内部已经包含了开发所需的常见依赖(如 JSON 解析、Web、WebMVC、Tomcat 等),无需开发者手动引入其它依赖。 结论: 起步依赖的核心原理就是 Maven 的依赖传递机制。 自动装配 在传统 Spring 开发中,需要在 applicationContext.xml 或 @Configuration 类里手工声明大量 Bean。Spring Boot 为了简化配置,引入了 自动装配(Auto Configuration),让我们只需少量注解就能“开箱即用”。 自动配置原理源码入口就是@SpringBootApplication注解,在这个注解中封装了3个注解,分别是: @SpringBootConfiguration 声明当前类是一个配置类,作用类似 @Configuration,但作为 Spring Boot 的专用配置入口标识。 @ComponentScan 用于 扫描项目自定义的 Bean,比如 @Component、@Service、@Repository、@Controller 等。 默认扫描 启动类所在包及其子包。 如果项目分为 server、common 等模块,可以通过: @ComponentScan({"com.your.package.server", "com.your.package.common"}) 来显式指定需要扫描的包路径。 @EnableAutoConfiguration(自动配置核心注解) 导入自动配置类 通过元注解 @Import(AutoConfigurationImportSelector.class),在启动时读取所有 JAR 包中 META‑INF/spring.factories 中声明的自动配置类,并注册到容器。 按条件注册 Bean 配置类内部使用条件注解(@ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty 等)。 仅在 类路径、配置属性、已有 Bean 满足条件时,才创建对应 Bean。 典型示例 DataSourceAutoConfiguration:检测到 JDBC 驱动和 spring.datasource. 配置时自动创建数据源。 WebMvcAutoConfiguration:配置 Spring MVC 默认组件;若用户提供了自定义 MVC 配置,则自动“退让”(Back Off)。 自动配置的效果: 如何让第三方bean以及配置类生效? 在 Spring Boot 中,默认只会扫描 启动类所在包及其子包 下的组件。 如果某些类或配置类(如 CommonConfig)不在扫描路径内,就不会被容器加载,需要手动导入。 常见的三种方式如下: 1.使用 @ComponentScan(批量导入) 通过指定包路径,扩大扫描范围,让其中的组件或配置类被容器加载: @SpringBootApplication(scanBasePackages = {"com.example.server", "com.example.common"}) public class DemoApplication {} 特点:适合批量导入,但可能引入无关类,配置繁琐、性能相对较低。 2.使用 @Import(精确导入) @Import 可以直接将类或配置类导入到容器中,更加灵活精准。 导入普通类 即使类上没有 @Component,也能被 Spring 管理: public class TokenParser { public void parse() { System.out.println("TokenParser ... parse ..."); } } @SpringBootApplication @Import(TokenParser.class) // 导入普通类 public class DemoApplication {} 导入配置类 配置类中集中定义多个 Bean: @Configuration public class HeaderConfig { @Bean public HeaderParser headerParser() { return new HeaderParser(); } @Bean public HeaderGenerator headerGenerator() { return new HeaderGenerator(); } } @SpringBootApplication @Import(HeaderConfig.class) // 导入配置类 public class DemoApplication {} 3.使用 @EnableXxx 注解(推荐) 很多第三方依赖会提供一个 @EnableXxx 注解,内部基于 @Import 封装,可以一次性导入多个配置类或 Bean。 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Import(MyImportSelector.class) // 指定要导入的 Bean 或配置类 public @interface EnableHeaderConfig {} 应用启动时,只需加上该注解即可: @EnableHeaderConfig // 开启第三方依赖提供的功能 @SpringBootApplication public class DemoApplication {} 特点:最常用、最优雅的方式,Spring Boot 内置的很多功能(如 @EnableScheduling、@EnableAsync、@EnableWebMvc)都是通过这种模式实现的。 如何自定义 Starter 实现自动装配 1.准备一个服务类 这是你希望自动装配到 Spring 容器中的类,比如一个简单的业务类。 package com.example.demo.service; public class HelloService { public String sayHello(String name) { return "Hello, " + name; } } 2.编写自动配置类 使用 @Configuration 注解,把上面的服务注册为一个 Spring Bean。 package com.example.demo.autoconfig; import com.example.demo.service.HelloService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class HelloServiceAutoConfiguration { @Bean public HelloService helloService() { return new HelloService(); } } 3.在 META-INF/spring.factories 中声明 在你的项目(或者 starter jar 包)里新建一个文件: src/main/resources/META-INF/spring.factories ,内容如下: org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.demo.autoconfig.HelloServiceAutoConfiguration Spring Boot 启动时会读取所有依赖里的 spring.factories 文件,把里面的配置类自动加载。 4.打包成 Starter Jar 把项目打成一个独立的 Jar(通常命名为 xxx-spring-boot-starter),发布到本地或远程仓库。 5.在应用中使用 只要在别的 Spring Boot 应用里引入这个依赖,就能直接注入并使用: @RestController public class TestController { private final HelloService helloService; public TestController(HelloService helloService) { this.helloService = helloService; //构造器注入 } @GetMapping("/hi") public String hi(String name) { return helloService.sayHello(name); } } Spring Boot 的 自动配置类(通过 spring.factories 声明的)通常是 打包成一个独立的 Jar(Starter),然后给别的项目引用。这样别人只要在 pom.xml 或 build.gradle 里加上依赖,就能自动获得你配置的 Bean。 常见的注解!! @RequestMapping("/jsonParam"):可以用于控制器级别,也可以用于方法级别。 用于方法:HTTP 请求路径为 /jsonParam 的请求将调用该方法。 @RequestMapping("/jsonParam") public String jsonParam(@RequestBody User user){ System.out.println(user); return "OK"; } 用于控制器: 所有方法的映射路径都会以这个前缀开始。 @RestController @RequestMapping("/depts") public class DeptController { @GetMapping("/{id}") public Dept getDept(@PathVariable Long id) { // 实现获取部门逻辑 } @PostMapping public void createDept(@RequestBody Dept dept) { // 实现新增部门逻辑 } } @RequestBody:这是一个方法参数级别的注解,用于告诉Spring框架将请求体的内容解析为指定的Java对象。 @RestController:这是一个类级别的注解,它告诉Spring框架这个类是一个控制器(Controller),并且处理HTTP请求并返回响应数据。与 @Controller 注解相比,@RestController 注解还会自动将控制器方法返回的数据转换为 JSON 格式,并写入到HTTP响应中,得益于@ResponseBody 。 @RestController = @Controller + @ResponseBody @PathVariable 注解用于将路径参数 {id} 的值绑定到方法的参数 id 上。当请求的路径是 "/path/123" 时,@PathVariable 会将路径中的 "123" 值绑定到方法的参数 id 上。 public String pathParam(@PathVariable Integer id) { System.out.println(id); return "OK"; } //参数名与路径名不同 @GetMapping("/{id}") public ResponseEntity<User> getUserById(@PathVariable("id") Long userId) { } @RequestParam, 1)如果方法的参数名与请求参数名不同,需要在 @RequestParam 注解中指定请求参数的名字。 类似@PathVariable,可以指定参数名称。 @RequestMapping("/example") public String exampleMethod(@RequestParam String name, @RequestParam("age") int userAge) { // 在方法内部使用获取到的参数值进行处理 System.out.println("Name: " + name); System.out.println("Age: " + userAge); return "OK"; } 2)还可以设置默认值 @RequestMapping("/greet") public String greet(@RequestParam(defaultValue = "Guest") String name) { return "Hello, " + name; } 3)如果既改请求参数名字,又要设置默认值 @RequestMapping("/greet") public String greet(@RequestParam(value = "age", defaultValue = "25") int userAge) { return "Age: " + userAge; } 4)如果方法参数是简单类型(int/Integer、String、boolean/Boolean 等及它们的一维数组),那么无需使用@RequestParam,如果是Collection集合类型,必须使用。 List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids); 控制反转与依赖注入: @Component、@Service、@Repository 用于标识 bean 并让容器管理它们,从而实现 IoC。 @Autowired、@Configuration、@Bean 用于实现 DI,通过容器自动装配或配置 bean 的依赖。 数据库相关。 @Mapper注解:表示是mybatis中的Mapper接口,程序运行时,框架会自动生成接口的实现类对象(代理对象),并交给Spring的IOC容器管理 @Select注解:代表的就是select查询,用于书写select查询语句 @SpringBootTest:它会启动 Spring 应用程序上下文,并在测试期间模拟运行整个 Spring Boot 应用程序。这意味着你可以在集成测试中使用 Spring 的各种功能,例如自动装配、依赖注入、配置加载等。 lombok的相关注解。非常实用的工具库。 在pom.xml文件中引入依赖 <!-- 在springboot的父工程中,已经集成了lombok并指定了版本号,故当前引入依赖时不需要指定version --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> 在实体类上添加以下注解(加粗为常用) 注解 作用 @Getter/@Setter 为所有的属性提供get/set方法 @ToString 会给类自动生成易阅读的 toString 方法 @EqualsAndHashCode 根据类所拥有的非静态字段自动重写 equals 方法和 hashCode 方法 @Data 提供了更综合的生成代码功能(@Getter + @Setter + @ToString + @EqualsAndHashCode) @NoArgsConstructor 为实体类生成无参的构造器方法 @AllArgsConstructor 为实体类生成除了static修饰的字段之外带有各参数的构造器方法。 @Slf4j 可以log.info("输出日志信息"); //equals 方法用于比较两个对象的内容是否相同 Address addr1 = new Address("SomeProvince", "SomeCity"); Address addr2 = new Address("SomeProvince", "SomeCity"); System.out.println(addr1.equals(addr2)); // 输出 true log: log.info("应用启动成功"); Long empId = 12L; log.info("当前员工id:{}", empId); //带占位符,推荐! log.info("当前员工id:" + empId); //不错,但不推荐 log.info("当前员工id:", empId); //错误的! @Test,Junit测试单元,可在测试类中定义测试函数,一次性执行所有@Test注解下的函数,不用写main方法 @Override,当一个方法在子类中覆盖(重写)了父类中的同名方法时,为了确保正确性,可以使用 @Override 注解来标记这个方法,这样编译器就能够帮助检查是否正确地重写了父类的方法。 @DateTimeFormat将日期转化为指定的格式。Spring会尝试将接收到的字符串参数转换为控制器方法参数的相应类型。 @RestController public class DateController { // 例如:请求 URL 为 /search?begin=2025-03-28 @GetMapping("/search") public String search(@RequestParam("begin") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin) { // 此时 begin 已经是 LocalDate 类型,可以直接使用 return "接收到的日期是: " + begin; } } @RestControllerAdvice= @ControllerAdvice + @ResponseBody。加上这个注解就代表我们定义了一个全局异常处理器,而且处理异常的方法返回值会转换为json后再响应给前端 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public String handleException(Exception ex) { // 返回错误提示或错误详情 return "系统发生异常:" + ex.getMessage(); } } @Configuration和@Bean配合使用,可以对第三方bean进行集中的配置管理,依赖注入!!@Bean用于方法上。加了@Configuration,当Spring Boot应用启动时,它会执行一系列的自动配置步骤。 @ComponentScan指定了Spring应该在哪些包下搜索带有@Component、@Service、@Repository、@Controller等注解的类,以便将这些类自动注册为Spring容器管理的Bean.@SpringBootApplication它是一个便利的注解,组合了@Configuration、@EnableAutoConfiguration和@ComponentScan注解。 @Async 注解,异步执行 1.在你的配置类或主启动类上添加: @Configuration @EnableAsync public class AsyncConfig { // 可以自定义线程池 Bean(可选) } 2.在你希望异步执行的方法或它所在的 Bean 上,添加 @Async @Service public class EmailService { @Async public void sendWelcomeEmail(String userId) { // 这个方法会在独立线程中执行 // 调用线程会立即返回,不会等待方法内部逻辑完成 // … 发送邮件的耗时操作 … } } 登录校验 会话技术 会话是和浏览器关联的,当有三个浏览器客户端和服务器建立了连接时,就会有三个会话。同一个浏览器在未关闭之前请求了多次服务器,这多次请求是属于同一个会话。比如:1、2、3这三个请求都是属于同一个会话。当我们关闭浏览器之后,这次会话就结束了。而如果我们是直接把web服务器关了,那么所有的会话就都结束了。 会话跟踪技术有三种: Cookie(客户端会话跟踪技术) Session(服务端会话跟踪技术) 令牌技术 Cookie 原理:会话数据存储在客户端浏览器中,通过浏览器自动管理。 优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的) 缺点: 移动端APP(Android、IOS)中无法使用Cookie 不安全,用户可以自己禁用Cookie Cookie不能跨域传递 Session 1.基本原理 服务端:存储会话数据(内存、Redis 等)。 客户端:仅保存会话 ID(如 JSESSIONID),通常通过 Cookie 传递。 2.数据结构 服务端会话存储(Map 或 Redis) { "abc123" -> HttpSession 实例 } HttpSession 结构: HttpSession ├─ id = "abc123" ├─ creationTime = ... ├─ lastAccessedTime = ... └─ attributes └─ "USER_LOGIN_STATE" -> user 实体对象 3.请求流程 首次请求 浏览器没有 JSESSIONID,服务端调用 createSession() 创建一个新会话(ID 通常是 UUID)。 服务端返回响应头 Set-Cookie: JSESSIONID=<新ID>; Max-Age=2592000(30 天有效期)。 浏览器将 JSESSIONID 写入本地 Cookie(持久化保存)。 后续请求 浏览器自动在请求头中附带 Cookie: JSESSIONID=<ID>。 服务端用该 ID 在会话存储中查找对应的 HttpSession 实例,恢复用户状态。 ┌───────────────┐ (带 Cookie JSESSIONID=abc123) │ Browser │ ───────►│ Tomcat │ └───────────────┘ └──────────┘ │ │ 用 abc123 做 key ▼ {abc123 → HttpSession} ← 找到 │ ▼ 取 attributes["USER_LOGIN_STATE"] → 得到 userrequest.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, user); 4.后端使用示例 保存登录状态: request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, user); request.getSession() 会自动获取当前请求关联的 HttpSession 实例。 获取登录状态: User user = (User) request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE); 退出登录: request.getSession().removeAttribute(UserConstant.USER_LOGIN_STATE); 相当于清空当前会话中的用户信息。浏览器本地的 JSESSIONID 依然存在,只不过后端啥也没了。 优点 会话数据保存在服务端,相比直接将数据存储在客户端更安全(防篡改)。 缺点 分布式集群下 Session 无法自动共享(需借助 Redis 等集中存储)。 客户端禁用 Cookie 时,Session 会失效。 服务端需要维护会话数据,高并发环境下可能带来内存或性能压力。 令牌JWT(推荐) 优点: 支持PC端、移动端 解决集群环境下的认证问题 减轻服务器的存储压力(无需在服务器端存储) 缺点:需要自己实现(包括令牌的生成、令牌的传递、令牌的校验) 跨域问题 跨域问题指的是在浏览器中,一个网页试图去访问另一个域下的资源时,浏览器出于安全考虑,默认会阻止这种操作。这是浏览器的同源策略(Same-Origin Policy)导致的行为。 同源策略(Same-Origin Policy) 同源策略是浏览器的一种安全机制,它要求: 协议(如 http、https) 域名/IP(如 example.com) 端口(如 80 或 443) 这三者必须完全相同,才能被视为同源。 举例: http://192.168.150.200/login.html ----------> https://192.168.150.200/login [协议不同,跨域] http://192.168.150.200/login.html ----------> http://192.168.150.100/login [IP不同,跨域] http://192.168.150.200/login.html ----------> http://192.168.150.200:8080/login [端口不同,跨域] http://192.168.150.200/login.html ----------> http://192.168.150.200/login [不跨域] 解决跨域问题的方法: CORS(Cross-Origin Resource Sharing)是解决跨域问题的标准机制。它允许服务器在响应头中加上特定的 CORS 头部信息,明确表示允许哪些外域访问其资源。 服务器端配置:服务器返回带有 Access-Control-Allow-Origin 头部的响应,告诉浏览器允许哪些域访问资源。 Access-Control-Allow-Origin: *(表示允许所有域访问) Access-Control-Allow-Origin: http://site1.com(表示只允许 http://site1.com 访问) 全局统一配置 import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebCorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") // 匹配所有 /api/** 路径 .allowedOrigins("http://allowed-domain.com") // 允许的域名 .allowedMethods("GET","POST","PUT","DELETE","OPTIONS") .allowedHeaders("Content-Type","Authorization") .allowCredentials(true) // 是否允许携带 Cookie .maxAge(3600); // 预检请求缓存 1 小时 } } Nginx解决方案 统一域名入口: 前端和 API 均通过 Nginx 以相同的域名(例如 https://example.com)提供服务。前端发送 AJAX 请求时,目标也是该域名的地址,如 https://example.com/api,从而避免了跨域校验。 Nginx 作为中间代理: Nginx 将特定路径(例如 /api/)的请求转发到后端服务器。对浏览器来说,请求和响应均来自同一域名,代理过程对浏览器透明。 “黑匣子”处理: 浏览器只与 Nginx 交互,不关心 Nginx 内部如何转发请求。无论后端位置如何,浏览器都认为响应源自统一域名,从而解决跨域问题。 总结 普通的跨域请求依然会送达服务器,服务器并不主动拦截;它只是通过响应头声明哪些来源被允许访问,而真正的拦截与安全检查,则由浏览器根据同源策略来完成。 JWT令牌 特性 Session JWT(JSON Web Token) 存储方式 服务端存储会话数据(如内存、Redis) 客户端存储完整的令牌(通常在 Header 或 Cookie) 标识方式 客户端持有一个 Session ID 客户端持有一个自包含的 Token 状态管理 有状态(Stateful),服务器要维护会话 无状态(Stateless),服务器不存会话 生成和校验 引入依赖 <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> 生成令牌与解析令牌: public class JwtUtils { private static String signKey = "zy123"; private static Long expire = 43200000L; //单位毫秒 12小时 /** * 生成JWT令牌 * @param claims JWT第二部分负载 payload 中存储的内容 * @return */ public static String generateJwt(Map<String, Object> claims){ String jwt = Jwts.builder() .addClaims(claims) .signWith(SignatureAlgorithm.HS256, signKey) .setExpiration(new Date(System.currentTimeMillis() + expire)) .compact(); return jwt; } /** * 解析JWT令牌 * @param jwt JWT令牌 * @return JWT第二部分负载 payload 中存储的内容 */ public static Claims parseJWT(String jwt){ Claims claims = Jwts.parser() .setSigningKey(signKey) .parseClaimsJws(jwt) .getBody(); return claims; } } 令牌可以存储当前登录用户的信息:id、username等等,传入claims Object 类型能够容纳字符串、数字等各种对象。 Map<String, Object> claims = new HashMap<>(); claims.put("id", emp.getId()); // 假设 emp.getId() 返回一个数字(如 Long 类型) claims.put("name", e.getName()); // 假设 e.getName() 返回一个字符串 claims.put("username", e.getUsername()); // 假设 e.getUsername() 返回一个字符串 String jwt = JwtUtils.generateJwt(claims); 解析令牌: @Autowired private HttpServletRequest request; String jwt = request.getHeader("token"); Claims claims = JwtUtils.parseJWT(jwt); // 解析 JWT 令牌 // 获取存储的 id, name, username Long id = (Long) claims.get("id"); // 如果 "id" 是 Long 类型 String name = (String) claims.get("name"); String username = (String) claims.get("username"); JWT 登录认证流程 用户登录 用户发起登录请求,校验密码、登录成功后,生成 JWT 令牌,并将其返回给前端。 前端存储令牌 前端接收到 JWT 令牌,存储在浏览器中(通常存储在 LocalStorage 或 Cookie 中)。 // 登录成功后,存储 JWT 令牌到 LocalStorage const token = response.data.token; // 从响应中获取令牌 localStorage.setItem('token', token); // 存储到 LocalStorage // 在后续请求中获取令牌并附加到请求头 const storedToken = localStorage.getItem('token'); fetch("https://your-api.com/protected-endpoint", { method: "GET", headers: { "token": storedToken // 添加 token 到请求头 } }) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.log('Error:', error)); 请求带上令牌 后续的每次请求,前端将 JWT 令牌携带上。 服务端校验令牌 服务端接收到请求后,拦截请求并检查是否携带令牌。若没有令牌,拒绝访问;若令牌存在,校验令牌的有效性(包括有效期),若有效则放行,进行请求处理。 注意,使用APIFOX测试时,需要在headers中添加 {token:"jwt令牌..."}否则会无法通过拦截器。 拦截器(Interceptor) 在拦截器当中,我们通常也是做一些通用性的操作,比如:我们可以通过拦截器来拦截前端发起的请求,将登录校验的逻辑全部编写在拦截器当中。在校验的过程当中,如发现用户登录了(携带JWT令牌且是合法令牌),就可以直接放行,去访问spring当中的资源。如果校验时发现并没有登录或是非法令牌,就可以直接给前端响应未登录的错误信息。 快速入门 定义拦截器,实现HandlerInterceptor接口,并重写其所有方法 //自定义拦截器 @Component public class JwtTokenUserInterceptor implements HandlerInterceptor { //目标资源方法执行前执行。 返回true:放行 返回false:不放行 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle .... "); return true; //true表示放行 } //目标资源方法执行后执行 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle ... "); } //视图渲染完毕后执行,最后执行 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion .... "); } } 注意: preHandle方法:目标资源方法执行前执行。 返回true:放行 返回false:不放行 postHandle方法:目标资源方法执行后执行 afterCompletion方法:视图渲染完毕后执行,最后执行 注册配置拦截器,实现WebMvcConfigurer接口,并重写addInterceptors方法 @Configuration public class WebConfig implements WebMvcConfigurer { //自定义的拦截器对象 @Autowired private JwtTokenUserInterceptor jwtTokenUserInterceptor; @Override protected void addInterceptors(InterceptorRegistry registry) { log.info("开始注册自定义拦截器..."); registry.addInterceptor(jwtTokenUserInterceptor) .addPathPatterns("/user/**") .excludePathPatterns("/user/user/login") .excludePathPatterns("/user/shop/status"); } } WebMvcConfigurer接口: 拦截器配置 通过实现 addInterceptors 方法,可以添加自定义的拦截器,从而在请求进入处理之前或之后执行一些逻辑操作,如权限校验、日志记录等。 静态资源映射 通过 addResourceHandlers 方法,可以自定义静态资源(如 HTML、CSS、JavaScript)的映射路径,这对于使用前后端分离或者集成第三方文档工具(如 Swagger/Knife4j)非常有用。 消息转换器扩展 通过 extendMessageConverters 方法,可以在默认配置的基础上,追加自定义的 HTTP 消息转换器,如将 Java 对象转换为 JSON 格式。 跨域配置 使用 addCorsMappings 方法,可以灵活配置跨域资源共享(CORS)策略,方便前后端跨域请求。 拦截路径 addPathPatterns指定拦截路径; 调用excludePathPatterns("不拦截的路径")方法,指定哪些资源不需要拦截。 拦截路径 含义 举例 /* 一级路径 能匹配/depts,/emps,/login,不能匹配 /depts/1 /** 任意级路径 能匹配/depts,/depts/1,/depts/1/2 /depts/* /depts下的一级路径 能匹配/depts/1,不能匹配/depts/1/2,/depts /depts/** /depts下的任意级路径 能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1 登录校验 主要在preHandle中写逻辑 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判断当前拦截到的是Controller的方法还是其他资源 if (!(handler instanceof HandlerMethod)) { //当前拦截到的不是动态方法,直接放行 return true; } //1、从请求头中获取令牌 String token = request.getHeader(jwtProperties.getUserTokenName()); //2、校验令牌 try { log.info("jwt校验:{}", token); Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token); Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString()); log.info("当前用户id:", userId); BaseContext.setCurrentId(userId); //3、通过,放行 return true; } catch (Exception ex) { //4、不通过,响应401状态码 response.setStatus(401); return false; } } 全局异常处理 **当前问题:**如果程序因不知名原因报错,响应回来的数据是一个JSON格式的数据,但这种JSON格式的数据不符合开发规范当中所提到的统一响应结果Result,导致前端不能解析出响应的JSON数据。 当我们没有做任何的异常处理时,我们三层架构处理异常的方案: Mapper接口在操作数据库的时候出错了,此时异常会往上抛(谁调用Mapper就抛给谁),会抛给service。 service 中也存在异常了,会抛给controller。 而在controller当中,我们也没有做任何的异常处理,所以最终异常会再往上抛。最终抛给框架之后,框架就会返回一个JSON格式的数据,里面封装的就是错误的信息,但是框架返回的JSON格式的数据并不符合我们的开发规范。 如何解决: 方案一:在所有Controller的所有方法中进行try…catch处理 缺点:代码臃肿(不推荐) 方案二:全局异常处理器 好处:简单、优雅(推荐) 全局异常处理 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解**@RestControllerAdvice**,加上这个注解就代表我们定义了一个全局异常处理器。 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解**@ExceptionHandler**。通过 @ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。 @RestControllerAdvice public class GlobalExceptionHandler { //处理 RuntimeException 异常 @ExceptionHandler(RuntimeException.class) public Result handleRuntimeException(RuntimeException e) { e.printStackTrace(); return Result.error("系统错误,请稍后再试"); } // 处理 NullPointerException 异常 @ExceptionHandler(NullPointerException.class) public Result handleNullPointerException(NullPointerException e) { e.printStackTrace(); return Result.error("空指针异常,请检查代码逻辑"); } //处理异常 @ExceptionHandler(Exception.class) //指定能够处理的异常类型,Exception.class捕获所有异常 public Result ex(Exception e){ e.printStackTrace();//打印堆栈中的异常信息 //捕获到异常之后,响应一个标准的Result return Result.error("对不起,操作失败,请联系管理员"); } } 模拟NullPointerException String str = null; // 调用 null 对象的方法会抛出 NullPointerException System.out.println(str.length()); // 这里会抛出 NullPointerException 模拟RuntimeException int res=10/0; 事务 场景与问题: @Slf4j @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Autowired private EmpMapper empMapper; //根据部门id,删除部门信息及部门下的所有员工 @Override public void delete(Integer id){ //根据部门id删除部门信息 deptMapper.deleteById(id); //模拟:异常发生 int i = 1/0; //删除部门下的所有员工信息 empMapper.deleteByDeptId(id); } } 问题:出现异常后,部门已被删除,但员工记录仍然存在,造成数据不一致。 原因:整组操作没有被事务包裹,无法做到“要么全部成功,要么全部失败”。 @Transactional 注解 位置 作用 方法级 仅当前方法受事务管理 类级 类中所有方法受事务管理 接口级 接口下 所有实现类 的全部方法受事务管理 在实际开发中,推荐只在 Service 层的方法或类上标注,保持粒度清晰。 常用属性 属性 说明 默认值 rollbackFor 指定哪些异常触发回滚 仅 RuntimeException propagation 指定事务传播行为 Propagation.REQUIRED ① 回滚规则(rollbackFor) @Transactional(rollbackFor = Exception.class) // 捕获所有异常并回滚 public void delete(Integer id) { ... } 如果只写 @Transactional,则 仅 运行时异常(RuntimeException)会触发回滚。 如要让 检查时异常(Exception)也能回滚,就需显式指定 rollbackFor。 ② 事务传播行为(propagation) 传播行为 父事务已存在时 父事务不存在时 典型用途 / 说明 REQUIRED (默认) 加入父事务→ 共提交 / 回滚 创建新事务 日常业务写操作,保持一致性 REQUIRES_NEW 挂起父事务→ 自己新建事务 自己新建事务 写日志、发送 MQ 等:外层失败也要单独成功 SUPPORTS 加入父事务 非事务方式执行 只读查询:有事务跟随一致性,没有就轻量查询 NOT_SUPPORTED 挂起父事务→ 非事务方式执行 非事务方式执行 大批量/耗时操作,避免长事务锁表 MANDATORY 加入父事务 立即抛异常 防御性编程:强制要求调用方已开启事务 NEVER 立即抛异常 非事务方式执行 禁止在事务里跑的代码(如特殊 DDL) NESTED 同一物理事务,打 SAVEPOINT→ 子回滚只回到保存点 创建新事务(与 REQUIRED 效果相同) 分段回滚;需 DB / JDBC 支持保存点 需要“互不影响”时用 REQUIRES_NEW——强制新建事务: @Transactional // 外层保存订单 public void saveOrder(Order order){ orderMapper.insert(order); // 总是单独提交日志 logService.saveLog(...); // 后面出现异常 if(order.getAmount() < 0){ throw new IllegalArgumentException("非法金额"); } } //操作日志、审计表、MQ 消息等,不能因为业务失败而丢记录。 @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveLog(Log log) { ... } 调试:Spring事务日志开关 在 application.yml 中添加: logging: level: org.springframework.jdbc.support.JdbcTransactionManager: debug 效果:控制台会打印事务生命周期日志(开启、提交、回滚等),方便排查。 总结 当 Service 层发生异常 时,Spring 会按照以下顺序处理: 事务的回滚:如果 Service 层抛出了一个异常(如 RuntimeException),并且这个方法是 @Transactional 注解标注的,Spring 会在方法抛出异常时 回滚事务。Spring 事务管理器会自动触发回滚操作。 异常传播到 Controller 层:如果异常在 Service 层处理后未被捕获,它会传播到 Controller 层(即调用 Service 方法的地方)。 全局异常处理器:当异常传播到 Controller 层时,全局异常处理器(@RestControllerAdvice 或 @ControllerAdvice)会捕获并处理该异常,返回给前端一个标准的错误响应。 AOP AOP(Aspect-Oriented Programming,面向切面编程)是一种编程思想,旨在将横切关注点(如日志、性能监控等)从核心业务逻辑中分离出来。简单来说,AOP 是通过对特定方法的增强(如统计方法执行耗时)来实现代码复用和关注点分离。 快速入门 实现业务方法执行耗时统计的步骤 定义模板方法:将记录方法执行耗时的公共逻辑提取到模板方法中。 记录开始时间:在方法执行前记录开始时间。 执行原始业务方法:中间部分执行实际的业务方法。 记录结束时间:在方法执行后记录结束时间,计算并输出执行时间。 通过 AOP,我们可以在不修改原有业务代码的情况下,完成对方法执行耗时的统计。 实现步骤: 导入依赖:在pom.xml中导入AOP的依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> 编写AOP程序:针对于特定方法根据业务需要进行编程 @Component @Aspect //当前类为切面类 @Slf4j public class TimeAspect { ////第一个星号表示任意返回值,第二个星号表示类/接口,第三个星号表示所有方法。 @Around("execution(* edu.whut.zy123.service.*.*(..))") public Object recordTime(ProceedingJoinPoint pjp) throws Throwable { //记录方法执行开始时间 long begin = System.currentTimeMillis(); //执行原始方法 Object result = pjp.proceed(); //记录方法执行结束时间 long end = System.currentTimeMillis(); //计算方法执行耗时,pjp.getSignature()获得函数名 log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin); return result; } } 我们通过AOP入门程序完成了业务方法执行耗时的统计,那其实AOP的功能远不止于此,常见的应用场景如下: 记录系统的操作日志 权限控制 事务管理:我们前面所讲解的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务 核心概念 1. 连接点:JoinPoint,可以被AOP控制的方法,代表方法的执行位置 2. 通知:Advice,指对目标方法的“增强”操作 (体现为额外的代码) 3. 切入点:PointCut,是一个表达式,匹配连接点的条件,它指定了 在目标方法的哪些位置插入通知,比如在哪些方法调用之前、之后、或者哪些方法抛出异常时进行增强。 4. 切面:Aspect,通知与切入点的结合 5.目标对象:Target,被 AOP 代理的对象,通知会作用到目标对象的对应方法上。 示例: @Slf4j @Component @Aspect public class MyAspect { @Before("execution(* edu.whut.zy123.service.MyService.doSomething(..))") public void beforeMethod(JoinPoint joinPoint) { // 连接点:目标方法执行位置 System.out.println("Before method: " + joinPoint.getSignature().getName()); } } joinPoint 代表的是 doSomething() 方法执行的连接点。 beforeMethod() 方法就是一个前置通知 "execution(* com.example.service.MyService.doSomething(..))"是切入点 MyAspect是切面。 com.example.service.MyService 类的实例是目标对象 通知类型 @Around:环绕通知。此通知会在目标方法前后都执行。 @Before:前置通知。此通知在目标方法执行之前执行。 @After :后置通知。此通知在目标方法执行后执行,无论方法是否抛出异常。 @AfterReturning : 返回后通知。此通知在目标方法正常返回后执行,发生异常时不会执行。 @AfterThrowing : 异常后通知。此通知在目标方法抛出异常后执行。 在使用通知时的注意事项: @Around 通知必须调用 ProceedingJoinPoint.proceed() 才能执行目标方法,其他通知不需要。 @Around 通知的返回值必须是 Object 类型,用于接收原始方法的返回值。 只有@Around需要在通知中主动执行方法,其他通知只能获取目标方法的参数等。 通知执行顺序 默认情况下,不同切面类的通知执行顺序由类名的字母顺序决定。 可以通过 @Order 注解指定切面类的执行顺序,数字越小,优先级越高。 例如:@Order(1) 表示该切面类的通知优先执行。 @Aspect @Order(1) // 优先级1 @Component public class AspectOne { @Before("execution(* edu.whut.zy123.service.MyService.*(..))") public void beforeMethod() { System.out.println("AspectOne: Before method"); } } @Aspect @Order(2) // 优先级2 @Component public class AspectTwo { @Before("execution(* edu.whut.zy123.service.MyService.*(..))") public void beforeMethod() { System.out.println("AspectTwo: Before method"); } } 如果调用 MyService 中的某个方法,AspectOne切面类中的通知会先执行。 结论:目标方法前的通知方法,Order小的或者类名的字母顺序在前的先执行。 目标方法后的通知方法,Order小的或者类名的字母顺序在前的后执行。 相对于显式设置(Order)的通知,默认通知的优先级最低。 切入点表达式 作用:主要用来决定项目中的哪些方法需要加入通知 常见形式: execution(……):根据方法的签名来匹配 @annotation(……) :根据注解匹配 execution execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为: execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?) 其中带?的表示可以省略的部分 访问修饰符:可省略(比如: public、protected) 包名.类名.: 可省略,但不建议 throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常) 示例: //如果希望匹配 public void delete(Integer id) @Before("execution(void edu.whut.zy123.service.impl.DeptServiceImpl.delete(java.lang.Integer))") //如果希望匹配 public void delete(int id) @Before("execution(void edu.whut.zy123.service.impl.DeptServiceImpl.delete(int))") 在 Pointcut 表达式中,为了确保匹配准确,通常建议对非基本数据类型使用全限定名。这意味着,对于像 Integer 这样的类,最好写成 java.lang.Integer 可以使用通配符描述切入点 * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分 execution(* edu.*.service.*.update*(*)) 这里update后面的'星'即通配方法名的一部分,() 中的'*'表示有且仅有一个任意参数 可以匹配: package edu.zju.service; public class UserService { public void updateUser(String username) { // 方法实现 } } .. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数 execution(* com.example.service.UserService.*(..)) 公共表示@Pointcut 使用 @Pointcut 注解可以将切点表达式提取到一个独立的方法中,提高代码复用性和可维护性。 @Aspect @Component public class LoggingAspect { // 定义一个切点,匹配com.example.service包下 UserService 类的所有方法 @Pointcut("execution(public * com.example.service.UserService.*(..))") public void userServiceMethods() { // 该方法仅用来作为切点标识,无需实现任何内容 } // 在目标方法执行前执行通知,引用上面的切点 @Before("userServiceMethods()") public void beforeUserServiceMethods() { System.out.println("【日志】即将执行 UserService 中的方法"); } } @annotation 在实际项目中,有时我们需要对多个方法(比如 list() 和 delete())进行统一拦截,这些方法可能命名无规律、无法用 execution() 之类的表达式轻松匹配。 这时就可以: 给这些方法统一加一个自定义注解; 在 AOP 切面里用 @annotation(...) 表达式匹配这些方法; 这样写的切入点既简单又易维护。 实现步骤: ① 定义注解 import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) // 运行时可反射获取 @Target(ElementType.METHOD) // 只能标记方法 public @interface MyLog { String description() default "default description"; // 描述信息 int value() default 0; // 额外参数 } @Retention(RUNTIME) 保证运行时可以通过反射拿到注解。 @Target(METHOD) 限制只能用于方法。 ②在业务方法上加注解 @Service public class DeptService { @MyLog(description = "删除部门", value = 1) public void delete(Integer id) { deptMapper.delete(id); } @MyLog(description = "查询部门列表") public List<Dept> list() { return deptMapper.findAll(); } } ③定义切面 @Aspect @Component public class MyLogAspect { @Before("@annotation(myLog)") // 绑定注解对象到参数 public void before(JoinPoint joinPoint, MyLog myLog) { String methodName = joinPoint.getSignature().getName(); System.out.println("方法:" + methodName); System.out.println("注解描述:" + myLog.description()); System.out.println("注解值:" + myLog.value()); } } @annotation(myLog) 表示匹配所有带 @MyLog 的方法; myLog 参数 会直接被赋值为该方法上的注解实例,可以直接读取注解里的属性值; 不需要手动反射去找注解,Spring AOP 自动完成了注解解析和注入。 连接点JoinPoint 执行: ProceedingJoinPoint 和 JoinPoint 都是调用 proceed() 就会执行被代理的方法 Object result = joinPoint.proceed(); 获取调用方法时传递的参数 ,即使只有一个参数, 也以数组形式返回: Object[] args = joinPoint.getArgs(); getSignature(): 返回一个Signature类型的对象,这个对象包含了被拦截点的签名信息。在方法调用的上下文中,这包括了方法的名称、声明类型等信息。 方法名称:可以通过调用getName()方法获得。 声明类型:方法所在的类或接口的完全限定名,可以通过getDeclaringTypeName()方法获取。 返回类型(对于方法签名):可以通过将Signature对象转换为更具体的MethodSignature类型,并调用getReturnType()方法获取。 WEB开发总体图
后端学习
zy123
3月21日
0
25
0
2025-03-21
git基本操作
Git linux上安装Git sudo apt update sudo apt install git Git Bash:与linux风格接近,使用最多,推荐 Git CMD:windows风格 Git GUI:图形界面,不推荐 查看配置 git config -l :查看所有配置 git config --global --list 查看用户配置的 git config --system --list :系统配置的 核心原理 index是暂存区 respository 是本地代码仓库,保存着本地的各个版本代码 remote是远程仓库,通常是github/gitee码云 如何克隆别人已有的项目 获取项目地址http 打开本地文件系统,选择你需要保存项目的位置并打开cmd git clone xxx,这个过程可能需要验证身份,输入用户名密码 如何将本地仓库与远程仓库连接?从零开始 法1: 首先在Github上新建一个代码仓库,拷贝地址 eg:https://github.com/zhangww-web/JianShu.git 在你本地文件夹下鼠标右键git bash here git init 新建本地仓库,生成.git隐藏文件,点进去有head文件。 git remote add origin url,可以绑定远程仓库 继续输入git pull origin master(若你远程仓库为空,不需要这一步) git add .(.表示所有的,注意这个‘点’)—将本地文件提交至暂存区 git commit -m '提交信息' git push origin master 法2: 第2种方法比较简单,直接用把远程仓库拉到本地,然后再把自己本地的项目拷贝到仓库中去。然后push到远程仓库上去即可。要求远程仓库为新建的空仓库!!! 在空文件夹中git bash here(不用init!) 首先git clone https://github.com/zhangww-web/JianShu.git 然后复制自己项目的所有文件到刚刚克隆下来的仓库中 git push -u origin master 法3:在android studio中集成 配置Git 关联自己的github 对于任何安卓项目,前两步都是通用的!!! 这里的Remote填远程仓库地址,eg: https://github.com/zhangww-web/JianShu.git IDEA 方法类似,先init本地仓库, 点击顶部菜单栏的 Git -> Manage Remotes。 如果Share project on github一直失败,可以这样: 前面步骤不变,现在去github上新建一个空白仓库,获得一个remote url,然后本地仓库提交到该空白仓库,即可建立链接。 迁移代码仓库 法一(推荐):访问令牌(Access Token)获取:点击右上角头像,选择“Settings”(设置)。 在左侧菜单中找到“Developer settings”(开发者设置),然后点击“Personal access tokens”(个人访问令牌)。点击“Generate new token”(生成新令牌),按照需要选择相应的权限(通常建议勾选 repo、read:org 等,根据你的实际需求)。生成后将令牌复制下来,填入 Gitea 的迁移界面中。 直接推完整git信息,推荐 法二: 本地有完整的代码已经git提交记录,不想丢失,推送到新的远程仓库: 在本地文件夹下git bash here git remote add origin <新仓库地址> git push -u origin --all 注意:只会将你本地仓库已经 checkout 出来的所有分支(即本地存在的分支)推送到新远程仓库中。 Git 常用命令 提交代码 git add--> git commit --> git push git add . 提交所有文件到暂存区,此时git status显示changes to be commited git commit -m "describe描述性内容" 提交到本地仓库 分支操作 git banch -a 可以查看所有分支 git branch dev 可以新建dev分支 git checkout dev 可以切换到dev分支 git branch -d dev 删除dev分支 git branch -m dev cs :将dev分支修改名称为cs分支 push失败问题 首先,科学上网一定能打开github的官网,但是idea中仍然无法push 此时查看你的代理服务器的端口,发现是7890 然后打开git bash,输入以下代码: 使用http代理 git config --global http.proxy http://127.0.0.1:7890 git config --global https.proxy https://127.0.0.1:7890 使用socks5代理 git config --global http.proxy socks5://127.0.0.1:7890 git config --global https.proxy socks5://127.0.0.1:7890 取消代理 git config --global --unset http.proxy git config --global --unset https.proxy 或者使用国产的gitee或者自己服务器上搭建gitea进行代码托管!!! 拉取代码与解决冲突 git pull 命令用于从远程仓库拉取(fetch)并合并(merge)最新的更改到本地仓库。它实际上执行了两个操作: git fetch: 这个操作会从远程仓库下载最新的更改到本地仓库,但不会自动合并到当前分支。它将远程仓库的更改存储在本地仓库中,使你能够查看它们,但不会更改你当前工作目录中的文件。 git merge: 这个操作将远程仓库的更改合并到当前分支。如果有冲突,你需要解决冲突后再次提交合并的结果 情况1:本地修改代码未提交,拉取代码的时候就会报错: 错误:您对下列文件的本地修改将被合并操作覆盖: docker-compose.yaml 请在合并前提交或贮藏您的修改。 正在终止 暂存未提交的更改(git stash) 如果还不想提交本地修改,可以将更改暂存起来: git stash git pull git stash pop git stash 会将当前工作区的改动保存到一个栈中,拉取完成后通过 git stash pop 恢复修改。如果恢复过程中出现冲突,同样需要手动解决。 舍弃本地修改 如果确定不需要这些修改,可以放弃它们: git reset --hard git pull 但是推荐先提交本地的代码!!! git add . git commit -m "描述本次修改的提交信息" git pull 情况2:权限校验问题 一般可以保持git网页端账号的登录状态,再pull,会有弹窗出来输入git的用户名和密码,成功后即可拉取。 情况3 合并冲突,以pycharm为例 1.触发冲突解决界面 当你执行 git pull 后如果出现冲突,PyCharm 会在右下角或版本控制工具窗口中提醒有冲突文件。你可以点击提示信息或在版本控制面板中找到冲突文件。 2.启动合并工具 双击冲突文件后,PyCharm 会自动打开三方合并工具。界面通常分为三部分: 左侧:本地修改(当前分支的更改) 右侧:远程修改(要合并进来的更改) 中间:合并结果(你需要编辑的区域) 3.手动选择并合并代码 在合并工具中,你可以逐个查看冲突部分: 点击左侧或右侧的按钮来选择保留哪部分内容。 如果需要,你也可以手动编辑中间的合并结果区域,直接输入合适的代码。 合并工具通常会有跳转到下一个冲突的按钮,方便你逐个解决。 4.保存合并结果并标记解决 合并完成后,点击工具窗口上的“Apply”或“Accept Merge”按钮,保存你的修改。此时,冲突文件会标记为已解决。 5.提交合并后的更改 返回主界面后,你可以在版本控制面板中看到已解决的文件。检查确认无误后,通过 VCS 菜单或直接点击工具栏中的提交按钮,将合并结果提交到仓库。 其他Git相关 SSH公私钥 公私钥生成 在linux中,使用账号密码链接github报错如下: remote: Support for password authentication was removed on August 13, 2021. remote: Please see https://docs.github.com/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication. 致命错误:'https://github.com/zhangww-web/reptile.git/' 鉴权失败 原因是linux不支持账号密码链接!! 配置ssh,可以在git push的时候直接推送,github会通过ssh来验证你的身份。 如何在linux中配置私钥? 生成 SSH 密钥: 如果你还没有 SSH 密钥,可以使用以下命令生成: ssh-keygen -t rsa -b 4096 -C "your_email@example.com" 按照提示保存密钥文件。 添加 SSH 密钥到 GitHub: 复制生成的公钥内容(通常在 ~/.ssh/id_rsa.pub 文件中)。 登录到 GitHub。 进入 SSH and GPG keys 页面。 点击 "New SSH key" 按钮,粘贴公钥内容并保存。 使用 SSH URL 克隆仓库: git clone git@github.com:zhangww-web/reptile.git SSH 连接 GitHub 并触发身份验证,流程如下: GitHub 发送一个随机挑战(Challenge) GitHub 服务器会向你的 Linux 服务器发送一个随机字符串,并用 你的公钥 进行加密。 你的 Linux 服务器用私钥解密 你的 SSH 客户端(ssh 命令或 git)会自动使用本地的 私钥(id_rsa) 进行解密。如果解密成功,证明你拥有匹配的私钥。 返回解密后的数据 你的客户端将解密后的数据返回给 GitHub。 GitHub 验证解密结果 GitHub 服务器检查解密结果是否匹配它最初发送的随机挑战。如果匹配,则认证成功。 身份验证逻辑:GitHub 发送加密数据 → 你的私钥解密 → 返回结果 → GitHub 确认一致性 → 认证成功。 如果避免每次git pull都要验证身份? git config --global credential.helper store //将凭据保存到磁盘上(明文存储): .gitignore(忽略某些文件) .gitignore 的生效规则 对未跟踪的文件: 被标记的文件会被忽略,不会出现在 Git 提交列表中(IDEA 中会显示为灰色或隐藏)。 对已跟踪的文件: 如果 application-local.yml 之前已经被 Git 跟踪过(即曾经提交过),.gitignore 不会自动将其从版本控制中移除。它仍会出现在提交列表中。 如果不小心commit了或者git add(暂存但未提交)如何撤销? 例:如果在添加.gitignore文件前不小心提交了.idea文件夹,到项目根目录,git bash here git rm -r --cached -f .idea git commit -m "Remove .idea from tracking" git rm:从版本控制中删除文件 -r:递归删除指定目录下的所有文件。 --cached:关键选项,指定操作只针对暂存区,不动工作目录 为什么.gitignore文件不放在.git文件夹中? 便于版本控制:.gitignore文件放在项目的根目录中,可以和项目代码一起被版本控制,这样其他协作开发者也能看到和使用相同的忽略规则。如果把.gitignore放在.git文件夹中,它就不会被版本控制系统追踪到。 撤销Git版本控制 1.直接把项目文件夹中的.git文件夹删除即可(开启查看隐藏文件夹可看到) 2:使用 Git 命令(保留文件,仅移除版本控制) git init # 重新初始化(可选,非必须) git rm -r --cached . # 移除所有文件的跟踪状态 添加协作者 协作者权限 如果不使用组织的话,你也可以单独为每个仓库添加协作者。这样做的话,公钥仍然应该添加到你的个人设置中,但是你可以在每个仓库的设置中单独管理协作者访问权限。 设置步骤包括: 打开你想要添加协作者的仓库。 导航到仓库设置中的“Manage access”(管理访问)或“Collaborators”(协作者)部分。 添加协作者的GitHub用户名,并设置他们的访问级别。
杂项
zy123
3月21日
0
5
0
2025-03-21
linux服务器
llinux服务器(以debian为例) 预先准备 域名购买与解析 购买:Low-Cost Domain Names & Hosting from $0.99 | NameSilo 教程:【服务器、域名购买】Namesilo优惠码和域名解析教程(附带服务器购买推荐和注意事项) | 爱玩实验室 DNS解析:可能需等待几分钟生效 买的域名的r2studying.top 这里的HOSTNAME相当于二级域名,如npm.r2studying.top 现改为CloudFlare来做DNS解析,不用namesilo,它有两种模式: Proxied(橙色云朵):当你将某个 DNS 记录设置为 Proxied 时,Cloudflare 会作为反向代理服务器处理该域名的 HTTP/HTTPS 流量。这样做有几个效果: 隐藏真实 IP:用户访问时看到的是 Cloudflare 的 Anycast IP,而不是你服务器的真实 IP,从而提高安全性。 提供 CDN 加速:Cloudflare 会缓存静态资源,并通过全球节点加速内容传输,提升网站响应速度。 附加安全防护:包括 DDoS 防护和 Web 应用防火墙等功能。 DNS Only(灰色云朵):如果你设置为 DNS Only,Cloudflare 只负责 DNS 解析,不会对流量进行代理或处理。也就是说,用户访问时会直接获取并连接到你服务器的真实 IP。 安全组设置 登录云服务器的控制台设置 入站规则 入站规则(Inbound Rules)是指防火墙中用来控制外部流量进入服务器的规则。通过这些规则,你可以指定允许或拒绝哪些类型的网络流量(例如特定协议、端口号、IP地址等)进入你的服务器。 类型 协议端口 源地址 描述 IPv4 TCP : 22 0.0.0.0/0 允许外部访问安全组内实例的SSH(22)端口,用于远程登录Linux实例。 IPv4 TCP : 3389 0.0.0.0/0 允许外部访问安全组内实例的RDP(3389)端口,用于远程登录Windows实例。 IPv4 TCP : 80 0.0.0.0/0 允许外部访问安全组内实例的HTTP(80)端口,用于通过HTTP协议访问网站。 IPv4 TCP : 443 0.0.0.0/0 允许外部访问安全组内实例的HTTPS(443)端口,用于通过HTTPS协议访问网站。 IPv4 TCP : 20-21 0.0.0.0/0 允许通过FTP上传和下载文件。 IPv4 ICMP: 全部 0.0.0.0/0 允许外部使用ping命令验证安全组内实例的网络连通性。 出站规则 出站规则(Outbound Rules)则是指控制服务器向外部发送流量的规则。你可以通过出站规则来限制服务器发出的数据包,比如限制服务器访问某些外部服务或 IP 地址。 凡是服务正常启动但是浏览器上无法访问的,都首先想想防火墙端口是否打开!!! 常用的命令 cat 用于查看文本文件的内容 head 查看前n行 head -n 100 文件名 tail 查看后n行 tail -n 3 file wc (word count )统计行数、单词数 Hello world Linux is great wc -l file #统计行数: 2 file wc -w file #统计单词数:5 file touch 新建文本文件,如touch /home/hello.py 将在home 文件夹下新建hello.py ls 列出所有文件,但默认只是显示出最基础的文件和文件夹,如果需要更详细的信息,则使用ls -la,这将列出包括隐藏文件在内的所有文件和文件夹,并且给出对应的权限、大小和日期等信息。 zy123@hcss-ecs-588d:~$ ls -la total 44 drwxr-xr-x 6 zy123 zy123 4096 Feb 26 08:53 . drwxr-xr-x 3 root root 4096 Feb 24 16:33 .. -rw------- 1 zy123 zy123 6317 Feb 25 19:41 .bash_history -rw-r--r-- 1 zy123 zy123 220 Feb 24 16:33 .bash_logout -rw-r--r-- 1 zy123 zy123 3526 Feb 24 16:33 .bashrc drwx------ 3 zy123 zy123 4096 Feb 24 16:35 .config drwxr-xr-x 3 zy123 zy123 4096 Feb 24 16:36 .local -rw-r--r-- 1 zy123 zy123 807 Feb 24 16:33 .profile drwxr-xr-x 5 zy123 zy123 4096 Feb 26 08:53 zbparse drwxr-xr-x 3 root root 4096 Feb 26 08:54 zbparse_output 权限与文件类型(第一列): 第一个字符表示文件类型: “d”表示目录(directory) “-”表示普通文件(regular file) “l”表示符号链接(symbolic link) 后续的9个字符分为3组,每组三个字符,分别代表所有者、所属组和其他用户的权限(读、写、执行)。 硬链接数(第二列): 表示指向该文件的硬链接数量。对于目录来说,这个数字通常会大于1,因为“.”和“..”也算在内。 所有者(第三列): 显示该文件或目录的拥有者用户名。 所属组(第四列): 显示该文件或目录所属的用户组。 文件大小(第五列): 以字节为单位显示文件的大小。对于目录,通常显示的是目录文件占用的磁盘空间(一般为4096字节)。 最后修改日期和时间(第六列): 显示文件最后一次修改的日期和时间(可能包含月、日和具体时间或年份)。 文件名或目录名(第七列): 显示文件或目录的名称。 cd 进入指定文件夹,如cd /home 将进入home目录。返回上层目录的命令是 cd .. ,返回刚才操作的目录的命令是 cd - mkdir 新建文件夹,如 mkdir /home/Python 将在home 文件夹下新建一个Python 文件夹。 mv 移动文件和文件夹,也可以用来重命名,如: mv /home/hello.py /home/helloworld.py 将上文的hello.py重命名为helloworld.py, mv /home/helloworld.py /home/Python/helloworld.py 将helloworld.py 由home文件夹移动到了次级的Python文件夹。 mv /home/hello.py . 将/home/hello.py 移动到当前目录下 cp 复制文件 cp /home/Python/hellowrold.py /home/Python/HelloWorld.py 将helloworld.py复制为HelloWolrd.py。注意:Linux系统严格区分大小写,helloworld.py和HelloWolrd.py是两个文件。 rm 删除,即江湖传说中 rm -rf ,r为递归,可以删除文件夹中的文件,f为强制删除。rm /home/Python/helloworld.py 可以删除刚才的helloworld.py 文件,而想删除包括Python 在内的所有文件,则是 rm -rf /home/Python 。 grep 是用于在文件或标准输入中搜索符合条件的行的命令。 #test.txt Hello World hello linux Grep is useful HELLO GREP # 精确匹配大小写 grep "Hello" test.txt 输出:Hello World -i忽略大小写 -n显示行号 # 忽略大小写匹配 grep -i "hello" test.txt 输出: Hello World hello linux HELLO GREP awk 是一个功能强大的文本处理工具,它可以对文本文件进行分列处理、模式匹配和报告生成。它的语法类似一种简单的脚本语言。 awk 'pattern { action }' filename pattern:用于匹配文本的条件(可以省略,默认对所有行生效)。 action:在匹配的行上执行的操作。 $0:代表整行内容$1+ $2+ $3+ ...。 $1, $2, ...:代表各个列(默认分隔符是空白字符,可以通过 -F 参数指定其他分隔符)。 对于: A B C 1 Alice 25 Engineer 2 Bob 30 Designer awk '{print $1, $3}' filename #印指定列 输出: Alice Engineer Bob Designer sed sed -n '1000,$p' file 对于 filename这个文件,请只打印从第1000行开始一直到文件末尾的所有内容。$代表结尾,p是print打印。 管道 | 是将一个命令的输出直接传递给另一个命令作为输入的一种机制。 示例:将 grep 与 awk 联合使用:假设有一个日志文件 access.log,需要先用 grep 过滤出包含 "ERROR" 的行,再用 awk 提取时间字段: 127.0.0.1 27/Feb/2025:10:30:25 "GET /index.html HTTP/1.1" 200 1024 192.168.1.1 27/Feb/2025:10:31:45 "POST /login HTTP/1.1" 302 512 10.0.0.5 27/Feb/2025:10:32:10 "GET /error_page HTTP/1.1" 500 2048 ERROR grep 'ERROR' access.log | awk '{print $2}' 输出:27/Feb/2025:10:32:10 usermode 修改用户账户信息的命令 sudo usermod -aG docker zy123 #-aG一起用,添加zy123到group组 chmod 命令用于修改文件或目录的权限 数字方式:数字方式使用三个(或四个)数字来表示所有者、组用户和其他用户的权限。每个数字代表读 (4)、写 (2)、执行 (1) 权限的和。 chmod 644 filename #所有者:读 + 写 = 6 组用户:读 = 4 其他用户:读 = 4 符号方式 chmod [用户类别][操作符][权限] filename 用户类别: u:所有者(user) g:组用户(group) o:其他用户(others) a:所有用户(all),等同于 u+g+o 操作符: +:增加权限 -:去掉权限 =:直接设置权限 权限: r:读权限 w:写权限 x:执行权限 chmod u+x filename #为所有者增加执行权限 系统分析 df -h 查看磁盘空间使用情况 lsof"List Open Files"显示系统中当前打开的文件。 -i会显示所有正在使用网络连接的进程 lsof -i :80 #查看 80 端口上的进程,或者判断80端口是否被占用! netstat -ano | findstr :8080这是windows cmd下检查端口占用的命令 top 用于实时查看系统进程和资源占用情况。(CPU、内存、PID、用户等)。 top -p <pid> 只监控某个进程。 交互操作(进入 top 后按键):P → 按 CPU 占用率排序;M → 按 内存占用率排序 q → 退出 ps 用于查看当前系统中的进程快照。 ps -ef 查看所有进程的详细信息(常用)。 ps -aux 以 BSD 风格显示进程(带 CPU、内存占用率)。 ps -u <username> 查看某个用户的进程。 ps -p <pid> 查看某个进程的详细信息。 文本编辑器 nano:Debian 11自带了简便易用的nano文本编辑器 nano /etc/apt/sources.list #打开sources.list文件 Ctrl+O:保存修改 ->弹出询问-> Y则保存,N则不保存,ctrl+c 取消操作。 Ctrl+X:退出 vim 普通模式(Normal Mode): 打开 Vim 后默认进入普通模式。在该模式下,可以执行移动光标、删除、复制、粘贴等命令。 光标移动: 0:跳到当前行行首 $:跳到当前行行尾 gg: 将光标移到文件开头 文本操作: dd:删除(剪切)整行 yy:复制(拷贝)整行 p:在光标后粘贴(把剪切或复制的内容贴上) u:撤销上一步操作 Ctrl + r:重做上一步撤销的操作 插入模式(Insert Mode): 在普通模式下按 i、I、a、A 等键可进入插入模式,此时可以像普通编辑器那样输入文本。 按 ESC 键退出插入模式,返回普通模式。 可视模式(Visual Mode): 用于选中一段文本,自动进入可视模式。选中文本后可以进行复制、剪切等操作。 在可视模式下选中后按 d 删除 按 y 复制选中区域 命令行模式(Command-Line Mode): 在普通模式下,输入 : 进入命令行模式,可以执行保存、退出、查找等命令。 :w:保存当前文件 :q:退出 Vim(如果有未保存的修改会警告) :wq 或 :x:保存文件并退出 :q!:不保存强制退出 :set number:显示行号 :set paste:适用于代码或格式敏感的文本,确保粘贴操作不会破坏原有的布局和缩进。 :%d: 删除全文 抓包 sudo tcpdump -nn -i any port 1000 //查看请求端口1000的源 IP 地址 SSH 1. 生成密钥对 使用以下命令生成安全的 SSH 密钥对: ssh-keygen -t rsa -b 4096 -C "your_email@example.com" 生成的文件默认保存在用户主目录的 ~/.ssh/文件夹中: 私钥 (id_rsa):必须妥善保存在本地,严禁泄露。这是证明身份的核心凭证。 公钥 (id_rsa.pub):需要提供给服务端用于身份验证。 2. 核心概念:理解客户端与服务端 SSH 认证是双向的,关键在于分清每次连接中谁是客户端(主动发起连接的一方),谁是服务端(被动接受连接的一方)。 场景一:从服务器拉取 GitHub 代码 角色分配 客户端:你的 Linux 服务器(执行 git clone/push命令的机器) 服务端:GitHub 代码托管平台 认证流程 在客户端(你的服务器)生成 SSH 密钥对 将客户端的公钥内容添加到 GitHub 账户的 SSH Keys 设置中 当服务器执行 git clone git@github.com:...时: GitHub 会用存储的公钥发起挑战 你的服务器用本地私钥完成验证 本质:让你的服务器向 GitHub 证明其合法身份 场景二:通过 FinalShell 连接 Linux 服务器 角色分配 客户端:你的本地 Windows 电脑(运行 FinalShell 的机器) 服务端:目标 Linux 服务器 认证流程 在客户端(你的电脑)生成 SSH 密钥对 将客户端的公钥内容添加到服务端的 ~/.ssh/authorized_keys文件中 当 FinalShell 连接时: Linux 服务器用 authorized_keys中的公钥发起挑战 FinalShell 用本地私钥完成验证 本质:让你的电脑向 Linux 服务器证明你的用户身份 文件系统 在 Linux 系统中,整个文件系统从根目录 / 开始,下面简单介绍一些主要目录及其存放的文件类型: /bin 存放系统启动和运行时必需的用户二进制可执行文件,如常用的 shell 命令(例如 ls、cp、mv 等)。 /boot 包含启动加载器(如 GRUB)的配置文件和内核映像(kernel image),这些文件用于系统启动过程。 /dev 包含设备文件,这些文件代表系统中的各种硬件设备(例如硬盘、终端、USB 设备等),以及一些伪设备。 /etc 存放系统范围内的配置文件,例如网络配置、用户账户信息、服务配置等。这些文件通常由管理员维护。 /home 为普通用户提供的家目录,每个用户在这里有一个独立的子目录,存放个人文件和配置。 /lib 和 /lib64 存放系统和应用程序所需的共享库文件,这些库支持 /bin、/sbin 及其他程序的运行。 /media 通常用于挂载移动介质,如光盘、U 盘或其他可移动存储设备。 /mnt 提供一个临时挂载点,一般供系统管理员在需要手动挂载文件系统时使用。 /opt 用于安装附加的应用程序软件包,通常是第三方提供的独立软件,不与系统核心软件混合。 /proc 这是一个虚拟文件系统,提供内核和进程信息,例如系统资源使用情况、硬件信息、内核参数等。这里的文件不占用实际磁盘空间。 /root 系统管理员(root 用户)的家目录,与普通用户的 /home 分开存放。 /run 存储系统启动后运行时产生的临时数据,比如 PID 文件、锁文件等,系统重启后会清空该目录。 /sbin 存放系统管理和维护所需的二进制文件(系统级命令),例如网络配置、磁盘管理工具等,这些通常只由 root 用户使用。 /srv 用于存放由系统提供的服务数据,如 FTP、HTTP 服务的数据目录等。 /tmp 用于存放临时文件,许多应用程序在运行时会将临时数据写入这里,系统重启后通常会清空该目录。 /usr 存放大量用户应用程序和共享资源,其子目录包括: /usr/bin:大部分用户命令和应用程序。 /usr/sbin:非基本系统维护工具,主要供系统管理员使用。 /usr/lib:程序库文件。 /usr/share:共享数据,如文档、图标、配置样本等。 /var 存放经常变化的数据,如日志文件、缓存、邮件、打印队列和临时应用数据等。 Bash #!/bin/bash # 定义变量,注意等号两边不能有空格 name="World" # 如果脚本传入了参数,则使用第一个参数覆盖默认值 if [ $# -ge 1 ]; then # $# 表示传入脚本的参数个数 name=$1 # $1 表示第一个参数。 fi # 输出问候语 echo "Hello, $name!" #变量引用时要用 $ 符号,如 $name。 # 循环示例:遍历1到5的数字 for i in {1..5}; do echo "当前数字:$i" done # 定义一个函数,函数名为greet greet() { echo "函数内问候: Hello, $1!" } # 调用函数,并传入变量name作为参数 greet $name 如何运行? 赋予可执行权限 chmod +x hello_world.sh 执行 ./hello_world.sh 或者 ./hello_world.sh Alice #传参 在 Linux 系统中,默认情况下当前目录(.)并不包含在 PATH 环境变量中。这意味着,如果你在终端中直接输入脚本名(例如 hello_world.sh),系统不会在当前目录下查找这个脚本,而是只在 PATH 中指定的目录中查找可执行程序。使用 ./hello_world.sh 表示“在当前目录下执行 hello_world.sh”,从而告诉系统正确的路径。 如何设置定时任务? sudo crontab -e #在里面添加: 10 0 * * * /path/toyour/xx.sh #让gpt写 反向代理神器——Nginx Proxy Manager 【Docker系列】一个反向代理神器——Nginx Proxy Manager-我不是咕咕鸽 概念 正向代理是代理客户端的行为,即代理服务器代表客户端向目标服务器发出请求。客户端将自己的请求先发送给代理服务器,由代理服务器转发给真正的目标服务器,然后再将返回结果传递给客户端。 特点: 保护访问者(客户端)的信息:目标服务器只会看到代理服务器的请求,无法直接获知真正发起请求的客户端是谁。 应用场景:当客户端出于隐私、访问控制或跨越网络限制的目的,需要隐藏自己的真实IP或身份时,会使用正向代理。 反向代理是代理服务器代表目标服务器接收客户端请求,并将请求转发给内部的真实服务器,然后将结果返回给客户端。客户端只与代理服务器通信,而不知道背后实际处理请求的服务器。 特点: 保护服务器端的信息:客户端看不到真正的服务器细节,只知道代理服务器。这样可以隐藏真实服务器的 IP 和其他内部结构信息,从而增强安全性和负载均衡等功能。 应用场景:在大型网站或应用中,为了防止恶意攻击或实现负载均衡,通常会在真实服务器前部署一个反向代理服务器。 docker 部署 version: '3' services: app: image: 'jc21/nginx-proxy-manager:latest' restart: unless-stopped ports: - '80:80' # 保持默认即可,不建议修改左侧的80 - '81:81' # 冒号左边可以改成自己服务器未被占用的端口 - '443:443' # 保持默认即可,不建议修改左侧的443 volumes: - ./data:/data # 冒号左边可以改路径,现在是表示把数据存放在在当前文件夹下的 data 文件夹中 - ./letsencrypt:/etc/letsencrypt # 冒号左边可以改路径,现在是表示把数据存放在在当前文件夹下的 letsencrypt 文件夹中 NPM后台管理网站运行在81号端口,NPM服务本身监听80(http)和443(https)端口 工作原理 用户访问网站(80/443端口) 当用户访问 https://blog.test.com 时,请求到达服务器的443端口。 Nginx根据域名匹配代理规则(由NPM配置),将请求转发到后端服务(如 192.168.1.100:8080)。 管理员访问后台(81端口) 管理员通过 http://服务器IP:81 访问NPM管理界面,配置代理规则。 配置完成后,NPM会自动生成Nginx配置文件并重启Nginx服务,使新规则生效。 两种方法实现SSL安全连接 1. 开启橙云+Cloudflare Origin CA:网站SSL证书自动续期又又又失败了?试试CloudFlare的免费证书,15年有效期!-我不是咕咕鸽 cloudflare解析DNS,开启橙云 Cloudflare Full (strict)模式(灵活模式下无需以下步骤) 在 Nginx Proxy Manager(NPM)添加 Cloudflare Origin CA 配置 Proxy Host 使其使用 选择刚刚添加的 Cloudflare Origin CA 证书 选择 Force SSL(强制 HTTPS) 启用 HTTP/2 支持 Cloudflare Origin CA作用:实现cloudflare与源服务器之间的流量加密 2.通配符SSL证书:【Docker系列】反向代理神器NginxProxyManager——通配符SSL证书申请-我不是咕咕鸽 更推荐第二种!!!目前正在使用,但是3个月续签一次证书! 浏览器 → (HTTPS) → NPM → (HTTP 或 HTTPS) → Gitea 这就是反向代理常见的工作流程。 默认经常是 NPM 做 SSL 终止(内网用 HTTP 转发给 Gitea)。如果你想内外全程加密,就要让 NPM -> Gitea 这段也走 HTTPS,并在 Gitea 上正确配置证书。 正向代理(用于拉取镜像) 下载Clash客户端:Release Clash-Premium · DustinWin/proxy-tools 或者Index of /Linux/centOS/ 我是windows上下载clashpremium-release-linux-amd64.tar.gz 然后FTP上传到linux服务器上。 下载配置文件config.yaml:每个人独一无二的 wget -O /home/zy123/VPN/config.yaml "https://illo1.no-mad-world.club/link/2zXAEzExPjAi6xij?clash=3" 类似这样: port: 7890 socks-port: 7891 allow-lan: false mode: Rule //Global log-level: info external-controller: 0.0.0.0:9090 unified-delay: true hosts: time.facebook.com: 17.253.84.125 time.android.com: 17.253.84.125 dns: enable: true use-hosts: true nameserver: - 119.29.29.29 - 223.5.5.5 - 223.6.6.6 - tcp://223.5.5.5 - tcp://223.6.6.6 - tls://dns.google:853 - tls://8.8.8.8:853 - tls://8.8.4.4:853 - tls://dns.alidns.com - tls://223.5.5.5 - tls://223.6.6.6 - tls://dot.pub - tls://1.12.12.12 - tls://120.53.53.53 - https://dns.google/dns-query - https://8.8.8.8/dns-query - https://8.8.4.4/dns-query - https://dns.alidns.com/dns-query - https://223.5.5.5/dns-query - https://223.6.6.6/dns-query - https://doh.pub/dns-query - https://1.12.12.12/dns-query - https://120.53.53.53/dns-query default-nameserver: - 119.29.29.29 - 223.5.5.5 - 223.6.6.6 - tcp://119.29.29.29 - tcp://223.5.5.5 - tcp://223.6.6.6 proxies: - {name: 🇭🇰 香港Y01, server: qvhh1-g03.hk01-ae5.entry.v50307shvkaa.art, port: 19273, type: ss, cipher: aes-256-gcm, password: 6e4124c4-456e-36a3-b144-c0e1a618d04c, udp: true} 注意,魔戒vpn给的是一个订阅地址!!!还需要解码 简便方法:windows上将订阅链接导入,自动解析成yaml配置文件,然后直接把该文件传到服务器上! 启动Clash ./CrashCore -d . & //后台启动 为Clash创建服务 1.创建 systemd 服务文件 sudo vim /etc/systemd/system/clash.service 2.在文件中添加以下内容: [Unit] Description=Clash Proxy Service After=network.target [Service] ExecStart=/home/zy123/VPN/CrashCore -d /home/zy123/VPN WorkingDirectory=/home/zy123/VPN Restart=always User=zy123 Group=zy123 [Install] WantedBy=multi-user.target 这段配置将 Clash 配置为: 在网络服务启动后运行。 在启动时自动进入后台,执行 CrashCore 服务。 如果服务意外停止,它将自动重启。 以 zy123 用户身份运行 Clash。 启动服务: sudo systemctl start clash 停止服务: sudo systemctl stop clash 查看服务状态: sudo systemctl status clash 配置YACD YACD 是一个基于 Clash 的 Web 管理面板,用于管理您的 Clash 配置、查看流量和节点信息等。 直接用现成的:https://yacd.haishan.me/ 服务器上部署:目前是npm手动构建安装启动的。 下载yacd git clone https://github.com/haishanh/yacd.git 安装npm 构建yacd cd ~/VPN/yacd pnpm install pnpm build 启动yacd nohup pnpm serve --host 0.0.0.0 & //如果不是0.0.0.0 不能在windows上打开 停止进程 ps aux | grep pnpm kill xxx 通过http://124.71.159.195:4173/ 访问yacd控制面板。手动添加crash服务所在的ip:端口。 如果连不上:yacd面板和crash都是http协议就行了。 连接测试 直连: curl -v https://www.google.com 使用代理:curl -x http://127.0.0.1:7890 https://www.google.com File Browser 文件分享 https://github.com/filebrowser/filebrowser Docker 部署 File Browser 文件管理系统_filebrowser docker-CSDN博客 1.创建数据目录 mkdir -p /data/filebrowser/{srv,config,db} 2.目录授权 chmod -R 777 /data/filebrowser/ 3.编辑 docker-compose.yaml 文件 version: '3' services: filebrowser: image: filebrowser/filebrowser:s6 container_name: filebrowser restart: always ports: - "2000:80" # 将容器的80端口映射到宿主机2000端口 volumes: - /data/filebrowser/srv:/srv #保存用户上传的文件 - /data/filebrowser/config:/config # 配置文件存储路径 - /data/filebrowser/db:/database #数据库存储路径 调用API实现文件上传与下载 登录: # 登录 → 获取 JWT token(原始字符串) curl -X POST "yourdomain/api/login" \ -H "Content-Type: application/json" \ -d '{ "username": "admin", "password": "123456" }' 响应: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…" //jwt令牌 上传: # 先把 token 保存到环境变量(假设你已经执行过 /api/login 并拿到了 JWT) export TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…" # 定义要上传的本地文件和目标名称 FILE_PATH="/path/to/local/photo.jpg" OBJECT_NAME="photo.jpg" # 对远程路径做 URL 编码(保留斜杠) REMOTE_PATH="store/${OBJECT_NAME}" ENCODED_PATH=$(printf "%s" "${REMOTE_PATH}" \ | jq -sRr @uri \ | sed 's/%2F/\//g') # 发起上传请求(raw body 模式) curl -v -X POST "https://yourdomain/api/resources/${ENCODED_PATH}?override=true" \ -H "X-Auth: ${TOKEN}" \ -H "Content-Type: image/jpeg" \ # 根据文件后缀改成 image/png 等 --data-binary "@${FILE_PATH}" 获取分享链接url: # 假设已经执行过 /api/login 并把 JWT 保存在环境变量 TOKEN # 同样复用之前算好的 ENCODED_PATH 和 REMOTE_PATH # 调用分享接口(注意:POST,body 为空 JSON "{}") curl -s -X POST "https://yourdomain/api/share/${ENCODED_PATH}" \ -H "X-Auth: ${TOKEN}" \ -H "Cookie: auth=${TOKEN}" \ -H "Content-Type: text/plain;charset=UTF-8" \ -d '{}' \ | jq -r '.hash' \ | xargs -I{} printf "https://yourdomain/api/public/dl/%s/%s\n" {} "${REMOTE_PATH}" Gitea Gitea Docker 安装与使用详解:轻量级自托管 Git 服务教程-CSDN博客 version: "3" services: gitea: image: gitea/gitea:latest container_name: gitea environment: - USER_UID=1000 - USER_GID=1000 restart: always ports: - "3000:3000" # 将宿主机的3000端口映射到容器的3000端口 volumes: - /data/gitea:/data # 持久化存储Gitea数据(包括仓库、配置、日志等) EasyImage 【好玩儿的Docker项目】10分钟搭建一个简单图床——Easyimage-我不是咕咕鸽 github地址:icret/EasyImages2.0: 简单图床 - 一款功能强大无数据库的图床 2.0版 sudo -i # 切换到root用户 apt update -y # 升级packages apt install wget curl sudo vim git # Debian系统比较干净,安装常用的软件 version: '3.3' services: easyimage: image: ddsderek/easyimage:latest container_name: easyimage ports: - '1000:80' environment: - TZ=Asia/Shanghai - PUID=1000 - PGID=1000 volumes: - '/root/data/docker_data/easyimage/config:/app/web/config' - '/root/data/docker_data/easyimage/i:/app/web/i' restart: unless-stopped 网页打开显示bug: cd /data/easyimage/config/config.php 这里添加上https 网站域名 图片域名设置可以改变图片的url:IP或域名 picgo安装: Releases · Molunerfinn/PicGo 1.右下角小窗打开 2.插件设置,搜索web-uploader 1.1.1 (自定义web图床) 旧版有搜索不出来的情况!建议直接安装最新版! 3.配置如下,API地址从easyimage-设置-API设置中获取 typora设置 左上角文件-偏好设置-图像-插入图片时{ 上传图片-picgo服务器-填写picgo安装路径 } ps:还可以选择上传到./assets,每个md文件独立 或者上传到指定路径如/image,多个md文件共享 py脚本1:将所有md文件中的图片路径改为本地,统一保存到本地output文件夹中 py脚本2:将每个md文件及其所需图片单独保存,保存到本地,但每个md文件有自己独立的assets文件夹 py脚本3:将本地图片上传到easyimage图床并将链接返回替换md文件中的本地路径 Typecho 【好玩儿的Docker项目】10分钟搭建一个Typecho博客|太破口!念念不忘,必有回响!-我不是咕咕鸽 typecho:https://github.com/typecho/typecho/ 注意:nginx一定要对typecho目录有操作权限! sudo chmod 755 -R ./typecho services: nginx: image: nginx ports: - "4000:80" # 左边可以改成任意没使用的端口 restart: always environment: - TZ=Asia/Shanghai volumes: - ./typecho:/var/www/html - ./nginx:/etc/nginx/conf.d - ./logs:/var/log/nginx depends_on: - php networks: - web php: build: php restart: always expose: - "9000" # 不暴露公网,故没有写9000:9000 volumes: - ./typecho:/var/www/html environment: - TZ=Asia/Shanghai depends_on: - mysql networks: - web pyapp: build: ./markdown_operation # Dockerfile所在的目录 restart: "no" networks: - web env_file: - .env depends_on: - mysql mysql: image: mysql:5.7 restart: always environment: - TZ=Asia/Shanghai expose: - "3306" # 不暴露公网,故没有写3306:3306 volumes: - ./mysql/data:/var/lib/mysql - ./mysql/logs:/var/log/mysql - ./mysql/conf:/etc/mysql/conf.d env_file: - mysql.env networks: - web networks: web: 卸载: sudo -i # 切换到root cd /root/data/docker_data/typecho # 进入docker-compose所在的文件夹 docker-compose down # 停止容器,此时不会删除映射到本地的数据 cd ~ rm -rf /root/data/docker_data/typecho # 完全删除映射到本地的数据 主题 Joe主题:https://github.com/HaoOuBa/Joe Joe再续前缘主题 - 搭建本站同款网站 - 易航博客 自定义文章详情页的上方信息(如更新日期/文章字数第): /typecho/usr/themes/Joe/functions.php中定义art_count,统计字数(粗略)。 原始 Markdown → [1] 删除代码块/行内代码/图片/链接标记/标题列表标记/强调符号 → [2] strip_tags() + html_entity_decode() → [3] 正则保留 “文字+数字+标点”,去除其它 → 结果用 mb_strlen() 得到最终字数 typecho/usr/themes/Joe/module/single/batten.php <?php if (!defined('__TYPECHO_ROOT_DIR__')) { http_response_code(404); exit; } ?> <h1 class="joe_detail__title"><?php $this->title() ?></h1> <div class="joe_detail__count"> <div class="joe_detail__count-information"> <a href="<?php $this->author->permalink(); ?>"> <img width="38" height="38" class="avatar lazyload" src="<?php joe\getAvatarLazyload(); ?>" data-src="<?php joe\getAvatarByMail($this->author->mail) ?>" alt="<?php $this->author(); ?>" /> </a> <div class="meta ml10"> <div class="author"> <a class="link" href="<?php $this->author->permalink(); ?>" title="<?php $this->author(); ?>"><?php $this->author(); ?></a> </div> <div class="item"> <span class="text"> <?php echo $this->date('Y-m-d'); ?> / <?php $this->commentsNum('%d'); ?> 评论 / <?php echo joe\getAgree($this); ?> 点赞 / <?php echo joe\getViews($this); ?> 阅读 / <?php echo art_count($this->cid); ?> 字 </span> </div> </div> </div> </div> <div class="relative" style="padding-right: 40px;"> <i class="line-form-line"></i> <!-- 新增最近修改日期显示 --> <div style="font-size: 1.0em; position: absolute; right: 20px; top: 50%; transform: translateY(-50%);"> 最后更新于 <?php echo date('m-d', $this->modified); ?> </div> <div class="flex ac single-metabox abs-right"> <div class="post-metas"> <!-- 原图标及其他冗余信息已删除 --> </div> <div class="clearfix ml6"> <!-- 编辑文章/页面链接已删除 --> </div> </div> </div> 修改代码块背景色: typecho/usr/themes/Joe/assets/css/joe.global.css .joe_detail__article code:not([class]) { border-radius: var(--radius-inner, 4px); /* 可以设置一个默认值 */ background: #f5f5f5; /* 稍微偏灰的背景色 */ color: #000000; /* 黑色字体 */ padding: 2px 6px; /* 内边距可以适当增大 */ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; word-break: break-word; font-weight: normal; -webkit-text-size-adjust: 100%; -webkit-font-smoothing: antialiased; white-space: pre-wrap; /* 保持代码换行 */ font-size: 0.875em; margin-inline-start: 0.25em; margin-inline-end: 0.25em; } 大坑: 会显示为勾选框,无法正常进行latex公式解析,因为typecho/usr/themes/Joe/public/short.php中设置了短代码替换,在文章输出前对 $content 中的特定标记或短代码进行搜索和替换,从而实现一系列自定义功能。现已全部注释。 typecho/usr/themes/Joe/assets/js/joe.single.js原版: 显示弹窗,点叉后消失 { document.querySelector('.joe_detail__article').addEventListener('copy', () => { autolog.log(`本文版权属于 ${Joe.options.title} 转载请标明出处!`, 'warn', false); }); } 显示5秒后消失: document.querySelector('.joe_detail__article').addEventListener('copy', () => { // 显示 autolog 消息 autolog.log(`本文版权属于 ${Joe.options.title} 转载请标明出处!`, 'warn', false); // 5 秒后删除该消息 setTimeout(() => { const warnElem = document.querySelector('.autolog-warn'); if (warnElem) { warnElem.remove(); // 或者使用 warnElem.style.display = 'none'; } }, 5000); }); markdown编辑与解析 确保代码块```后面紧跟着语言,如```java,否则无法正确显示。 markdown编辑器插件:https://xiamp.net/archives/aaeditor-is-another-typecho-editor-plugin.html '开启公式解析!' markdown解析器插件:mrgeneralgoo/typecho-markdown: A markdown parse plugin for typecho. 关闭公式解析,仅开启代码解析! slug为页面缩略名,在新增文章时可以传入,默认是index数字。 qBittorrent 【好玩的Docker项目】10分钟搭建你专属的下载神器——qbittorrent-我不是咕咕鸽 docker pull linuxserver/qbittorrent cd ~ mkdir /root/data/docker_data/qBittorrent #创建qbitorrent数据文件夹 cd /root/data/docker_data/qBittorrent mkdir config downloads #创建配置文件目录与下载目录 nano docker-compose.yml #创建并编辑文件 services: qbittorrent: image: linuxserver/qbittorrent container_name: qbittorrent environment: - PUID=1000 - PGID=1000 - TZ=Asia/Shanghai # 你的时区 - UMASK_SET=022 - WEBUI_PORT=8081 # 将此处修改成你欲使用的 WEB 管理平台端口 volumes: - ./config:/config # 绝对路径请修改为自己的config文件夹 - ./downloads:/downloads # 绝对路径请修改为自己的downloads文件夹 ports: # 要使用的映射下载端口与内部下载端口,可保持默认,安装完成后在管理页面仍然可以改成其他端口。 - 6881:6881 - 6881:6881/udp # 此处WEB UI 目标端口与内部端口务必保证相同,见问题1 - 8081:8081 restart: unless-stopped 有个bug!qBittorrent 登录遇到 unauthorized的解决方法: 1、先停止容器docker compose down 2、进入配置文件夹config,编辑qBittorrent.conf在文末加上: WebUI\HostHeaderValidation=false WebUI\CSRFProtection=false 3.如果还是进不去,在地址后端添加'/' 内网穿透 使用服务提供商 网址:https://natapp.cn 1.隧道查看 2.隧道配置 配置映射关系: 3.客户端下载与配置 https://natapp.cn/#download ,参考官方文档,配置本地token,启动本地客户端。 自己搭建FRP 数据流转流程 外部请求到达 frp-server 你在 frps.ini(服务端配置)里为某个 proxy 分配了一个 remote_port(或 custom_domains)。 当外部用户(浏览器、移动端、其他服务)访问 http://your.frps.ip:remote_port/... 时,流量首先打到你部署在公网的 frp-server 上。 frp-server 转发到 frp-client frp-server 维护着一个与每个 frp-client 的长连接(control connection)。 收到外部连接后,frp-server 会基于这个 control 通道告诉对应的 frp-client: “有一个新连接,请你去拿 proxy(name) 对应的后端服务数据。” frp-server 不自己去连 Java 后端,它只是做信令和数据的“管道”管理。 frp-client 建立到本地 Java 服务的连接 frp-client 收到信令后,内部根据 proxies 配置: [[proxies]] name = "my-java-app" type = "tcp" local_ip = "127.0.0.1" local_port = 8080 remote_port= 18080 它会在容器或宿主机内部打开一个新的 TCP 连接,指向 127.0.0.1:8080(即你的 Java 服务) frp-server frps.toml: # https://github.com/fatedier/frp/blob/dev/conf/frps_full_example.toml [common] # 监听端口 bind_port = 7000 # 面板端口 dashboard_port = 7500 # 登录面板的账号密码(修改成自己的) dashboard_user = admin dashboard_pwd = admin # token = docker-compose: version: '3.9' services: frps: image: fatedier/frps:v0.60.0 hostname: frps container_name: frps volumes: - "./config/frps.toml:/frps.toml" command: - "-c" - "/frps.toml" network_mode: "host" frp-client frpc.toml: # 服务端地址 https://github.com/fatedier/frp/blob/dev/conf/frpc_full_example.toml serverAddr = "124.71.159.195" # 服务端配置的bindPort serverPort = 7000 # token = [[proxies]] # 代理应用名称,根据自己需要进行配置 name = "smile-dev-tech-01" # 代理类型 有tcp\udp\stcp\p2p type = "tcp" # 客户端代理应用IP localIP = "host.docker.internal" # 客户端代理应用端口 localPort = 8234 # 服务端反向代理端口;提供给外部访问 remotePort = 8234 [[proxies]] # 代理应用名称,根据自己需要进行配置 name = "smile-dev-tech-02" # 代理类型 有tcp\udp\stcp\p2p type = "tcp" # 客户端代理应用IP localIP = "host.docker.internal" # 客户端代理应用端口 localPort = 9001 # 服务端反向代理端口;提供给外部访问 remotePort = 9001 docker-compose: # 命令执行 docker-compose -f docker-compose.yml up -d version: '3.9' services: frpc: image: fatedier/frpc:v0.60.0 hostname: frpc container_name: frpc volumes: - "./config/frpc.toml:/frpc.toml" command: - "-c" - "/frpc.toml" network_mode: "host" frp-server必须部署在有公网ip的服务器,frp-client客户端有两种部署方式: 1.docker部署,如上 2.下载windows客户端https://github.com/fatedier/frp/releases/tag/v0.60.0 **注意:**如果java后端和client都部署在docker同一网络中,frpc.toml 中 localIP = "localhost" 如果client在docker容器,java在idea中启动,那么 localIP = "host.docker.internal" 如果client在windows下启动,java在idea中启动, localIP = "localhost" 总之,关键是让client和java互通;client和server互通比较容易!!! 公网ip:7500 可以查看面板。
杂项
zy123
3月21日
0
8
0
2025-03-15
欢迎使用 Typecho
如果您看到这篇文章,表示您的 blog 已经安装成功.
默认分类
zy123
3月15日
1
12
0
上一页
1
...
11
12