利用java反射机制解析json

本文深入探讨了JSON数据格式与Java反射机制的结合应用,详细介绍了如何通过反射解析JSON数据到具体Java类中,实现数据的高效映射与属性赋值。包括基础概念解释、JSON数据结构分析、Java反射机制概述、反射解析JSON的代码实现、以及实际案例演示。重点在于提供一种实用的解决方案,帮助开发者在数据交互中减少流量消耗,提升用户体验。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

刚工作的时候接手了前辈留下的一个项目,客户端和服务器交互使用的是xml格式数据,虽然已有成熟的解析技术,但是使用起来还是感觉不尽人意,加上老大提出要尽量减少每次交互的数据量,以节省用户流量和服务器带宽,于是换成json数据格式刻不容缓,我也由此开始认识到Json。

Json基础

先看一个Json数据:

{
 "persons":[
		{
			"name":"Bob",
			"age":14,
			"married":false,
			"salary":null
		},
		{
			"name":"Kate",
			"age":26,
			"married":true,
			"salary":8888.88
		}
	],
 "team":"Android"
}

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。结合上面的例子,Json的数据是由键值对的形式存放的,键值间采用冒号:分割,其中键用双引号"或单引号'引起,数据或数组元素间使用逗号,分割,方括号[]表示对象数组,花括号{}表示每一个对象。Json的取值有很多种,可以是数字(整数或浮点数)、字符串(双引号或单引号中)、逻辑值(true或false)、json数组(方括号中)、json对象(花括号中)或者空值(null)。

弄清大概后我们再来看看上例中的Json,它表示一个包含有persons数组和team变量的对象,persons数组又包含了两个人员对象,它们具有相同的属性,其中名字name的值是字符串,年龄age的值是整数,已婚married的值是逻辑值,薪水salary是空值或浮点数。我们可以理解成Android小组有Bob和Kate两个成员。

Java反射机制基础

Java是一门很强大的语言,它的反射机制几乎让程序猿们可以为所欲为,刚理解完反射机制的时候我感觉自己掌握了一门不可思议的技术,逼格一下提高了一个档次,然后大神告诉我图样图森破,后来我才发现自己激动过火儿了,因为java程序猿一般都会告诉我:不认识女人,还能不认识反射吗。那么什么是反射机制?反射机制(Java Reflection)是在java运行状态中可以动态地解析一个类的结构,调用一个类的属性和方法,甚至可以修改一个类的私有private属性的值等的一种机制。怎么样,有没有操作内存的感觉!

由于反射机制的东西太多,这里只简单讲解下我们用反射解析json时用到的东西。照样是先看一个例子:

package test.reflection;

/**
 * Created by Chelly on 15/9/23.
 */
public class Person {
    /**
     * 姓名
     */
    private String name;
    /**
     * 年龄
     */
    private int age;
    /**
     * 婚否
     */
    private boolean married;
    /**
     * 工资
     */
    private double salary;

    public Person() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public boolean isMarried() {
        return married;
    }

    public void setMarried(boolean married) {
        this.married = married;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", married=" + married +
                ", salary=" + salary +
                '}';
    }

    /**
     * 自我介绍
     *
     * @param ending 结束语
     */
    private void introduce(String ending) {
        StringBuilder sb = new StringBuilder();
        sb.append("Hi,I'm " + getName() + "!");
        sb.append("I'm " + getAge() + " years old!");
        sb.append("I'm " + (isMarried() ? "" : "not ") + "married!");
        sb.append(ending == null ? "" : ending);
        System.out.println(sb.toString());
    }
}

这是一个Person类,有name、age、married、salary四个私有属性,由于大家都比较害羞,连自我介绍方法introduce也是私有的,为了让团队熟悉起来,我们要鼓励大家自我介绍,那我们先拿年纪最小的Bob开刀吧!为了打开小Bob的心扉,我们暂时先把Person类的introduce函数改成public的公有方法,然后让小Bob自己练习下自我介绍:

package test.reflection;

import android.app.Activity;
import android.os.Bundle;

/**
 * Created by Chelly on 15/9/23.
 */
public class TestActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //先初始化Bob
        Person person = new Person();
        person.setName("Bob");
        person.setAge(14);
        person.setMarried(false);
        person.setSalary(0f);
        //让Bob做自我介绍
        person.introduce("I'm shy!");
    }
}

执行上面的代码打印结果如下:


由于Bob自己承认了很害羞,他只愿意开口自我介绍一次,所以我们再把Person的introduce函数改成私有private方法,大家又变的害羞了,接着我们要想办法用反射机制让大家再次开口自我介绍,并且我们还会在大家的名字加上Little字样以显得亲切,这样我们需要对运行时Person的实例进行如下操作了:

package test.reflection;

import android.app.Activity;
import android.os.Bundle;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Created by Chelly on 15/9/23.
 */
public class TestActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //先初始化Bob
        Person person = new Person();
        person.setName("Bob");
        person.setAge(14);
        person.setMarried(false);
        person.setSalary(0f);
        letPersonIntroduce(person);
    }

    /**
     * 利用反射机制修改Person私有属性name和调用私有方法introduce
     *
     * @param person 被修改和调用实例
     */
    public void letPersonIntroduce(Person person) {

        try {
            //获取Person类的Class实例
            Class clazz = Person.class;
            //获取Person类中申明的所有属性
            Field[] fields = clazz.getDeclaredFields();
            //遍历Person类属性
            for (Field field : fields) {
                //设置该属性可以被访问和改写,public和protected可以不用设置
                field.setAccessible(true);
                //如果属性名是name,我们在要修改的Person实例的值前面加上Little 字样
                if ("name".equals(field.getName())) {
                    //修改name属性的值
                    field.set(person, "Little " + field.get(person));
                }
                //打印Person实例的属性和值
                System.out.println(field.getName() + "=" + field.get(person));
            }
            //获取Person类申明的introduce方法,该方法第一个参数是方法名,后面是该方法参数类型Class的不定长度参数
            //即有几个参数就要依次传入几个参数类型对应的Class
            Method method = clazz.getDeclaredMethod("introduce", String.class);
            //设置该方法可以被访问
            method.setAccessible(true);
            //设置被修改Person类实例的自我介绍结束语
            String ending = "I'm not shy any more!!!";
            //调用被修改Person类实例的introduce方法,第一个参数是被修改的实例,后面依次是具体参数的值的不定长度参数
            method.invoke(person, ending);
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

运行结果如下:


从结果可以看出我们不仅打印出了私有变量的名称和值,而且还修改了私有变量name的值(Bob改成Little Bob),甚至违背Person的本意擅自调用了私有方法introduce,普通方法无法实现的功能这里都实现了,已经很逆天了,大家可以直观的看到差别,这也正是为什么我要把例子的属性和方法都设为私有的原因,然而反射机制的功能远不止这些,上例中的Bob实例可以直接通过反射机制创建,还可以知道变量和方法的修饰符、方法参数的类型和返回类型,类实现的接口等等,有兴趣的可以研究研究。

反射解析Json

现在我们已经基本了解Json和Java的反射机制了,左手屠龙,右手倚天,可以试着挑战怪蜀黍救出小萝莉了。挑战前需要先制定好方案,基本思路是为需要解析成的类定义好属性和属性的set方法,属性名需要和要解析的json数据键对应,属性类型和json数据的值类型对应,用List对应json数组,然后用反射机制遍历类的属性,根据属性名称去获取json对应的值并赋值。由于是对象的属性同样需要解析,所以需要采用递归算法完成所有属性的赋值。我们先写一个通用的使用对象的set方法给属性赋值的函数,请看下面的代码:

    /**
     * 设置实例类的属性
     *
     * @param obj      要被赋值的实例类
     * @param field    要被赋值的属性
     * @param valueObj 要被赋值的属性的值
     * @throws SecurityException
     * @throws NoSuchMethodException
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     */
    private static void setProperty(Object obj, Field field, Object valueObj)
            throws SecurityException, IllegalAccessException,
            InvocationTargetException {

        try {
            Class<?> clazz = obj.getClass();
            //获取类的setXxx方法,xxx为属性
            Method method = clazz.getDeclaredMethod(
                    "set"
                            + field.getName().substring(0, 1)
                            .toUpperCase(Locale.getDefault())
                            + field.getName().substring(1), field.getType());
            //设置set方法可访问
            method.setAccessible(true);
            //调用set方法为类的实例赋值
            method.invoke(obj, valueObj);
        } catch (NoSuchMethodException e) {
            Log.d(TAG,
                    "method [set"
                            + field.getName().substring(0, 1)
                            .toUpperCase(Locale.getDefault())
                            + field.getName().substring(1) + "] not found");
        } catch (IllegalArgumentException e) {
            Log.d(TAG,
                    "method [set"
                            + field.getName().substring(0, 1)
                            .toUpperCase(Locale.getDefault())
                            + field.getName().substring(1)
                            + "] illegal argument:" + valueObj + "," + e.getMessage());
        }
    }

在setProperty方法中,分别传入三个参数:被修改的对象,被修改的属性和其对应的值,由于set方法我们采用camelCase(小驼峰)命名法,简单来说就是名字首单词的首字母小写,其它单词的首字母大写,这样我们就能很好的用反射得到set方法的具体名称,并调用该方法为对象赋值。接着我们将要解析的类和JsonObject对应解析:

   /**
     * 解析JSONObject对象到具体类,递归算法
     *
     * @param clazz      和JSON对象对应的类的Class,必须拥有setXxx()函数,其中xxx为属性
     * @param jsonObject 被解析的JSON对象
     * @return 返回传入的Object对象实例
     * @throws ClassNotFoundException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     * @throws SecurityException
     * @throws InstantiationException
     * @throws JSONException
     */
    public static <T> T parseJson2Object(Class<T> clazz, JSONObject jsonObject)
            throws ClassNotFoundException, SecurityException,
            IllegalAccessException, InvocationTargetException,
            InstantiationException, JSONException {

        //获取clazz的实例
        T obj = clazz.newInstance();
        // 获取属性列表
        Field[] fields = clazz.getDeclaredFields();
        // 遍历每个属性,如果为基本类型和String则直接赋值,如果为List则得到每个Item添加后再赋值,如果是其它类则得到类的实例后赋值
        for (Field field : fields) {
            // 设置属性可操作
            field.setAccessible(true);
            // 获取字段类型
            Class<?> typeClazz = field.getType();
            // 是否基础变量
            if (typeClazz.isPrimitive()) {
                setProperty(obj, field, jsonObject.opt(field.getName()));
            } else {
                // 得到类型实例
                Object typeObj = typeClazz.newInstance();
                // 是否为List
                if (typeObj instanceof List) {
                    // 得到类型的结构,如:java.util.ArrayList<com.xxx.xxx>
                    Type type = field.getGenericType();
                    ParameterizedType pt = (ParameterizedType) type;
                    // 获得List元素类型
                    Class<?> dataClass = (Class<?>) pt.getActualTypeArguments()[0];
                    // 得到List的JSONArray数组
                    JSONArray jArray = jsonObject.getJSONArray(field.getName());
                    // 将每个元素的实例类加入到类型的实例中
                    for (int i = 0; i < jArray.length(); i++) {
                        //对于数组,递归调用解析子元素
                        ((List<Object>) typeObj).add(parseJson2Object(dataClass,
                                jsonObject.getJSONArray(field.getName())
                                        .getJSONObject(i)));
                    }
                    setProperty(obj, field, typeObj);
                }
                // 是否为String
                else if (typeObj instanceof String) {
                    setProperty(obj, field, jsonObject.opt(field.getName()));
                }
                // 是否为其它对象
                else {
                    //递归解析对象
                    setProperty(
                            obj,
                            field,
                            parseJson2Object(typeClazz,
                                    jsonObject.getJSONObject(field.getName())));
                }
            }
        }

        return obj;
    }
这里我们借助了android的JsonObject和JsonArray,主要做的是将JsonObject的值尽量的映射到对象的属性上去,如果JsonObject拥有的属性而被影射的类没有定义则会继续,如果有人需要强制保证数据的完整性,可以在setProperty方法中把捕获到的NoSuchMethodException再次抛出。该方法中我们根据函数的类型来进行不同的操作,如果是基础变量或String类型就直接赋值,如果是List我们递归调用本函数将解析的结果加入到List中再赋值,如果是普通对象我们同样递归调用本函数再将结果赋值。接下来我们对上面的方法再进行一次重载,使其可以直接将json字符串作为参数使用:
    /**
     * 解析JSON字符串到具体类
     *
     * @param clazz 和JSON对象对应的类的Class,必须拥有setXxx()函数,其中xxx为属性
     * @param json  被解析的JSON字符串
     * @return 返回传入的Object对象实例
     * @throws ClassNotFoundException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     * @throws SecurityException
     * @throws InstantiationException
     * @throws JSONException
     */
    public static <T> T parseJson2Object(Class<T> clazz, String json)
            throws ClassNotFoundException, SecurityException,
            IllegalAccessException, InvocationTargetException,
            InstantiationException, JSONException {

        JSONObject jsonObject = new JSONObject(json);
        return parseJson2Object(clazz, jsonObject);
    }

最后我们把这些方法封装到一个工具类,命名为JsonParser,封装后代码如下:

package test.reflection;

import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Locale;

/**
 * 利用反射解析JSON对象到具体类
 */
public class JsonParser {

    private static final String TAG = "JsonParser";

    /**
     * 解析JSON字符串到具体类
     *
     * @param clazz 和JSON对象对应的类的Class,必须拥有setXxx()函数,其中xxx为属性
     * @param json  被解析的JSON字符串
     * @return 返回传入的Object对象实例
     * @throws ClassNotFoundException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     * @throws SecurityException
     * @throws InstantiationException
     * @throws JSONException
     */
    public static <T> T parseJson2Object(Class<T> clazz, String json)
            throws ClassNotFoundException, SecurityException,
            IllegalAccessException, InvocationTargetException,
            InstantiationException, JSONException {

        JSONObject jsonObject = new JSONObject(json);
        return parseJson2Object(clazz, jsonObject);
    }

    /**
     * 解析JSONObject对象到具体类,递归算法
     *
     * @param clazz      和JSON对象对应的类的Class,必须拥有setXxx()函数,其中xxx为属性
     * @param jsonObject 被解析的JSON对象
     * @return 返回传入的Object对象实例
     * @throws ClassNotFoundException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     * @throws SecurityException
     * @throws InstantiationException
     * @throws JSONException
     */
    public static <T> T parseJson2Object(Class<T> clazz, JSONObject jsonObject)
            throws ClassNotFoundException, SecurityException,
            IllegalAccessException, InvocationTargetException,
            InstantiationException, JSONException {

        //获取clazz的实例
        T obj = clazz.newInstance();
        // 获取属性列表
        Field[] fields = clazz.getDeclaredFields();
        // 遍历每个属性,如果为基本类型和String则直接赋值,如果为List则得到每个Item添加后再赋值,如果是其它类则得到类的实例后赋值
        for (Field field : fields) {
            // 设置属性可操作
            field.setAccessible(true);
            // 获取字段类型
            Class<?> typeClazz = field.getType();
            // 是否基础变量
            if (typeClazz.isPrimitive()) {
                setProperty(obj, field, jsonObject.opt(field.getName()));
            } else {
                // 得到类型实例
                Object typeObj = typeClazz.newInstance();
                // 是否为List
                if (typeObj instanceof List) {
                    // 得到类型的结构,如:java.util.ArrayList<com.xxx.xxx>
                    Type type = field.getGenericType();
                    ParameterizedType pt = (ParameterizedType) type;
                    // 获得List元素类型
                    Class<?> dataClass = (Class<?>) pt.getActualTypeArguments()[0];
                    // 得到List的JSONArray数组
                    JSONArray jArray = jsonObject.getJSONArray(field.getName());
                    // 将每个元素的实例类加入到类型的实例中
                    for (int i = 0; i < jArray.length(); i++) {
                        //对于数组,递归调用解析子元素
                        ((List<Object>) typeObj).add(parseJson2Object(dataClass,
                                jsonObject.getJSONArray(field.getName())
                                        .getJSONObject(i)));
                    }
                    setProperty(obj, field, typeObj);
                }
                // 是否为String
                else if (typeObj instanceof String) {
                    setProperty(obj, field, jsonObject.opt(field.getName()));
                }
                // 是否为其它对象
                else {
                    //递归解析对象
                    setProperty(
                            obj,
                            field,
                            parseJson2Object(typeClazz,
                                    jsonObject.getJSONObject(field.getName())));
                }
            }
        }

        return obj;
    }

    /**
     * 设置实例类的属性
     *
     * @param obj      要被赋值的实例类
     * @param field    要被赋值的属性
     * @param valueObj 要被赋值的属性的值
     * @throws SecurityException
     * @throws NoSuchMethodException
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     */
    private static void setProperty(Object obj, Field field, Object valueObj)
            throws SecurityException, IllegalAccessException,
            InvocationTargetException {

        try {
            Class<?> clazz = obj.getClass();
            //获取类的setXxx方法,xxx为属性
            Method method = clazz.getDeclaredMethod(
                    "set"
                            + field.getName().substring(0, 1)
                            .toUpperCase(Locale.getDefault())
                            + field.getName().substring(1), field.getType());
            //设置set方法可访问
            method.setAccessible(true);
            //调用set方法为类的实例赋值
            method.invoke(obj, valueObj);
        } catch (NoSuchMethodException e) {
            Log.d(TAG,
                    "method [set"
                            + field.getName().substring(0, 1)
                            .toUpperCase(Locale.getDefault())
                            + field.getName().substring(1) + "] not found");
        } catch (IllegalArgumentException e) {
            Log.d(TAG,
                    "method [set"
                            + field.getName().substring(0, 1)
                            .toUpperCase(Locale.getDefault())
                            + field.getName().substring(1)
                            + "] illegal argument:" + valueObj + "," + e.getMessage());
        }
    }
}

我们来测试一下劳动成果,拿最开始的json来测试,首先我们需要为其构造一个影射的类,命名为Team,该类拥有persons和team两个属性,其中persons是Person类的数组集合,由于前面我们已经定义过了,这里直接拿来用:

package test.reflection;

import java.util.ArrayList;

/**
 * Created by Chelly on 15/9/24.
 */
public class Team {
    //人员
    private ArrayList<Person> persons;
    //小组名
    private String team;

    public ArrayList<Person> getPersons() {
        return persons;
    }

    public void setPersons(ArrayList<Person> persons) {
        this.persons = persons;
    }

    public String getTeam() {
        return team;
    }

    public void setTeam(String team) {
        this.team = team;
    }

    @Override
    public String toString() {
        return "Team{" +
                "persons=" + persons +
                ", team='" + team + '\'' +
                '}';
    }
}

最后激动人心的时刻到来了,马上就可以打败怪蜀黍,救出小萝莉,迎娶白富美,出任CEO,走上人生巅峰了,到时候就不用在苦逼的写代码了,想想还有点小激动呢。来看测试代码:

package test.reflection;

import android.app.Activity;
import android.os.Bundle;

import org.json.JSONException;

import java.lang.reflect.InvocationTargetException;

/**
 * Created by Chelly on 15/9/23.
 */
public class TestActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //先初始化Bob
//        Person person = new Person();
//        person.setName("Bob");
//        person.setAge(14);
//        person.setMarried(false);
//        person.setSalary(0f);
//        letPersonIntroduce(person);

        //测试JsonParser
        try {
            String json = "{\"persons\":[{\"name\":\"Bob\",\"age\":14,\"married\":false,\"salary\":null},{\"name\":\"Kate\",\"age\":26,\"married\":true,\"salary\":8888.88}],\"team\":\"Android\"}";
            System.out.println(JsonParser.parseJson2Object(Team.class, json));
            json = "{\"name\":null,\"age\":14,\"married\":false,\"salary\":null}";
            System.out.println(JsonParser.parseJson2Object(Person.class, json));
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

//    /**
//     * 利用反射机制修改Person私有属性name和调用私有方法introduce
//     *
//     * @param person 被修改和调用实例
//     */
//    public void letPersonIntroduce(Person person) {
//
//        try {
//            //获取Person类的Class实例
//            Class clazz = Person.class;
//            //获取Person类中申明的所有属性
//            Field[] fields = clazz.getDeclaredFields();
//            //遍历Person类属性
//            for (Field field : fields) {
//                //设置该属性可以被访问和改写,public和protected可以不用设置
//                field.setAccessible(true);
//                //如果属性名是name,我们在要修改的Person实例的值前面加上Little 字样
//                if ("name".equals(field.getName())) {
//                    //修改name属性的值
//                    field.set(person, "Little " + field.get(person));
//                }
//                //打印Person实例的属性和值
//                System.out.println(field.getName() + "=" + field.get(person));
//            }
//            //获取Person类申明的introduce方法,该方法第一个参数是方法名,后面是该方法参数类型Class的不定长度参数
//            //即有几个参数就要依次传入几个参数类型对应的Class
//            Method method = clazz.getDeclaredMethod("introduce", String.class);
//            //设置该方法可以被访问
//            method.setAccessible(true);
//            //设置被修改Person类实例的自我介绍结束语
//            String ending = "I'm not shy any more!!!";
//            //调用被修改Person类实例的introduce方法,第一个参数是被修改的实例,后面依次是具体参数的值的不定长度参数
//            method.invoke(person, ending);
//        } catch (InvocationTargetException e) {
//            e.printStackTrace();
//        } catch (NoSuchMethodException e) {
//            e.printStackTrace();
//        } catch (IllegalAccessException e) {
//            e.printStackTrace();
//        }
//    }
}

我们分别测试了Team和Person两个类,在测试Person的时候故意把json字符串的name改成了null,这时会出现illegal argument提示,解析出的Person实例的name属性将会是默认值,全部结果如下:


上图中分别打印出了解析后的Team和Person两个类,可以看出对于json字符串中的null值将使用定义该类时的默认值,还有无论解析成功或失败该函数都会返回一个类的实例,所以接下来使用时还需要判断一下某个属性是否是默认值来确定解析是否成功,比如Team中的team默认值是null,只要判断到解析后的team!=null就算成功,当然还要保证json数据的完整性,另外一个需要注意的地方是由于我们利用的是set函数赋值,做代码混淆的时候记住不要混淆这些类的属性和方法名,至此我们已经完成了全部的工作,利用Json和Java反射机制打造出了专属自己的JsonParser这把利剑,虽然还存在很多不足,由于本人的要求不高,经本人实测,对于一般的项目已经够用了,当然大家使用Gson之类成熟的第三方库更好,也可以按照自己的需求进行修改,本篇文章只为起到一个抛砖引玉的作用。


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值