深入拆解Spring第二大核心思想:AOP

什么是AOP

Aspect Oriented Programming(面向切面编程)

  • 什么是面向切面编程呢? 切⾯就是指某⼀类特定问题, 所以AOP也可以理解为面向特定方法编程.

  • 什么是面向特定方法编程呢? 比如对于"登录校验", 就是⼀类特定问题. 登录校验拦截器, 就是对"登录校验"这类问题的统⼀处理. 所以, 拦截器也是AOP的⼀种应⽤. AOP是⼀种思想, 拦截器是AOP思想的⼀种实现. Spring框架实现了这种思想, 提供了拦截器技术的相关接⼝.

  • 同样的, 统⼀数据返回格式和统⼀异常处理, 也是AOP思想的⼀种实现.

  • 简单来说AOP是一种思想,是对某一类事情的集中处理,实现的方式有很多,有SpringAOP,AspectJ,CGLIB等

SpringAOP快速入门

我们通过一个经典的“方法耗时统计”案例来快速上手 Spring AOP。

引入AOP依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId> 
</dependency>

编写切面代码

一个切面包含了我们要执行的操作(通知) 和指定在何处执行(切点)

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;@Aspect      // 声明这是一个切面类
@Component   // 将其作为Bean交由Spring容器管理
@Slf4j
public class TimeRecordAspect {// 1. 使用 @Pointcut 定义一个可重用的切点// 匹配 com.example.service 包及其子包下的所有类的所有方法@Pointcut("execution(* com.example.service..*.*(..))")public void serviceMethods() {}// 2. 定义通知(Advice),并引用上面的切点@Around("serviceMethods()")public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {// joinPoint 代表被拦截的目标方法String methodName = joinPoint.getSignature().toShortString();long startTime = System.currentTimeMillis();log.info("==> 开始执行 [{}], 参数: {}", methodName, joinPoint.getArgs());// 3. 调用 proceed() 执行原始的目标方法Object result = joinPoint.proceed();long endTime = System.currentTimeMillis();log.info("<== 执行 [{}] 结束, 耗时: {} ms, 返回值: {}", methodName, (endTime - startTime), result);return result;}
}

代码解析:

  • @Aspect: 标志着这个类是一个切面。
  • @Pointcut: 用于声明一个可重用的切点表达式。这样,当多个通知需要应用在相同的切点时,我们就不需要重复书写冗长的 execution 表达式了。
  • @Around: 环绕通知。这是功能最强大的通知类型,它能完全控制目标方法的执行,你可以在方法执行前后添加自定义逻辑,甚至可以决定是否执行目标方法。
  • ProceedingJoinPoint: 连接点对象,只在 @Around 通知中使用。它代表了被拦截的目标方法,通过调用其 proceed() 方法来执行原始方法。

[!INFO] SpringAOP只是使用了Aspect的注解
注解分为两个步骤:1. 声明 2. 实现

Spring中AOP的通知类型有以下几种:

  • @Around: 环绕通知。在目标方法执行前后都可执行,可以控制目标方法的执行。
  • @Before: 前置通知。在目标方法执行前执行。
  • @After: 后置通知。在目标方法执行后执行,无论方法是正常返回还是抛出异常,它都会执行(类似于 finally 块)。
  • @AfterReturning: 返回后通知。在目标方法成功执行并返回结果后执行,如果方法抛出异常则不会执行。
  • @AfterThrowing: 异常后通知。在目标方法抛出异常后执行。

@PointCut

如果有大量的方法,那么就会存在大量的切点表达式,此时可以使用@PointCut把公共的切点表达式提取出来,需要时引入即可

@Aspect  
@Component  
@Slf4j  
public class TimeRecordAspect {  @Pointcut("execution(* com.doublez.springbook.controller.*.*(..))")  private void pt(){}  @Around("pt()")  public Object timeRecordAspect(ProceedingJoinPoint joinPoint) throws Throwable {  //... }@Around("pt()")public Object timeRecordAspect1(ProceedingJoinPoint joinPoint) throws Throwable {//...}@Around("pt()")public Object timeRecordAspect2(ProceedingJoinPoint joinPoint) throws Throwable {//...}@Around("pt()")public Object timeRecordAspect3(ProceedingJoinPoint joinPoint) throws Throwable {//...}
}

当切点定义使用private修饰时, 仅能在当前切⾯类中使用, 当其他切面类也要使用当前切点定义时, 就需要把private改为public. 引用方式为: 全限定类名.方法名()

切点表达式

常见的有两种:@execution @annotation

execution

在这里插入图片描述

  1. 通配符 * 的用法:

    • 匹配任意字符,但仅匹配一个元素,如返回类型、包名、类名、方法名或方法参数。
    • * 在包名中表示任意包(一层包)。
    • * 在类名中表示任意类。
    • * 在返回值中表示任意返回值类型。
    • * 在方法名中表示任意方法。
    • * 在参数中表示一个任意类型的参数。
  2. 通配符 .. 的用法:

    • 匹配多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数。
    • .. 配置包名时,标识此包及其所有子包。
    • .. 配置参数时,表示任意个任意类型的参数。
  3. 切点表达式示例:

    • execution(public String com.example.demo.controller.TestController.t1()):匹配 TestController 下的 public 修饰,返回类型为 String,方法名为 t1,无参方法。
    • execution(String com.example.demo.controller.TestController.t1()):省略访问修饰符的情况。
    • execution(* com.example.demo.controller.TestController.t1()):匹配所有返回类型。
    • execution(* com.example.demo.controller.TestController.*()):匹配 TestController 下的所有无参方法。
    • execution(* com.example.demo.controller.TestController.*(..)):匹配 TestController 下的所有方法。
    • execution(* com.example.demo.controller.*.*(..)):匹配 controller 包下所有类的所有方法。
    • execution(* com..TestController.*(..)):匹配所有包下面的 TestController。
    • execution(* com.example.demo...(..)):匹配 com.example.demo 包下,子孙包下的所有类的所有方法。

AOP可以识别类的私有方法吗,可以的话推荐吗? ->here

@annotation

用于自定义类的实现:[[@annotation自定义注解实现|here]]

切面优先级@Order

在这里插入图片描述

  • 数字越大,优先级越低(可以有负数)
@Aspect  
@Component  
@Order(1) 
public class Demo {
}
@Aspect  
@Component  
@Order(3) 
public class Demo2 {
}

AOP原理

Spring AOP 并没有在编译期修改你的代码,而是在运行时通过动态代理技术实现的。当你从 Spring 容器中获取一个 Bean 时,如果这个 Bean 需要被 AOP 增强,那么你拿到的其实是一个代理对象,而不是原始对象。所有对方法的调用都会先经过这个代理对象,由它来决定何时执行切面逻辑、何时执行原始方法。

[[代理模式]]

  • 定义:为其他对象提供⼀种代理以控制对这个对象的访问. 它的作用就是通过提供⼀个代理类, 让我们在调用目标方法的时候, 不再是直接对目标方法进行调用, 而是通过代理类间接调用.

  • 在某些情况下, ⼀个对象不适合或者不能直接引用另⼀个对象,而代理对象可以在客户端和目标对象之间起到中介的作用

代理模式的主要角色

  1. Subject: 业务接口类,可以是抽象类或者接口(不一定有)
  2. RealSubject:业务实现类,具体的业务执行,也就是被代理对象
  3. Proxy:代理类。RealSubject的代理
  • 根据代理的创建时期,代理模式可以分为静态代理动态代理

静态代理

静态代理需要我们手动为每个被代理的类创建一个代理类,代理类和被代理类实现相同的接口。

  • 优点:简单明了,容易理解。
  • 缺点:非常不灵活。如果接口增加一个方法,被代理类和代理类都需要修改。并且,每个业务类都需要一个对应的代理类,会导致类的数量急剧膨胀。

让我们用一个发短信的例子来说明:

// 1. 业务接口
interface SmsService {void send(String message);
}// 2. 业务实现类(被代理对象)
class SmsServiceImpl implements SmsService {@Overridepublic void send(String message) {System.out.println("发送短信: " + message);}
}// 3. 静态代理类
class SmsServiceStaticProxy implements SmsService {private final SmsService target;public SmsServiceStaticProxy(SmsService target) {this.target = target;}@Overridepublic void send(String message) {System.out.println("[静态代理] 发送短信前,进行日志记录...");// 调用原始对象的方法target.send(message);System.out.println("[静态代理] 发送短信后,操作完成。");}
}// 使用
public static void main(String[] args) {SmsService smsService = new SmsServiceImpl();SmsServiceStaticProxy proxy = new SmsServiceStaticProxy(smsService);proxy.send("Hello, Static Proxy!");
}

动态代理

由于静态代理的局限性,动态代理应运而生。它不需要我们手动创建代理类,而是在程序运行时动态地生成代理对象。Spring 主要使用两种动态代理技术:

JDK 动态代理

这是 Java 官方提供的代理方式,它要求被代理的类必须实现至少一个接口。它的核心是 java.lang.reflect.Proxy 类和 InvocationHandler 接口。

定义 JDK 动态代理类 (JDKInvocationHandler):

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;// JDK代理工厂
class JdkProxyFactory {public static Object getProxy(Object target) {return Proxy.newProxyInstance(target.getClass().getClassLoader(), // 目标类的类加载器target.getClass().getInterfaces(),  // 目标类实现的接口new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("[JDK动态代理] 方法 " + method.getName() + " 执行前...");Object result = method.invoke(target, args); // 执行目标方法System.out.println("[JDK动态代理] 方法 " + method.getName() + " 执行后...");return result;}});}
}// 使用
public static void main(String[] args) {SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl());smsService.send("Hello, JDK Proxy!");
}

代码简单讲解:

  1. InvocationHandler
    InvocationHandler 接口是 Java 动态代理的关键接口之一,它定义了一个单一方法 invoke(),用于处理被代理对象的方法调用。
    public interface InvocationHandler {// proxy: 代理对象// method: 代理对象调用的实际方法,即其中需要增强的方法// args: 方法的参数Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
    }
    
  2. Proxy
    Proxy 类中使用的最高频率的方法是 newProxyInstance(),这个方法主要用来生成一个代理对象。
    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
    
    • loader: 类加载器,用于加载代理对象。
    • interfaces: 被代理类实现的一组接口(这个参数的定义,也决定了 JDK 动态代理只能代理实现了接口的类)。
    • h: 实现 InvocationHandler 接口的对象。

CGLIB 动态代理

JDK 动态代理有一个最致命的缺陷是其只能代理实现了接口的类。在有些场景下,业务代码是直接实现的类,并没有实现接口。为了解决这个问题,可以使用 CGLIB 动态代理机制来解决。
CGLIB 动态代理的特点:
CGLIB (Code Generation Library) 是一个基于 ASM 的字节码生成库,它允许在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理,很多知名的开源框架都使用了 CGLIB,例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,默认采用 JDK 代理,否则采用 CGLIB 代理。

CGLIB 动态代理实现步骤:

  1. 定义一个类(被代理类)。
  2. 自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于增强目标方法。
  3. 通过 Enhancer 类的 create() 创建代理类(子类)。
  4. 因此,它不要求被代理类实现接口,但要求该类不能是 final 的,方法也不能是 final 的。

接下来看下实现:

JDK 动态代理不同,CGLIB (Code Generation Library) 实际是属于一个开源项目,如果需要使用它的话,需要手动添加相关依赖。

<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version>
</dependency>

自定义 MethodInterceptor (方法拦截器) (CGLIBInterceptor):

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;// CGLIB代理工厂
class CglibProxyFactory {public static Object getProxy(Object target) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(target.getClass()); // 设置父类(被代理类)enhancer.setCallback(new MethodInterceptor() {@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {System.out.println("[CGLIB动态代理] 方法 " + method.getName() + " 执行前...");Object result = method.invoke(target, args); // 执行目标方法System.out.println("[CGLIB动态代理] 方法 " + method.getName() + " 执行后...");return result;}});return enhancer.create(); // 创建代理对象}
}// 使用(假设 SmsServiceImpl 没有实现接口)
public static void main(String[] args) {SmsServiceImpl smsService = (SmsServiceImpl) CglibProxyFactory.getProxy(new SmsServiceImpl());smsService.send("Hello, CGLIB Proxy!");
}

Spring 如何选择代理方式?

  • 如果目标对象实现了接口,Spring AOP 默认会使用 JDK 动态代理
  • 如果目标对象没有实现接口,Spring AOP 会使用 CGLIB 动态代理
  • 在 Spring Boot 2.x 之后,默认的代理方式改为了 CGLIB。你也可以通过配置文件 spring.aop.proxy-target-class=false 来强制使用 JDK 动态代理(前提是目标类实现了接口)。

代码简单讲解:

  1. MethodInterceptor
    MethodInterceptor 和 JDK 代理中的 InvocationHandler 类似,它只定义了一个方法 intercept(),用于增强目标方法。
    public interface MethodInterceptor extends Callback {/*** 参数说明:* o: 被代理的对象* method: 目标方法(被拦截的方法,也就是需要增强的方法)* objects: 方法入参* methodProxy: 用于调用原始方法*/Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable;
    }
    
  2. Enhancer.create()
    Enhancer.create() 用来生成一个代理对象。
    public static Object create(Class type, Callback callback) {// ...省略
    }
    
    • type: 被代理类的类型(类或接口)。
    • callback: 自定义方法拦截器 MethodInterceptor

AOP 的一个重要“陷阱”:方法内部调用失效

当一个被 AOP 增强的 Bean,其内部的一个方法 methodB() 调用了另一个被增强的方法 methodA() 时,methodA() 的增强(如 @Transactional 或我们自定义的切面)会失效。

@Service
public class OrderService {@Transactional // 我们希望这个方法有事务public void createOrder() {// ... 业务逻辑 ...}public void processOrders() {// ... 其他逻辑 ...// 这种调用方式,createOrder() 的事务会失效!this.createOrder();}
}

原因:AOP 是通过代理对象实现的。外部调用 orderService.processOrders() 时,调用的是代理对象的方法。但是,当 processOrders() 内部执行 this.createOrder() 时,这里的 this 指向的是原始的 OrderService 对象,而不是代理对象。这次调用绕过了代理,直接访问了原始对象的方法,因此所有的切面逻辑都不会被触发。

如何解决?

  1. 注入自己:将自身的代理对象注入进来,通过代理对象来调用。

    @Service
    public class OrderService {@Autowiredprivate OrderService self; // 注入自身的代理对象@Transactionalpublic void createOrder() { ... }public void processOrders() {// 通过代理对象调用self.createOrder();}
    }
    

    注意:这可能会导致循环依赖问题,需要 Spring Boot 2.6+ 或额外配置来解决

  2. 使用 AopContext:更优雅的方式是使用 AopContext 来获取当前的代理对象。

    @Service
    public class OrderService {@Transactionalpublic void createOrder() { ... }public void processOrders() {// 获取当前代理对象并调用((OrderService) AopContext.currentProxy()).createOrder();}
    }
    

    为了使 AopContext.currentProxy() 生效,需要在启动类上添加@EnableAspectJAutoProxy(exposeProxy = true)

总 结

  1. AOP是一种思想,是对某一类事情的集中处理。Spring框架实现了AOP,称之为SpringAOP
  2. Spring AOP常见实现方式有两种:1. 基于注解@Aspect来实现 2. 基于自定义注解来实现,还有一些更原始的方式,比如基于代理,基于xml配置的方式,但目标比较少见
  3. Spring AOP 是基于动态代理实现的,有两种方式:1. 基本JDK动态代理实现 2. 基于CGLIB动态代理实现。运行时使用哪种方式与项目配置和代理的对象有关。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/89261.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/89261.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux服务器stress-ng的使用

安装方法 • Ubuntu/Debian&#xff1a;sudo apt update && sudo apt install stress-ng -y• CentOS/RHEL&#xff08;需EPEL源&#xff09;&#xff1a;sudo yum install epel-release -ysudo yum install stress-ng -y• 源码编译&#xff08;适合定制化需求&#x…

探索阿里云DMS:解锁高效数据管理新姿势

一、阿里云 DMS 是什么 阿里云 DMS&#xff0c;全称为 Data Management Service&#xff0c;即数据管理服务 &#xff0c;是一种集数据管理、结构管理、安全管理于一体的全面数据库服务平台。它能够有效地支持各类数据库产品&#xff0c;包括但不限于 MySQL、SQL Server、Post…

python爬取新浪财经网站上行业板块股票信息的代码

在这个多行业持续高速发展的时代&#xff0c;科技正在改变着我们的生活。 在世界科技领域中&#xff0c;中国正占据越来越重要的位置。当下&#xff0c;每个行业都提到了区块链、人工智能、大数据、5G等科技力量&#xff0c;强调了科技在行业咨询与数据分析领域的重要意义。 随…

【JAVA】监听windows中鼠标侧面键的按钮按下事件

监听windows中鼠标侧面键的按钮按下事件用到的包核心类使用这个类用到的包 jna-5.11.0.jar jna-platform-5.11.0.jar核心类 package sample.tt.mouse;import com.sun.jna.Pointer; import com.sun.jna.platform.win32.*; import com.sun.jna.platform.win32.WinDef.HMODULE; …

Redis突发写入阻断?解析“MISCONF Redis is configured to save RDB…“故障处理

当你的Redis服务器突然拒绝写入并抛出 MISCONF Redis is configured to save RDB snapshots... 错误时&#xff0c;别慌&#xff01;这是Redis的数据安全保护机制在发挥作用。本文带你深度解析故障根因&#xff0c;并提供完整的解决方案。&#x1f525; 故障现象还原 客户端&am…

产品更新丨谷云科技 iPaaS 集成平台 V7.6 版本发布

六月&#xff0c;谷云科技iPaaS集成平台更新了V7.6版本。这次更新中我们着重对API网关、API编排、组织管理权限、API监控等功能进行了增强以及优化&#xff0c;一起来看看有什么新变化吧&#xff01; 网关、监控、编排、组织权限全方位升级 1.API网关 错误码预警&#xff0c;可…

图像处理中的模板匹配:原理与实现

目录 一、什么是模板匹配&#xff1f; 二、模板匹配的匹配方法 1. 平方差匹配&#xff08;cv2.TM_SQDIFF&#xff09; 2. 归一化平方差匹配&#xff08;cv2.TM_SQDIFF_NORMED&#xff09; 3. 相关匹配&#xff08;cv2.TM_CCORR&#xff09; 4. 归一化相关匹配&#xff08…

高性能架构模式——高性能NoSQL

目录 一、关系数据库的缺点二、常见的 NoSQL 方案分 类2.1、K-V 存储2.2、文档数据库2.3、列式数据库2.4、全文搜索引擎三、高性能 NoSQL 方案的典型特征和应用场景3.1、K-V 存储典型特征和应用场景3.2、文档数据库典型特征和应用场景3.1.1、文档数据库的 no-schema 特性的优势…

正确选择光伏方案设计软件:人力成本优化的关键一步

在竞争激烈的市场环境中&#xff0c;企业无不追求效率提升与成本控制。设计环节作为产品开发的核心流程&#xff0c;其效率高低直接影响整体项目进度与资源消耗。错误的设计软件选择如同在信息高速公路上设置路障——它不会阻止前行&#xff0c;却会让每一次沟通、每一次修改都…

Git问题排查与故障解决详解

前言 在使用Git进行版本控制的过程中&#xff0c;开发者常常会遇到各种各样的问题和错误。本文将详细介绍常见的Git问题及其解决方法&#xff0c;帮助开发者快速定位和解决问题&#xff0c;避免在开发过程中浪费时间。 1. 基础错误与解决 1.1 身份配置问题 问题&#xff1a…

使用Xinference部署语音模型实现文本转语音:完整指南

文章目录引言环境准备1. 安装Xinference2. 启动Xinference服务3. 部署语音模型Python实现文本转语音关键参数说明应用场景性能优化建议常见问题解决结语引言 文本转语音&#xff08;Text-to-Speech, TTS&#xff09;技术在智能助手、有声读物、语音导航等应用中扮演着重要角色…

【C#】实体类定义的是long和值识别到的是Int64,实体类反射容易出现Object does not match target type

&#x1f339;欢迎来到《小5讲堂》&#x1f339; &#x1f339;这是《C#》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解。&#x1f339; &#x1f339;温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01;&#…

C#获取当前系统账户是否为管理员账户

传统方式&#xff1a;WindowsPrincipal winPrincipal new WindowsPrincipal(WindowsIdentity.GetCurrent()); bool admin winPrincipal.IsInRole(WindowsBuiltInRole.Administrator);这种方式虽然是最常用的检测管理员权限的方法&#xff0c;但是有个致命的缺陷&#xff0c;就…

【c++深入系列】:万字详解list(附模拟实现的list源码)

&#x1f525; 本文专栏&#xff1a;c &#x1f338;作者主页&#xff1a;努力努力再努力wz &#x1f4aa; 今日博客励志语录&#xff1a; 当你觉得累的时候&#xff0c;说明你在走上坡路 ★★★ 本文前置知识&#xff1a; 模版 那么在之前的学习中&#xff0c;我们已经学习了…

PandaWiki与GitBook深度对比:AI时代的知识管理工具,选谁好?

在当今信息爆炸的时代&#xff0c;知识管理工具已成为个人学习、团队协作和企业文档管理的必需品。PandaWik作为AI时代迅速崛起的广受欢迎知识管理平台&#xff0c;代表了新一代AI驱动的知识库系统。本文将从功能特性、技术架构、适用场景等多个维度进行全面对比分析。产品定位…

清除 Android 手机 SIM 卡数据的4 种简单方法

SIM 卡存储了联系人、短信和通话记录等信息。在更换新 SIM 卡之前&#xff0c;彻底清除旧卡上的所有个人数据&#xff08;如 SIM 卡联系人、短信、通话记录和手机号码&#xff09;非常重要。要在 Android 手机上清除 SIM 卡内存&#xff0c;您可以参考以下方法。但在开始之前&a…

算法学习笔记:20.分治法——从原理到实战,涵盖 LeetCode 与考研 408 例题

分治法&#xff08;Divide and Conquer&#xff09;是计算机科学中最经典的算法设计思想之一&#xff0c;其核心思想是将复杂问题分解为若干个规模较小的子问题&#xff0c;通过解决子问题并合并结果来求解原问题。这种思想不仅在排序、搜索等基础算法中广泛应用&#xff0c;也…

@classmethod

1. 基本概念 classmethod 是 Python 中用于定义类方法的一种装饰器。类方法与常规的实例方法不同&#xff0c;它的第一个参数是 cls&#xff0c;表示类本身&#xff0c;而不是实例。 class MyClass:class_attr "Class Attribute"classmethoddef class_method(cls):p…

Qt 中使用 SQLite 数据库

一、SQLite 数据库介绍 SQLite 是一个轻量级的嵌入式关系型数据库管理系统&#xff0c;它以库的形式提供&#xff0c;不需要单独的服务器进程&#xff0c;直接访问存储在普通磁盘文件中的数据库。 主要特性 无服务器架构&#xff1a;SQLite 不需要单独的服务器进程 零配置&a…

【Unity】IL2CPP相关理论知识学习

一种编译技术。优点&#xff1a;性能优化&#xff1a;IL2CPP生成C代码后由本地编译器优化&#xff0c;一般在CPU性能和GC方面都优于Mono。特别在移动端或主机平台&#xff0c;性能差距更加明显。跨平台支持&#xff1a;Unity作为跨平台引擎&#xff0c;IL2CPP是支持iOS、Androi…