公司不是你家,领导不是你妈。本文已被 https://www.yourbatman.cn 收录,外面一并有 Spring 技术栈、MyBatis、JVM、中间件等小而美的 专栏 供以收费学习。关注公众号【BAT 的乌托邦】一一击破,深刻把握,回绝浅尝辄止。
前言
各位好,我是 A 哥 (YourBatman)。上篇文章:3. 懂了这些,方敢在简历上说会用 Jackson 写 JSON 聊完, 流式 API的写局部能够认为你已齐全把握了,本文理解它读的局部。
版本约定
- Jackson 版本:
2.11.0
- Spring Framework 版本:
5.2.6.RELEASE
- Spring Boot 版本:
2.3.0.RELEASE
小贴士:截止到本文,本系列 后面 所有示例都只仅仅导入
jackson-core
而已,后续若要新增 jar 包我会额定阐明,否则雷同
注释
什么叫读 JSON?就是把一个 JSON 字符串 解析为对象 or 树模型嘛,因而也称作解析 JSON 串。Jackson 底层流式 API 应用JsonParser
来实现 JSON 字符串 的解析。
最简应用 Demo
筹备一个 POJO:
@Data
public class Person {
private String name;
private Integer age;
}
测试用例:把一个 JSON 字符串绑定(封装)进一个 POJO 对象里
@Test
public void test1() throws IOException {String jsonStr = "{\"name\":\"YourBatman\",\"age\":18}";
Person person = new Person();
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {// 只有还没完结 "}",就始终读
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {String fieldname = jsonParser.getCurrentName();
if ("name".equals(fieldname)) {jsonParser.nextToken();
person.setName(jsonParser.getText());
} else if ("age".equals(fieldname)) {jsonParser.nextToken();
person.setAge(jsonParser.getIntValue());
}
}
System.out.println(person);
}
}
运行程序,输入:
Person(name=YourBatman, age=18)
胜利把一个 JSON 字符串的值解析到 Person 对象。你可能会疑难,怎么这么麻烦?那当然,这是底层流式 API,纯 手动档 嘛。你取得了性能,可不要失去一些便捷性嘛。
小贴士:底层流式 API 个别面向“专业人士”,利用级开发应用高阶 API
ObjectMapper
即可。当然,读完本系列就能让你齐全具备“专业人士”的实力????
JsonParser
针对不同的 value 类型,提供了十分多的办法用于理论值的获取。
间接 值获取:
// 获取字符串类型
public abstract String getText() throws IOException;
// 数字 Number 类型值 标量值(反对的 Number 类型参照 NumberType 枚举)public abstract Number getNumberValue() throws IOException;
public enum NumberType {INT, LONG, BIG_INTEGER, FLOAT, DOUBLE, BIG_DECIMAL};
public abstract int getIntValue() throws IOException;
public abstract long getLongValue() throws IOException;
...
public abstract byte[] getBinaryValue(Base64Variant bv) throws IOException;
这类办法可能会抛出异样:比方 value 值本不是数字但你调用了 getInValue()办法~
小贴士:如果 value 值是 null,像 getIntValue()、getBooleanValue()等这种间接获取办法是会抛出异样的,但 getText()不会
带默认值 的值获取,具备更好安全性:
public String getValueAsString() throws IOException {return getValueAsString(null);
}
public abstract String getValueAsString(String def) throws IOException;
...
public long getValueAsLong() throws IOException {return getValueAsLong(0);
}
public abstract long getValueAsLong(long def) throws IOException;
...
此类办法若碰到数据的转换失败时,不会抛出异样 ,把def
作为默认值返回。
组合办法
同 JsonGenerator
一样,JsonParser 也提供了 高钙片 组合办法,让你更加便捷的应用。
主动绑定
听起来像高级性能,是的,它必须依赖于 ObjectCodec
去实现,因为理论是全副委托给了它去实现的,也就是咱们最为相熟的 readXXX 系列办法:
咱们晓得,ObjectMapper 就是一个 ObjectCodec,它属于高级 API,本文显然不会用到 ObjectMapper 它喽,因而咱们本人手敲一个实现来实现此性能。
自定义一个 ObjectCodec,Person 类专用:用于把 JSON 串主动绑定到实例属性。
public class PersonObjectCodec extends ObjectCodec {
...
@SneakyThrows
@Override
public <T> T readValue(JsonParser jsonParser, Class<T> valueType) throws IOException {Person person = (Person) valueType.newInstance();
// 只有还没完结 "}",就始终读
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {String fieldname = jsonParser.getCurrentName();
if ("name".equals(fieldname)) {jsonParser.nextToken();
person.setName(jsonParser.getText());
} else if ("age".equals(fieldname)) {jsonParser.nextToken();
person.setAge(jsonParser.getIntValue());
}
}
return (T) person;
}
...
}
有了它,就能够实现咱们的 主动 绑定了,书写测试用例:
@Test
public void test3() throws IOException {String jsonStr = "{\"name\":\"YourBatman\",\"age\":18, \"pickName\":null}";
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {jsonParser.setCodec(new PersonObjectCodec());
System.out.println(jsonParser.readValueAs(Person.class));
}
}
运行程序,输入:
Person(name=YourBatman, age=18)
这就是 ObjectMapper 主动绑定的外围原理所在,其它更为弱小能力将在后续章节具体开展。
JsonToken
在上例解析过程中,有一个十分重要的角色,那便是:JsonToken。它示意解析 JSON 内容时,用于返回后果的根本 标记类型 的枚举。
public enum JsonToken {NOT_AVAILABLE(null, JsonTokenId.ID_NOT_AVAILABLE),
START_OBJECT("{", JsonTokenId.ID_START_OBJECT),
END_OBJECT("}", JsonTokenId.ID_END_OBJECT),
START_ARRAY("[", JsonTokenId.ID_START_ARRAY),
END_ARRAY("]", JsonTokenId.ID_END_ARRAY),
// 属性名(key)FIELD_NAME(null, JsonTokenId.ID_FIELD_NAME),
// 值(value)VALUE_EMBEDDED_OBJECT(null, JsonTokenId.ID_EMBEDDED_OBJECT),
VALUE_STRING(null, JsonTokenId.ID_STRING),
VALUE_NUMBER_INT(null, JsonTokenId.ID_NUMBER_INT),
VALUE_NUMBER_FLOAT(null, JsonTokenId.ID_NUMBER_FLOAT),
VALUE_TRUE("true", JsonTokenId.ID_TRUE),
VALUE_FALSE("false", JsonTokenId.ID_FALSE),
VALUE_NULL("null", JsonTokenId.ID_NULL),
}
为了辅助了解,A 哥用一个例子,输入各个局部高深莫测:
@Test
public void test2() throws IOException {String jsonStr = "{\"name\":\"YourBatman\",\"age\":18, \"pickName\":null}";
System.out.println(jsonStr);
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {while (true) {JsonToken token = jsonParser.nextToken();
System.out.println(token + "-> 值为:" + jsonParser.getValueAsString());
if (token == JsonToken.END_OBJECT) {break;}
}
}
}
运行程序,输入:
{"name":"YourBatman","age":18, "pickName":null}
START_OBJECT -> 值为:null
FIELD_NAME -> 值为:name
VALUE_STRING -> 值为:YourBatman
FIELD_NAME -> 值为:age
VALUE_NUMBER_INT -> 值为:18
FIELD_NAME -> 值为:pickName
VALUE_NULL -> 值为:null
END_OBJECT -> 值为:null
从左至右 解析,一一对应。各个局部用上面这张图能够简略示意进去:
小贴士:解析时请确保你的的 JSON 串是非法的,否则抛出
JsonParseException
异样
JsonParser 的 Feature
它是 JsonParser 的一个外部枚举类,共 15 个枚举值:
public enum Feature {AUTO_CLOSE_SOURCE(true),
ALLOW_COMMENTS(false),
ALLOW_YAML_COMMENTS(false),
ALLOW_UNQUOTED_FIELD_NAMES(false),
ALLOW_SINGLE_QUOTES(false),
@Deprecated
ALLOW_UNQUOTED_CONTROL_CHARS(false),
@Deprecated
ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER(false),
@Deprecated
ALLOW_NUMERIC_LEADING_ZEROS(false),
@Deprecated
ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS(false),
@Deprecated
ALLOW_NON_NUMERIC_NUMBERS(false),
@Deprecated
ALLOW_MISSING_VALUES(false),
@Deprecated
ALLOW_TRAILING_COMMA(false),
STRICT_DUPLICATE_DETECTION(false),
IGNORE_UNDEFINED(false),
INCLUDE_SOURCE_IN_LOCATION(true);
}
小贴士:枚举值均为 bool 类型,括号内为默认值
每个枚举值都管制着 JsonParser
不同的行为。上面分类进行解释
底层 I / O 流相干
自 2.10 版本后,应用
StreamReadFeature#AUTO_CLOSE_SOURCE
代替
Jackson 的流式 API 指的是 I / O 流,所以即便是 读,底层也是用 I / O 流(Reader)去读取而后解析的。
AUTO_CLOSE_SOURCE(true)
原理和 JsonGenerator 的 AUTO_CLOSE_TARGET(true)
一样,不再解释,详见上篇文章对应局部。
反对非标准格局
JSON 是有标准的,在它的标准里并没有形容到对正文的规定、对控制字符的解决等等,也就是说这些均属于 非标准 行为。比方这个 JSON 串:
{
"name" : "YourBarman", // 名字
"age" : 18 // 年龄
}
你看,若你这么写 IDEA 都会飘红提醒你:
然而,在很多应用场景(特地是 JavaScript)里,咱们会在 JSON 串里写正文(属性多时尤甚)那么对于这种串,JsonParser 如何管制解决呢?它提供了对非标准 JSON 格局的兼容,通过上面这些特征值来管制。
ALLOW_COMMENTS(false)
自 2.10 版本后,应用
JsonReadFeature#ALLOW_JAVA_COMMENTS
代替
是否容许 /* */
或者 //
这种类型的正文呈现。
@Test
public void test4() throws IOException {
String jsonStr = "{\n" +
"\t\"name\": \"YourBarman\", // 名字 \n" +
"\t\"age\": 18 // 年龄 \n" +
"}";
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {
// 开启正文反对
// jsonParser.enable(JsonParser.Feature.ALLOW_COMMENTS);
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {String fieldname = jsonParser.getCurrentName();
if ("name".equals(fieldname)) {jsonParser.nextToken();
System.out.println(jsonParser.getText());
} else if ("age".equals(fieldname)) {jsonParser.nextToken();
System.out.println(jsonParser.getIntValue());
}
}
}
}
运行程序,抛出异样:
com.fasterxml.jackson.core.JsonParseException: Unexpected character ('/' (code 47)): maybe a (non-standard) comment? (not recognized as one since Feature 'ALLOW_COMMENTS' not enabled for parser)
at [Source: (String)"{"name":"YourBarman", // 名字"age" : 18 // 年龄}"; line: 2, column: 26]
放开正文的代码,再次运行程序,失常 work。
ALLOW_YAML_COMMENTS(false)
自 2.10 版本后,应用
JsonReadFeature#ALLOW_YAML_COMMENTS
代替
顾名思义,开启后将反对 Yaml 格局的的正文,也就是 #
模式的正文语法。
ALLOW_UNQUOTED_FIELD_NAMES(false)
自 2.10 版本后,应用
JsonReadFeature#ALLOW_UNQUOTED_FIELD_NAMES
代替
是否容许属性名 不带双引号 ””,比较简单,示例略。
ALLOW_SINGLE_QUOTES(false)
自 2.10 版本后,应用
JsonReadFeature#ALLOW_SINGLE_QUOTES
代替
是否容许属性名反对单引号,也就是应用 ''
包裹,形如这样:
{'age' : 18}
ALLOW_UNQUOTED_CONTROL_CHARS(false)
自 2.10 版本后,应用
JsonReadFeature#ALLOW_UNESCAPED_CONTROL_CHARS
代替
是否容许 JSON 字符串蕴含非引号 控制字符(值小于 32 的 ASCII 字符,蕴含制表符和换行符)。因为 JSON 标准要求对所有控制字符应用引号,这是一个非标准的个性,因而默认禁用。
那么,哪些字符属于控制字符呢?做个简略科普:咱们个别说的 ASCII 码共 128 个字符(7bit),共分为两大类
控制字符
控制字符,也叫不可打印字符。第 0~32 号 落第 127 号 (共 34 个) 是控制字符,例如常见的:LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BS(退格)等都属于此类。
控制字符 大部分曾经废除 不必了,它们的用处次要是用来操控曾经解决过的文字,ASCII 值为 8、9、10 和 13 别离转换为退格、制表、换行和回车字符。它们 并没有特定的图形显示,但会依不同的应用程序,而对文本显示有不同的影响。
话外音:你看不见我,但我对你影响还蛮大
非控制字符
也叫可显示字符,或者可打印字符,能从键盘间接输出的字符。比方 0 - 9 数字,逗号、分号这些等等。
话外音:你肉眼能看到的字符就属于非控制字符
ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER(false)
自 2.10 版本后,应用
JsonReadFeature#ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER
代替
是否容许 * 反斜杠 * 本义任何字符。这句话不是十分好了解,看上面这个例子:
@Test
public void test4() throws IOException {String jsonStr = "{\"name\": \"YourB\\'atman\"}";
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {// jsonParser.enable(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER);
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {String fieldname = jsonParser.getCurrentName();
if ("name".equals(fieldname)) {jsonParser.nextToken();
System.out.println(jsonParser.getText());
}
}
}
}
运行程序,报错:
com.fasterxml.jackson.core.JsonParseException: Unrecognized character escape ''' (code 39)
at [Source: (String)"{"name":"YourB\'atman"}"; line: 1, column: 19]
...
放开正文掉的代码,再次运行程序,一切正常,输入:YourB'atman
。
ALLOW_NUMERIC_LEADING_ZEROS(false)
自 2.10 版本后,应用
JsonReadFeature#ALLOW_LEADING_ZEROS_FOR_NUMBERS
代替
是否容许像 00001
这样的“数字”呈现(而不报错)。看例子:
@Test
public void test5() throws IOException {String jsonStr = "{\"age\": 00018}";
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {// jsonParser.enable(JsonParser.Feature.ALLOW_NUMERIC_LEADING_ZEROS);
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {String fieldname = jsonParser.getCurrentName();
if ("age".equals(fieldname)) {jsonParser.nextToken();
System.out.println(jsonParser.getIntValue());
}
}
}
}
运行程序,输入:
com.fasterxml.jackson.core.JsonParseException: Invalid numeric value: Leading zeroes not allowed
at [Source: (String)"{"age": 00018}"; line: 1, column: 11]
...
放开注掉的代码,再次运行程序,一切正常。输入18
。
ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS(false)
自 2.10 版本后,应用
JsonReadFeature#ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS
代替
是否容许小数点 .
打头,也就是说 .1
这种小数格局是否非法。默认是不非法的,须要开启此特色能力反对,例子就略了,根本同上。
ALLOW_NON_NUMERIC_NUMBERS(false)
自 2.10 版本后,应用
JsonReadFeature#ALLOW_NON_NUMERIC_NUMBERS
代替
是否容许一些解析器辨认一组 “非数字”(如 NaN) 作为非法的浮点数值。这个属性和上篇文章的 JsonGenerator#QUOTE_NON_NUMERIC_NUMBERS
特征值是一唱一和的。
@Test
public void test5() throws IOException {String jsonStr = "{\"percent\": NaN}";
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {// jsonParser.enable(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS);
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {String fieldname = jsonParser.getCurrentName();
if ("percent".equals(fieldname)) {jsonParser.nextToken();
System.out.println(jsonParser.getFloatValue());
}
}
}
}
运行程序,抛错:
com.fasterxml.jackson.core.JsonParseException: Non-standard token 'NaN': enable JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS to allow
at [Source: (String)"{"percent": NaN}"; line: 1, column: 17]
放开正文掉的代码,再次运行,一切正常。输入:
NaN
小贴士:NaN 也能够示意一个 Float 对象,是的你没听错,即便它不是 数字 但它也是 Float 类型。具体你能够看看 Float 源码里的那几个常量
ALLOW_MISSING_VALUES(false)
自 2.10 版本后,应用
JsonReadFeature#ALLOW_MISSING_VALUES
代替
是否容许反对JSON 数组中“缺失”值。怎么了解:数组中缺失了值示意两个逗号之间,啥都没有,形如这样[value1, , value3]
。
@Test
public void test6() throws IOException {String jsonStr = "{\"names\": [\"YourBatman\",,\"A 哥 \",,] }";
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {// jsonParser.enable(JsonParser.Feature.ALLOW_MISSING_VALUES);
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {String fieldname = jsonParser.getCurrentName();
if ("names".equals(fieldname)) {jsonParser.nextToken();
while (jsonParser.nextToken() != JsonToken.END_ARRAY) {System.out.println(jsonParser.getText());
}
}
}
}
}
运行程序,抛错:
YourBatman // 能输入一个,毕竟第一个 part(JsonToken)是失常的嘛
com.fasterxml.jackson.core.JsonParseException: Unexpected character (',' (code 44)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
at [Source: (String)"{"names": ["YourBatman",,"A 哥 ",,] }"; line: 1, column: 27]
放开正文掉的代码,再次运行,一切正常,后果为:
YourBatman
null
A 哥
null
null
请留神:此时数组的长度是 5 哦。
小贴士:此处用的 String 类型展现后果,是因为 null 能够作为 String 类型(
jsonParser.getText()
失去 null 是非法的)。但如果你应用的 int 类型(或者 bool 类型),那么如果是 null 的话就报错喽Current token (VALUE_NULL) not of boolean type
,有趣味的亲可自行尝试,坚固下了解的成果。报错起因文上已有阐明~
ALLOW_TRAILING_COMMA(false)
自 2.10 版本后,应用
JsonReadFeature#ALLOW_TRAILING_COMMA
代替
是否容许最初一个多余的逗号(肯定是最初一个)。这个特色是 十分重要 的,若开关关上,有如下成果:
- [true,true,]等价于[true, true]
- {“a”: true,}等价于{“a”: true}
当这个特色和下面的 ALLOW_MISSING_VALUES
特色同时应用时,本特色优先级更高。也就是说:会先去除掉最初一个逗号后,再进行数组长度的计算。
举个例子:当然这两个特色开关都关上时,[true,true,]等价于 [true, true] 好了解;并且呢,[true,true,,]
是等价于 [true, true, null]
的哦,可千万别疏忽最初的这个 null。
@Test
public void test7() throws IOException {String jsonStr = "{\"results\": [true,true,,] }";
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {jsonParser.enable(JsonParser.Feature.ALLOW_MISSING_VALUES);
// jsonParser.enable(JsonParser.Feature.ALLOW_TRAILING_COMMA);
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {String fieldname = jsonParser.getCurrentName();
if ("results".equals(fieldname)) {jsonParser.nextToken();
while (jsonParser.nextToken() != JsonToken.END_ARRAY) {System.out.println(jsonParser.getBooleanValue());
}
}
}
}
}
运行程序,输入:
YourBatman
null
A 哥
null
null
这齐全就是上例的成果嘛。当初我放开正文掉的代码,再次运行,后果为:
YourBatman
null
A 哥
null
请留神比照前后的后果差别,并本人能能本人正当解释。
校验相干
Jackson 在 JSON 规范 之外,给出了两个校验相干的特色。
STRICT_DUPLICATE_DETECTION(false)
自 2.10 版本后,应用
StreamReadFeature#STRICT_DUPLICATE_DETECTION
代替
是否容许 JSON 串有两个雷同的属性 key,默认是 容许的。
@Test
public void test8() throws IOException {String jsonStr = "{\"age\":18, \"age\": 28}";
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {// jsonParser.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION);
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {String fieldname = jsonParser.getCurrentName();
if ("age".equals(fieldname)) {jsonParser.nextToken();
System.out.println(jsonParser.getIntValue());
}
}
}
}
运行程序,失常输入:
18
28
若放开正文代码,再次运行,则抛错:
18 // 第一个数字还是能失常输入的哟
com.fasterxml.jackson.core.JsonParseException: Duplicate field 'age'
at [Source: (String)"{"age":18,"age": 28}"; line: 1, column: 17]
IGNORE_UNDEFINED(false)
自 2.10 版本后,应用
StreamReadFeature#IGNORE_UNDEFINED
代替
是否疏忽 没有定义 的属性 key。和 JsonGenerator.Feature#IGNORE_UNKNOWN
的这个特色一样,它作用于事后定义了格局的数据类型,如 Avro、protobuf
等等,JSON 是不须要事后定义的哦~
同样的,你能够通过这个 API 事后设置格局:
JsonParser:
public void setSchema(FormatSchema schema) {...}
其它
INCLUDE_SOURCE_IN_LOCATION(true)
自 2.10 版本后,应用
StreamReadFeature#INCLUDE_SOURCE_IN_LOCATION
代替
是否构建 JsonLocation
对象来示意每个 part 的起源,你能够通过 JsonParser#getCurrentLocation()
来拜访。作用不大,就此略过。
总结
本文介绍了底层流式 API JsonParser 读 JSON 的形式,它不仅仅可能解决 规范 JSON,也能通过 Feature 特征值来管制,开启对一些非标准但又比拟罕用的 JSON 串的反对,这不正式一个优良框架 / 库应有的态度麽:兼容性。
联合上篇文章对写 JSON 时 JsonGenerator
的形容,可能总结出两点准则:
- 写:100% 遵循标准
- 读:最大水平 兼容 并包
写代表你的输入,遵循标准的输入能确保第三方在用你输入的数据时不至于对你破口大骂,所以这是你应该做好的 本分 。读代表你的输出,可能解决标准的格局是你的职责,但我若还能额定的解决一些非标准格局(个别为罕用的),那相对是闪耀点,也就是你给的 情分。本分是你应该做的,而情分就是你的加分项。
相干举荐:
- Fastjson 到了说再见的时候了
- 1. 初识 Jackson — 世界上最好的 JSON 库
- 2. 妈呀,Jackson 原来是这样写 JSON 的
- 3. 懂了这些,方敢在简历上说会用 Jackson 写 JSON
关注 A 哥
Author | A 哥(YourBatman) |
---|---|
集体站点 | www.yourbatman.cn |
yourbatman@qq.com | |
微 信 | fsx641385712 |
沉闷平台 |
|
公众号 | BAT 的乌托邦(ID:BAT-utopia) |
常识星球 | BAT 的乌托邦 |
每日文章举荐 | 每日文章举荐 |