深入理解代理模式

代理模式是一种设计模式,提供了对目标对象额外的访问方式,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。简言之,代理模式就是设置一个中间代理来控制访问原目标对象,以达到增强原对象的功能和简化访问方式


代理模式

不论是静态代理还是动态代理,其本质都是代理模式的一种实现,那么什么是代理模式呢?

代理模式, 即给某一个对象提供一个代理,并由代理对象控制原对象的引用.

代理模式其实取材于实际生活, 例如我们生活中常见的房屋租赁代理, 我们在租房时, 一般不是直接和房东打交道, 而是和中间商打交道, 即中间商代理了房东, 我们通过中间商完成与房东的间接沟通.

代理模式主要涉及三个角色:

  • Subject: 抽象角色, 声明真实对象和代理对象的共同接口.
  • Proxy: 代理角色, 它是真实角色的封装, 其内部持有真实角色的引用, 并且提供了和真实角色一样的接口, 因此程序中可以通过代理角色来操作真实的角色, 并且还可以附带其他额外的操作.
  • RealSubject: 真实角色, 代理角色所代表的真实对象, 是我们最终要引用的对象.

这三个角色的UML图如下(图片引用自维基百科)

在这里插入图片描述

代理模式的优点

  • 代理模式能够协调调用者和被调用者, 在一定程度上降低了系统的耦合度.
  • 代理模式可以提供更大的灵活性

代理模式的缺点

  • 由于在客户端和真实主题之间增加了代理对象, 因此有些类型的代理模式可能会造成请求的处理速度变慢
  • 实现代理模式需要额外的工作, 有些代理模式的实现非常复杂

代理模式的常用实现

远程代理(remote proxy): 用本地对象来代表一个远端的对象, 对本地对象方法的调用都会作用于远端对象. 远程代理最常见的例子是 ATM 机, 这里 ATM 机充当的就是本地代理对象, 而远端对象就是银行中的存取钱系统, 我们通过 ATM 机来间接地和远端系统打交道.

虚拟代理(virtual proxy): 虚拟代理是大型对象或复杂操作的占位符. 它常用的场景是实现延时加载或复杂任务的后台执行. 例如当一个对象需要很长的时间来初始化时, 那么可以先创建一个虚拟代理对象, 当程序实际需要使用此对象时, 才真正地实例化它, 这样就缩短了程序的启动时间, 即所谓的延时加载.

保护代理(protect proxy): 控制对一个对象的访问, 可以给不同的用户提供不同级别的使用权限. 例如我们可以在代理中检查用户的权限, 当权限不足时, 禁止用户调用此对象的方法.

缓存代理(cache proxy): 对实际对象的调用结果进行缓存. 例如一些复杂的操作, 如数据库读取等, 可以通过缓存代理将结果存储起来, 下次再调用时, 直接返回缓存的结果.

图片代理(image proxy): 当用户需要加载大型图片时, 可以通过代理对象的方法来进行处理, 即在代理对象的方法中, 先使用一个线程向客户端浏览器加载一个小图片, 然后在后台使用另一个线程来调用大图片的加载方法将大图片加载到客户端.


静态代理

为了弄懂 Java 的动态代理, 我们首先来了解一下静态代理吧.

这种代理方式需要代理对象和目标对象实现一样的接口

优点

  • 可以在不修改目标对象的前提下扩展目标对象的功能。

缺点

  • 冗余。由于代理对象要实现与目标对象一致的接口,会产生过多的代理类。
  • 不易维护。一旦接口增加方法,目标对象与代理对象都要进行修改。

举例保存用户功能的静态代理实现

接口类: IUserDao

package cn.wideth.util.proxy;

/**
 * 接口类: IUserDao
 */
public interface IUserDao {

    public void save();
}

目标对象:UserDao

package cn.wideth.util.proxy;

/**
 * 目标对象:UserDao
 */
public class UserDao implements IUserDao{

    @Override
    public void save() {
        System.out.println("保存数据");
    }
}

静态代理对象:UserDaoProxy 需要实现IUserDao接口!

package cn.wideth.util.proxy;


/**
 * 静态代理对象:UserDapProxy
 * 需要实现IUserDao接口!
 */
public class UserDaoProxy implements IUserDao{

    private IUserDao target;
    public UserDaoProxy(IUserDao target) {
        this.target = target;
    }

    @Override
    public void save() {
        System.out.println("开启事务");//扩展了额外功能
        target.save();
        System.out.println("提交事务");
    }
}

测试类:testStaticProxy

package cn.wideth.util.proxy;

import org.junit.Test;

public class StaticUserProxy {

    @Test
    public void testStaticProxy(){
        //目标对象
        IUserDao target = new UserDao();
        //代理对象
        UserDaoProxy proxy = new UserDaoProxy(target);
        proxy.save();
    }
}

程序运行结果

在这里插入图片描述


动态代理

动态代理利用了JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为JDK代理或接口代理

静态代理与动态代理的区别主要在:

  • 静态代理在编译时就已经实现,编译完成后代理类是一个实际的class文件
  • 动态代理是在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中

特点

动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理。

JDK中生成代理对象主要涉及的类有

  • java.lang.reflect Proxy,主要方法为
static Object    newProxyInstance(ClassLoader loader,  //指定当前目标对象使用类加载器

 Class<?>[] interfaces,    //目标对象实现的接口的类型
 InvocationHandler h      //事件处理器
) 
//返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。
  • java.lang.reflect InvocationHandler,主要方法为
 Object    invoke(Object proxy, Method method, Object[] args) 
// 在代理实例上处理方法调用并返回结果。

使用动态代理的步骤很简单, 可以概括为如下两步

  1. 实现 InvocationHandler 接口, 并在 invoke 中调用真实对象的对应方法.
  2. 通过 Proxy.newProxyInstance 静态方法获取一个代理对象.

举例:保存用户功能的动态代理实现

接口类: IUserDao

package cn.wideth.util.proxy;

/**
 * 接口类: IUserDao
 */
public interface IUserDao {

    public void save();
}

目标对象:UserDao

package cn.wideth.util.proxy;

/**
 * 目标对象:UserDao
 */
public class UserDao implements IUserDao{

    @Override
    public void save() {
        System.out.println("保存数据");
    }
}

动态代理对象:UserProxyFactory

创建ProxyFactory类,实现InvocationHandler接口,这个类中持有一个被代理对象的实例target。InvocationHandler中有一个invoke方法,所有执行代理对象的方法都会被替换成执行invoke方法。在invoke方法中执行被代理对象target的相应方法。在代理过程中,我们在真正执行被代理对象的方法前加入自己其他处理。这也是Spring中的AOP实现的主要原理,这里还涉及到一个很重要的关于java反射方面的基础知识

package cn.wideth.util.proxy;


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

/**
 * 动态代理对象:UserProxyFactory
 */
public class ProxyFactory implements InvocationHandler{

    private Object target;// 维护一个目标对象

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        System.out.println("开启事务");
        // 执行目标对象方法
        Object returnValue = method.invoke(target, args);
        System.out.println("提交事务");
        return returnValue;
    }
}

测试类:testDynamicProxy

package cn.wideth.util.proxy;

import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

/**
 * 测试类:testDynamicProxy
 */
public class DynamicProxy {

    @Test
    public void testDynamicProxy (){

        //创建一个实例对象,这个对象是被代理的对象
        IUserDao target = new UserDao();

        //输出目标对象信息
        System.out.println(target.getClass());

        //创建一个与代理对象相关联的InvocationHandler
        InvocationHandler userHandler = new ProxyFactory(target);

        //创建一个代理对象userProxy来代理target,代理对象的每个执行方法都会替换执行Invocation中的invoke方法
        IUserDao userProxy = (IUserDao) Proxy.newProxyInstance(IUserDao.class.getClassLoader(), new Class<?>[]{IUserDao.class}, userHandler);

        //输出代理对象信息
        System.out.println(userProxy.getClass());

        //执行代理方法
        userProxy.save();
    }

}

程序结果

在这里插入图片描述


动态代理原理分析

动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。是因为所有被代理执行的方法,都是通过在InvocationHandler中的invoke方法调用的,所以我们只要在invoke方法中统一处理,就可以对所有被代理的方法进行相同的操作了

动态代理的过程,代理对象和被代理对象的关系不像静态代理那样一目了然,清晰明了。因为动态代理的过程中,我们并没有实际看到代理类,也没有很清晰地的看到代理类的具体样子,而且动态代理中被代理对象和代理对象是通过InvocationHandler来完成的代理过程的,其中具体是怎样操作的,为什么代理对象执行的方法都会通过InvocationHandler中的invoke方法来执行。带着这些问题,我们就需要对java动态代理的源码进行简要的分析,弄清楚其中缘由。

Proxy类的newProxyInstance方法源码


//文档说明
Returns an instance of a proxy class for the specified interfaces that dispatches method invocations to the specified invocation handler.
Proxy.newProxyInstance throws IllegalArgumentException for the same reasons that Proxy.getProxyClass does.
Params:
loader – the class loader to define the proxy class
interfaces – the list of interfaces for the proxy class to implement
h – the invocation handler to dispatch method invocations to
Returns:
a proxy instance with the specified invocation handler of a proxy class that is defined by the specified class loader and that implements the specified interfaces
Throws:
IllegalArgumentExceptionif any of the restrictions on the parameters that may be passed to getProxyClass are violated
SecurityExceptionif a security manager, s, is present and any of the following conditions is met:
the given loader is null and the caller's class loader is not null and the invocation of s.checkPermission with RuntimePermission("getClassLoader") permission denies access;
for each proxy interface, intf, the caller's class loader is not the same as or an ancestor of the class loader for intf and invocation of s.checkPackageAccess() denies access to intf;
any of the given proxy interfaces is non-public and the caller class is not in the same runtime package as the non-public interface and the invocation of s.checkPermission with ReflectPermission("newProxyInPackage.{package name}") permission denies access.
NullPointerExceptionif the interfaces array argument or any of its elements are null, or if the invocation handler, h, is null

//源代码
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);

        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * Look up or generate the designated proxy class.
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * Invoke its constructor with the designated invocation handler.
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            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);
        }
    }

重点是这四处位置


final Class<?>[] intfs = interfaces.clone();
Class<?> cl = getProxyClass0(loader, intfs);
final Constructor<?> cons = cl.getConstructor(constructorParams);
return cons.newInstance(new Object[]{h});

最应该关注的是 Class<?> cl = getProxyClass0(loader, intfs);这句,这里产生了代理类,后面代码中的构造器也是通过这里产生的类来获得,可以看出,这个类的产生就是整个动态代理的关键,由于是动态生成的类文件,我这里不具体进入分析如何产生的这个类文件,只需要知道这个类文件时缓存在java虚拟机中的,我们可以通过下面的方法将其打印到文件里面,一睹真容

package cn.wideth.util.proxy;

import sun.misc.ProxyGenerator;
import java.io.File;
import java.io.FileOutputStream;

public class Main {

    public static void main(String[] args) {

        byte[] data = ProxyGenerator.generateProxyClass("$Proxy0",UserDao.class.getInterfaces());
        File file= new File("E:\\userProxy.class");
        try(FileOutputStream fos = new FileOutputStream(file)) {
            fos.write(data);
            fos.flush();
            System.out.println("代理类class文件写入成功");
        } catch (Exception e) {
            System.out.println("写文件错误");
        }

    }
}

对这个class文件进行反编译,我们看看jdk为我们生成了什么样的内容

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

import cn.wideth.util.proxy.IUserDao;
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 IUserDao {

    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

/**
  *注意这里是生成代理类的构造方法,方法参数为InvocationHandler类型,看到这,
  是不是就有明白为何代理对象调用方法都是执行InvocationHandler中的invoke方法,
  而InvocationHandler又持有一个被代理对象的实例,不禁会想难道是....? 
  没错,就是你想的那样。
  *
  *super(paramInvocationHandler),是调用父类Proxy的构造方法。
  *父类持有:protected InvocationHandler h;
  *Proxy构造方法:
  *    protected Proxy(InvocationHandler h) {
  *         Objects.requireNonNull(h);
  *         this.h = h;
  *     }
  *
  */

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

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

 /**
  *这里调用代理对象的save() 方法,直接就调用了InvocationHandler中的invoke方法,
  并把m3传了进去。this.h.invoke(this, m3, null);这里简单,明了。
  *再想想代理对象持有一个InvocationHandler对象,InvocationHandler对象持有一个
  被代理的对象,再联系到InvacationHandler中的invoke方法。嗯,就是这样。
  */
    public final void save() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        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 int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            //看看这儿静态块儿里面有什么,是不是找到了save方法。请记住save通过反射得到的名字m3,其他的先不管
            m3 = Class.forName("cn.wideth.util.proxy.IUserDao").getMethod("save");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

jdk为我们的生成了一个叫$Proxy0(这个名字后面的0是编号,有多个代理类会一次递增)的代理类,这个类文件时放在内存中的,我们在创建代理对象时,就是通过反射获得这个类的构造方法,然后创建的代理实例。通过对这个生成的代理类源码的查看,我们很容易能看出,动态代理实现的具体过程。

我们可以对InvocationHandler看做一个中介类,中介类持有一个被代理对象,在invoke方法中调用了被代理对象的相应方法。通过聚合方式持有被代理对象的引用,把外部对invoke的调用最终都转为对被代理对象的调用。

代理类调用自己方法时,通过自身持有的中介类对象来调用中介类对象的invoke方法,从而达到代理执行被代理对象的方法。也就是说,动态代理通过中介类实现了具体的代理功能。

总结:生成的代理类:$Proxy0 extends Proxy implements IUserDao我们看到代理类继承了Proxy类,所以也就决定了java动态代理只能对接口进行代理,Java的继承机制注定了这些动态代理类们无法实现对class的动态代理。上面的动态代理的例子,其实就是AOP的一个简单实现了,在目标对象的方法执行之前和执行之后进行了处理,进行了简单的模拟事务操作。Spring的AOP实现其实也是用了Proxy和InvocationHandler这两个东西的。


InvocationHandler接口和Proxy类详解

InvocationHandler接口是proxy代理实例的调用处理程序实现的一个接口,每一个proxy代理实例都有一个关联的调用处理程序;在代理实例调用方法时,方法调用被编码分派到调用处理程序的invoke方法。

看下官方文档对InvocationHandler接口的描述:

{@code InvocationHandler} is the interface implemented by
     the <i>invocation handler</i> of a proxy instance.
     <p>Each proxy instance has an associated invocation handler.
     When a method is invoked on a proxy instance, the method
     invocation is encoded and dispatched to the {@code invoke}
     method of its invocation handler

当我们通过动态代理对象调用一个方法时候,这个方法的调用就会被转发到实现InvocationHandler接口类的invoke方法来调用,看如下invoke方法:

 /**
    * proxy:代理类代理的真实代理对象com.sun.proxy.$Proxy0
    * method:我们所要调用某个对象真实的方法的Method对象
    * args:指代代理对象方法传递的参数
    */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

Proxy类就是用来创建一个代理对象的类,它提供了很多方法,但是我们最常用的是newProxyInstance方法。

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

这个方法的作用就是创建一个代理类对象,它接收三个参数,我们来看下几个参数的含义

loader:一个classloader对象,定义了由哪个classloader对象对生成的代理类进行加载
interfaces:一个interface对象数组,表示我们将要给我们的代理对象提供一组什么样的接口,如果我们提供了这样一个接口对象数组,那么也就是声明了代理类实现了这些接口,代理类就可以调用接口中声明的所有方法。
h:一个InvocationHandler对象,表示的是当动态代理对象调用方法的时候会关联到哪一个InvocationHandler对象上,并最终由其调用。

反射中,Class.forName 和 ClassLoader 区别

Class.forName(className)方法,其实调用的方法是Class.forName(className,true,classloader);注意看第2个boolean参数,它表示的意思,在loadClass后必须初始化。比较下我们前面准备jvm加载类的知识,我们可以清晰的看到在执行过此方法后,目标对象的 static块代码已经被执行,static参数也已经被初始化。

再看ClassLoader.loadClass(className)方法,其实他调用的方法是ClassLoader.loadClass(className,false);还是注意看第2个 boolean参数,该参数表示目标对象被装载后不进行链接,这就意味这不会去执行该类静态块中间的内容。因此2者的区别就显而易见了。


cglib代理

cglib is a powerful, high performance and quality Code Generation Library. It can extend JAVA classes and implement interfaces at runtime.

cglib (Code Generation Library )是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。

cglib特点

  • JDK的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口。如果想代理没有实现接口的类,就可以使用CGLIB实现。
  • CGLIB是一个强大的高性能的代码生成包,它可以在运行期扩展Java类与实现Java接口。它广泛的被许多AOP的框架使用,例如Spring AOP,为他们提供方法的interception(拦截)。
  • CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。 不鼓励直接使用ASM,因为它需要你对JVM内部结构包括class文件的格式和指令集都很熟悉。

cglib与动态代理最大的区别就是

  • 使用动态代理的对象必须实现一个或多个接口
  • 使用cglib代理的对象则无需实现接口,达到代理类无侵入。

使用cglib需要引入cglib的jar包,如果你已经有spring-core的jar包,则无需引入,因为spring中包含了cglib

cglib的Maven

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

举例:保存用户功能的动态代理实现

目标对象:UserDao

package cn.wideth.util.proxy;

/**
 * 目标对象:UserDao
 */
public class UserDao{

    public void save() {
        System.out.println("保存数据");
    }
}

代理对象:ProxyFactory

package cn.wideth.util.proxy;

import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class ProxyFactory implements MethodInterceptor{

    private Object target;//维护一个目标对象
    
    public ProxyFactory(Object target) {
        this.target = target;
    }

    //为目标对象生成代理对象
    public Object getProxyInstance() {
        //工具类
        Enhancer en = new Enhancer();
        //设置父类
        en.setSuperclass(target.getClass());
        //设置回调函数
        en.setCallback(this);
        //创建子类对象代理
        return en.create();
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    
        System.out.println("开启事务");
        // 执行目标对象的方法
        Object returnValue = method.invoke(target, args);
        System.out.println("关闭事务");
        return null;
    }
}

测试类:TestProxy

package cn.wideth.util.proxy;

import org.junit.Test;

/**
 * 测试类:testCglibProxy
 */

public class CglibProxy {

    @Test
    public void testCglibProxy(){
        //目标对象
        UserDao target = new UserDao();
        System.out.println(target.getClass());
        //代理对象
        UserDao proxy = (UserDao) new ProxyFactory(target).getProxyInstance();
        System.out.println(proxy.getClass());
        //执行代理对象方法
        proxy.save();
    }
}

程序结果

在这里插入图片描述


三种代理的区别

  • 静态代理实现较简单,只要代理对象对目标对象进行包装,即可实现增强功能,但静态代理只能为一个目标对象服务,如果目标对象过多,则会产生很多代理类。
  • JDK动态代理需要目标对象实现业务接口,代理类只需实现InvocationHandler接口。
  • 静态代理在编译时产生class字节码文件,可以直接使用,效率高。
  • 动态代理必须实现InvocationHandler接口,通过反射代理方法,比较消耗系统性能,但可以减少代理类的数量,使用更灵活。
  • cglib代理无需实现接口,通过生成类字节码实现代理,比反射稍快,不存在性能问题,但cglib会继承目标对象,需要重写方法,所以目标对象不能为final类。

本文小结

本文详细介绍了静态代理,动态代理以及cglib代理,三种常见的代理模式。以及相关的原理分析。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值