Spring core源码

  • ClassUtils.

getDefaultClassLoader : 获取默认类加载器
forName :初始化类
resolveClassName 和forName只有处理异常的差别
resolvePrimitiveClassName 处理int等原始几个类
isPresent 是否能初始化该类
getUserClass :获取class,处理了 CGLIB_CLASS_SEPARATOR 的类
isCacheSafe :
getShortName : 根据类的完整名获取短名称 java.lang.String --> String 处理了CGLIB_CLASS_SEPARATOR,不会处理开头字母
getShortNameAsProperty : 根据类得到名称,并且会转化成峰坨格式 ,小写字母开头
getClassFileName: 得到有.class文件名称
getPackageName : 获取类的包名,没有类名
getQualifiedName : 获取完整的类名 java.lang.Double[]
getQualifiedMethodName : 完整的方法名
getDescriptiveType : 类签名信息,一般返回完整类名 com.sun.proxy.$Proxy4 implementing com.bydbl.proxy.staticmodel.UserService
matchesTypeName:类是否匹配名称
getConstructorIfAvailable:返回构造器,没有合适的返回空
hasMethod : 是否有方法
getMethod : 查找方法,没找到会报错 IllegalStateException
getMethodIfAvailable :返回方法
getMethodCountForName : 递归返回指定方法名出现的总数,覆写
hasAtLeastOneMethodWithName: 
getMostSpecificMethod :获取最适合的方法
getStaticMethod : 
isPrimitiveWrapper : 是否是基础类型的包装类
isAssignable: 是否是签名,是否为其子类,处理了基础类型
isAssignableValue : 和isAssignable差不多,这个传入的是Object,参数不一样
convertResourcePathToClassName: / --> .
convertClassNameToResourcePath : . --> /
addResourcePathToPackagePath  :
classPackageAsResourcePath: . -->
classNamesToString: 多个类名称的数组格式,以逗号分隔
toClassArray : 顾名思义
getAllInterfaces : 和getAllInterfacesForClass,getAllInterfacesAsSet 一样,就是多了个判空报错
getAllInterfacesForClassAsSet : 递归获取所有的接口
ClassUtils.createCompositeInterface(NaiveWaiter.class.getInterfaces(),ClassUtils.getDefaultClassLoader()) 创建接口的代理对象
determineCommonAncestor : 找到共同的祖先,继承关系
isCglibProxyClass: -->isCglibProxyClassName , 根据是否包含 CGLIB_CLASS_SEPARATOR 标识来判断
  • IdGenerator

其有3个实现类,SimpleIdGenerator ,JdkIdGenerator,AlternativeJdkIdGenerator

        IdGenerator idGenerator = new SimpleIdGenerator();
        for(int i =0 ;i<500;i++) {
            System.out.println(idGenerator.generateId().toString());
        }
输出:
00000000-0000-0000-0000-000000000001
00000000-0000-0000-0000-000000000002
00000000-0000-0000-0000-000000000003
00000000-0000-0000-0000-000000000004

        System.out.println("------------jdk---------");

        idGenerator = new JdkIdGenerator();
        for(int i =0 ;i<5;i++) {
            System.out.println(idGenerator.generateId().toString());
        }
输出:
00d1ca6a-a5aa-40a0-8fef-2b138e3d3634
9466039f-bbfb-480b-bd06-0a50b4df1889

        System.out.println("-----------alter---------");
        idGenerator = new AlternativeJdkIdGenerator();
        for(int i =0;i<5;i++) {
            System.out.println(idGenerator.generateId().toString());
        }
输出:
dd57c906-7ea5-015c-8a8a-14ed117e5710
c562d92d-f035-6f74-db40-b284148e4c7f
  • CollectionUtils:

arrayToList :参数是Object,传入数组,数组转List
mergeArrayIntoCollection : 数组合并到List,参数是Object,传入数组
mergePropertiesIntoMap : Properties 合并到Map
contains :是否包含,用equal比较
containsInstance : 是否包含,用== 比较,比较实例
containsAny : 第一个参考是否包含后一个参数中的任何一个
findFirstMatch :找到第一个参数包含第二个参数的第一个匹配对象
findValueOfType : 在一个集合中,包含各种类的集合,里面找到指定的类型
hasUniqueObject : 判断给定的集合是否为只包含一个值
findCommonElementType : 有任何一个不同类型就返回空,全部相同就返回对象类型,hasUniqueObject 是用== 比较,这个是用getClass比较
toArray : 转数组,可以是子类的集合转父类的数组
toIterator :
toMultiValueMap :  Map<K, List<V>> 转 MultiValueMap<K, V>
unmodifiableMultiValueMap : MultiValueMap 变成线程安全的 MultiValueMap
  • ConcurrencyThrottleSupport:

控制并发,可见 ConcurrencyThrottleInterceptor

    public ConcurrencyThrottleInterceptor() {
                //设置并发数
        setConcurrencyLimit(1);
    }

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
              //进入前拿锁,数量加1
        beforeAccess();
        try {
            return methodInvocation.proceed();
        }
        finally {
                        //退出后释放,数量减1
            afterAccess();
        }
    }
  • ConcurrentReferenceHashMap

并发Hash Map,其键和值可以是弱引用或者软引用。
在JVM回收时会比硬引用更早回收
http://yuhuiblog6338999322098842.iteye.com/blog/2375746

  • DefaultPropertiesPersister

属性Properties的持久化,可以转成XML

        Properties p = new Properties();
        p.load(Files.newInputStream(Paths.get(fileName)));
        DefaultPropertiesPersister pp = new DefaultPropertiesPersister();
        pp.storeToXml(p,Files.newOutputStream(Paths.get(outXml)),"备注信息");

结果如下:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>备注信息</comment>
<entry key="user.uuid">"用户UUID"</entry>
<entry key="server.port">8093</entry>
<entry key="user.age">100</entry>
<entry key="user.desc">用户信息描述</entry>
<entry key="home.desc">dev: I'm living in ${home.province} ${home.city}.</entry>
<entry key="home.city"/>
<entry key="home.province">ZheJiang</entry>
<entry key="user.id">50</entry>
</properties>

也可以读取已经生成的XML到properties

    @Test
    public void loadFromXml() throws IOException {
        DefaultPropertiesPersister pp = new DefaultPropertiesPersister();
        Properties p = new Properties();
        pp.loadFromXml(p,Files.newInputStream(Paths.get(outXml)));
        System.out.println(p);
    }
  • DigestUtils 数字签名 MD5

    @Test
    public void md5Digest() throws UnsupportedEncodingException {
        String value = "123";
        System.out.println(new String(DigestUtils.md5Digest(value.getBytes("utf-8"))));// ,�b�Y�[�K��-#Kp
        System.out.println(DigestUtils.md5DigestAsHex(value.getBytes("utf-8")));//202cb962ac59075b964b07152d234b70
    }
  • FileCopyUtils

复制文件的方法,各种复制...
和 commons-io的IOUtil差不多,但是这个只有复制功能,commons-io的IOUtils和FileUtils功能

  • FileSystemUtils :复制或删除文件(文件夹)操作,递归
  • LinkedCaseInsensitiveMap : 里面存的是一些大小写的key,但是get 和remove的时候可以不区分大小写
  • LinkedMultiValueMap : 线程不安全,实现了MultiValueMap数据结构,Map<K, List<V>>
  • MethodInvoker:方法调用,静态方法也可以反射调用
    @Test
    public void invoker() throws NoSuchMethodException, ClassNotFoundException, InvocationTargetException, IllegalAccessException {
        MethodInvoker methodInvoker = new MethodInvoker();
        methodInvoker.setTargetClass(User.class);
        methodInvoker.setStaticMethod("com.bydbl.proxy.util.User.hello");
        methodInvoker.prepare();
        Object o = methodInvoker.invoke();
    }
package com.bydbl.proxy.util;

public class User {

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

    public static void hello() {
        System.out.println("hello...");
    }
}
  • MimeType :处理HTTP的Mime类型

MimeTypeUtils

public static final MimeType X_JAVA_OBJECT = MimeType.valueOf("application/x-java-object")
private static final MimeType[] DEFAULT_SMILE_MIME_TYPES = new MimeType[] {
            new MimeType("application", "x-jackson-smile", StandardCharsets.UTF_8),
            new MimeType("application", "*+x-jackson-smile", StandardCharsets.UTF_8)};
  • NumberUtils

convertNumberToTargetClass : 将数字类型转换成目标类
parseNumber:将字符串转换成目标类型的数字,重载了两个方法,可以指定 NumberFormat

    @Test
    public void convertNumberToTargetClass() {
        BigDecimal b = NumberUtils.convertNumberToTargetClass(11111111111111111111111D, BigDecimal.class);
        System.out.println(b);
        BigDecimal decimal = NumberUtils.parseNumber("1111111111111111111111111", BigDecimal.class);
        System.out.println(decimal);
        decimal = NumberUtils.parseNumber("999,999,999", BigDecimal.class, NumberFormat.getInstance());
        System.out.println(decimal);

    }
  • PathMatcher

AntPathMatcher
extractUriTemplateVariables --> 转换成我们需要的Map

        String path = "/hotels/{hotel}";
        String pattern = "/hotels/1";
        PathMatcher pm = new AntPathMatcher();
        Map<String, String> map = pm.extractUriTemplateVariables(path,pattern );
        System.out.println(map.toString());//输出:{hotel=1}
  • PatternMatchUtils

simpleMatch

  • PropertyPlaceholderHelper

将给定的模板替换掉,一般处理我们的属性文件的占位符

        Properties p = new Properties();
        p.put("name","aaaaa");
        p.put("age","23");
        p.put("address","china");
        PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${","}");
        System.out.println(helper.replacePlaceholders("${name}", p)); //aaaaa
        System.out.println(helper.replacePlaceholders("aaa ${name} bbb=${age} ccc = ${address}",p)); //aaa aaaaa bbb=23 ccc = china
        helper = new PropertyPlaceholderHelper("{","}");
        System.out.println(helper.replacePlaceholders("${name}", p));//$aaaaa

  • ReflectionUtils

有关反射的优先从此类中找方法,很多实用的,举一个小例子

findField :查找属性
    @Test
    public void setField() {
        User user = new User();
        user.setName("aaa");
        user.setAge(18);
        user.setBirth(new Date());

        Field field = ReflectionUtils.findField(User.class, "birth");
        ReflectionUtils.makeAccessible(field);
        Date birth = (Date)ReflectionUtils.getField(field, user);
        System.out.println(birth);
    }
  • ResourceUtils

处理了classpath:file,war,jar,file,zip等路径的工具类

    @Test
    public void getFile() throws FileNotFoundException {
        String location = "classpath:application.properties";
        //输出:D:\mytest\proxy\target\classes\application.properties
        System.out.println(ResourceUtils.getFile(location).getPath());
    }
  • InstanceComparator 用法实例

/**
 * FileName :InstanceComparatorTest
 * Author :zengzhijun
 * Date : 2018/5/21 10:58
 * Description:
 */
package org.springframework.util.comparator;

import org.aspectj.lang.annotation.*;
import org.junit.Test;
import org.springframework.core.convert.converter.ConvertingComparator;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/**
 * <pre>
 * 下例先按 输入到 InstanceComparator 的先后顺序排队再根据方法名称排序
 * 因为要获取方法的注解,所以要用 ConvertingComparator 先转换到对应的注解
 * 参考 ReflectiveAspectJAdvisorFactory 内的方法
 *
 * @author : zengzhijun
 * @date : 2018/5/21 11:25
 **/
public class InstanceComparatorTest {


    private static final Comparator<Method> METHOD_COMPARATOR;

    static {
        Comparator<Method> adviceKindComparator =  new ConvertingComparator<>(
                new InstanceComparator<>(
                        Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
                 method -> {
                    Annotation[] annotations = method.getDeclaredAnnotations();
                    return annotations[0];
                });
        Comparator<Method> methodNameComparator = new ConvertingComparator<>(Method::getName);
        METHOD_COMPARATOR = adviceKindComparator.thenComparing(methodNameComparator);
    }

    @Test
    public void comparate() {
        Method[] methods = ComparatorService.class.getDeclaredMethods();
        List<Method> methodList =  Arrays.asList(methods);
        methodList.sort(METHOD_COMPARATOR);
        methodList.stream().map(Method::getName).forEach(System.out::println);
    }

    class ComparatorService{

        @Around("around")
        public void around() {

        }

        @Around("bround")
        public void bround() {

        }

        @AfterThrowing
        public void afterThrowing() {

        }

        @AfterReturning
        public void afterReturning() {

        }

        @Before("before")
        public void before() {

        }


    }
}

  • AnnotationBeanUtils.copyPropertiesToBean
/**
 * FileName :AnnotationBeanUtilsTest
 * Author :zengzhijun
 * Date : 2018/5/21 14:35
 * Description:
 */
package org.springframework.beans.annotation;

import org.aspectj.lang.annotation.Before;
import org.junit.Test;
import org.springframework.core.annotation.AnnotationUtils;


/**
 * <pre>
 * AnnotationBeanUtils : 将一个注解的信息对应到一个实体类的属性上
 * 本例展示将某一个方法上的注解信息转换到一个实体类上
 * 另: AnnotationBeanUtils 支持传入一个 StringValueResolver 和 excludedProperties 两个参数
 * 这样可以做更多处理
 * 
 * @author : zengzhijun
 * @date : 2018/5/21 14:45
 **/
public class AnnotationBeanUtilsTest {

    @Test
    public void copyPropertiesToBean() throws NoSuchMethodException {
        ServiceA serviceA = new ServiceA();
        Before annotation = AnnotationUtils.findAnnotation(ServiceA.class.getMethod("before"), Before.class);

        BeforeAttribute ba = new BeforeAttribute();
        AnnotationBeanUtils.copyPropertiesToBean(annotation,ba);
        System.out.println(ba);
    }

    class ServiceA {
        @Before(value = "aaa",argNames = "args")
        public void before() {

        }
    }
    class ServiceB {
        @Before(value = "bbb",argNames = "argsBBB")
        public void before() {

        }
    }
    class BeforeAttribute{
        private String value;
        private String argNames;

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }

        public String getArgNames() {
            return argNames;
        }

        public void setArgNames(String argNames) {
            this.argNames = argNames;
        }

        @Override
        public String toString() {
            return "BeforeAttribute{" +
                    "value='" + value + '\'' +
                    ", args='" + argNames + '\'' +
                    '}';
        }
    }
}

  • BeanUtils
/*
 * FileName :BeanUtilsTest
 * Author :zengzhijun
 * Date : 2018/5/21 14:54
 * Description:
 */
package org.springframework.beans;

import org.junit.Test;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Date;

public class BeanUtilsTest {

    /**
     *初始化类,Spring5 考虑到了 Kotlin
     **/
    @Test
    public void instantiateClass() throws NoSuchMethodException {
        PropertyValue propertyValue = BeanUtils.instantiateClass(PropertyValue.class.getConstructor(String.class, Object.class),"name","aaa");
        propertyValue.setAttribute("attr1","value1");
        System.out.println(propertyValue);
    }

    @Test
    public void instantiateClass1() throws ClassNotFoundException {
        String className = Person.class.getCanonicalName();
        Class<?> clazz = ClassUtils.forName(className, ClassUtils.getDefaultClassLoader());
        Person person = BeanUtils.instantiateClass(clazz, Person.class);
        person.setAge(18);
        person.setName("name");
        System.out.println(person);
    }

    @Test
    public void findMethod() {
        //findMethod 不会递归查找父类
        Method sing = BeanUtils.findMethod(SuperMan.class, "fly");
        //会递归查找方法
        Method sing1 = BeanUtils.findDeclaredMethod(SuperMan.class, "sing");
        System.out.println(sing);
        System.out.println(sing1);

        //找到参数最小的同名方法,相当实用
        Method fly = BeanUtils.findDeclaredMethodWithMinimalParameters(SuperMan.class, "fly");
        SuperMan superMan = new SuperMan();

        ReflectionUtils.invokeMethod(fly, superMan);
    }

    @Test
    public void resolveSignature() {
        //methodName[([arg_list])
        //找到一个参数为空的方法
        Method exactlyFly = BeanUtils.resolveSignature("fly()", SuperMan.class);
        System.out.println(exactlyFly);
        //找到一个方法名为play的,参数最小的会返回
        Method play = BeanUtils.resolveSignature("play", SuperMan.class);
        System.out.println(play);
        //找到一个参数名为指定类型(完整的类型名称)的指定方法
        Method fly1 = BeanUtils.resolveSignature("fly(java.lang.String)", SuperMan.class);
        System.out.println(fly1);

        Method fly2 = BeanUtils.resolveSignature("fly(java.lang.String,java.lang.String)", SuperMan.class);
        System.out.println(fly2);

    }

    @Test
    public void getPropertyDescriptors() {
        //获取类的 getter 和 setter 方法,神器啊.
        PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(SuperMan.class);
        for (PropertyDescriptor descriptor : descriptors) {
            Method readMethod = descriptor.getReadMethod();
            Method writeMethod = descriptor.getWriteMethod();
            if (readMethod == null) {
                System.out.println("ignore readMethod : " + descriptor.getName());
                continue;
            }
            if (writeMethod == null) {
                System.out.println("ignore writeMethod : " + descriptor.getName());
                continue;
            }
            System.out.println("readMethod Name: " + readMethod.getName());
            System.out.println("writeMethod Name: " + writeMethod.getName());
        }
    }


    @Test
    public void findPropertyType() {
        //获取类中的属性的类型值
        Class<?> age = BeanUtils.findPropertyType("age", Person.class);
        System.out.println(age);
    }

    @Test
    public void copyProperties() {
        //拷贝属性,最常用的方法
        SuperMan superMan = new SuperMan();
        superMan.setBirthday(new Date());
        superMan.setAge(18);
        superMan.setName("superman");

        SuperMan cloneMan = new SuperMan();
        //匹配上的属性全部复制
        BeanUtils.copyProperties(superMan,cloneMan);
        System.out.println(cloneMan);
        cloneMan = new SuperMan();
        //忽略 age 属性
        BeanUtils.copyProperties(superMan,cloneMan,"age");
        System.out.println(cloneMan);
        cloneMan = new SuperMan();
        //只复制 Person 里面的属性
        BeanUtils.copyProperties(superMan,cloneMan,Person.class);
        System.out.println(cloneMan);
    }
}

  • PropertyAccessorUtils

    @Test
    public void getPropertyName() {
        //输出:person
        System.out.println(PropertyAccessorUtils.getPropertyName("person[1]"));
        //是否是数组或者嵌套的属性格式
        System.out.println(PropertyAccessorUtils.isNestedOrIndexedProperty("person[1]"));
        System.out.println(PropertyAccessorUtils.isNestedOrIndexedProperty("person.age"));

        System.out.println(PropertyAccessorUtils.canonicalPropertyName("Asd_Wer"));
        System.out.println(PropertyAccessorUtils.canonicalPropertyName("asd_wer"));
        System.out.println(PropertyAccessorUtils.canonicalPropertyName("asdWer"));
        //输出:map[key]
        System.out.println(PropertyAccessorUtils.canonicalPropertyName("map[\"key\"]"));
        //true
        //true
        //Asd_Wer
        //asd_wer
        //asdWer
        //map[key]
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,928评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,748评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,282评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,065评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,101评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,855评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,521评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,414评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,931评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,053评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,191评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,873评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,529评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,074评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,188评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,491评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,173评论 2 357

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,673评论 18 139
  • 概述 org.springframework.context.ApplicationContext接口是Sprin...
    0d1b415a365b阅读 5,495评论 1 3
  • 从下定决心买车,看车,下定金,贷款,付全额,全过程20天,已经是我行人生中执行力最大化的一次。这一次的大额消费,却...
    王山三阅读 201评论 0 0
  • “伪球迷”可能就是那种偶尔也会热球看球,当然一般都是在大的赛事之时或日常游戏之中,但是不会像真正的球迷一样将球列为...
    _Arile_阅读 125评论 0 1
  • 你初学angular4.0,记录一下学习的零碎知识,以下对angular4.0都直接书写为ng ng的核心思想是组...
    洱月阅读 395评论 0 1