详细分析Spring的动态代理机制
文章目录
- 1. JDK动态代理和CGLIB动态代理的区别
- 1.1 适用范围
- 1.2 生成的代理类
- 1.3 调用方式
- 2. 问题引入
- 3. 创建工程验证 Spring 默认采用的动态代理机制
- 3.1 引入 Maven 依赖
- 3.2 UserController.java
- 3.3 UserService.java
- 3.4 UserServiceImpl.java(save方法添加了@Transactional注解)
- 3.5 User.java
- 3.6 编写配置文件
- 4. 测试结果及测试结果分析
- 4.1 测试结果
- 4.2 测试结果分析
- 4.3 Spring Framework工程和SpringBoot工程中分别使用哪种动态代理
- 4.4 源码分析
- 4.5 手动指定 AOP 使用哪种动态代理
- 5. 为什么SpringBoot默认会选用CGLIB动态代理(JDK动态代理的局限性)
- 5.1 使用JDK动态代理时目标对象必须要实现接口
- 5.2 使用JDK动态代理时必须要用接口来接收
- 5.3 使用JDK动态代理时获取不到实现类方法上的注解
- 6. 补充:调试时this指针指的是哪个对象
- 7. 总结
- 8. 参考资料
1. JDK动态代理和CGLIB动态代理的区别
1.1 适用范围
- 如果要使用 JDK 动态代理,必须要保证目标类实现了接口
- 如果要使用 CGLIB 动态代理,目标类不能使用 final 关键字进行修饰,也就是说,目标类是可继承的
1.2 生成的代理类
- JDK 动态代理生成的代理对象会实现目标类实现的所有接口,这也是 JDK 动态代理为什么要保证目标类实现了接口的原因
- CGLIB 动态代理生成的代理对象会继承目标类,所以说目标类不能使用 final 关键字进行修饰,因为一个类加了final 关键字就不能被继承了
1.3 调用方式
JDK 动态代理是采用反射的方式来调用目标方法,而 CGLIB 是通过子类直接调用父类的方式来调用目标方法
2. 问题引入
Spring 框架在创建代理对象时,默认情况下如果目标对象实现了至少一个接口,那么会使用 JDK 动态代理;如果目标对象没有实现任何接口,则会使用 CGLIB 动态代理
以上内容相信大家都非常熟悉,然而事实真的是这样吗,我们创建一个工程来验证一下
3. 创建工程验证 Spring 默认采用的动态代理机制
工程环境:SpringBoot 3.0.2
+ JDK 17.0.7
3.1 引入 Maven 依赖
为了方便测试,我们引入以下依赖
- Spring Web
- MyBatis Framework
- aspectjweaver
- MySQL 连接驱动
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>3.0.2</version>
</dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId>
</dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope>
</dependency>
我们创建几个简单的类,验证 Spring 默认采用的动态代理机制
3.2 UserController.java
import cn.edu.scau.pojo.User;
import cn.edu.scau.service.UserService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/user")
public class UserController {private final UserService userService;public UserController(UserService userService) {this.userService = userService;}@PostMapping("/save")public void save(@RequestBody User user) {userService.save(user);}}
3.3 UserService.java
import cn.edu.scau.pojo.User;public interface UserService {void save(User user);}
3.4 UserServiceImpl.java(save方法添加了@Transactional注解)
import cn.edu.scau.pojo.User;
import cn.edu.scau.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Service
public class UserServiceImpl implements UserService {@Transactionalpublic void save(User user) {System.out.println("保存用户:" + user);}}
3.5 User.java
public class User {}
3.6 编写配置文件
application.yml
spring:datasource:url: jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver
4. 测试结果及测试结果分析
4.1 测试结果
我们在 controller 层打上断点,以 debug 的方法访问接口
可以看到,UserService 的类名中出现了 CGLIB 关键字,说明采用的是 CGLIB 动态代理
怎么回事呢,明明我的 UserServiceImpl 是实现了 UserService 接口的,为什么会使用 CGLIB 动态代理呢
4.2 测试结果分析
在 Spring Framework 中,是使用 @EnableAspectJAutoProxy
注解来开启 Spring AOP
相关功能的
我们查看一下@EnableAspectJAutoProxy
注解类的源码
在这里插入图片描述
通过源码的注释我们可以了解到:在 Spring Framework 中,proxyTargetClass 的默认取值是 false(false 表示不启用 CGLIB 动态代理),也就是说,Spring Framework 默认采用的还是使用 JDK 动态代理
难道说,Spring 源码的注释写错了???
为了进一步进行测试,我们在项目的启动类上手动指定使用 JDK 动态代理
@EnableAspectJAutoProxy(proxyTargetClass = false)
可以发现,Spring 默认使用的就是 JDK 动态代理
打断点后再次进行测试,发现使用的还是 CGLIB 动态代理
难道 @EnableAspectJAutoProxy 的 proxyTargetClass 属性无效了?
4.3 Spring Framework工程和SpringBoot工程中分别使用哪种动态代理
等一等,我们是不是遗漏了什么?
我们的测试工程使用的 SpringBoot 环境,那如果不用 SpringBoot,只用 Spring Framework 会怎么样呢
测试过程与上述测试相同,就不再赘述了
测试结果表明,在 Spring Framework 中,如果类实现了接口,默认还是使用 JDK 动态代理
4.4 源码分析
结果上面的分析,很有可能是 SpringBoot 修改了 Spring AOP 的相关配置,那我们就来一波源码分析,看一下 SpringBoot 内部到底做了什么
源码分析,找对入口很重要。那这次的入口在哪里呢?
@SpringBootApplication 是一个组合注解,该注解中使用 @EnableAutoConfiguration 实现了大量的自动装配
EnableAutoConfiguration 也是一个组合注解,在该注解上被标志了 @Import
AutoConfigurationImportSelector 类实现了 DeferredImportSelector 接口
DeferredImportSelector 接口中有一个 getImportGroup 方法,AutoConfigurationImportSelector 类重写了该方法
getImportGroup 方法返回了AutoConfigurationGroup 类,AutoConfigurationGroup 是 AutoConfigurationImportSelector 中的一个私有内部类,实现了 DeferredImportSelector.Group 接口
SpringBoot 就是通过 AutoConfigurationImportSelector.AutoConfigurationGroup 类的 process 方法来导入自动配置类的
我们在 process 方法打上断点,重新启动 SpringBoot 测试工程
通过调试信息,我们可以看到与 AOP 相关的自动配置是通过 org.springframework.boot.autoconfigure.aop.AopAutoConfiguration 类来进行配置的
我们查看 AopAutoConfiguration 类的源码,发现如果我们没有在配置文件中指定使用哪种代理,默认就会使用 CGLIB 动态代理
4.5 手动指定 AOP 使用哪种动态代理
通过源码我们也就可以知道,在 SpringBoot 中如果需要指定 AOP 使用哪种动态代理,需要通过 spring.aop.proxy-target-class 这个配置项来修改
在 application.yml 文件中通过 spring.aop.proxy-target-class 属性来配置
在编写文件时我们也可以看到,proxy-target-class 属性默认为 true
我们改为 false 之后再次启动测试工程,发现已经使用了 JDK 动态代理
5. 为什么SpringBoot默认会选用CGLIB动态代理(JDK动态代理的局限性)
为什么 SpringBoot 默认会选用 CGLIB 动态代理呢,当然是因为 JDK 动态代理存在一定的局限性
5.1 使用JDK动态代理时目标对象必须要实现接口
如果要使用 JDK 动态代理,必须要保证目标类实现了接口,这是由 JDK 动态代理的实现原理决定的
5.2 使用JDK动态代理时必须要用接口来接收
我们现在有 UserServiceImpl 和 UserService 两个类,此时需要在 UserContoller 中注入 UserService 类
我们通常都习惯这样写代码
@Autowired
private UserService userService;
在这种情况下,无论是使用 JDK 动态代理,还是 CGLIB 动态代理都不会出现问题
但是,如果代码是这样的呢
@Autowired
private UserServiceImpl userService;
这个时候,如果我们是使用 JDK 动态代理,那在启动时就会报错:
The bean is of type ‘jdk.proxy2.$Proxy59’ and implements:
cn.edu.scau.service.UserService
org.springframework.aop.SpringProxy
org.springframework.aop.framework.Advised
org.springframework.core.DecoratingProxy
Expected a bean of type ‘cn.edu.scau.service.impl.UserServiceImpl’ which implements:
cn.edu.scau.service.UserService
Action:
Consider injecting the bean as one of its interfaces or forcing the use of CGLib-based proxies by setting proxyTargetClass=true on @EnableAsync and/or @EnableCaching.
因为 JDK 动态代理是基于接口的,代理生成的对象只能赋值给接口变量
而 CGLIB 就不存在这个问题,因为 CGLIB 是通过生成目标对象子类来实现的,代理对象无论是赋值给接口还是实现类,两者都是代理对象的父类,使用 CGLIB 动态代理可以避免类型转换导致的错误
5.3 使用JDK动态代理时获取不到实现类方法上的注解
我们在 UserServiceImpl 类的 save 方法上添加一个自定义的 Log 注解
import java.lang.annotation.*;@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {/*** 操作名称*/String name() default "";}
同时对方法上带有 Log 注解的类进行增强
import cn.edu.scau.annotation.Log;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;/*** 切面类*/
@Component
@Aspect
public class SystemAspect {@Pointcut("@annotation(cn.edu.scau.annotation.Log)")private void pointcut() {}@Around("pointcut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {// 通过解析 session 或 token 获取用户名// 获取被增强类和方法的信息Signature signature = joinPoint.getSignature();MethodSignature methodSignature = (MethodSignature) signature;// 获取被增强的方法对象Method method = methodSignature.getMethod();// 从方法中解析注解if (method != null) {Log logAnnotation = method.getAnnotation(Log.class);System.out.println("模块名称:" + logAnnotation.name());}// 方法名字String name = null;if (method != null) {name = method.getName();}System.out.println("方法名:" + name);// 通过工具类获取Request对象RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;HttpServletRequest request = null;if (servletRequestAttributes != null) {request = servletRequestAttributes.getRequest();}// 访问的URLString url = null;if (request != null) {url = request.getRequestURI();}System.out.println("访问的URL:" + url);// 请求方式String methodName = null;if (request != null) {methodName = request.getMethod();}System.out.println("请求方式:" + methodName);// 登录IPString ipAddress = null;if (request != null) {ipAddress = getIpAddress(request);}System.out.println("登录IP:" + ipAddress);// 操作时间System.out.println("操作时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));// 将操作日志保存到数据库return joinPoint.proceed();}/*** 获取 IP 地址** @param request HttpServletRequest* @return String*/public String getIpAddress(HttpServletRequest request) {String ip = request.getHeader("x-forwarded-for");if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("Proxy-Client-IP");}if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("WL-Proxy-Client-IP");}if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;}}
以 debug 的方式启动项目,在切面中发现获取到的注解数量为 0,获取不到任何注解,因为 UserService 接口上并没有注解(当然,我们也可以在接口上添加注解,但是这不符合我们的编码习惯)
method.getAnnotations()
改用 CGLIB 动态代理后,再次以 debug 的方式启动项目,在切面中发现获取到的注解数量为 2,获取到了方法上的所有注解
6. 补充:调试时this指针指的是哪个对象
哪个对象调用了this所在的代码块,this 指针就指向哪个对象
如果我们在打断点调试时,输出 this,会发现 this 指针指向的不是代理对象,为什么呢
因为代理对象只是对目标对象进行了增强,真正调用目标方法还是需要目标对象
以 CGLIB 动态代理为例,代理对象会保留目标对象的引用,代理对象的简单结构如下
我们记住,哪个对象调用了 this 所在的代码块,this 指针就指向哪个对象
在进行断点调试时,this 所在的代码块指向的对象就是目标对象,而不是代理对象
7. 总结
- 在 Spring Framework 工程中 AOP 默认使用 JDK 动态代理
- 为了解决使用 JDK 动态代理可能导致的类型转化异常问题,SpringBoot 默认使用 CGLIB 动态代理
- 在 SpringBoot 中,如果需要默认使用 JDK 动态代理,可以通过配置项
spring.aop.proxy-target-class=false
来进行修改,在启动类上通过 @EnableAspectJAutoProxy(proxyTargetClass = false) 配置已无效
8. 参考资料
- 小米二面:为什么SpringBoot使用cglib作为默认动态代理 ?AOP使用jdk动态代理会有什么问题 ?_哔哩哔哩_bilibili
- 什么鬼?弃用JDK动态代理,Spring5 默认使用 CGLIB 了?|jdk|aop|boot|proxy|spring|framework_网易订阅 (163.com)
- 腾讯二面:Spring AOP底层实现原理是怎么样的?JDK和CGlib动态代理有什么区别 ?_哔哩哔哩_bilibili
相关文章:

详细分析Spring的动态代理机制
文章目录 1. JDK动态代理和CGLIB动态代理的区别1.1 适用范围1.2 生成的代理类1.3 调用方式 2. 问题引入3. 创建工程验证 Spring 默认采用的动态代理机制3.1 引入 Maven 依赖3.2 UserController.java3.3 UserService.java3.4 UserServiceImpl.java(save方法添加了Tra…...
Redis数据类型,使用场景,事物及分布式锁
文章目录 关于Redis1.常用数据类型1.字符串(String)2.哈希(Hash)3.列表(List)4.集合(Set)5.有序集合(Sorted Set)6.位图(Bitmap)7.超日…...

目标检测系列(一)什么是目标检测
目录 一、相关名词解释 二、目标检测算法 三、目标检测模型 四、目标检测应用 五、目标检测数据集 六、目标检测常用标注工具 一、相关名词解释 关于图像识别的计算机视觉四大类任务: 分类(Classification):解决“是什么&…...

STM32CubeIDE | 使用HAL库的ADC读取内部传感器温度
1、cubemx配置 1.1、系统配置 1.2、GPIO配置 PB2设置为“GPIO_Output” user label设置为“LED” 1.3、串口配置 模式选择为“Asynchronous”,其他默认 1.4、时钟树配置 全部保持默认 2、ADC配置 通道选择“Temperature Sensor Channel”,其他默认 …...

茶思屋直播|TinyEngine+AI:聚焦主航道,在实践中探索低代码技术黑土地
低代码引擎使能开发者定制低代码平台。它是低代码平台的底座,提供可视化搭建页面等基础能力,既可以通过线上搭配组合,也可以通过cli创建个人工程进行二次开发,实时定制出自己的低代码平台。适用于多场景的低代码平台开发ÿ…...

Ansible流程控制-条件_循环_错误处理_包含导入_块异常处理
文章目录 Ansible流程控制介绍1. 条件判断2. 循环3. 循环控制4. 错误处理5. 包含和导入6. 块和异常处理7. 角色的流程控制*include_tasks、import_tasks_include之间的区别 条件语句再细说且、或、非、是模糊条件when指令的详细使用方法 循环语句再细说如何使用使用item变量结合…...
Mybatis-为什么使用Mybatis,它存在哪些优点和缺点?
优点: 基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL单独写,解除SQL与程序代码的耦合,便于统⼀管理。与 JDBC 相比,减少了 50%以上的代码量,消除…...

银河麒麟高级服务器操作系统V10外接硬盘挂载指南
银河麒麟高级服务器操作系统V10外接硬盘挂载指南 1、临时挂载外接硬盘2、永久挂载外接硬盘3、总结 💖The Begin💖点点关注,收藏不迷路💖 在使用银河麒麟高级服务器操作系统V10时,您可能希望将外接硬盘(如sd…...

免费制作证件照的小程序源码
1、效果展示 证件照制作,证件照免费制作,证件照调用api源码,解析代码。证件照制作小程序包,可以下载程序包,最初级版本免费下载。以上是高级版本。如果你有开发能力的话可以自己写前端,然后以下调用以下api…...
面经宝典【1】-拼多多
这个专题我主要想的是吧这些面经题目给整合起来,自己时不时可以看看然后回答回答,然后再根据一些面经去查漏补缺,具体的答案我都记录在在自己的语雀笔记当中,如果想要的可以私聊我,当然这些答案一般在网上都能找到。 今…...
AI画图用到的网站与资源
1、画图 爱灯泡 midjourney官网 可以使用stable-diffusion的网站 2、素材 花瓣 figma 3、...

C++——多线程编程(从入门到放弃)
进程:运行中的程序 线程:进程中的进程 线程的最大数量取决于CPU的核心数 一、将两个函数添加到不同线程中 demo:两个函数test01()和test02(),实现将用户输入的参数进行打印输出1000次 将这两个函数均放到独立的线程t1和t2中&…...

江协科技STM32学习- P14 示例程序(定时器定时中断和定时器外部时钟)
🚀write in front🚀 🔎大家好,我是黄桃罐头,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流 🎁欢迎各位→点赞👍 收藏⭐️ 留言📝…...

2024年CSP-J认证 CCF信息学奥赛C++ 中小学初级组 第一轮真题-阅读程序题解析
2024 CCF认证第一轮(CSP-J)真题 二、阅读程序题 (程序输入不超过数组或字符串定义的范围,判断题正确填√错误填X;除特殊说明外,判断题 1.5分,选择题3分,共计40 分) 第一题 01 #include <iostream>…...
Hive ROW_NUMBER() 简介
在 Apache Hive 中,ROW_NUMBER() 是一个窗口函数,常用于为查询结果中的每一行生成唯一的行号。它在 SQL 查询结果集中按照指定的排序规则对每一行进行编号。ROW_NUMBER() 的实现依赖于 Hive 的分布式执行框架和排序机制。 为了理解 ROW_NUMBER() 的底层实…...
java是干什么的
Java 是一种广泛使用的编程语言,主要用于以下几个方面: Web 开发:Java 可以用于创建动态网页和 Web 应用程序,常见的框架有 Spring 和 JavaServer Faces(JSF)。 企业级应用:Java 被广泛应用于…...

AI与量化投资人才培养计划-连接职场 助力走在金融行业前沿
AI与量化投资人才培养计划-连接职场 助力走在金融行业前沿 人工智能(AI)的快速发展,量化投资已逐渐成为金融行业的新趋势,对专业人才的需求日益迫切。本文将深入探讨一项针对AI与量化投资的人才培养计划,旨在为金融专业…...

《CUDA编程》2.CUDA中的线程组织
0 来自GPU的hello world 在visua studio 中新建一个CUDA runtime项目,然后把kernel.cu中的代码删掉,输入以下代码 #include"cuda_runtime.h" #include"device_launch_parameters.h"#include<stdio.h>__global__ void hello_…...
学习篇 | Dockerized GitLab 安装使用(简单实操版)
1. 详细步骤 1.1 安装启动 postgresql 服务 docker pull sameersbn/postgresql:14-20230628docker run --name gitlab-postgresql -d \--env DB_NAMEgitlabhq_production \--env DB_USERgitlab --env DB_PASSpassword \--env DB_EXTENSIONpg_trgm,btree_gist \--volume /srv/…...

Linux服务器磁盘扩容
文章目录 扩容挂载 扩容 [rootserver8 ~]# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT sr0 11:0 1 1024M 0 rom vda 252:0 0 1T 0 disk ├─vda1 252:1 0 1G 0 par…...

多云管理“拦路虎”:深入解析网络互联、身份同步与成本可视化的技术复杂度
一、引言:多云环境的技术复杂性本质 企业采用多云策略已从技术选型升维至生存刚需。当业务系统分散部署在多个云平台时,基础设施的技术债呈现指数级积累。网络连接、身份认证、成本管理这三大核心挑战相互嵌套:跨云网络构建数据…...

TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...

Spark 之 入门讲解详细版(1)
1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室(Algorithms, Machines, and People Lab)开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目,8个月后成为Apache顶级项目,速度之快足见过人之处&…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...

智能仓储的未来:自动化、AI与数据分析如何重塑物流中心
当仓库学会“思考”,物流的终极形态正在诞生 想象这样的场景: 凌晨3点,某物流中心灯火通明却空无一人。AGV机器人集群根据实时订单动态规划路径;AI视觉系统在0.1秒内扫描包裹信息;数字孪生平台正模拟次日峰值流量压力…...
Hive 存储格式深度解析:从 TextFile 到 ORC,如何选对数据存储方案?
在大数据处理领域,Hive 作为 Hadoop 生态中重要的数据仓库工具,其存储格式的选择直接影响数据存储成本、查询效率和计算资源消耗。面对 TextFile、SequenceFile、Parquet、RCFile、ORC 等多种存储格式,很多开发者常常陷入选择困境。本文将从底…...

[大语言模型]在个人电脑上部署ollama 并进行管理,最后配置AI程序开发助手.
ollama官网: 下载 https://ollama.com/ 安装 查看可以使用的模型 https://ollama.com/search 例如 https://ollama.com/library/deepseek-r1/tags # deepseek-r1:7bollama pull deepseek-r1:7b改token数量为409622 16384 ollama命令说明 ollama serve #:…...
【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案
目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后,迭代器会失效,因为顺序迭代器在内存中是连续存储的,元素删除后,后续元素会前移。 但一些场景中,我们又需要在执行删除操作…...

Proxmox Mail Gateway安装指南:从零开始配置高效邮件过滤系统
💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「storms…...

uniapp 小程序 学习(一)
利用Hbuilder 创建项目 运行到内置浏览器看效果 下载微信小程序 安装到Hbuilder 下载地址 :开发者工具默认安装 设置服务端口号 在Hbuilder中设置微信小程序 配置 找到运行设置,将微信开发者工具放入到Hbuilder中, 打开后出现 如下 bug 解…...