深度理解 JAVA 动态代理

news2024/11/15 17:43:41

在这里插入图片描述
本文篇幅比较长,在确定您是否需要仔细阅读本文前,可以先思考一下下面几个问题:

  1. 动态代理是什么?
  2. 如何实现动态代理?
  3. 所有类都能实现动态代理吗?
  4. 非目标方法是否会被代理?
  5. 为什么 JDK 实现动态代理必须要求被代理类实现接口?
  6. 为什么 CGLib 实现动态代理要求被代理类为非 final类?

为了理解动态代理,我们需要先了解代理模式是怎么回事。

代理模式

代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。可以将代理模式理解为生活中常见的中介,UML 图如下。

在这里插入图片描述

public interface Subject {
    public void doOperation();
}
public class RealSubject implements Subject {
    @Override
    public void doOperation() {
        System.out.println("RealSubject doOperation...");
    }
}
public class Proxy implements Subject {
    private Subject subject;
    public Proxy(Subject subject) {
        this.subject = subject;
    }
    @Override
    public void doOperation() {
        System.out.println("Proxy before RealSubject doOperation...");
        subject.doOperation();
        System.out.println("Proxy after RealSubject doOperation...");
    }
}
public class Client {
    public static void main(String[] args) {
        Subject subject = new Proxy(new RealSubject());
        subject.doOperation();
    }
}

上述代码输出如下:

Proxy before RealSubject doOperation...
RealSubject doOperation...
Proxy after RealSubject doOperation...

通过代理模式,我们可以做到在不修改目标对象的前提下,对目标对象进行功能扩展。但是上述静态代理的不足之处在于需要事先写好相应的代理类,而且在接口发生变化时需要对被代理类及代理类进行修改,对此 Java 引入了动态代理的概念。

动态代理

与静态代理需要事先构建不同,动态代理是动态地在内存中生成的。一般而言,动态代理可以由 JDK 动态代理及CGLib 动态代理实现。

JDK动态代理

使用JDK动态代理只需要3步即可完成:

  1. 创建被代理的对象 RealSubject
  2. 创建被代理对象的处理对象,持有目标(被代理)对象 JDKInvocationHandler
  3. 使用Proxy的静态方法 newProxyInstance 创建代理对象
public class JDKInvocationHandler implements java.lang.reflect.InvocationHandler {
    private Subject subject;
    public JDKInvocationHandler(Subject subject) {
        this.subject = subject;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("JDKProxy before RealSubject doOperation...");
        Object ret = method.invoke(subject, args);
        System.out.println("JDKProxy after RealSubject doOperation...");
        return ret;
    }
}
public class Client {
    public static void main(String[] args) {
        //创建被代理的对象 realSubject
        RealSubject realSubject = new RealSubject();
        //创建被代理对象的处理对象
        InvocationHandler handler = new JDKInvocationHandler(realSubject);
        //创建代理对象
        Subject proxy = (Subject) Proxy.newProxyInstance(
            RealSubject.class.getClassLoader(),
            RealSubject.class.getInterfaces(),
            handler);
        //执行相应的方法
        proxy.doOperation();
    }
}

上述代码输出如下:

JDKProxy before RealSubject doOperation...
RealSubject doOperation...
JDKProxy after RealSubject doOperation...

其中,InvocationHandler 是 Java 自带的接口,其定义如下:

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

Proxy 静态方法的定义如下:

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)

其中,

  1. loader 为类加载器,出于安全性,要求 loader 对 interfaces 可见,通常使用被代理类的ClassLoader。
  2. interfaces 为被代理对象需要实现的所有接口。
  3. h为方法调用的实际处理者,通过 InvocationHandler 对被代理类进行拓展。
    (ps:类加载器后面会有专门介绍。)

但是,等一下,为什么需要这三个参数呢?是不是任意一个 Java 类都可以动态代理呢?我们不妨深入看看 Proxy类到底做了什么?Proxy.newProxyInstance 方法的主要内容(删除了安全检测等内容)如下:

public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,
                   InvocationHandler h) throws IllegalArgumentException{
    final Class<?>[] intfs = interfaces.clone();
    //根据ClassLoader及接口获取指定的代理类的Class信息
    Class<?> cl = getProxyClass0(loader, intfs);
    try {
        //在Proxy中 constructorParams被硬编码为{InvocationHandler.class};
        //获取代理类的参数为 InvocationHandler 的构造器
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        //生成代理类
        return cons.newInstance(new Object[]{h});
    } catch (XXException e) {
        throw new XXException(e.toString(), e);
    }
}
//从proxyClassCache中获取代理类的Class信息,如果没有则根据classLoader、ingerfaces生成加载并缓存
private static Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces) {
        return proxyClassCache.get(loader, interfaces);
}

动态编译生成代理类的代码如下:

//通过反射动态生成、编译代理类,得到代理类的字节码数据
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                                    proxyName,interfaces, accessFlags);
try {
    //动态加载代理类
    return defineClass0(loader,proxyName,proxyClassFile,0,proxyClassFile.length);
} catch (ClassFormatError e) {
    throw new IllegalArgumentException(e.toString());
}

至此,我们知道了 Proxy 的运行逻辑,为了进一步了解动态生成的代理类的内容,我们不妨输出并使用反编译工具查看动态生成的代理类的信息:

通过在程序运行时设置:

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

即可将生成的代理类保存在项目根目录下,路径为:com/sun/proxy/$ProxyNum.class

使用Java Decompiler工具查看,结果如下:

package com.sun.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements Subject {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler paramInvocationHandler) {
        super(paramInvocationHandler);
    }

    @Override
    public final boolean equals(Object paramObject) {
        try {
            return ((Boolean) this.h.invoke(this, m1, 
                            new Object[] { paramObject })).booleanValue();
        } catch (Error | RuntimeException localError) {
            throw localError;
        } catch (Throwable localThrowable) {
            throw new UndeclaredThrowableException(localThrowable);
        }
    }

    @Override
    public final String toString() {
        try {
            return (String) this.h.invoke(this, m2, null);
        } catch (Error | RuntimeException localError) {
            throw localError;
        } catch (Throwable localThrowable) {
            throw new UndeclaredThrowableException(localThrowable);
        }
    }

    @Override
    public final void doOperation() {
        try {
            this.h.invoke(this, m3, null);
            return;
        } catch (Error | RuntimeException localError) {
            throw localError;
        } catch (Throwable localThrowable) {
            throw new UndeclaredThrowableException(localThrowable);
        }
    }

    @Override
    public final int hashCode() {
        try {
            return ((Integer) this.h.invoke(this, m0, null)).intValue();
        } catch (Error | RuntimeException localError) {
            throw localError;
        } catch (Throwable localThrowable) {
            throw new UndeclaredThrowableException(localThrowable);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals",
                    new Class[] { Class.forName("java.lang.Object") });
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m3 = Class.forName("com.iqts.proxy.Subject").getMethod("doOperation", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            return;
        } catch (NoSuchMethodException localNoSuchMethodException) {
            throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
        } catch (ClassNotFoundException localClassNotFoundException) {
            throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
        }
    }
}

到这里可以大概推测出,JDK 动态代理是通过继承 Proxy 类,实现被代理类的所有接口生成动态代理类。这也解释了采用 JDK 动态代理时为什么只能使用接口引用指向代理,而不能使用被代理的具体类引用指向代理。

Subject proxy = (Subject) Proxy.newProxyInstance(RealSubject.class.getClassLoader(),
                RealSubject.class.getInterfaces(), handler);//ok

//java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to RealSubject
RealSubject proxy = (RealSubject) Proxy.newProxyInstance(
                RealSubject.class.getClassLoader(),
                RealSubject.class.getInterfaces(), handler);

此外,除了实现了被代理所有接口中的方法外,JDK 动态代理还重写了 Object 类中的 hashCode、equals、toString 三个方法。

为了更好地看出类之间的依赖关系,上述代码可以简化如下:

public interface Subject {
    public void doOperation();
}
public class RealSubject implements Subject {
    @Override
    public void doOperation() {
        System.out.println("RealSubject doOperation...");
    }
}
public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
public class Proxy implements java.io.Serializable {
    protected InvocationHandler h;
    protected Proxy(InvocationHandler h) {
        this.h = h;
    }
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h){
        Class<?> cl = getProxyClass0(loader, intfs);
        final Constructor<?> cons = cl.getConstructor(InvocationHandler.class);
        return cons.newInstance(new Object[]{h});
    }
}
public final class $Proxy0 extends Proxy implements Subject {
    private static Method m3;
    public $Proxy0(InvocationHandler paramInvocationHandler) {
        super(paramInvocationHandler);
    }
    @Override
    public final void doOperation() {
        try {
            this.h.invoke(this, m3, null);
        } catch (Error | RuntimeException localError) {
            throw localError;
        } catch (Throwable localThrowable) {
            throw new UndeclaredThrowableException(localThrowable);
        }
    }
}

到这里我们就不难理解,为什么 JDK 实现动态代理必须要求被代理类实现接口,这是由于动态代理动态生成的代理类需要继承 Proxy 类,而 Java 中只能单继承的限制使得被代理类必须实现接口才能实现动态代理。

JDK 能够很好地实现动态代理,但是如果被代理的类没有实现接口就无法实现动态代理,这时候我们就需要使用第三方工具来帮忙了。

CGLib

CGLib 是一个强大的高性能的代码生成包,它可以在运行期扩展 Java 类及实现Java接口、提供方法的拦截,因此被众多 AOP 框架使用。CGLib 包的底层是通过使用字节码处理框架 ASM 来转换字节码并生成新的类。

  1. 使用 CGLib 实现动态代理也很简单,首先

  2. 创建Enhancer对象

  3. 设置被代理类

  4. 回调对象(回调类实现 MethodInterceptor或InvocationHandler接口)

  5. 创建并设置回调对象

  6. 创建代理对象

public class CGLib {
    public static void main(String[] args) {
        // 创建Enhancer对象
        Enhancer enhancer = new Enhancer();
        // 设置被代理类
        enhancer.setSuperclass(ConcreteSubject.class);
        
        // 创建回调对象
        //实现 MethodInterceptor 接口
        Callback callback = new CGLibMethodInterceptor();
        //实现 InvocationHandler 接口
        Callback callback = new CGLibInvocationHandler(new ConcreteSubject());
        
        // 设置回调对象
        enhancer.setCallback(callback);
        // 创建代理对象
        ConcreteSubject subject = (ConcreteSubject) enhancer.create();
        subject.doOperation();
    }
}
//回调对象 实现 MethodInterceptor
public class CGLibMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj,Method method,Object[] args,MethodProxy proxy) 
        throws Throwable {
        Object ret = null;
        System.out.println("CGLib before ConcreteSubject doOperation...");
        ret = proxy.invokeSuper(obj, args);
        System.out.println("CGLib after ConcreteSubject doOperation...");
        return ret;
    }
}
//回调对象 实现 InvocationHandler
public class CGLibInvocationHandler implements InvocationHandler {
    
        private Object realSubject;

        public CGLibInvocationHandler(Object realSubject) {
            super();
            this.realSubject = realSubject;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) 
            throws Throwable {
            System.out.println("CGLib before ConcreteSubject doOperation...");
            Object ret = method.invoke(realSubject, args);
            System.out.println("CGLib after ConcreteSubject doOperation...");
            return ret;
        }
    }
//被代理类
public class ConcreteSubject {
    public void doOperation() {
        System.out.println("ConcreteSubject doOperation...");
    }
}

输出如下:

CGLib before ConcreteSubject doOperation...
ConcreteSubject doOperation...
CGLib after ConcreteSubject doOperation...

为了进一步理解 CGLib 动态代理的生成机制,我们不妨将生成的动态代理类保存到文件中,可以通过设置:

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "E://CGLib//proxy//");

来导出动态代理类,使用Java Decompiler工具查看,结果如下:

public class ConcreteSubject$$EnhancerByCGLib$$7e8b8caf
    extends ConcreteSubject implements Factory {
    
    private MethodInterceptor CGLib$CALLBACK_0;

    final void CGLib$doOperation$0() {
        super.doOperation();
    }
    
    @Override
    public final void doOperation(){
        MethodInterceptor tmp4_1 = this.CGLib$CALLBACK_0;
        if (tmp4_1 == null){
          tmp4_1;
          CGLib$BIND_CALLBACKS(this);
        }
        if (this.CGLib$CALLBACK_0 != null) {
          return;
        }
        super.doOperation();
    }
    @Override
    public final boolean equals(Object paramObject)...
    @Override
    public final String toString()...
    @Override
    public final int hashCode()...
    @Override
    public final Object clone()...
}

通过反编译得到代码可以看出,CGLib 是通过继承被代理类 ConcreteSubject 实现动态代理的,这也就要求被代理的类不能是 final 类。此外,与 JDK 动态代理相比,CGLib 不仅重写了 Object 类的 hashCode、equals、toString方法,还重写了 clone 方法。

此外,值得注意的是创建回调对象时,采用实现 MethodInterceptor 接口与 InvocationHandler 接口这两种方式除了是否需要额外创建被代理对象以及方法调用的差异外,还有一个小细节:采用实现 InvocationHandler 接口的方式生成的代理类在调用的方法内部如果还调用该代理类的其他成员方法时,会对被调用的其他方法进行代理,而采用 MethodInterceptor 接口方式不会。(ps: JDK 动态代理也不会对非目标方法进行代理。)

为被代理类添加两个方法:

public class ConcreteSubject {

    public static void fn() {
        System.out.println("fn");
    }

    public void doOperation() {
        System.out.println("ConcreteSubject doOperation...");
        other();// InvocationHandler 同时会代理非目标方法
        fn();//静态方法不代理
    }

    public void other() {
        System.out.println("ConcreteSubject other...");
    }
}

相应的输出如下:

invocationHandler
CGLib before RealSubject doOperation...
ConcreteSubject doOperation...
ConcreteSubject other...
fn
CGLib after RealSubject doOperation...
==================
methodInterceptor
Cglib before ConcreteSubject doOperation...
ConcreteSubject doOperation...
Cglib before ConcreteSubject doOperation...
ConcreteSubject other...
Cglib after ConcreteSubject doOperation...
fn
Cglib after ConcreteSubject doOperation...

CGLib 生成动态代理的两种方式的区别总结如下:

项目MethodInterceptorInvocationHandler
是否依赖被代理对象实例不依赖依赖
目标方法执行方式method.invokeSuper(proxy, args)method.invoke(realSubject, args);
非目标方法是否进行代理不代理代理

JDK 与 CGLib 动态代理的区别

结合 JDK 动态代理的实现,可以得出下列区别:

项目JDKCGLib
被代理对象的要求必须实现接口(可为 final 类)非final类
代理类生成方式继承 Proxy,实现被代理类的所有接口继承被代理类,实现 Factory 接口
非目标方法是否进行代理不进行代理可通过 InvocationHandler 进行代理

至此,动态代理两种实现方式及其原理已经介绍完毕,在理解了相关原理后,我们完全可以通过反射及Java动态编译技术实现动态代理。

既然JDK动态代理要求被代理类必须实现接口,而CGLib要求被代理类不能是final类,那么能不能为没有实现接口的final类进行动态代理呢?

答案是不能,但是可以通过反射来实现类似动态代理的功能,只需要将Proxy进行改造即可,如:

public class MyProxy {
    protected InvocationHandler invocationHandler;
    public MyProxy(InvocationHandler invocationHandler) {
        super();
        this.invocationHandler = invocationHandler;
    }
    public static Object newProxyInstance(ClassLoader loader, 
                                          Class<?> clz, InvocationHandler h){
        ...
    }
    public Object invoke(String methodName, Object... args){
        Class<?>[] parameterTypes = getParameterTypes(args);
        Method method = this.getClass().getDeclaredMethod(methodName,parameterTypes);
        return method.invoke(this, args);
    }
}

使用时通过 MyProxy 的 invoke 方法实现被代理对象方法的调用:

MyProxy proxy = (MyProxy) MyProxy.newProxyInstance(
                                        FinalSubject.class.getClassLoader(),
                                        FinalSubject.class,
                                        new FinalInvocationHandler(new FinalSubject()));
proxy.invoke("doOperation");
proxy.invoke("other",args);

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

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

相关文章

平安养老险广东分公司积极开展“7·8全国保险公众宣传日”系列活动

2023年是全面贯彻落实党的二十大精神的开局之年&#xff0c;是实施“十四五”规划承上启下的关键之年。在国家金融监督管理总局指导、中国保险行业协会组织下&#xff0c;平安养老保险股份有限公司广东分公司&#xff08;以下简称“平安养老险广东分公司”&#xff09;以“78全…

ETHERNET/IP转MODBUS-RTU协议网关

远创智控YC-EIP-RTU是自主研发的一款ETHERNET/IP从站功能的通讯网关。该产品主要功能是将各种MODBUS-RTU设备接入到ETHERNET/IP网络中。 远创智控YC-EIP-RTU连接到ETHERNET/IP总线中做为从站使用&#xff0c;连接到MODBUS-RTU总线中做为主站或从站使用。 2.ETHERNET/IP转MODBU…

IDEA中配置Java反编译工具javap -c

IDEA中配置Java反编译工具javap -c 一、前置条件二、新建外部工具三、使用方式 欢迎访问我的个人博客&#xff1a;https://wk-blog.vip 一、前置条件 确保 IDEA 已经开启了编译 javac 。默认 IDEA 是开启的。 二、新建外部工具 首先进入 Settings ---> Tools ---> Ext…

自动化测试报告样式HTMLTestRunner、BeautifulReport、HTMLReport、Allure你喜欢哪个?

自动化测试报告样式HTMLTestRunner、BeautifulReport、HTMLReport、Allure你喜欢哪个&#xff1f; 1 框架设计(准备工作)1.1 简易框架图1.2 common/reportOut.py1.3 report1.4 testcase/test_baidu.py1.5 mian.py 2 HTMLTestRunner2.1 下载使用2.2 reportOut.py设计2.3 报告样式…

十一、框架与大数据模型

frameset标签 1、frameset标签 这种结构&#xff0c;基本上被淘汰 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title> </head> <frameset cols"25%,75%">&…

统计每列中不同值出现的次数.apply(pd.value_counts)

在Python 的pandas.DataFrame中有一种操作&#xff0c;它可以大大减轻我们的工作量&#xff0c;方便我们更快地进行数据分析&#xff0c;加快处理工作的效率。这就是 .apply(pd.value_counts) pandas 的强大&#xff0c;越使用&#xff0c;也就越爱了。现在就来夸夸它的作用啦…

统计学习导论(ISLR) 第八章树模型课后习题

统计学习导论(ISLR) 第八章树模型课后习题 🌸个人主页:JOJO数据科学📝个人介绍:统计学top3高校统计学硕士在读💌如果文章对你有帮助,欢迎✌关注、👍点赞、✌收藏、👍订阅专栏✨本文收录于【R语言数据科学】本系列主要介绍R语言在数据科学领域的应用包括: R语言…

二叉树 — 返回最大的二叉搜索子树大小

题目&#xff1a; 给定一棵二叉树的head节点&#xff0c;返回这颗二叉树中最大的二叉搜索子树的大小。 一颗二叉树来讲&#xff0c;可能整棵树不是搜索二叉树&#xff0c;但子树是一颗搜索二叉树。如下图所示&#xff0c;这时要返回这颗子搜索二叉树的最大节点个数。下图中&…

【node报错】cannot be loaded because running scripts is disabled on this system.

了解意思&#xff1a; cannot be loaded because running scripts is disabled on this system. 因为在该系统上禁用了运行脚本&#xff1b; 这个错误是由于你的系统执行策略&#xff08;Execution Policies&#xff09;禁止运行脚本导致的。Windows 系统默认情况下禁止运行…

ChatGPT前身GPT的论文-译文

写在前面 论文 Improving Language Understanding by Generative Pre-Training 地址 https://www.cs.ubc.ca/~amuham01/LING530/papers/radford2018improving.pdf ChatGPT火了&#xff0c;改论文作为ChatGPT的前身&#xff0c;可以从这里看到ChatGPT的原始影子。 摘要 自然语言…

制造业怎么应用大数据?_光点科技

随着信息技术的迅猛发展&#xff0c;大数据正逐渐成为各行各业的重要资源和工具。在制造业中&#xff0c;大数据的应用也逐渐得到了广泛关注。 制造业如何应用大数据&#xff1f; 首先&#xff0c;数据采集是制造业应用大数据的重要一环。制造业的生产过程中涉及到大量的数据&a…

二叉树OJ题:LeetCode--101.对称二叉树

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家解读一下LeetCode中第144道二叉树OJ题&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; 数据结构与算法专栏&#xff1a;数据结构与算法 个 人…

49天精通Java,第0天,编程语言类型有哪些?我心中的TOP1编程语言,什么是java跨平台性?

目录 一、常见的编程语言类型1、机器语言2、汇编语言3、高级语言 二、计算机编程语言三、跨平台性1、跨平台的优势包括&#xff1a;2、实现跨平台的方式包括&#xff1a; 四、Java的跨平台性五、java运行时和虚拟机六、Java内存管理和Java垃圾回收1、Java内存管理2、Java垃圾回…

基于matlab使用虚幻引擎模拟开发视觉SLAM算法(附源码)

一、前言 本示例展示了如何使用从虚幻引擎模拟环境中获取的图像数据开发可视化同步定位和映射&#xff08;SLAM&#xff09;算法。 视觉SLAM是计算摄像机相对于周围环境的位置和方向&#xff0c;同时映射环境的过程。开发可视化 SLAM 算法并评估其在不同条件下的性能是一项具…

抖音短视频矩阵管理系统源码开发部署(开源定制)

一、什么是短视频矩阵管理系统&#xff1f; 短视频矩阵管理系统是专门为企业号商家、普通号商家提供帐号运营从流量 到转化成交的一站式服务方案&#xff0c;具体包含&#xff1a;点赞关注评论主动私信 &#xff0c;评论区回复&#xff0c;自动潜客户挖掘&#xff0c;矩阵号营销…

今日小课堂:会议录音转文字怎么弄的

小君&#xff1a;嘿&#xff0c;你知道怎么把录音转成文字吗&#xff1f;我今天录了一个重要的会议&#xff0c;但是时间有限&#xff0c;需要快速整理笔记。 小辉&#xff1a;当然&#xff01;你可以使用一些工具来将录音转换为文字。最简单的方法就是利用语音转文字的应用。…

C语言 while 和 do while 区别

先简单介绍一下C语言中的while和do while语句 while循环 语法 C 语言中 while 循环的语法&#xff1a; while(condition) {statement(s); } 在这里&#xff0c;statement(s) 可以是一个单独的语句&#xff0c;也可以是几个语句组成的代码块。 condition 可以是任意的表达…

Kubernetes 启动Pod的方法-Pod的调度算法-Pod间的通信-k8s的控制器-Pod资源控制-发布Service服务

目录 Pod 参考文档&#xff1a;Pod | Kubernetes Pod配置文件&#xff1a;simple-pod.yaml 对master进行如下操作 Pod的状态有&#xff1a; 参考文档&#xff1a;(70条消息) Pod生命周期中的状态解释_pod状态_闹玩儿扣眼珠子的博客-CSDN博客 进入Pod内的nginx容器&#…

【Ubuntu】系统U盘变为普通U盘

如果您在 Ubuntu 系统上没有磁盘工具可用&#xff0c;您可以尝试使用命令行工具来格式化系统 U 盘。请按照以下步骤进行操作&#xff1a; 打开终端&#xff1a;在 Ubuntu 桌面上&#xff0c;按下 Ctrl Alt T 快捷键&#xff0c;或者在应用程序菜单中搜索并打开 "终端&qu…

PPT文件,使用python删除链接

文章目录 一、需求二、处理方式三、代码实现 一、需求 如下图所示&#xff0c;将PPT文件中的链接进行删除&#xff0c;且不保留链接名。 原始文件&#xff1a; 处理后文件&#xff1a; 二、处理方式 使用python 的pptx模块进行处理&#xff0c;读取文字块&#xff0c;然后…