深入探索JDK动态代理:从入门到原理的全面解析

news2024/9/29 5:28:12

在这里插入图片描述

文章目录

    • 基本概念
    • 入门案例
      • 实现JDK动态代理的步骤
      • 入门实操
      • 拓展--动态生成代理类的几种方法
        • 方式一:通过getProxyClass方法获取代理实例
        • 方式二:通过newProxyInstance方法获取代理实例(常用!)
        • 方式三:通过Lambda表达式简化实现
    • 生成并查看JDK动态代理类的的字节码
      • JDK8及之前版本
      • Java 9及之后
      • 如何查看?
      • 如何确定版本?
      • 为什么这样设置就可以生成代理类字节码到本地?
    • 底层原理解析
      • 代理类的创建
        • `Proxy.newProxyInstance`方法分析
      • 代理类$Proxy是什么样子
      • 注意的地方

在我们日常开发中,代理模式是一种非常常见也非常有用的设计模式。它能够为其他对象提供一种代理以控制对这个对象的访问( 说人话就是,增强我们原有对象的功能,在原有对象基础上增加一些我们自己要的操作,比如:事务处理、记录日志等)。在 Java 中,代理可以是 静态的也可以是 动态的。接下来我将深入探索 JDK 中的动态代理——会从基础概念讲起,通过入门示例到探究其实现原理,助你彻底理解动态代理。

基本概念

JDK动态代理是一种在运行时创建代理对象的技术,它允许我们在不修改目标类代码的情况下,增强目标对象的功能。这种代理模式主要用于实现AOP(面向切面编程)

入门案例

实现JDK动态代理的步骤

  1. 接口的定义:首先,需要有一个或多个接口来定义代理类的行为。这些接口将被代理类实现,从而确保代理类能够执行目标对象的方法
  2. 创建代理类:使用Proxy类和InvocationHandler接口来创建代理类。Proxy类提供了静态方法newProxyInstance(),它接受三个参数:类加载器、接口数组和一个InvocationHandler实例。通过这个方法,可以在运行时动态地创建代理类的实例
  3. 编写InvocationHandlerInvocationHandler是一个接口,用于处理代理对象的调用。开发者需要实现这个接口,并在其中定义如何处理目标对象的方法调用。这包括了方法的增强处理,如日志记录、事务管理等
  4. 注册InvocationHandler:通过Proxy.newProxyInstance ()方法创建代理对象时,传入的InvocationHandler实例就是用来注册这个处理程序的。这样,当代理对象的方法被调用时,就会触发InvocationHandler中的逻辑
  5. 使用代理对象:最后,可以像使用普通对象一样使用代理对象。由于代理对象实现了目标接口,因此可以调用其方法。当这些方法被调用时,实际上是调用了代理对象内部的增强逻辑
    在这里插入图片描述

入门实操

首先,我们需要定义一个接口,例如 ServiceInterface,其中包含一些方法,比如 execute。同时我们创建一个实现了 ServiceInterface 接口的类,例如 ServiceImpl,并实现这些方法。

// 定义服务接口
public interface ServiceInterface {
    void execute();
}

// 实现服务接口
public class ServiceImpl implements ServiceInterface {
    public void execute() {
        System.out.println("执行服务方法");
    }
}

现在,我们将使用动态代理来创建代理对象。我们需要实现一个 InvocationHandler 接口,并重写它的 invoke 方法。

// 实现 InvocationHandler
public class MyInvocationHandler implements InvocationHandler {
    private Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("方法执行前的代理操作...");
        // 通过反射调用目标方法
        Object result = method.invoke(target, args);
        System.out.println("方法执行后的代理操作...");
        return result;
    }
}

现在,我们可以编写一个测试类来验证动态代理的使用,这个例子中,我们通过动态代理实现了一个代理对象,当调用代理对象的方法时,会先执行 invoke 方法中的前置处理逻辑,然后再调用目标对象的方法,最后执行后置处理逻辑。

  • 其中Proxy.newProxyInstance动态生成代理并使用

  • 注意这行代码System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); 是生成动态代理类的字节码的,接下来我们讲讲有几种方式生成!

// 动态生成代理并使用
public class ProxyTest {
    public static void main(String[] args) {
		System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

        ServiceInterface service = new ServiceImpl();
        InvocationHandler handler = new MyInvocationHandler(service);

        ServiceInterface proxyInstance = (ServiceInterface) Proxy.newProxyInstance(
                ServiceInterface.class.getClassLoader(),// 指定类加载器
                new Class<?>[] {ServiceInterface.class},// 指定要代理的接口
                handler);// 指定InvocationHandler

        // 执行的是代理的方法
        proxyInstance.execute();
    }
}

拓展–动态生成代理类的几种方法

方式一:通过getProxyClass方法获取代理实例
  1. 根据类加载器和接口数组获取代理类的Class对象
  2. 过Class对象的构造器创建一个实例(代理类的实例)
  3. 将代理实例强转成目标接口ServiceInterface(因为代理类实现了目标接口,所以可以强转)。
  4. 最后使用代理进行方法调用。
@Test
public void test1() throws Exception {
    ServiceInterface service = new ServiceImpl();
    // 根据类加载器和接口数组获取代理类的Class对象
    Class<?> proxyClass = Proxy.getProxyClass(ServiceInterface.class.getClassLoader(), ServiceInterface.class);

    // 通过Class对象的构造器创建一个实例(代理类的实例)
    ServiceInterface serviceInterfaceProxy = (ServiceInterface) proxyClass.getConstructor(InvocationHandler.class)
        .newInstance(new MyInvocationHandler(ServiceInterface));

    // 调用 execute 方法
    String value = serviceInterfaceProxy.execute();

}
方式二:通过newProxyInstance方法获取代理实例(常用!)

也就是我们上面的例子,通过这种方法是最简单的,也是推荐使用的,通过该方法可以直接获取代理对象。

注:其实该方法后台实现实际与上面使用getProxyClass方法的过程一样。

ServiceInterface service = new ServiceImpl();
InvocationHandler handler = new MyInvocationHandler(service);

ServiceInterface proxyInstance = (ServiceInterface) Proxy.newProxyInstance(
        ServiceInterface.class.getClassLoader(),
        new Class<?>[] {ServiceInterface.class},
        handler);

// 执行的是代理的方法
proxyInstance.execute();
方式三:通过Lambda表达式简化实现

其实InvocationHander接口也不用创建一个实现类,可以使用Lambad表达式进行简化的实现,如下代码:

@Test
public void test3() {
    ServiceInterface service = new ServiceImpl();

    ServiceInterface proxyInstance = (ServiceInterface) Proxy.newProxyInstance(
            ServiceInterface.class.getClassLoader(),
            new Class<?>[] {ServiceInterface.class},(proxy, method, args1) ->{
                System.out.println("方法执行前的代理操作...");
                // 通过反射调用目标方法
                Object result = method.invoke(service, args);
                System.out.println("方法执行后的代理操作...");
                return result;
            }
          );
   // 执行的是代理的方法
	proxyInstance.execute();
}

生成并查看JDK动态代理类的的字节码

在标准的Java JDK实现中,生成的动态代理类(字节码)默认是在内存中动态生成并直接加载的,不会写入磁盘成为文件。所以,通常我们无法直接获取到这些字节码文件。

不过,有一种办法可以让JVM生成的代理类字节码被保存到磁盘上,这是通过设置系统属性来实现的。可以在启动Java应用程序时通过命令行设置系统属性

JDK8及之前版本

在Java 8及以下版本,可以在idea启动时这样设置属性:

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

image-20240324163232054
在程序中,可通过以下代码进行设置:

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

Java 9及之后

对于Java 9及之后的版本,由于模块化系统的引入,该系统属性可能不再起作用,相应的系统属性变更为:

-Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true

或者在Java代码中进行设置:

System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");

添加了这个系统属性后,当创建动态代理实例的时候,JVM会将生成的代理类以 .class 文件形式保存在工程的根目录或者 com/sun/proxy 目录下。

如何查看?

为了调试目的,我们要查看或修改这些动态生成的代理类,正常我们还需要反编译工具(比如JD-GUI或JAD)来查看生成的类的源代码,但是我们不用那么麻烦,直接用IDEA打开即可

如何确定版本?

如果还有人不知道如何确定是sun.misc.ProxyGenerator.saveGeneratedFiles还是jdk.proxy.ProxyGenerator.saveGeneratedFiles,可以打开sun.misc.ProxyGenerator类查看,注意这个变量saveGeneratedFiles

 boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));

image-20240324162403033

为什么这样设置就可以生成代理类字节码到本地?

还是在sun.misc.ProxyGenerator这里,搜索查看sun.misc.ProxyGenerator#generateProxyClass(java.lang.String, java.lang.Class<?>[], int)这个方法,可以发现上面设置完了后,我们的saveGeneratedFiles是true,此时就会生成对应的字节码到本地图中那个路径com/sun/poxy

image-20240324162853030

我的项目中生成如下

image-20240324163138825

底层原理解析

JDK动态代理是基于Java的反射机制实现的。在创建代理对象时,JDK会动态生成一个新的类,实现了目标对象所实现的接口,并将代理逻辑写入 invoke 方法中。当调用代理对象的方法时,实际上是调用了 invoke 方法,然后由 invoke 方法根据方法名来调用目标对象的方法。

代理类的创建

Java动态代理的实现是通过 java.lang.reflect.Proxy 类来完成的。当我们调用 Proxy.newProxyInstance 方法时,它会做以下几件事情:

  1. 检查提供的接口是否全部为公共接口。
  2. 确定要使用的类加载器。
  3. 生成代理类的二进制字节码。
  4. 加载这些二进制字节码,定义代理类。
 ServiceInterface proxyInstance = (ServiceInterface) Proxy.newProxyInstance(
                ServiceInterface.class.getClassLoader(),// 指定类加载器
                new Class<?>[] {ServiceInterface.class},// 指定要代理的接口
                handler);// 指定InvocationHandler
   // 执行的是代理的方法
  proxyInstance.execute();

生成的代理类将会继承 Proxy 类,并实现了指定的所有接口方法。在这个过程中,JVM将使用一个叫做 $Proxy0 的类实现了 ServiceInterface 接口(序号可能会根据实际生成的代理数量不同而变化)。这个代理类从 Proxy 继承了很多处理逻辑,同时也将接口方法全部委托给 InvocationHandler 处理。

Proxy.newProxyInstance方法分析
public class Proxy {
    // 获取或创建一个动态代理类的实例的静态方法
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        // 确保提供的InvocationHandler不为null
        Objects.requireNonNull(h);

        // 克隆接口数组,以避免对原数组的潜在修改
        final Class<?>[] intfs = interfaces.clone();
        // 获取系统的安全管理器
        final SecurityManager sm = System.getSecurityManager();
        // 如果有安全管理器,检查创建代理实例的权限
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * 查找或生成指定的代理类。
         */
        // 获取代理类
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * 调用其构造函数并用指定的调用处理器InvocationHandler作为参数。
         */
        try {
            // 如果安全管理器存在,进一步检查权限
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            // 查找代理类的构造函数,这个构造函数预期接收一个InvocationHandler作为参数
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            // 如果代理类不是public的,需要特权访问权限,以便设置构造函数为可访问状态
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            // 使用提供的InvocationHandler实例化代理类
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            // 如果代理对象无法正常创建,抛出内部错误
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            // 如果构造函数调用抛出异常,解包异常并处理
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            // 如果没有找到预期的构造函数,抛出内部错误
            throw new InternalError(e.toString(), e);
        }
    }
    // 其他省略的代码...
}

此方法的执行流程如下:

  1. 参数验证:验证传入的 InvocationHandler h 是否为 null
  2. 安全权限检查(如果存在安全管理器):使用 checkProxyAccess 方法检查是否有创建代理实例的权限。
  3. 代理类的查找或生成:调用 getProxyClass0 方法来获取或生成相应的代理类。
  4. 调用代理类的构造函数创建实例:找到代理类的唯一构造函数(该构造函数以 InvocationHandler 为参数),并判断是否需要通过权限检查调用 setAccessible 来允许访问非公共构造函数。
  5. 创建并返回代理实例:最后,通过反射调用构造器的 newInstance 方法,并将 InvocationHandler h 传递为参数,创建代理对象的实例。

代理类$Proxy是什么样子

那么生成的proxyInstance对象到底是什么,为什么调用它的execute方法会执行MyInvocationHandler的invoke方法呢???

看到下面生成的代理对象的字节码文件,是不是一切都明白你了,原理竟然如此简单!

上面我们已经介绍了如何生成代理类的字节码到本地,打开进行查看

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

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

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

	.....省略toString/hashCode/equals等方法

    static {
        try {
            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.useful.tool.ServiceInterface").getMethod("execute");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

通过该文件可以看出:

  • 代理类继承了Proxy类,其主要目的是为了传递InvocationHandler
  • 代理类实现了被代理的接口ServiceInterface,这也是为什么代理类可以直接强转成接口的原因。
  • 有一个公开的构造函数,参数为指定的InvocationHandler,并将参数传递到父类Proxy中。
  • 每一个实现的方法,都会调用InvocationHandler中的invoke方法,并将代理类本身、Method实例、入参三个参数进行传递。这也是为什么调用代理类中的方法时,总会分派到InvocationHandler中的invoke方法的原因。

注意的地方

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

上面的super也就是我们的Proxy类,super.h也就是Proxy里面的InvocationHandler h变量

protected InvocationHandler h;

image-20240324171335334

不知道细心的小伙伴有没有注意到,为什么我们在debug的时候生成的代理类的变量总是一个h,我之前就一直很奇怪,直到这次深入了解后发现原来如此~~~

image-20240324171701213

参考文章:
https://segmentfault.com/a/1190000039303463#item-3
https://blog.csdn.net/MrYushiwen/article/details/111473126

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

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

相关文章

二十三 超级数据查看器 讲解稿 设置

二十三 超级数据查看器 讲解稿 设置 ​点击此处 以新页面 打开B站 播放当前教学视频 点击访问app下载页面 百度手机助手 下载地址 大家好&#xff0c;这节课我们讲一下&#xff0c;超级数据查看器的设置功能。 首先&#xff0c;我们打开超级数据查看器&#xff0c; 我…

AcWing 796. 子矩阵的和

这个题的重点是仿照一维的数组&#xff0c;所以a[N][N]也是从1索引开始的。画个图举个例子就非常清晰了 之所以不好理解是因为没画格子&#xff0c;一个格子代表一个点&#xff0c;就很好理解了。 java代码&#xff1a; import java.io.*; public class Main{static int N 1…

Java二阶知识点总结(七)SVN和Git

SVN 1、SVN和Git的区别 SVN是集中式的&#xff0c;也就是会有一个服务器保存所有代码&#xff0c;拉取代码的时候只能从这个服务器上拉取&#xff1b;Git是分布式的&#xff0c;也就是说每个人都保存有所有代码&#xff0c;如果要获取代码&#xff0c;可以从其他人手上获取SV…

使用 STL 容器发生异常的常见原因分析与总结

目录 1、概述 2、使用STL列表中的元素越界 3、遍历STL列表删除元素时对迭代器自加处理有问题引发越界 4、更隐蔽的遍历STL列表删除元素时引发越界的场景 5、多线程同时操作STL列表时没有加锁导致冲突 6、对包含STL列表对象的结构体进行memset操作导致STL列表对象内存出异…

操作符的属性:优先级、结合性

操作符的属性&#xff1a;优先级、结合性 优先级结合性 C语言的操作符有2个重要的属性&#xff1a;优先级、结合性&#xff0c;这两个属性决定了表达式求值的计算顺序。 优先级 优先级指的是&#xff0c;如果⼀个表达式包含多个运算符&#xff0c;哪个运算符应该优先执行。各…

RabbitMQ3.x之一_WindowServer2019中安装RabbitMQ详细教程

RabbitMQ3.x之一_WindowServer2019中安装RabbitMQ详细教程 文章目录 RabbitMQ3.x之一_WindowServer2019中安装RabbitMQ详细教程1. 安装环境说明1. WindowServer20192. ErLang与RabbitMQ对应版本 2 安装Erlang1. 安装Erlang2. ErLnag环境变量配置3. 查看是否安装成功 3. 安装Rab…

数据结构面试常见问题之串的模式匹配(KMP算法)系列-大师改进

&#x1f600;前言 KMP算法是一种改进的字符串匹配算法&#xff0c;由D.E.Knuth&#xff0c;J.H.Morris和V.R.Pratt提出&#xff0c;因此人们称它为克努特—莫里斯—普拉特操作&#xff08;简称KMP算法&#xff09; KMP算法的优势: 提高了匹配效率&#xff0c;时间复杂度为O(m…

【C++】用哈希桶模拟实现unordered_set和unordered_map

目录 一、哈希介绍1.1 哈希概念1.2 哈希冲突解决1.2.1 闭散列1.2.2 开散列 二、哈希桶2.1 实现哈希桶2.1.1 构造节点和声明成员变量2.1.2 构造与析构2.1.3 仿函数2.1.4 查找2.1.5 插入2.1.6 删除 2.2 kv模型哈希桶源代码 三、改造哈希桶3.1 beginend3.2 迭代器3.2.1 前置 3.3 改…

Linux_ubuntu中进行断点调试

文章目录 一、安装gdb调试器&#xff1a;二、使用gcc编译程序&#xff1a;三、使用gdb对程序进行调试&#xff1a;1.设置断点&#xff1a;使用break命令或简写为b来设置断点2.调试运行——run&#xff1a;3.继续执行——continue/c&#xff1a;4.单步执行&#xff1a;5.监视变量…

6.windows ubuntu 子系统 测序数据质量控制。

上一个分享&#xff0c;我们对测序数据进行了质量评估&#xff0c;接下来我们需要对数据进行数据质量控制。 数据预处理&#xff08;Data Preprocessing&#xff09;&#xff1a;包括去除接头序列&#xff08;adapter trimming&#xff09;、去除低质量序列&#xff08;qualit…

【Spring Boot 源码学习】共享 MetadataReaderFactory 上下文初始化器

《Spring Boot 源码学习系列》 共享 MetadataReaderFactory 上下文初始化器 一、引言二、往期内容三、主要内容3.1 源码初识3.2 CachingMetadataReaderFactoryPostProcessor3.2.1 register 方法3.2.1 configureConfigurationClassPostProcessor 方法 3.3 ConfigurationClassPos…

SpringMVC结合设计模式:解决MyBatisPlus传递嵌套JSON数据的难题

&#x1f389;&#x1f389;欢迎光临&#xff0c;终于等到你啦&#x1f389;&#x1f389; &#x1f3c5;我是苏泽&#xff0c;一位对技术充满热情的探索者和分享者。&#x1f680;&#x1f680; &#x1f31f;持续更新的专栏《Spring 狂野之旅&#xff1a;从入门到入魔》 &a…

Learn OpenGL 24 点光源阴影

点光源阴影 上个教程我们学到了如何使用阴影映射技术创建动态阴影。效果不错&#xff0c;但它只适合定向光&#xff0c;因为阴影只是在单一定向光源下生成的。所以它也叫定向阴影映射&#xff0c;深度&#xff08;阴影&#xff09;贴图生成自定向光的视角。 本节我们的焦点是…

Java进阶—GC回收(垃圾回收)

1. 什么是垃圾回收 垃圾回收(Garbage Collection&#xff0c;GC)是Java虚拟机(JVM)的一项重要功能&#xff0c;用于自动管理程序中不再使用的内存。在Java中&#xff0c;程序员不需要手动释放内存&#xff0c;因为GC会自动检测并回收不再使用的对象&#xff0c;从而减少内存泄…

Java基础【上】韩顺平(反射、类加载、final接口、抽象类、内部类)

涵盖知识点&#xff1a;反射、类加载、单例模式、final、抽象类、接口、内部类&#xff08;局部内部类、匿名内部类、成员内部类、静态内部类&#xff09; P711 反射机制原理 创建如下目录结构&#xff0c;在模块下创建src文件夹&#xff0c;文件夹要设置为Sources文件夹&…

Git使用:实现文件在不同设备之间进行同步

一、注册Gitee&#xff0c;创建远程仓库 注册网址&#xff1a;登录 - Gitee.com 打开Gitee&#xff0c;注册完进行登录&#xff0c;点击右上角【】创建一个仓库 新建仓库&#xff1a; 点击创建&#xff0c;仓库创建完毕。 二、下载Git安装包&#xff0c;并创建本地仓库 下载网…

正则表达式具体用法大全~持续更新

# 正则表达式&#xff1a; ## 单字符匹配&#xff1a; python # 匹配某个字符串&#xff1a; # text "abc" # ret re.match(b,text) # print(ret.group()) # 点&#xff08;.&#xff09;&#xff1a;匹配任意的字符(除了\n)&#xff1a; # text "\nabc&quo…

day02_mysql-DDLDMLDQL_课后练习 - 参考答案

文章目录 day02_mysql_课后练习第1题第2题 day02_mysql_课后练习 第1题 案例&#xff1a; 1、创建数据库test02_library 2、创建表格books 字段名字段说明数据类型b_id书编号int(11)b_name书名varchar&#xff08;50&#xff09;authors作者varchar(100)price价格floatpub…

【C语言】——指针四:字符指针与函数指针变量

【C语言】——指针四&#xff1a;字符指针与函数指针变量 一、字符指针二、函数指针变量2.1、 函数指针变量的创建2.2、两段有趣的代码 三、typedef关键字3.1、typedef的使用3.2、typedef与define比较 四、函数指针数组 一、字符指针 在前面的学习中&#xff0c;我们知道有一种…

FaceBook广告账号验证教程

1.登录facebook账号,点击左边的ads manager。 2.点击Create ad创建广告。 3.选择广告投放意向。 4.填写广告信息。 5.创建广告后选择付款方式&#xff0c;这里我是使用信用卡付款。这里我是使用Fomepay的虚拟卡进行绑定的。 6.填写信用卡的持卡人姓名 卡号 有效期 安全码 7.填写…