深度剖析java中JDK动态代理机制

本篇文章主要介绍了深度剖析java中JDK动态代理机制 ,动态代理避免了开发人员编写各个繁锁的静态代理类,只需简单地指定一组接口及目标类对象就能动态的获得代理对象。
 

摘要

相比于静态代理,动态代理避免了开发人员编写各个繁锁的静态代理类,只需简单地指定一组接口及目标类对象就能动态的获得代理对象。

代理模式

使用代理模式必须要让代理类和目标类实现相同的接口,客户端通过代理类来调用目标方法,代理类会将所有的方法调用分派到目标对象上反射执行,还可以在分派过程中添加"前置通知"和后置处理(如在调用目标方法前校验权限,在调用完目标方法后打印日志等)等功能。

使用动态代理的五大步骤

1.通过实现InvocationHandler接口来自定义自己的InvocationHandler; 

2.通过Proxy.getProxyClass获得动态代理类 

3.通过反射机制获得代理类的构造方法,方法签名为getConstructor(InvocationHandler.class) 

4.通过构造函数获得代理对象并将自定义的InvocationHandler实例对象传为参数传入 

5.通过代理对象调用目标方法 

动态代理的使用

例1(方式一)

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class MyProxy {
 
   public interface IHello{
 
     void sayHello();
 
   }
 
   static class Hello implements IHello{
 
     public void sayHello() {
 
       System.out.println( "Hello world!!" );
 
     }
 
   }
 
   //自定义InvocationHandler
 
   static class HWInvocationHandler implements InvocationHandler{
 
     //目标对象
 
     private Object target;
 
     public HWInvocationHandler(Object target){
 
       this .target = target;
 
     }
 
     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 
       System.out.println( "------插入前置通知代码-------------" );
 
       //执行相应的目标方法
 
       Object rs = method.invoke(target,args);
 
       System.out.println( "------插入后置处理代码-------------" );
 
       return rs;
 
     }
 
   }
 
   public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetExc  eption, InstantiationException {
 
     //生成$Proxy0的class文件
 
     System.getProperties().put( "sun.misc.ProxyGenerator.saveGeneratedFiles" , "true" );
 
     //获取动态代理类
 
     Class proxyClazz = Proxy.getProxyClass(IHello. class .getClassLoader(),IHello. class );
 
     //获得代理类的构造函数,并传入参数类型InvocationHandler.class
 
     Constructor constructor = proxyClazz.getConstructor(InvocationHandler. class );
 
     //通过构造函数来创建动态代理对象,将自定义的InvocationHandler实例传入
 
     IHello iHello = (IHello) constructor.newInstance( new HWInvocationHandler( new Hello()));
 
     //通过代理对象调用目标方法
 
     iHello.sayHello();
 
   }
 
}

输出:

------插入前置通知代码-------------
Hello world!!
------插入后置处理代码-------------

Proxy类中还有个将2~4步骤封装好的简便方法来创建动态代理对象,其方法签名为:newProxyInstance(ClassLoader loader,Class<?>[] instance, InvocationHandler h),如下例:

(方式二)

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
 
     //生成$Proxy0的class文件
 
     System.getProperties().put( "sun.misc.ProxyGenerator.saveGeneratedFiles" , "true" );
 
     IHello ihello = (IHello) Proxy.newProxyInstance(IHello. class .getClassLoader(), //加载接口的类加载器
 
         new Class[]{IHello. class },   //一组接口
 
         new HWInvocationHandler( new Hello())); //自定义的InvocationHandler
 
     ihello.sayHello();
 
   }

输出结果一样.

下面以newProxyInstance方法为切入点来剖析代理类的生成及代理方法的调用

(为了篇幅整洁去掉了次要的代码)

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public static Object newProxyInstance(ClassLoader loader,
 
                      Class<!--?-->[] interfaces,
 
                      InvocationHandler h)
 
       throws IllegalArgumentException
 
   {
 
     if (h == null ) {  //如果h为空直接抛出异常,所以InvocationHandler实例对象是必须的
 
       throw new NullPointerException();
 
     }
 
     //对象的拷贝,暂不知道这里拷贝下的意义是啥?
 
     final Class<!--?-->[] intfs = interfaces.clone();
 
     //一些安全的权限检查
 
     final SecurityManager sm = System.getSecurityManager();
 
     if (sm != null ) {
 
       checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
 
     }
 
     //产生代理类
 
     Class<!--?--> cl = getProxyClass0(loader, intfs);
 
  
 
     //获取代理类的构造函数对象
 
     //参数constructorParames为常量值:private static final Class<!--?-->[] constructorParams = { InvocationHandler.class };
 
     final Constructor<!--?--> cons = cl.getConstructor(constructorParames);
 
     final InvocationHandler ih = h;
 
     //根据代理类的构造函数对象来创建代理类对象
 
     return newInstance(cons, ih);
 
        
 
   }

这段代码就是对代理类对象的创建,就是对例1中34~38行封装,其中getProxyClass0就是生成代理类的方法

getProxyClass0方法剖析

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static Class<!--?--> getProxyClass0(ClassLoader loader,
 
                       Class<!--?-->... interfaces) {
 
//接口数不得超过65535个
 
     if (interfaces.length > 65535 ) {
 
       throw new IllegalArgumentException( "interface limit exceeded" );
 
     }
 
//代理类缓存,如果缓存中有代理类了直接返回,否则将由ProxyClassFactory创建代理类
 
     return proxyClassCache.get(loader, interfaces);
 
   }

看看ProxyClassFactory是怎样生成代理类的?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
private static final class ProxyClassFactory
 
     implements BiFunction<classloader, class <?= "" >[], Class<!--?-->>
 
   {
 
     //统一代理类的前缀名都以$Proxy开关
 
     private static final String proxyClassNamePrefix = "$Proxy" ;
 
     //使用唯一的编号给作为代理类名的一部分,如$Proxy0,$Proxy1等
 
     private static final AtomicLong nextUniqueNumber = new AtomicLong();
 
     @Override
 
     public Class<!--?--> apply(ClassLoader loader, Class<!--?-->[] interfaces) {
 
       Map< class <?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
 
       for (Class<!--?--> intf : interfaces) {
 
         //验证指定的类加载器(loader)加载接口所得到的Class对象(interfaceClass)是否与intf对象相同
 
         Class<!--?--> interfaceClass = null ;
 
         try {
 
           interfaceClass = Class.forName(intf.getName(), false , loader);
 
         } catch (ClassNotFoundException e) {
 
         }
 
         if (interfaceClass != intf) {
 
           throw new IllegalArgumentException(
 
             intf + " is not visible from class loader" );
 
         }
 
         //验证该Class对象是不是接口
 
         if (!interfaceClass.isInterface()) {
 
           throw new IllegalArgumentException(
 
             interfaceClass.getName() + " is not an interface" );
 
         }
 
         // 验证该接口是否重复了
 
         if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null ) {
 
           throw new IllegalArgumentException(
 
             "repeated interface: " + interfaceClass.getName());
 
         }
 
       }
 
          //声明代理类所在包
 
       String proxyPkg = null
 
       /*验证你传入的接口中是否有非public接口,只要有一个接口是非public的,那么这些接口都必须在同一包中
 
       这里的接口修饰符直接影响到System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true")所生成
 
       的代理类的路径,往下看!!*/
 
       for (Class<!--?--> intf : interfaces) {
 
         int flags = intf.getModifiers();
 
         if (!Modifier.isPublic(flags)) {
 
           String name = intf.getName();
 
           int n = name.lastIndexOf('.');
 
           //截取完整包名
 
           String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
 
           if (proxyPkg == null) {
 
             proxyPkg = pkg;
 
           } else if (!pkg.equals(proxyPkg)) {
 
             throw new IllegalArgumentException(
 
               "non-public interfaces from different packages");
 
           }
 
         }
 
       }
 
        
 
       if (proxyPkg == null) {
 
         /*如果都是public接口,那么生成的代理类就在com.sun.proxy包下如果报java.io.FileNotFoundException: com\sun\proxy\$Proxy0.c         lass (系统找不到指定的路径。)的错误,就先在你项目中创建com.sun.proxy路径*/     
 
         proxyPkg = ReflectUtil.PROXY_PACKAGE + "." ;
 
       }
 
  
 
        //将当前nextUniqueNumber的值以原子的方式的加1,所以第一次生成代理类的名字为$Proxy0.class
 
       long num = nextUniqueNumber.getAndIncrement();
 
       //代理类的完全限定名,如com.sun.proxy.$Proxy0.calss,
 
       String proxyName = proxyPkg + proxyClassNamePrefix + num;
 
       //生成代理类字节码文件      
 
       byte [] proxyClassFile = ProxyGenerator.generateProxyClass(
 
         proxyName, interfaces);
 
       try {
 
         return defineClass0(loader, proxyName,
 
                   proxyClassFile, 0 , proxyClassFile.length);
 
       } catch (ClassFormatError e) {
 
         throw new IllegalArgumentException(e.toString());
 
       }
 
     }
 
   }</ class <?></classloader,>

而生成代理类字节码文件又主要通过ProxyGenerate的generateProxyClass(proxyName,interfaces)

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public static byte [] generateProxyClass( final String var0, Class[] var1) {
 
     ProxyGenerator var2 = new ProxyGenerator(var0, var1);
 
     //生成代理类字节码文件的真正方法
 
     final byte [] var3 = var2.generateClassFile();
 
     //保存文件
 
     if (saveGeneratedFiles) {
 
       AccessController.doPrivileged( new PrivilegedAction() {
 
         public Void run() {
 
           try {
 
             FileOutputStream var1 = new FileOutputStream(ProxyGenerator.dotToSlash(var0) + ".class" );
 
             var1.write(var3);
 
             var1.close();
 
             return null ;
 
           } catch (IOException var2) {
 
             throw new InternalError( "I/O exception saving generated file: " + var2);
 
           }
 
         }
 
       });
 
     }
 
     return var3;
 
   }

层层调用后,最终generateClassFile才是真正生成代理类字节码文件的方法,注意开头的三个addProxyMethod方法是只将Object的hashcode,equals,toString方法添加到代理方法容器中,代理类除此之外并没有重写其他Object的方法,所以除这三个方法外,代理类调用其他方法的行为与Object调用这些方法的行为一样不通过Invoke

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
private byte [] generateClassFile() {
 
     /addProxyMethod系列方法就是将接口的方法和Object的hashCode,equals,toString方法添加到代理方法容器(proxyMethods),
 
      其中方法签名作为key,proxyMethod作为value*/
 
     /*hashCodeMethod方法位于静态代码块中通过Object对象获得,hashCodeMethod=Object.class.getMethod("hashCode",new Class[0]),
 
      相当于从Object中继承过来了这三个方法equalsMethod,toStringMethod*/ 
 
     this.addProxyMethod(hashCodeMethod, Object.class);  -->
 
     this.addProxyMethod(equalsMethod, Object.class);
 
     this.addProxyMethod(toStringMethod, Object.class);
 
     int var1;
 
     int var3;
 
      //获得所有接口中的所有方法,并将方法添加到代理方法中
 
     for(var1 = 0; var1 < this.interfaces.length; ++var1) {
 
       Method[] var2 = this.interfaces[var1].getMethods();     
 
       for(var3 = 0; var3 < var2.length; ++var3) {
 
         this.addProxyMethod(var2[var3], this.interfaces[var1]);
 
       }
 
     }
 
      
 
     Iterator var7 = this.proxyMethods.values().iterator();
 
     List var8;
 
     while(var7.hasNext()) {
 
       var8 = (List)var7.next();
 
       checkReturnTypes(var8);  //验证具有相同方法签名的的方法的返回值类型是否一致,因为不可能有两个方法名相同,参数相同,而返回值却不同的方法
 
     };
 
   //接下来就是写代理类文件的步骤了
 
     Iterator var11
 
     try {
 
        //生成代理类的构造函数
 
       this.methods.add(this.generateConstructor());
 
       var7 = this.proxyMethods.values().iterator();
 
       while(var7.hasNext()) {
 
         var8 = (List)var7.next();
 
         var11 = var8.iterator();
 
         while(var11.hasNext()) {
 
           ProxyGenerator.ProxyMethod var4 = (ProxyGenerator.ProxyMethod)var11.next();
 
           /将代理字段声明为Method,10为ACC_PRIVATE和ACC_STATAIC的与运算,表示该字段的修饰符为private static
 
            所以代理类的字段都是private static Method XXX*/
 
           this .fields.add( new ProxyGenerator.FieldInfo(var4.methodFieldName, "Ljava/lang/reflect/Method;" , 10 ));
 
           //生成代理类的代理方法
 
           this .methods.add(var4.generateMethod());
 
         }
 
       }
 
       //为代理类生成静态代码块,对一些字段进行初始化
 
       this .methods.add( this .generateStaticInitializer());
 
     } catch (IOException var6) {
 
       throw new InternalError( "unexpected I/O Exception" );
 
     }
 
     
 
     if ( this .methods.size() > '\uffff' ) { //代理方法超过65535将抛出异常
 
       throw new IllegalArgumentException( "method limit exceeded" );
 
     } else if ( this .fields.size() > '\uffff' ) {  //代理类的字段超过65535将抛出异常
 
       throw new IllegalArgumentException( "field limit exceeded" );
 
     } else {
 
      //这里开始就是一些代理类文件的过程,此过程略过
 
       this .cp.getClass(dotToSlash( this .className));
 
       this .cp.getClass( "java/lang/reflect/Proxy" );
 
       for (var1 = 0 ; var1 < this .interfaces.length; ++var1) {
 
         this .cp.getClass(dotToSlash( this .interfaces[var1].getName()));
 
       }
 
       this .cp.setReadOnly();
 
       ByteArrayOutputStream var9 = new ByteArrayOutputStream();
 
       DataOutputStream var10 = new DataOutputStream(var9);
 
       try {
 
         var10.writeInt(- 889275714 );
 
         var10.writeShort( 0 );
 
         var10.writeShort( 49 );
 
         this .cp.write(var10);
 
         var10.writeShort( 49 );
 
         var10.writeShort( this .cp.getClass(dotToSlash( this .className)));
 
         var10.writeShort( this .cp.getClass( "java/lang/reflect/Proxy" ));
 
         var10.writeShort( this .interfaces.length);
 
         for (var3 = 0 ; var3 < this .interfaces.length; ++var3) {
 
           var10.writeShort( this .cp.getClass(dotToSlash( this .interfaces[var3].getName())));
 
         }
 
         var10.writeShort( this .fields.size());
 
         var11 = this .fields.iterator();
 
         while (var11.hasNext()) {
 
           ProxyGenerator.FieldInfo var12 = (ProxyGenerator.FieldInfo)var11.next();
 
           var12.write(var10);
 
         }
 
         var10.writeShort( this .methods.size());
 
         var11 = this .methods.iterator();
 
         while (var11.hasNext()) {
 
           ProxyGenerator.MethodInfo var13 = (ProxyGenerator.MethodInfo)var11.next();
 
           var13.write(var10);
 
         }
 
         var10.writeShort( 0 );
 
         return var9.toByteArray();
 
       } catch (IOException var5) {
 
         throw new InternalError( "unexpected I/O Exception" );
 
       }
 
     }
 
   }

addProxyMethod方法剖析

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
private void addProxyMethod(Method var1, Class var2) {
 
     String var3 = var1.getName(); //方法名
 
     Class[] var4 = var1.getParameterTypes();  //方法参数类型数组
 
     Class var5 = var1.getReturnType();  //返回值类型
 
     Class[] var6 = var1.getExceptionTypes();  //异常类型
 
     String var7 = var3 + getParameterDescriptors(var4);  //方法签名
 
     Object var8 = (List) this .proxyMethods.get(var7);  //根据方法签名却获得proxyMethods的Value
 
     if (var8 != null ) {  //处理多个代理接口中重复的方法的情况
 
       Iterator var9 = ((List)var8).iterator();
 
       while (var9.hasNext()) {
 
         ProxyGenerator.ProxyMethod var10 = (ProxyGenerator.ProxyMethod)var9.next();
 
         if (var5 == var10.returnType) {
 
           /*归约异常类型以至于让重写的方法抛出合适的异常类型,我认为这里可能是多个接口中有相同的方法,而这些相同的方法抛出的异常类           型又不同,所以对这些相同方法抛出的异常进行了归约*/
 
           ArrayList var11 = new ArrayList();
 
           collectCompatibleTypes(var6, var10.exceptionTypes, var11);
 
           collectCompatibleTypes(var10.exceptionTypes, var6, var11);
 
           var10.exceptionTypes = new Class[var11.size()];
 
           //将ArrayList转换为Class对象数组
 
           var10.exceptionTypes = (Class[])var11.toArray(var10.exceptionTypes);
 
           return;
 
         }
 
       }
 
     } else {
 
       var8 = new ArrayList(3);
 
       this.proxyMethods.put(var7, var8);
 
     }  
 
     ((List)var8).add(new ProxyGenerator.ProxyMethod(var3, var4, var5, var6, var2, null));
 
     /*24~27行的意思就是如果var8为空,就创建一个数组,并以方法签名为key,proxymethod对象数组为value添加到proxyMethods*/
 
   }

InvocationHandler的作用

在动态代理中InvocationHandler是核心,每个代理实例都具有一个关联的调用处理程序(InvocationHandler)。对代理实例调用方法时,将对方法调用进行编码并将其指派到它的调用处理程序(InvocationHandler)的 invoke 方法。所以对代理方法的调用都是通InvocationHadler的invoke来实现中,而invoke方法根据传入的代理对象,方法和参数来决定调用代理的哪个方法

invoke方法签名:invoke(Object Proxy,Method method,Object[] args)

 $Proxy0.class

来看看例1(MyProxy)的代理类是怎样的?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
public final class $Proxy0 extends Proxy implements IHello {  //继承了Proxy类和实现IHello接口
 
   //变量,都是private static Method XXX
 
   private static Method m3; 
 
   private static Method m1;
 
   private static Method m0;
 
   private static Method m2;
 
   //代理类的构造函数,其参数正是是InvocationHandler实例,Proxy.newInstance方法就是通过通过这个构造函数来创建代理实例的
 
   public $Proxy0(InvocationHandler var1) throws {
 
     super (var1);
 
   }
 
   //接口代理方法
 
   public final void sayHello() throws {
 
     try {
 
       super .h.invoke( this , m3, (Object[]) null );
 
     } catch (RuntimeException | Error var2) {
 
       throw var2;
 
     } catch (Throwable var3) {
 
       throw new UndeclaredThrowableException(var3);
 
     }
 
   }
 
   //以下Object中的三个方法
 
   public final boolean equals(Object var1) throws {
 
     try {
 
       return ((Boolean) super .h.invoke( this , m1, new Object[]{var1})).booleanValue();
 
     } catch (RuntimeException | Error var3) {
 
       throw var3;
 
     } catch (Throwable var4) {
 
       throw new UndeclaredThrowableException(var4);
 
     }
 
   }
 
   public final int hashCode() throws {
 
     try {
 
       return ((Integer) super .h.invoke( this , m0, (Object[]) null )).intValue();
 
     } 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);
 
     }
 
   }
 
   //对变量进行一些初始化工作
 
   static {
 
     try {
 
       m3 = Class.forName( "com.mobin.proxy.IHello" ).getMethod( "sayHello" , new Class[ 0 ]);
 
       m1 = Class.forName( "java.lang.Object" ).getMethod( "equals" , new Class[]{Class.forName( "java.lang.Object" )});
 
       m0 = Class.forName( "java.lang.Object" ).getMethod( "hashCode" , new Class[ 0 ]);
 
       m2 = Class.forName( "java.lang.Object" ).getMethod( "toString" , new Class[ 0 ]);
 
     } catch (NoSuchMethodException var2) {
 
       throw new NoSuchMethodError(var2.getMessage());
 
     } catch (ClassNotFoundException var3) {
 
       throw new NoClassDefFoundError(var3.getMessage());
 
     }
 
   }
 
}

以上就是对代理类如何生成,代理类方法如何被调用的分析!在很多框架都使用了动态代理如Spring,HDFS的RPC调用等等,分析过程中收获很多,如果想深入的了解JDK动态代理机制一定要深入到源码去剖析!!希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

转载于:https://www.cnblogs.com/kelelipeng/p/11399815.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值