当前位置: 首页 > news >正文

Java 注解详解(含底层原理)

        今天打算系统梳理一下 Java 注解的知识 —— 写这篇文章的初衷,一是帮自己把零散的理解串联成体系,真正内化这部分内容;二也是希望能给同样在学习注解的朋友提供一份清晰的参考。注解这东西看似简单,深究起来其实藏着不少门道,从基础语法到实际应用,值得好好掰扯掰扯。

开篇词:博主在学习注解的过程中,有特别多的疑问。比如:注解到底是什么?用反射获取注解获取的是什么?为了解决心中的疑虑,今天就来好好探究一下《Java注解》!!

目录

一、什么是注解?

二、标记注解的注解——元注解

@Target

@Retention

@Documented

@Inherited

@Repeatable

三、自定义注解

定义注解,使用 @interface 关键字,基本结构:

注解属性的类型限制:

"value" 属性的特殊性:

四、反射 API 获取注解信息

4.1 可访问注解信息的核心类

4.2 反射 API 中获取注解的核心方法

4.2.1 Class 类的注解方法

4.2.2 Field 类的注解方法

4.2.3 Method 类的注解方法

4.2.4 Constructor 类的注解方法

4.2.4 Parameter 类的注解方法

4.2.5 AnnotatedType 类的注解方法

五、代码示例

六、底层原理

编译时处理注解的过程:

运行时处理注解的过程:

七、总结


一、什么是注解?

定义注解:注解是Java语言的元数据(在Java层面,所有代码都可视为数据,而注解就是为代码添加特定数据的机制),用于修饰代码元素(类、方法、字段等)。它本身不直接影响程序运行,需要通过工具(编译器、运行时环境、反射API)解析处理,实现编译检查、代码生成、运行时配置等功能。

注解的价值:简化传统的XML配置方式、减少模板代码、提高代码可读性、实现逻辑与配置解耦

与注释的区别:

  • 替代传统XML配置方式
  • 减少重复代码
  • 提升代码可读性
  • 实现逻辑与配置分离
维度注解(Annotation)注释(Comment)
作用对象程序(编译器、框架可解析)开发者(仅用于阅读)
语法规范有严格的语法(需用@interface定义)无语法限制(// 或 /* */包裹)
运行时影响可通过反射API获取,影响程序逻辑编译时被忽略,无任何影响

注解的本质:注解的本质实际上是一个继承自 java.lang.annotation.Annotation 的接口

注解的处理时机:

  • 编译期间:编译器根据注解处理器(一个继承了 javax.annotation.processing.AbstractProcessor 抽象类的类,该类由 javac 编译器进行读取),可以在编译期间对注解进行处理(代码生成)
  • 运行期间:通过反射机制获取注解(此时 JVM 用动态代理为该注解生成了一个的代理类),可以在运行是获取注解的信息

二、标记注解的注解——元注解

元注解本质上是给 Java 工具链(编译器、JVM)“看” 的规则说明,用于告诉这些工具:“这个注解应该被如何处理?它能修饰什么?能保留到什么时候”。

Java 定义了 5 个标准的元注解类型且都位于 java.lang.annotation 包下:

@Target

用于指定注解可以应用的Java元素类型,例如类、方法、字段,属性的值由枚举类 java.lang.annotation.ElementType 提供:

字段说明
TYPE用于类、接口、注解、枚举
FIELD用于字段(类的成员变量),或者枚举常量
METHOD用于方法
PARAMETER用于方法或者构造方法的参数
CONSTRUCTOR用于构造方法
LOCAL_VARIABLE用于变量
ANNOTATION_TYPE用于注解
PACKAGE用于包
TYPE_PARAMETER用于泛型参数
TYPE_USE用于声明语句、泛型或者强制转换语句中的类型
MODULE用于模块

@Retention

用于指定注解的保留策略,即注解在哪个阶段保留,属性的值由枚举类 java.lang.annotation.RetentionPolicy 提供:

字段说明
SOURCE会被编译器丢弃,不会出现在class文件中。
CLASS默认值,会被保留在class文件中,但 JVM 加载类时不会处理它们。
RUNTIME会被保留在class文件中,还会被JVM加载,因此可以通过反射机制在运行时访问。这也是注解生命周期中最常用的一种策略

@Documented

用于标注其他注解,使其在生成 Javadoc 文档时被包含在内。

@Inherited

用于标注其他注解,被标记的注解具有继承性。

当被标记的注解用在一个类上,那么该类的子类可以继承这个被标记的注解。

注意:@Inherited 仅对类级别的注解有效,对方法、字段、参数等其他程序元素的注解无继承效果。

示例:

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited // 仅对类上的注解有效,方法和字段上的该注解不会被继承
public @interface MyAnnotation { ... }

@Repeatable

用于标记一个注解可以在同一个程序元素上重复使用。

使用时需要指定一个 “容器注解”(该容器注解的属性是当前注解的数组)。

示例:

// 容器注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotations {MyAnnotation[] value();
}// 重复注解(使用@Repeatable标记)
@Repeatable(MyAnnotations.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {String value();
}// 使用示例
@MyAnnotation("a")
@MyAnnotation("b")
public class MyClass {}

三、自定义注解

定义注解,使用 @interface 关键字,基本结构:

import java.lang.annotation.*;// 元注解:指定注解的适用范围(类、方法、字段等)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
// 元注解:指定注解的生命周期(SOURCE/CLASS/RUNTIME)
@Retention(RetentionPolicy.RUNTIME)
// 元注解:是否被javadoc文档提取
@Documented
// 元注解:是否允许子类继承该注解(仅对类注解有效)
@Inherited
public @interface MyAnnotation {// 注解属性(类似接口方法,但可指定默认值)String value(); // 必选属性(无默认值)int count() default 1; // 可选属性(有默认值)String[] tags() default {}; // 数组类型
}

注解属性的类型限制:

  • 基本数据类型
  • String、Class、枚举
  • 其他注解
  • 以上类型的数组
  • 如果使用其他类型,编译时会直接报错

"value" 属性的特殊性:

当注解有且仅有`value`一个属性时,使用时可省略属性名

// 定义仅含value属性的注解
public @interface MyAnnot {String value();
}// 使用时可简化
@MyAnnot("test")  // 等价于 @MyAnnot(value = "test")
public class Demo {}

当注解有多个属性,但仅需指定`value`时,仍可省略属性名

public @interface MyAnnot {String value();int count() default 1; // 有默认值
}// 仅指定value,可省略属性名
@MyAnnot("test")  // 等价于 @MyAnnot(value = "test", count = 1)
public class Demo {}

若需指定多个属性,`value`不能省略属性名

public @interface MyAnnot {String value();int count() default 1;
}// 错误写法:多属性时不能省略value=
// @MyAnnot("test", count = 2) // 正确写法:必须显式指定value=
@MyAnnot(value = "test", count = 2)
public class Demo {}

四、反射 API 获取注解信息

4.1 可访问注解信息的核心类

说明
java.lang.Class表示类、接口、枚举等类型,可访问类本身、父类、实现接口上的注解。
java.lang.reflect.Field表示类的字段,可访问字段上的注解。
java.lang.reflect.Method表示类的方法,可访问方法本身、方法参数、方法返回值上的注解。
java.lang.reflect.Constructor表示类的构造函数,可访问构造函数本身及参数上的注解。
java.lang.reflect.Parameter表示方法或构造函数的参数,可访问参数上的注解。
java.lang.reflect.AnnotatedType

表示被注解的类型(如泛型类型、数组类型等)。

4.2 反射 API 中获取注解的核心方法

4.2.1 Class 类的注解方法
方法说明
getAnnotation(Class<T> annotationClass)获取该类上指定类型的注解(若注解被@Inherited元注解标记,则包括从父类继承的注解),不存在则返回null
getAnnotations()获取该类上的所有注解(包括继承的)。
getDeclaredAnnotation(Class<T> annotationClass)获取该类上直接声明的指定类型注解(不包括继承的)。
getDeclaredAnnotations()获取该类上直接声明的所有注解(不包括继承的)。
isAnnotationPresent(Class<? extends Annotation> annotationClass)判断该类是否存在指定类型的注解(包括继承的)。
4.2.2 Field 类的注解方法
方法说明
getAnnotation(Class<T> annotationClass)获取该字段上指定类型的注解。
getAnnotations()获取该字段上的所有注解。
getDeclaredAnnotations()获取该字段上直接声明的所有注解(因字段注解不可继承,故结果与getAnnotations()一致)。
isAnnotationPresent(Class<? extends Annotation> annotationClass)判断该字段是否存在指定类型的注解。
4.2.3 Method 类的注解方法
方法说明
getAnnotation(Class<T> annotationClass)获取该方法上指定类型的注解。
getAnnotations()获取该方法上的所有注解。
getDeclaredAnnotations()getAnnotations()(方法注解不可继承)。
isAnnotationPresent(Class<? extends Annotation> annotationClass)判断该方法是否存在指定类型的注解。
getParameterAnnotations()获取该方法所有参数上的注解(返回二维数组,每个元素是一个参数的注解数组)。
getAnnotationsByType(Class<T> annotationClass)获取该方法上指定类型的所有注解(支持重复注解)。
4.2.4 Constructor 类的注解方法
方法说明
getAnnotation(Class<T> annotationClass)获取该构造函数上指定类型的注解。
getAnnotations()获取该构造函数上的所有注解。
getDeclaredAnnotations()getAnnotations()(构造函数注解不可继承)。
getParameterAnnotations()获取该构造函数所有参数上的注解(返回二维数组)。
4.2.4 Parameter 类的注解方法
方法说明
getAnnotation(Class<T> annotationClass)获取该参数上指定类型的注解。
getAnnotations()获取该参数上的所有注解。
getDeclaredAnnotations()getAnnotations()(参数注解不可继承)。
isAnnotationPresent(Class<? extends Annotation> annotationClass)判断该参数是否存在指定类型的注解。
4.2.5 AnnotatedType 类的注解方法
方法说明
getAnnotation(Class<T> annotationClass)获取指定类型的注解(若存在)。
getAnnotations()返回该类型上的所有注解(包括继承的)。
getDeclaredAnnotations()返回该类型上直接声明的注解(不包括继承的)。
getAnnotationsByType(Class<T> annotationClass)获取指定类型的所有注解(包括重复注解)。
Type getType()返回当前 AnnotatedType 所表示的原始类型(如 List<String> 对应的 Type 对象)。

五、代码示例

自定义日志注解,用于标记需要记录日志的方法

import java.lang.annotation.*;/*** 自定义日志注解* 用于标记需要记录日志的方法*/
@Target(ElementType.METHOD) // 仅用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,可通过反射获取
@Documented // 生成文档时包含该注解
public @interface Log {// 操作描述(value属性,可简化使用)String value() default "";// 是否记录参数boolean recordParams() default true;// 是否记录返回值boolean recordResult() default false;
}

运行时注解解析器,通过动态代理实现日志记录功能

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;/*** 运行时注解解析器* 通过动态代理实现日志记录功能*/
public class LogRuntimeParser implements InvocationHandler {// 目标对象private final Object target;public LogRuntimeParser(Object target) {this.target = target;}// 创建代理对象public static Object createProxy(Object target) {return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),new LogRuntimeParser(target));}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 检查方法是否有@Log注解if (method.isAnnotationPresent(Log.class)) {Log log = method.getAnnotation(Log.class);// 记录开始时间long startTime = System.currentTimeMillis();// 打印日志信息System.out.println("\n===== 日志开始 =====");System.out.println("操作描述: " + log.value());System.out.println("方法名称: " + method.getName());// 如果需要记录参数if (log.recordParams() && args != null) {System.out.println("参数列表: " + Arrays.toString(args));}// 执行目标方法Object result = method.invoke(target, args);// 如果需要记录返回值if (log.recordResult()) {System.out.println("返回结果: " + result);}// 记录执行时间System.out.println("执行时间: " + (System.currentTimeMillis() - startTime) + "ms");System.out.println("===== 日志结束 =====");return result;}// 没有@Log注解的方法,直接执行return method.invoke(target, args);}
}

用户服务接口,用于动态代理

/*** 用户服务接口,用于动态代理*/
public interface UserServiceInterface {boolean login(String username, String password);String register(String username, String email);void updateProfile(String username, String newEmail);
}

业务服务类,使用自定义的@Log注解

/*** 业务服务类,使用自定义的@Log注解*/
public class UserService {// 使用注解,仅指定value(可简化写法)@Log("用户登录")public boolean login(String username, String password) {System.out.println("执行登录逻辑...");return "admin".equals(username) && "123456".equals(password);}// 完整指定注解的所有属性@Log(value = "用户注册", recordParams = true, recordResult = true)public String register(String username, String email) {System.out.println("执行注册逻辑...");return "注册成功,用户ID: " + System.currentTimeMillis();}// 不使用注解的方法(不会被日志记录)public void updateProfile(String username, String newEmail) {System.out.println("执行更新资料逻辑...");}
}

测试主类

/*** 测试主类*/
public class Main {public static void main(String[] args) {// 创建目标对象UserService userService = new UserService();// 创建代理对象(用于运行时解析注解)UserServiceInterface proxy = (UserServiceInterface) LogRuntimeParser.createProxy(userService);// 调用被@Log注解的方法proxy.login("admin", "123456");proxy.register("testUser", "test@example.com");// 调用未被@Log注解的方法proxy.updateProfile("admin", "new@example.com");}
}

六、底层原理

编译时处理注解的过程:

编译时处理注解实际上是通过一个实现了 javax.annotation.processing.AbstractProcessor 抽象类的类来进行处理的,具体的操作可以参考网址:

Java注解处理器实战 | Desperado

运行时处理注解的过程:

先来看一段代码,一段自定义的注解源代码,注解当中包含一个 value() 属性。代码如下:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @author NanJi* @version 1.0* @annotationName InitMethod* @desc 自定义的初始化方法注解* @date 2025/8/2: 11:20*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InitMethod {String value() default "";
}

经过Java编译器编译之后的代码,再反编译回来是什么样子呢?代码如下:

// 编译后的 InitMethod.class
public interface InitMethod extends java.lang.annotation.Annotation {public abstract String value(); // 对应注解的value属性// 编译器自动添加:返回注解类型Class<? extends Annotation> annotationType();
}

可以看到,定义的 InitMethod 注解实际上是一个继承了 java.lang.annotation.Annotation 接口的接口。接下来我们来看一下运行时访问注解发生了什么?

目录结构如下:

同样以 InitMethod 为例:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @author NanJi* @version 1.0* @annotationName InitMethod* @desc 自定义的初始化方法注解* @date 2025/8/2: 11:20*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InitMethod {String value() default "";
}

在定义一个 InitDemo 类,用来测试 InitMethod 注解,代码如下:

/*** @author NanJi* @version 1.0* @className InitDemo* @desc 测试初始化方法注解* @date 2025/8/2 : 11:23*/
public class InitDemo {@InitMethod("init...")public void init() {System.out.println("正在初始化...");}
}

接着定义一个测试类 Main,代码如下:

import java.lang.reflect.*;/*** @author NanJi* @version 1.0* @className Main* @desc 测试类* @date 2025/8/2: 11:30*/
public class Main {public static void main(String[] args) throws Exception {// 写入代理类文件System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");Class<InitDemo> cls = InitDemo.class;// 获取 InitDemo 类的所有方法Method[] methods = cls.getMethods();// 遍历所有方法for (Method method : methods) {// 判断方法是否有 InitMethod 注解boolean isInitMethod = method.isAnnotationPresent(InitMethod.class);// 如果有 InitMethod 注解,则获取当前的注解代理类if (isInitMethod) {// 获取 InitMethod 注解的代理类InitMethod initMethod = method.getAnnotation(InitMethod.class);// 获取代理类的类实例System.out.println(initMethod.getClass());// 获取注解的值,实际上是通过代理类调用了注解的 value() 方法String value = initMethod.value();// 打印注解的 value() 方法的值System.out.println("InitMethod value: " + value);// 调用 InitDemo 类的 init() 方法method.invoke(cls.getConstructor().newInstance());}}}
}

现在我们来运行一下这段程序,看看使用 InitMethod 这个注解发生了什么

可以看到控制台打印了三条语句:

第一条 class jdk.proxy2.$Proxy1 实际上就是生成的代理类。

第二条 InitMethod value: init... 实际上是通过代理类调用了 InitMethod 注解类的 value() 方法。

第三条 正在初始化... 实际上是通过反射调用了 InitDemo 类的 init() 方法。

那生成的代理类在哪里呢?类中的内容又有什么呢?接着往下看:

在运行测试类的 main 方法时,方法中的第一行代码:

// 写入代理类文件
System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");

这行代码的作用就是允许代理类的class文件写入你的磁盘当中,默认是写入到你的项目模块中,会生成如下目录:

接着我们打开 jdk 目录下最后面的 proxy2 目录下的 $Proxy1 类,通过idea打开后查看类中的结构:

package jdk.proxy2;import com.ktjiaoyu.annotation.demo.init.InitMethod;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;public final class $Proxy1 extends Proxy implements InitMethod {private static final Method m0;private static final Method m1;private static final Method m2;private static final Method m3;private static final Method m4;public $Proxy1(InvocationHandler var1) {super(var1);}public final int hashCode() {try {return (Integer)super.h.invoke(this, m0, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}public final boolean equals(Object var1) {try {return (Boolean)super.h.invoke(this, m1, new Object[]{var1});} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}public final String toString() {try {return (String)super.h.invoke(this, m2, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}public final String value() {try {return (String)super.h.invoke(this, m3, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}public final Class annotationType() {try {return (Class)super.h.invoke(this, m4, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}static {try {m0 = Class.forName("java.lang.Object").getMethod("hashCode");m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));m2 = Class.forName("java.lang.Object").getMethod("toString");m3 = Class.forName("com.ktjiaoyu.annotation.demo.init.InitMethod").getMethod("value");m4 = Class.forName("com.ktjiaoyu.annotation.demo.init.InitMethod").getMethod("annotationType");} catch (NoSuchMethodException var2) {throw new NoSuchMethodError(((Throwable)var2).getMessage());} catch (ClassNotFoundException var3) {throw new NoClassDefFoundError(((Throwable)var3).getMessage());}}private static MethodHandles.Lookup proxyClassLookup(MethodHandles.Lookup var0) throws IllegalAccessException {if (var0.lookupClass() == Proxy.class && var0.hasFullPrivilegeAccess()) {return MethodHandles.lookup();} else {throw new IllegalAccessException(var0.toString());}}
}

这个类是通过反射 API 获取注解时由JVM实现的,在这个类的静态方法中为成员变量 m3 附了值,这个值实际上就是 InitMethod 注解中的 value() 方法。

static {try {m0 = Class.forName("java.lang.Object").getMethod("hashCode");m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));m2 = Class.forName("java.lang.Object").getMethod("toString");m3 = Class.forName("com.ktjiaoyu.annotation.demo.init.InitMethod").getMethod("value");m4 = Class.forName("com.ktjiaoyu.annotation.demo.init.InitMethod").getMethod("annotationType");} catch (NoSuchMethodException var2) {throw new NoSuchMethodError(((Throwable)var2).getMessage());} catch (ClassNotFoundException var3) {throw new NoClassDefFoundError(((Throwable)var3).getMessage());}}

在我们调用 value() 方法时,实际上是调用的代理类中的 value() 方法,代码如下:

public final String value() {try {return (String)super.h.invoke(this, m3, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}

这个 value() 方法就是获取注解 value 属性的值。到这里就清楚了,为什么可以通过反射去拿到注解的值。

七、总结

  1. 注解的作用是用来描述和标记Java代码当中的元素(类、方法、字段等)
  2. 元注解是用来标记注解的注解,作用是给 Java 工具链(编译器、JVM、注解处理器)去识别的规则,Java 工具链根据对应的规则进行对应的处理
  3. 注解的本质实际上是一个实现了 java.lang.annotation.Annotation 接口的接口
  4. 注解的处理时机发生在编译期,由一个实现了 javax.annotation.processing.AbstractProcessor 抽象类的类来进行操作
  5. 注解的处理时机发生在运行期,通过Java动态代理,去实现注解当中定义的方法,从而获取对应的值
  6. 通过 System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true") 方法,设置JVM的系统属性,告诉JVM将生成的动态代理类保存到文件系统中,方便开发者查看和调试。

欧了,到这里我应该解释的差不多啦,我是南极,大胆做自己,活出精彩的人生👊👊👊

http://www.lryc.cn/news/608135.html

相关文章:

  • Vue 3.0 Composition API:重新定义组件逻辑的组织方式
  • 算法训练营DAY46 第九章 动态规划part13
  • 全球化 2.0 | 中国香港教育机构通过云轴科技ZStack实现VMware替代
  • stm32103如果不用32k晶振,那引脚是悬空还是接地
  • SLAM中的非线性优化-2D图优化之零空间实战(十六)
  • Linux iptables防火墙操作
  • Apache Doris数据库——大数据技术
  • SpringBoot怎么查看服务端的日志
  • 【NLP舆情分析】基于python微博舆情分析可视化系统(flask+pandas+echarts) 视频教程 - 微博舆情数据可视化分析-热词情感趋势树形图
  • sqli-labs:Less-21关卡详细解析
  • 【BTC】挖矿难度调整
  • 人类学家与建筑师:区分UX研究和项目管理的需求分析
  • 隧道照明“隐形革命”:智能控制如何破解安全与节能双重命题
  • 【iOS】strong和copy工作流程探寻、OC属性关键字复习
  • 电脑手机热点方式通信(下)
  • 「iOS」————weak底层原理
  • 「iOS」————SideTable
  • JAVA国际版同城服务同城信息同城任务发布平台APP源码Android + IOS
  • Ajax——异步前后端交互提升OA系统性能体验
  • Dice Combinations(Dynamic Programming)
  • 8.2 状态机|贪心|dfs_dp
  • Linux初步认识与指令与权限
  • 机器学习——K 折交叉验证(K-Fold Cross Validation),实战案例:寻找逻辑回归最佳惩罚因子C
  • Jotai:React轻量级原子化状态管理,告别重渲染困扰
  • React ahooks——副作用类hooks之useThrottleFn
  • react 和 react native 的开发过程区别
  • Javascript面试题及详细答案150道之(016-030)
  • 【REACT18.x】使用vite创建的项目无法启动,报错TypeError: crypto.hash is not a function解决方法
  • NEXT.js 打包部署到服务器
  • OLTP,OLAP,HTAP是什么,数据库该怎么选